Compare commits

..

7 Commits

Author SHA1 Message Date
r-zakarya 092eb2f73a feat(fillbar): bouton Paste qui tape le presse-papiers Windows
Pour les clients a distance qui ne transmettent pas le presse-papiers
(TigerVNC vers un Mac) : le texte est tape par le chemin de l'autofill,
touches VNC et dispositions comprises, insere au curseur. Et l'arabe des
champs a chasse fixe s'affiche a la bonne taille (repli Segoe UI / Tahoma).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:53:09 +01:00
r-zakarya bf7a8f4d38 fix(autofill): texte arabe + latin tape en entier vers VNC
Un caractere sans touche dans la disposition courante est cherche dans les
autres dispositions installees ; la fenetre VNC y bascule le temps de le
taper, puis la disposition d'origine est rendue. Eprouve vers un Mac, clavier
francais comme arabe actif.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:55:09 +01:00
r-zakarya 9999dd113d fix(autofill): AltGr et caracteres sans touche vers VNC
AltGr envoye en Ctrl gauche + Alt droit etendu, avec scancodes : "@" et
"#" en AZERTY arrivaient sans AltGr. Un caractere sans touche dans la
disposition locale est saute et compte dans le journal (copier a la place).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:45:33 +01:00
r-zakarya a3e63ee8f1 fix(autofill): vraies touches vers un visualiseur VNC
VK_PACKET (texte Unicode) n'est pas retransmis par TigerVNC : le Mac
distant recevait un V par caractere. Cible VNC -> touches du clavier via
VkKeyScanEx dans la disposition de la cible, modificateurs compris.
Les autres fenetres gardent l'envoi Unicode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:34:47 +01:00
r-zakarya 6397ed008a docs: LISEZ-MOI du paquet entre au depot
Le mode d'emploi livre avec l'exe et l'APK ne vivait que dans le ZIP.
A jour des notes de commandes, de Precedent / Suivant et des raccourcis
desactivables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:47:39 +01:00
r-zakarya b1c0c1e11b feat(apk): notes de commandes, une commande copiee a la fois
Bloc Commands en tete d'une note "commands", groupe par groupe, avec un
bouton copier. ParseCommandNote est le portage du decoupage du poste
(groupes, lignes continuees). La copie passe par un rang, comme les autres
lignes du detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:39:38 +01:00
r-zakarya f112de508b feat(notes): commandes sur plusieurs lignes recollees en une seule
Fin de ligne en \ (bash), ` (PowerShell) ou ^ (cmd) : la ligne suivante
est rattachee, et la commande part sur une ligne - un retour a la ligne
tape dans un terminal la lancerait a moitie ecrite. Teste.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:08:08 +01:00
11 changed files with 636 additions and 24 deletions
+39
View File
@@ -316,6 +316,16 @@ Détails :
Le cadre est peint dans `WM_ERASEBKGND` plutôt que par `WS_BORDER`, qui se Le cadre est peint dans `WM_ERASEBKGND` plutôt que par `WS_BORDER`, qui se
sert dans les couleurs système. sert dans les couleurs système.
**Bouton Paste (2026-10-14)** : `LAZY_ID_PASTE` / cle `paste` (desactive par
defaut) → `PasteClipboardAsKeystrokes` lit le presse-papiers **Windows**
(`CF_UNICODETEXT`) et le **tape** dans la fenetre en avant par
`ExecuteAutofill(..., AClearFirst=False)` - donc vraies touches et bascule de
disposition vers un client VNC (TigerVNC vers un Mac ne transmet pas le
presse-papiers). Insere au curseur, saut de ligne final retire (un terminal
lancerait la commande), sauts internes tapes en Entree. Marche coffre ferme :
rien ne passe par la page. Pas de texte (image, fichier) → bulle « No text in
the clipboard ».
### Réglages de la barrette (2026-09-18) ### Réglages de la barrette (2026-09-18)
**Settings → onglet Fill bar**, et le **clic droit** sur la barrette pour les **Settings → onglet Fill bar**, et le **clic droit** sur la barrette pour les
@@ -1443,6 +1453,25 @@ d'accord.
### Contraintes connues autofill ### Contraintes connues autofill
- **VNC (2026-10-14)** : les caracteres partent en `KEYEVENTF_UNICODE`
(`VK_PACKET`), que TigerVNC & co ne savent pas retransmettre - un Mac
recevait un `V` par caractere. Cible dont l'exe contient « vnc »
(`TargetTypesByKeyCodes`) → **vraies touches** (`SendKeyCodeString` :
`VkKeyScanEx` dans la disposition de la cible, + Maj/Ctrl/Alt, AltGr =
Ctrl+Alt). **AltGr = `VK_LCONTROL` + `VK_RMENU` etendu**, comme le vrai
clavier : un Ctrl+Alt generique etait lu par TigerVNC comme deux
modificateurs, et `@` (AltGr+0 en AZERTY) arrivait en `a`. Chaque touche porte
aussi son scancode (`MapVirtualKeyEx`). **Texte melange (arabe + latin)** :
un caractere sans touche dans la disposition courante est cherche dans les
autres dispositions installees (`GetKeyboardLayoutList`) ; la fenetre cible
y est basculee (`WM_INPUTLANGCHANGEREQUEST`, attente que son fil l'ait
prise - TigerVNC traduit chaque touche avec la disposition de SON fil), et
la disposition d'origine est rendue a la fin. Seul un caractere qu'aucune
disposition installee ne sait taper est **saute et compte** - journal
`N character(s) have no key on this keyboard and were NOT typed - copy
instead`. Eprouve vers un Mac : `تاسيب... jhbs... @ #` complet, clavier
francais comme arabe actif.
- **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin, - **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin,
regedit → autofill silencieusement no-op. Tester avec apps non-élevées. regedit → autofill silencieusement no-op. Tester avec apps non-élevées.
- **Chrome password manager** : si Chrome a des credentials sauvegardés - **Chrome password manager** : si Chrome a des credentials sauvegardés
@@ -1587,6 +1616,16 @@ après chaque `DELETE /attachments/{id}`. Un petit vault sain ne paie rien.
(`ssh ••••••••••••.11`), une fin de 6 caracteres ou moins est masquee en (`ssh ••••••••••••.11`), une fin de 6 caracteres ou moins est masquee en
entier. Affichage seulement : filtre, copie et remplissage voient le vrai entier. Affichage seulement : filtre, copie et remplissage voient le vrai
texte. L'oeil re-declenche `input` sur le textarea pour redessiner la liste. texte. L'oeil re-declenche `input` sur le textarea pour redessiner la liste.
**Lignes continuees** : une ligne finie par `\`, `` ` `` ou `^` se recolle a
la suivante (sans le caractere) - une seule commande, sur UNE ligne, car un
retour a la ligne tape dans un terminal vaut Entree. Ligne vide ou fin du
texte = fin de la commande ; un `#` apres une continuation reste du texte.
Limite : un chemin Windows fini par `\` en fin de ligne passe pour une
continuation.
- **Polices a chasse fixe et arabe** (2026-10-14) : aucune n'a l'arabe, et
Windows empruntait une police plus petite. Chaque pile `…, monospace` de
`css/style.css` finit par `'Segoe UI', Tahoma, monospace` : le latin reste a
chasse fixe, l'arabe prend une taille comparable.
- **Settings search** : `#settingsSearch` filtre les `.setting-row` par - **Settings search** : `#settingsSearch` filtre les `.setting-row` par
texte ; match sur le label de section garde toute la section. Esc dans texte ; match sur le label de section garde toute la section. Esc dans
la search avec query = clear ; Esc vide = ferme Settings. la search avec query = clear ; Esc vide = ferme Settings.
+193
View File
@@ -0,0 +1,193 @@
Password Manager - installation
================================
Quatre fichiers dans ce dossier :
PMServer.exe l'application Windows - c'est elle qui compte
pmserverApk.apk l'application Android, FACULTATIVE (voir le point 7)
WEBDAV-LOCAL.md le guide pour synchroniser le telephone sans
depot en ligne, par ton propre PC (point 7)
GITHUB.md le guide pour synchroniser par un depot GitHub
prive, de partout (point 7)
Il n'y a pas d'installeur.
L'activation se fait en DEUX temps, parce que la licence est liee a
ta machine : tu m'envoies un code que l'application t'affiche, je te
renvoie ta cle. Compte un aller-retour avant de pouvoir t'en servir.
1. OU LE METTRE
Mets le dossier ou tu veux, SAUF dans "Program Files" :
l'application ecrit son coffre (vault.db) juste a cote de l'exe,
et Windows interdit l'ecriture dans Program Files.
Un bon endroit : C:\Users\<toi>\PasswordManager\
Choisis bien maintenant : deplacer l'exe plus tard deplace le coffre
avec lui, il faut penser a prendre le vault.db aussi.
2. PREMIER LANCEMENT - M'ENVOYER TON "INSTALLATION ID"
Double-clic sur PMServer.exe. Le premier ecran demande une cle
d'activation, que tu n'as pas encore. En BAS de cet ecran il y a un
code de seize caracteres, du genre :
Installation ID
CD6F-F1B3-25C4-0111
Envoie-moi ce code (photo de l'ecran, copier-coller, peu importe).
Il ne dit rien sur toi : c'est une empreinte de ta carte mere et de
ton disque, melangees et hachees. Il ne permet ni de t'identifier,
ni de remonter a ton materiel.
Je te renvoie ta cle. Tu la colles dans le champ, "Activate", et
c'est fini : elle n'est demandee qu'une fois.
- Un AUTRE compte Windows sur le meme PC la redemandera.
- Elle ne marchera QUE sur cette machine. Si tu changes de carte
mere ou de disque systeme, redemande-m'en une, c'est gratuit
et immediat.
3. CREER TON COFFRE
Ensuite, "Register" : tu choisis ton nom d'utilisateur et ton mot
de passe maitre.
LE MOT DE PASSE MAITRE N'EST STOCKE NULLE PART. Il n'y a aucun
moyen de le recuperer s'il est perdu - le coffre devient illisible,
y compris pour moi. Va dans Settings > Security des le premier jour
et genere un "recovery code", garde-le hors de l'ordinateur.
4. SI L'APPLICATION NE S'OUVRE PAS (fenetre blanche ou rien)
Il manque le moteur d'affichage de Microsoft, deja present sur
Windows 11 mais pas toujours sur Windows 10. Il est gratuit :
https://developer.microsoft.com/microsoft-edge/webview2/
-> "Evergreen Bootstrapper", telecharge, installe, relance l'app.
5. SAUVEGARDE
Tout le coffre tient dans vault.db, a cote de l'exe. Le copier
suffit a le sauvegarder. Settings > Backup fait une copie chiffree
automatique dans le dossier de ton choix - mets-le en route.
Si tu ranges ce dossier dans Google Drive, OneDrive ou Dropbox, la
sauvegarde part avec, et elle est chiffree : le service ne voit
qu'un fichier illisible.
6. AU QUOTIDIEN
- L'application vit dans la zone de notification (a cote de l'heure),
elle ne se ferme pas avec la croix. Clic droit sur l'icone > Quit.
- Ctrl+Shift+L remplit identifiant + mot de passe dans le navigateur :
clique d'abord le champ identifiant, puis la combinaison.
- Clic droit sur l'icone > "Fill bar" pour faire la meme chose
a la souris, sans raccourci. Settings > Fill bar (ou clic droit
sur la barrette) choisit ses boutons, icones ou texte, et sa
transparence (fond retire possible : seuls les boutons restent).
- Ctrl+Shift+Q ouvre une recherche rapide. Ouverte par ce raccourci
(ou le bouton Search de la barrette), elle remplit le champ ou tu
etais ; l'interrupteur Copy / Fill en haut permet de copier a la
place. Depuis l'icone du tray, elle copie seulement.
- Activer une 2FA sur un site : laisse son QR code a l'ecran, ouvre
l'entree, et clique le bouton QR du champ "Two-factor (TOTP)".
Le secret se remplit tout seul.
- Ouvert une entree par erreur ? Alt+fleche gauche (ou la fleche < en
haut du panneau) revient a la precedente, sans refaire la recherche.
Alt+fleche droite repart en avant.
- Un raccourci clavier gene une autre application ? Settings >
Autofill, clique le raccourci puis appuie sur Suppr : il passe a
"Off" et la combinaison redevient libre. La barrette continue de
remplir meme si les raccourcis sont coupes.
- NOTES DE COMMANDES (ssh, scripts...) : + > "Commands (note)", ou
coche "Command list" dans une note existante. Une commande par
ligne, et une ligne "# Titre" pour faire des groupes :
# Prod
ssh admin@10.0.0.11
ssh admin@10.0.0.12
Une commande trop longue peut continuer sur la ligne suivante si la
ligne finit par \ (ou ` en PowerShell, ^ en cmd).
Ctrl+Shift+Q, choisis la note, tape un bout de la commande, puis
Entree : elle est tapee dans ton terminal SANS etre lancee (c'est
toi qui valides). Ctrl+Entree la copie. Esc ou < revient aux
entrees. L'oeil de la note masque les commandes (partage d'ecran).
Rien ne sort de ton ordinateur : pas de compte en ligne, pas de
serveur. Tout est chiffre sur place.
7. LE TELEPHONE (pmserverApk.apk) - FACULTATIF
A LIRE AVANT D'INSTALLER : l'application Android ne sait pas creer
de coffre. Elle ne fait que reprendre celui du PC, et pour cela il
faut un endroit ou le PC publie une copie chiffree. Deux choix :
- UN WEBDAV LOCAL, sur ton propre PC : rien en ligne, pas de compte.
Le telephone se synchronise quand il est sur le meme Wi-Fi que le
PC, et que le PC est allume. Tout est explique pas a pas dans
WEBDAV-LOCAL.md - suis ce guide A LA PLACE des points a. et d.
ci-dessous.
- UN DEPOT PRIVE Gitea ou GitHub : la synchro marche de partout.
Un depot public est refuse : n'importe qui pourrait telecharger
ton coffre chiffre et tout son historique.
Sans l'un ou l'autre, l'application s'installe mais reste vide : ne
perds pas de temps avec elle, dis-le moi et on regarde ensemble.
Ce qui suit resume le depot Gitea / GitHub ; GITHUB.md le detaille
ecran par ecran (creer le depot, le jeton, regler PC et telephone).
Ce qu'il faut, une fois :
a. Sur le PC : Settings > Sync, renseigner l'adresse du depot et
le jeton, choisir un MOT DE PASSE DE SYNCHRO (different du mot
de passe maitre), puis "Sync now".
Adresse : celle du depot, telle que GitHub ou Gitea l'affiche,
par exemple https://github.com/<toi>/<depot>.git
Pour GitHub, un jeton "fine-grained" limite a ce depot, droit
Contents : Read and write. (GitLab n'est pas pris en charge.)
LE MOT DE PASSE DE SYNCHRO DOIT ETRE FORT (5 mots au hasard, ou
16 caracteres tires au hasard) : c'est lui seul qui protege la
copie qui part sur le depot.
b. Sur le telephone : autoriser l'installation d'applications
depuis cette source quand Android le demande, puis installer
pmserverApk.apk.
c. Sur le PC : Settings > Sync > "QR for phone". Un QR apparait
pour 60 secondes.
d. Sur le telephone : Settings > "Scan the QR shown on the PC",
viser le QR. L'adresse et le jeton se remplissent seuls.
Le QR ne contient PAS le mot de passe de synchro : tape-le a
la main, puis "Save", puis "Download vault".
Ensuite, "Unlock with" choisit comment le coffre s'ouvre sur le
telephone : le verrouillage d'ecran (empreinte, schema), un code
propre a l'application, ou le mot de passe de synchro.
- Si tu RETIRES le verrouillage d'ecran, Android detruit la cle
qui protegeait la copie locale. L'application demande alors le
mot de passe et rouvre le coffre : rien n'est perdu.
- Le telephone lit et modifie le coffre, et "Sync" fait l'aller-
retour avec le depot (ou le WebDAV). Le PC reste la reference.
- A l'ouverture, le telephone verifie en arriere-plan : si le PC a
publie du nouveau, le bouton Sync s'allume.
Pour la 2FA sur le telephone : dans une entree, le crayon, puis le
bouton QR du champ "2FA secret" ; vise le QR du site avec la camera.
Une note de commandes s'affiche sur le telephone commande par
commande, chacune avec son bouton copier.
La licence ne concerne pas le telephone : c'est l'application
Windows qui est licenciee.
+71
View File
@@ -37,6 +37,19 @@ function VaultToJson(AVault: TVault): string;
/// Les lignes d'une entree, dans l'ordre d'affichage. /// Les lignes d'une entree, dans l'ordre d'affichage.
function DetailToJson(const AEntry: TVaultEntry): string; function DetailToJson(const AEntry: TVaultEntry): string;
type
TCommandItem = record
Group, Cmd: string;
end;
/// NOTE DE COMMANDES (modele "commands"), meme regle que le poste
/// (js/app.js parseCommandNote) : "# titre" ouvre un groupe, chaque autre ligne
/// non vide est une commande ; une ligne finie par \, ` ou ^ se recolle a la
/// suivante, sur UNE ligne. Une ligne vide ou la fin du texte termine la
/// commande en cours.
function ParseCommandNote(const AText: string): TArray<TCommandItem>;
function IsCommandNote(const AEntry: TVaultEntry): Boolean;
/// Une ligne du detail par son rang, dans le MEME ordre que DetailToJson : la /// Une ligne du detail par son rang, dans le MEME ordre que DetailToJson : la
/// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui. /// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui.
/// Faux si le rang ne designe rien. /// Faux si le rang ne designe rien.
@@ -245,6 +258,49 @@ begin
Result := LRows; Result := LRows;
end; end;
function IsCommandNote(const AEntry: TVaultEntry): Boolean;
begin
Result := AEntry.IsNote and SameText(AEntry.Template, 'commands');
end;
function ParseCommandNote(const AText: string): TArray<TCommandItem>;
var
LGroup, LPending, LLine, LPart: string;
LCont: Boolean;
procedure Flush;
begin
if LPending <> '' then
begin
SetLength(Result, Length(Result) + 1);
Result[High(Result)].Group := LGroup;
Result[High(Result)].Cmd := LPending;
end;
LPending := '';
end;
begin
Result := nil;
LGroup := '';
LPending := '';
for LLine in AText.Replace(#13, '').Split([#10]) do
begin
var LTrim := LLine.Trim;
if LTrim = '' then begin Flush; Continue; end;
if (LPending = '') and LTrim.StartsWith('#') then
begin
LGroup := LTrim.TrimLeft(['#']).Trim;
Continue;
end;
LCont := CharInSet(LTrim[High(LTrim)], ['\', '`', '^']);
if LCont then LPart := Copy(LTrim, 1, Length(LTrim) - 1).TrimRight
else LPart := LTrim;
if LPending = '' then LPending := LPart else LPending := LPending + ' ' + LPart;
if not LCont then Flush;
end;
Flush;
end;
function DetailToJson(const AEntry: TVaultEntry): string; function DetailToJson(const AEntry: TVaultEntry): string;
var var
LRoot: TJSONObject; LRoot: TJSONObject;
@@ -278,6 +334,21 @@ begin
if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True)); if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True));
LArr.AddElement(LObj); LArr.AddElement(LObj);
end; end;
// La liste des commandes, pour l'affichage. La copie passe par un rang
// (copy?f=cmd&k=), comme les lignes : Delphi relit le clair chez lui.
if IsCommandNote(AEntry) then
begin
var LCmds := ParseCommandNote(AEntry.Password);
var LCArr := TJSONArray.Create;
LRoot.AddPair('cmds', LCArr);
for I := 0 to High(LCmds) do
begin
LObj := TJSONObject.Create;
LObj.AddPair('g', LCmds[I].Group);
LObj.AddPair('c', LCmds[I].Cmd);
LCArr.AddElement(LObj);
end;
end;
Result := LRoot.ToJSON; Result := LRoot.ToJSON;
finally finally
LRoot.Free; LRoot.Free;
+16
View File
@@ -447,6 +447,22 @@ l'ouverture, écriture 204, puis synchro du poste qui reprend la modification.
Portmaster (entrant autorisé pour Python) — sans le second, le port restait Portmaster (entrant autorisé pour Python) — sans le second, le port restait
fermé malgré la règle. fermé malgré la règle.
## Notes de commandes — 2026-10-10
Une note marquée **Commands** sur le poste (modèle `commands`) affiche en tête
un bloc *Commands* : une commande par ligne sous le titre de son groupe, avec
un bouton copier. Le texte entier reste dessous, dans *Content*.
- **Même découpage que le poste** : `ParseCommandNote` (PMA.WebUI) est le
portage de `parseCommandNote` (js/app.js) — `# titre` ouvre un groupe, une
ligne finie par `\`, `` ` `` ou `^` se recolle à la suivante. Les deux
doivent dire la même chose ; éprouvé sur les mêmes cas.
- **La copie passe par un rang** (`copy?f=cmd&k=`), comme les autres lignes :
Delphi relit la note et copie la commande.
- **Ni remplissage ni masquage** ici : pas de fenêtre cible, et le corps des
notes est déjà affiché en clair. Une note devient « Commands » depuis le
poste (case *Command list*) ; la synchro porte le modèle.
## Saisie : ce qui a résisté ## Saisie : ce qui a résisté
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne - **`user-select: none` du `body`** était hérité par les champs : le curseur ne
+23
View File
@@ -279,6 +279,11 @@
.field .val.multi { white-space: pre-wrap; word-break: normal; .field .val.multi { white-space: pre-wrap; word-break: normal;
overflow-wrap: anywhere; } overflow-wrap: anywhere; }
.field.totp .val { font-size: 30px; font-weight: 300; letter-spacing: 2px; } .field.totp .val { font-size: 30px; font-weight: 300; letter-spacing: 2px; }
/* Note de commandes : une commande par ligne, sous le titre de son groupe. */
.field.cmds .cmd-group { font-size: 12px; font-weight: 600; opacity: .7;
text-transform: uppercase; margin: 10px 0 2px; }
.field.cmds .cmd-line { padding: 4px 0; }
.field.cmds .val.cmd { font-family: monospace; font-size: 14px; }
.bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.18); .bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.18);
margin-top: 10px; overflow: hidden; } margin-top: 10px; overflow: hidden; }
.bar i { display: block; height: 100%; background: var(--text); .bar i { display: block; height: 100%; background: var(--text);
@@ -1089,6 +1094,23 @@ function renderDetail(d) {
+ '<button class="btn ico-btn" data-copy="totp">' + '<button class="btn ico-btn" data-copy="totp">'
+ ico('copy', 'Copy code') + '</button></div>' + ico('copy', 'Copy code') + '</button></div>'
+ '<div class="bar"><i id="totpb"></i></div></div>'; + '<div class="bar"><i id="totpb"></i></div></div>';
// NOTE DE COMMANDES : une ligne par commande, groupees sous leur titre, avec
// un bouton copier. Le texte entier reste plus bas, dans "Content".
if (d.cmds && d.cmds.length) {
var lastG = null;
html += '<div class="field cmds"><div class="cap">Commands</div>';
for (i = 0; i < d.cmds.length; i++) {
var cm = d.cmds[i];
if (cm.g !== lastG) {
lastG = cm.g;
if (cm.g) html += '<div class="cmd-group">' + esc(cm.g) + '</div>';
}
html += '<div class="line cmd-line"><div class="val cmd">' + esc(cm.c) + '</div>'
+ '<button class="btn ico-btn" data-copy="cmd" data-k="' + i + '">'
+ ico('copy', 'Copy command') + '</button></div>';
}
html += '</div>';
}
for (i = 0; i < detailRows.length; i++) { for (i = 0; i < detailRows.length; i++) {
var r = detailRows[i]; var r = detailRows[i];
html += '<div class="field"><div class="cap">' + esc(r.c) + '</div>' html += '<div class="field"><div class="cap">' + esc(r.c) + '</div>'
@@ -1170,6 +1192,7 @@ document.getElementById('fields').addEventListener('click', function (ev) {
var copy = t.getAttribute('data-copy'); var copy = t.getAttribute('data-copy');
if (copy === 'totp') { send('copy?f=totp'); return; } if (copy === 'totp') { send('copy?f=totp'); return; }
if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; } if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; }
if (copy === 'cmd') { send('copy?f=cmd&k=' + t.getAttribute('data-k')); return; }
}); });
+21 -17
View File
@@ -17,10 +17,14 @@ body {
overflow: hidden; overflow: hidden;
} }
/* Les polices a chasse fixe n'ont pas l'arabe : Windows empruntait une police
au hasard, plus petite. 'Segoe UI' / Tahoma, places avant 'monospace' dans
chaque pile, prennent ces caracteres a une taille comparable ; le latin reste
dans la police a chasse fixe. */
button { font-family: inherit; cursor: pointer; } button { font-family: inherit; cursor: pointer; }
input, select, textarea { font-family: inherit; font-size: inherit; } input, select, textarea { font-family: inherit; font-size: inherit; }
kbd { kbd {
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Segoe UI', Tahoma, monospace;
font-size: 11px; font-size: 11px;
padding: 2px 6px; padding: 2px 6px;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -248,7 +252,7 @@ input[type="range"]::-webkit-slider-thumb {
.licence-machine code { .licence-machine code {
display: inline-block; padding: 6px 10px; display: inline-block; padding: 6px 10px;
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-family: ui-monospace, "Cascadia Mono", Consolas, 'Segoe UI', Tahoma, monospace;
font-size: 14px; letter-spacing: 1px; color: var(--text); font-size: 14px; letter-spacing: 1px; color: var(--text);
user-select: all; user-select: all;
} }
@@ -1216,7 +1220,7 @@ input[type="range"]::-webkit-slider-thumb {
} }
.entry-pw { .entry-pw {
flex: 1; flex: 1;
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Segoe UI', Tahoma, monospace;
font-size: 12px; font-size: 12px;
color: var(--text-dim); color: var(--text-dim);
overflow: hidden; overflow: hidden;
@@ -1268,7 +1272,7 @@ input[type="range"]::-webkit-slider-thumb {
} }
.totp-code { .totp-code {
flex: 1; flex: 1;
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
font-size: 22px; font-size: 22px;
letter-spacing: 2px; letter-spacing: 2px;
color: var(--accent); color: var(--accent);
@@ -1330,7 +1334,7 @@ input[type="range"]::-webkit-slider-thumb {
} }
.auth-card-code { .auth-card-code {
flex: 1; flex: 1;
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
font-size: 26px; font-size: 26px;
letter-spacing: 3px; letter-spacing: 3px;
color: var(--accent); color: var(--accent);
@@ -1386,7 +1390,7 @@ input[type="range"]::-webkit-slider-thumb {
.health-score.is-danger { border-color: #ef4444; color: #ef4444; } .health-score.is-danger { border-color: #ef4444; color: #ef4444; }
.health-score-num { .health-score-num {
font-size: 36px; font-weight: 700; font-size: 36px; font-weight: 700;
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
} }
.health-score-lbl { .health-score-lbl {
font-size: 11px; font-weight: 600; font-size: 11px; font-weight: 600;
@@ -1417,7 +1421,7 @@ input[type="range"]::-webkit-slider-thumb {
.health-bd-n { text-align: right; font-variant-numeric: tabular-nums; } .health-bd-n { text-align: right; font-variant-numeric: tabular-nums; }
.health-bd-pts { .health-bd-pts {
text-align: right; text-align: right;
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
color: var(--text-faint); color: var(--text-faint);
} }
/* Nothing wrong in this category — dim it so the eye goes to what needs work. */ /* Nothing wrong in this category — dim it so the eye goes to what needs work. */
@@ -1572,7 +1576,7 @@ input[type="range"]::-webkit-slider-thumb {
.history-date { .history-date {
font-size: 11px; font-size: 11px;
color: var(--text-dim); color: var(--text-dim);
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
} }
.history-preview { .history-preview {
display: flex; align-items: flex-start; gap: 6px; display: flex; align-items: flex-start; gap: 6px;
@@ -1581,7 +1585,7 @@ input[type="range"]::-webkit-slider-thumb {
.history-value { .history-value {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
font-size: 12px; font-size: 12px;
color: var(--text); color: var(--text);
/* Long revealed passwords wrap to multiple lines (no ellipsis) so /* Long revealed passwords wrap to multiple lines (no ellipsis) so
@@ -1634,7 +1638,7 @@ input[type="range"]::-webkit-slider-thumb {
flex-wrap: wrap; flex-wrap: wrap;
} }
.cheatsheet-keys kbd { .cheatsheet-keys kbd {
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
font-size: 11px; font-size: 11px;
padding: 2px 7px; padding: 2px 7px;
background: var(--bg); background: var(--bg);
@@ -1814,7 +1818,7 @@ input[type="range"]::-webkit-slider-thumb {
.audit-row:hover { background: var(--bg); } .audit-row:hover { background: var(--bg); }
.audit-date { .audit-date {
color: var(--text-dim); color: var(--text-dim);
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
white-space: nowrap; white-space: nowrap;
} }
.audit-action { .audit-action {
@@ -1823,7 +1827,7 @@ input[type="range"]::-webkit-slider-thumb {
} }
.audit-ip { .audit-ip {
color: var(--text-faint); color: var(--text-faint);
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
text-align: right; text-align: right;
} }
@@ -1969,7 +1973,7 @@ input[type="range"]::-webkit-slider-thumb {
.so-note-body { .so-note-body {
min-height: 220px; min-height: 220px;
resize: vertical; resize: vertical;
font-family: 'JetBrains Mono', ui-monospace, monospace; font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
font-size: 13px; font-size: 13px;
line-height: 1.55; line-height: 1.55;
white-space: pre-wrap; white-space: pre-wrap;
@@ -1987,7 +1991,7 @@ input[type="range"]::-webkit-slider-thumb {
.so-cmds-cmd { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .so-cmds-cmd { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.so-cmds-empty { font-size: 12px; color: var(--text-dim); } .so-cmds-empty { font-size: 12px; color: var(--text-dim); }
#qsBack { flex-shrink: 0; } #qsBack { flex-shrink: 0; }
.qs-cmd { font-family: ui-monospace, Consolas, monospace; font-size: 13px; } .qs-cmd { font-family: ui-monospace, Consolas, 'Segoe UI', Tahoma, monospace; font-size: 13px; }
/* ---- + New dropdown ---------------------------------- */ /* ---- + New dropdown ---------------------------------- */
.new-entry-wrap { .new-entry-wrap {
@@ -2543,7 +2547,7 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', 'Segoe UI', Tahoma, monospace;
font-size: 13px; font-size: 13px;
word-break: break-all; word-break: break-all;
margin-bottom: 16px; margin-bottom: 16px;
@@ -2780,7 +2784,7 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
/* ---- Hotkey capture buttons (Settings → Autofill) ---- */ /* ---- Hotkey capture buttons (Settings → Autofill) ---- */
.hotkey-capture-btn { .hotkey-capture-btn {
font-family: ui-monospace, "Cascadia Code", Menlo, monospace; font-family: ui-monospace, "Cascadia Code", Menlo, 'Segoe UI', Tahoma, monospace;
font-size: 11px; font-size: 11px;
min-width: 140px; min-width: 140px;
text-align: center; text-align: center;
@@ -3240,7 +3244,7 @@ input[type="password"]::-ms-clear {
margin-bottom: 28px; margin-bottom: 28px;
} }
.prc-code { .prc-code {
font-family: 'Courier New', monospace; font-family: 'Courier New', 'Segoe UI', Tahoma, monospace;
font-size: 32px; font-size: 32px;
letter-spacing: 6px; letter-spacing: 6px;
text-align: center; text-align: center;
+221 -4
View File
@@ -183,6 +183,7 @@ type
// les declenche aussi, meme capture de la fenetre cible. // les declenche aussi, meme capture de la fenetre cible.
procedure FireNewEntryRequest; procedure FireNewEntryRequest;
procedure FireQuickSearchRequest; procedure FireQuickSearchRequest;
procedure PasteClipboardAsKeystrokes;
/// Pose l'opacite qui convient a l'etat de survol donne. /// Pose l'opacite qui convient a l'etat de survol donne.
procedure ApplyLazyAlpha(AHover: Boolean); procedure ApplyLazyAlpha(AHover: Boolean);
/// Menu du clic droit sur la barrette : le choix de l'opacite au repos. /// Menu du clic droit sur la barrette : le choix de l'opacite au repos.
@@ -450,6 +451,7 @@ const
LAZY_ID_ADD = 106; // nouvelle entree depuis la fenetre en avant (Ctrl+Shift+A) LAZY_ID_ADD = 106; // nouvelle entree depuis la fenetre en avant (Ctrl+Shift+A)
LAZY_ID_SEARCH = 107; // recherche rapide (Ctrl+Shift+Q) LAZY_ID_SEARCH = 107; // recherche rapide (Ctrl+Shift+Q)
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
LAZY_ID_PASTE = 109; // tape le presse-papiers Windows dans la fenetre en avant
LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu
LAZY_MENU_SETTINGS = 2; LAZY_MENU_SETTINGS = 2;
LAZY_MENU_CLEAR = 3; LAZY_MENU_CLEAR = 3;
@@ -1116,6 +1118,59 @@ end;
// The target of a fill is whatever window owns the foreground at the moment // The target of a fill is whatever window owns the foreground at the moment
// the user asks for it - a shortcut, or a click on the lazy strip, which takes // the user asks for it - a shortcut, or a click on the lazy strip, which takes
// no foreground precisely so this stays true. // no foreground precisely so this stays true.
/// COLLER EN TAPANT. Certains clients a distance (TigerVNC vers un Mac) ne
/// transmettent pas le presse-papiers Windows : Ctrl+V n'y colle rien. Le texte
/// est donc TAPE dans la fenetre en avant, par le meme chemin que l'autofill -
/// vraies touches pour un client VNC, dispositions basculees au besoin. Le
/// presse-papiers est celui de Windows : coffre ouvert ou non, rien ne passe
/// par la page. Pas de Ctrl+A + Suppr avant : on insere au curseur, comme un
/// collage.
procedure TPMBridge.PasteClipboardAsKeystrokes;
var
LTarget: HWND;
LData: THandle;
LPtr: PWideChar;
LText: string;
begin
LTarget := GetForegroundWindow;
if IsOwnProcessWindow(LTarget) then
begin
ShowBalloon('Nothing to paste into',
'Click where the text should go first, then press Paste.', True);
Exit;
end;
LText := '';
if OpenClipboard(0) then
try
LData := GetClipboardData(CF_UNICODETEXT);
if LData <> 0 then
begin
LPtr := GlobalLock(LData);
if LPtr <> nil then
try
LText := LPtr;
finally
GlobalUnlock(LData);
end;
end;
finally
CloseClipboard;
end;
// Un retour a la ligne final, tape dans un terminal, vaudrait Entree.
// Les sauts de ligne internes partent en #13 : Entree, comme un collage.
LText := LText.Replace(#13#10, #10).Replace(#13, #10).TrimRight([#10])
.Replace(#10, #13);
if LText = '' then
begin
// Une image ou un fichier copies ne sont pas du texte : le dire tel quel.
ShowBalloon('No text in the clipboard', 'Copy some text first, then press Paste.', True);
Exit;
end;
if not ExecuteAutofill(LTarget, '', LText, False, False, False) then
ShowBalloon('Paste blocked',
'The window did not accept the keystrokes (it may run as administrator).', True);
end;
procedure TPMBridge.FireAutofillRequest(AKind: TAutofillKind; procedure TPMBridge.FireAutofillRequest(AKind: TAutofillKind;
AFromBar: Boolean = False); AFromBar: Boolean = False);
var var
@@ -1302,6 +1357,7 @@ begin
LAZY_ID_SEARCH: LGlyph := #$E721; // Search LAZY_ID_SEARCH: LGlyph := #$E721; // Search
LAZY_ID_OPEN: LGlyph := #$E8A7; // OpenInNewWindow LAZY_ID_OPEN: LGlyph := #$E8A7; // OpenInNewWindow
LAZY_ID_SETTINGS: LGlyph := #$E713; // Settings (roue) LAZY_ID_SETTINGS: LGlyph := #$E713; // Settings (roue)
LAZY_ID_PASTE: LGlyph := #$E77F; // Paste
else else
LGlyph := #$E711; // Cancel (X) LGlyph := #$E711; // Cancel (X)
end; end;
@@ -1505,6 +1561,7 @@ begin
LAZY_ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore(); LAZY_ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore();
LAZY_ID_ADD: FireNewEntryRequest; LAZY_ID_ADD: FireNewEntryRequest;
LAZY_ID_SEARCH: FireQuickSearchRequest; LAZY_ID_SEARCH: FireQuickSearchRequest;
LAZY_ID_PASTE: PasteClipboardAsKeystrokes;
LAZY_ID_SETTINGS: LAZY_ID_SETTINGS:
begin begin
if Assigned(FOnTrayRestore) then FOnTrayRestore(); if Assigned(FOnTrayRestore) then FOnTrayRestore();
@@ -1669,6 +1726,7 @@ begin
AddButton('full', LAZY_ID_FULL, 'Full', 'Fill username and password', 56); AddButton('full', LAZY_ID_FULL, 'Full', 'Fill username and password', 56);
AddButton('pwd', LAZY_ID_PWD, 'Password', 'Fill the password only', 76); AddButton('pwd', LAZY_ID_PWD, 'Password', 'Fill the password only', 76);
AddButton('totp', LAZY_ID_TOTP, '2FA', 'Fill the 2FA code', 52); AddButton('totp', LAZY_ID_TOTP, '2FA', 'Fill the 2FA code', 52);
AddButton('paste', LAZY_ID_PASTE, 'Paste', 'Type the Windows clipboard into the current window', 52);
AddButton('add', LAZY_ID_ADD, 'Add', 'New entry from the current window', 48); AddButton('add', LAZY_ID_ADD, 'Add', 'New entry from the current window', 48);
AddButton('search', LAZY_ID_SEARCH, 'Search', 'Quick search', 60); AddButton('search', LAZY_ID_SEARCH, 'Search', 'Quick search', 60);
// Meme raison que le X : quand la barrette est allumee, elle est souvent la // Meme raison que le X : quand la barrette est allumee, elle est souvent la
@@ -2076,6 +2134,157 @@ begin
SendInput(LCount, @LInputs[0], SizeOf(TInput)); SendInput(LCount, @LInputs[0], SizeOf(TInput));
end; end;
function QueryFullProcessImageNameW(hProcess: THandle; dwFlags: DWORD;
lpExeName: PWideChar; var lpdwSize: DWORD): BOOL; stdcall;
external kernel32 name 'QueryFullProcessImageNameW';
/// UN VISUALISEUR VNC NE SAIT PAS TAPER DE L'UNICODE. SendUnicodeString envoie
/// chaque caractere en VK_PACKET ; une fenetre Windows le comprend, mais VNC
/// (TigerVNC, RealVNC, TightVNC, UltraVNC) traduit les VRAIES touches vers la
/// machine distante et ne sait que faire de VK_PACKET : un Mac recevait
/// "VVVVVV", un V par caractere. Pour lui on tape les touches du clavier.
/// ponytail: reconnu au nom de l'exe ("vnc") ; un autre client a distance qui
/// aurait le meme defaut s'ajouterait ici, ou deviendrait un reglage.
function TargetTypesByKeyCodes(AHwnd: HWND): Boolean;
const
PROCESS_QUERY_LIMITED_INFORMATION = $1000;
var
LPid, LLen: DWORD;
LProc: THandle;
LName: array[0..MAX_PATH] of WideChar;
begin
Result := False;
LPid := 0;
GetWindowThreadProcessId(AHwnd, LPid);
if LPid = 0 then Exit;
LProc := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, LPid);
if LProc = 0 then Exit;
try
LLen := Length(LName);
if QueryFullProcessImageNameW(LProc, 0, @LName[0], LLen) then
Result := ExtractFileName(string(LName)).ToLower.Contains('vnc');
finally
CloseHandle(LProc);
end;
end;
/// Le texte en VRAIES touches, dans la disposition clavier de la cible : chaque
/// caractere devient sa touche plus Maj / Ctrl / Alt selon VkKeyScanEx, avec
/// son scancode (certains clients VNC lisent la position physique).
/// ALTGR = Ctrl GAUCHE + Alt DROIT (touche etendue), comme le clavier le
/// produit : un Ctrl+Alt generique etait lu par TigerVNC comme deux
/// modificateurs ordinaires, et "@" (AltGr+0 en AZERTY) arrivait en "a".
/// TEXTE MELANGE (arabe + latin) : un caractere sans touche dans la
/// disposition courante est cherche dans les AUTRES dispositions installees
/// sur le poste ; on y bascule la fenetre cible (TigerVNC traduit chaque touche
/// avec la disposition de SON fil), on tape, et la disposition d'origine est
/// rendue a la fin. Seul un caractere qu'aucune disposition installee ne sait
/// taper est saute et compte - la copie, que TigerVNC synchronise, reste alors
/// le recours. Rend le nombre de sautes.
function SwitchTargetLayout(ATarget: HWND; ALayout: HKL): Boolean;
var
LThread: DWORD;
begin
LThread := GetWindowThreadProcessId(ATarget, nil);
PostMessage(ATarget, WM_INPUTLANGCHANGEREQUEST, 0, LPARAM(ALayout));
// La bascule est asynchrone : on attend qu'elle soit faite avant de taper,
// sinon les premieres touches partiraient encore dans l'ancienne.
for var I := 1 to 30 do
begin
if GetKeyboardLayout(LThread) = ALayout then Exit(True);
Sleep(10);
end;
Result := GetKeyboardLayout(LThread) = ALayout;
end;
function LayoutForChar(ACh: WideChar; ACurrent: HKL): HKL;
var
LList: array[0..31] of HKL;
N, I: Integer;
begin
Result := 0;
N := GetKeyboardLayoutList(Length(LList), LList);
for I := 0 to N - 1 do
if (LList[I] <> ACurrent) and (VkKeyScanEx(ACh, LList[I]) <> -1) then
Exit(LList[I]);
end;
function SendKeyCodeString(const AText: string; ATarget: HWND; ALayout: HKL): Integer;
var
LInputs: TArray<TInput>;
LCount: Integer;
procedure Key(AVk: Word; AUp: Boolean; AExtended: Boolean = False);
begin
if LCount >= Length(LInputs) then SetLength(LInputs, LCount + 16);
FillChar(LInputs[LCount], SizeOf(TInput), 0);
LInputs[LCount].Itype := INPUT_KEYBOARD;
LInputs[LCount].ki.wVk := AVk;
LInputs[LCount].ki.wScan := MapVirtualKeyEx(AVk, 0 {MAPVK_VK_TO_VSC}, ALayout);
if AUp then LInputs[LCount].ki.dwFlags := KEYEVENTF_KEYUP;
if AExtended then
LInputs[LCount].ki.dwFlags := LInputs[LCount].ki.dwFlags or KEYEVENTF_EXTENDEDKEY;
Inc(LCount);
end;
var
I: Integer;
LScan: SHORT;
LVk, LMods: Byte;
LShift, LCtrl, LAlt: Boolean;
begin
Result := 0;
var LOriginal := ALayout;
try
for I := 1 to Length(AText) do
begin
LScan := VkKeyScanEx(AText[I], ALayout);
if LScan = -1 then
begin
var LOther := LayoutForChar(AText[I], ALayout);
if (LOther <> 0) and SwitchTargetLayout(ATarget, LOther) then
begin
ALayout := LOther;
LScan := VkKeyScanEx(AText[I], ALayout);
end;
end;
if LScan = -1 then
begin
Inc(Result);
Continue;
end;
LVk := Lo(Word(LScan));
LMods := Hi(Word(LScan));
LShift := LMods and 1 <> 0;
LCtrl := LMods and 2 <> 0;
LAlt := LMods and 4 <> 0;
LCount := 0;
if LShift then Key(VK_LSHIFT, False);
if LCtrl then Key(VK_LCONTROL, False);
// Alt seul = Alt gauche ; avec Ctrl, c'est AltGr = Alt droit, etendu.
if LAlt and LCtrl then Key(VK_RMENU, False, True)
else if LAlt then Key(VK_LMENU, False);
Key(LVk, False);
Key(LVk, True);
if LAlt and LCtrl then Key(VK_RMENU, True, True)
else if LAlt then Key(VK_LMENU, True);
if LCtrl then Key(VK_LCONTROL, True);
if LShift then Key(VK_LSHIFT, True);
SendInput(LCount, @LInputs[0], SizeOf(TInput));
end;
finally
// Rendre a l'utilisateur la disposition qu'il avait choisie.
if ALayout <> LOriginal then SwitchTargetLayout(ATarget, LOriginal);
end;
end;
/// Rend le nombre de caracteres qui n'ont pas pu etre tapes (VNC seulement).
function SendTextTo(const AText: string; AByKeys: Boolean; ATarget: HWND;
ALayout: HKL): Integer;
begin
Result := 0;
if AByKeys then Result := SendKeyCodeString(AText, ATarget, ALayout)
else SendUnicodeString(AText);
end;
// Send one virtual-key press (down+up). // Send one virtual-key press (down+up).
procedure SendVKey(AVk: Word); procedure SendVKey(AVk: Word);
var var
@@ -2290,8 +2499,12 @@ begin
LSw := TStopwatch.StartNew; LSw := TStopwatch.StartNew;
WaitForModifierRelease(1000); WaitForModifierRelease(1000);
ForceReleaseModifiers; // timeout hit with keys still down → clean state ForceReleaseModifiers; // timeout hit with keys still down → clean state
var LByKeys := (ATargetHWND <> 0) and TargetTypesByKeyCodes(ATargetHWND);
var LLayout := GetKeyboardLayout(GetWindowThreadProcessId(ATargetHWND, nil));
FLastFillWaits := Format('foreground=%d ms, modifiers=%d ms', FLastFillWaits := Format('foreground=%d ms, modifiers=%d ms',
[LFgMs, LSw.ElapsedMilliseconds]); [LFgMs, LSw.ElapsedMilliseconds]);
if LByKeys then FLastFillWaits := FLastFillWaits + ', typed as key codes (VNC)';
var LMissing := 0;
Sleep(FocusSettleDelayMs); Sleep(FocusSettleDelayMs);
// Never type into our own window: if the target refused the foreground, // Never type into our own window: if the target refused the foreground,
@@ -2315,19 +2528,19 @@ begin
if AUsernameOnly then if AUsernameOnly then
begin begin
ClearFieldIfWanted; ClearFieldIfWanted;
SendUnicodeString(AUsername); Inc(LMissing, SendTextTo(AUsername, LByKeys, ATargetHWND, LLayout));
Exit; Exit;
end; end;
if AUsername = '' then if AUsername = '' then
begin begin
ClearFieldIfWanted; ClearFieldIfWanted;
SendUnicodeString(APassword); Inc(LMissing, SendTextTo(APassword, LByKeys, ATargetHWND, LLayout));
Exit; Exit;
end; end;
ClearFieldIfWanted; ClearFieldIfWanted;
SendUnicodeString(AUsername); Inc(LMissing, SendTextTo(AUsername, LByKeys, ATargetHWND, LLayout));
Sleep(200); Sleep(200);
// Real-VK send: a physically held Shift here turns Tab into Shift+Tab — // Real-VK send: a physically held Shift here turns Tab into Shift+Tab —
// focus moves BACKWARD and the password lands in the username field. // focus moves BACKWARD and the password lands in the username field.
@@ -2339,8 +2552,12 @@ begin
// Tab lands somewhere that is not an editable field (a link, a reveal // Tab lands somewhere that is not an editable field (a link, a reveal
// button, the document itself, as on the Reddit login form) it selects the // button, the document itself, as on the Reddit login form) it selects the
// ENTIRE PAGE, and the password is then typed into nothing. // ENTIRE PAGE, and the password is then typed into nothing.
SendUnicodeString(APassword); Inc(LMissing, SendTextTo(APassword, LByKeys, ATargetHWND, LLayout));
finally finally
if LMissing > 0 then
FLastFillWaits := FLastFillWaits + Format(
', %d character(s) have no key on this keyboard and were NOT typed - copy instead',
[LMissing]);
// Re-register with the LIVE combo (user may have rebound it), not the // Re-register with the LIVE combo (user may have rebound it), not the
// hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A. // hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A.
if FNewEntryHotkeyRegistered then if FNewEntryHotkeyRegistered then
+1
View File
@@ -768,6 +768,7 @@
<label><input type="checkbox" class="fb-btn" data-key="full"> Full</label> <label><input type="checkbox" class="fb-btn" data-key="full"> Full</label>
<label><input type="checkbox" class="fb-btn" data-key="pwd"> Password</label> <label><input type="checkbox" class="fb-btn" data-key="pwd"> Password</label>
<label><input type="checkbox" class="fb-btn" data-key="totp"> 2FA</label> <label><input type="checkbox" class="fb-btn" data-key="totp"> 2FA</label>
<label><input type="checkbox" class="fb-btn" data-key="paste"> Paste (types the clipboard)</label>
<label><input type="checkbox" class="fb-btn" data-key="add"> Add current window</label> <label><input type="checkbox" class="fb-btn" data-key="add"> Add current window</label>
<label><input type="checkbox" class="fb-btn" data-key="search"> Search</label> <label><input type="checkbox" class="fb-btn" data-key="search"> Search</label>
<label><input type="checkbox" class="fb-btn" data-key="open"> Open</label> <label><input type="checkbox" class="fb-btn" data-key="open"> Open</label>
+14 -3
View File
@@ -4038,15 +4038,26 @@ const ENTRY_TEMPLATES = {
// DECOUPE D'UNE NOTE DE COMMANDES. "# titre" ouvre un groupe, toute autre ligne // DECOUPE D'UNE NOTE DE COMMANDES. "# titre" ouvre un groupe, toute autre ligne
// non vide est une commande, les lignes vides sont ignorees. Rend // non vide est une commande, les lignes vides sont ignorees. Rend
// [{group, cmd}] dans l'ordre du texte. Pur : tests/commands.test.js. // [{group, cmd}] dans l'ordre du texte. Pur : tests/commands.test.js.
// LIGNE CONTINUEE : une ligne finie par \ (bash), ` (PowerShell) ou ^ (cmd)
// se recolle a la suivante, sans le caractere ni le retour a la ligne. Le
// resultat tient sur UNE ligne : tapee dans un terminal, une fin de ligne vaut
// Entree et lancerait une commande a moitie ecrite. Une ligne vide ou la fin du
// texte termine la commande en cours.
function parseCommandNote(text) { function parseCommandNote(text) {
const out = []; const out = [];
let group = ''; let group = '';
let pending = '';
const flush = () => { if (pending) out.push({ group, cmd: pending }); pending = ''; };
for (const raw of String(text || '').split(/\r?\n/)) { for (const raw of String(text || '').split(/\r?\n/)) {
const line = raw.trim(); const line = raw.trim();
if (!line) continue; if (!line) { flush(); continue; }
if (line.startsWith('#')) { group = line.replace(/^#+\s*/, ''); continue; } if (!pending && line.startsWith('#')) { group = line.replace(/^#+\s*/, ''); continue; }
out.push({ group, cmd: line }); const cont = /[\\`^]$/.test(line);
const part = cont ? line.slice(0, -1).trimEnd() : line;
pending = pending ? pending + ' ' + part : part;
if (!cont) flush();
} }
flush();
return out; return out;
} }
+29
View File
@@ -36,6 +36,35 @@ test('masque : fin courte masquee en entier, pas d\'espace', () => {
assert.equal(T.maskCommand(''), ''); assert.equal(T.maskCommand(''), '');
}); });
test('lignes continuees (\\, `, ^) : une seule commande, sur une ligne', () => {
const note = [
'# D',
'docker run -d \\',
' -p 8080:80 \\',
' nginx',
'Get-Item a `',
' -Force',
'dir ^',
' /s',
].join('\n');
const r = T.parseCommandNote(note);
assert.deepEqual(plain(r).map(x => x.cmd), [
'docker run -d -p 8080:80 nginx',
'Get-Item a -Force',
'dir /s',
]);
assert.equal(r[0].group, 'D');
});
test('continuation coupee par une ligne vide ou la fin du texte', () => {
assert.deepEqual(plain(T.parseCommandNote('a \\\n\nb')).map(x => x.cmd), ['a', 'b']);
assert.deepEqual(plain(T.parseCommandNote('a \\')).map(x => x.cmd), ['a']);
});
test('un "#" apres une continuation fait partie de la commande', () => {
assert.equal(T.parseCommandNote('echo \\\n#x')[0].cmd, 'echo #x');
});
test('texte vide ou absent', () => { test('texte vide ou absent', () => {
assert.equal(T.parseCommandNote('').length, 0); assert.equal(T.parseCommandNote('').length, 0);
assert.equal(T.parseCommandNote(null).length, 0); assert.equal(T.parseCommandNote(null).length, 0);
+8
View File
@@ -2124,6 +2124,14 @@ begin
if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then
CopyToClipboard(LValue, LCaption); CopyToClipboard(LValue, LCaption);
end end
// Une commande d'une note de commandes, par son rang dans la liste.
else if Param('f') = 'cmd' then
begin
var LCmds := ParseCommandNote(FCurrent.Password);
var LK := StrToIntDef(Param('k'), -1);
if (LK >= 0) and (LK <= High(LCmds)) then
CopyToClipboard(LCmds[LK].Cmd, 'Command');
end
else else
begin begin
LIdx := StrToIntDef(Param('i'), -1); LIdx := StrToIntDef(Param('i'), -1);