This commit is contained in:
2025-12-07 16:53:36 +01:00
parent 4bcc5bc9b2
commit a55d5cca65
6 changed files with 482 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
// dbRepository.js
// Couche daccès aux données pour lextension.
// Utilise le helper Python password_db.py pour manipuler SQLite.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
const Me = ExtensionUtils.getCurrentExtension();
const APP_DATA_DIR = GLib.build_filenamev([
GLib.get_home_dir(),
'.local', 'share', 'gnome-shell', 'password-applet'
]);
export class PasswordRepository {
constructor() {
this._helperPath = Me.dir.get_child('password_db.py').get_path();
}
init() {
if (!GLib.file_test(APP_DATA_DIR, GLib.FileTest.IS_DIR)) {
GLib.mkdir_with_parents(APP_DATA_DIR, 0o700);
}
this._runHelperSync(['init']);
}
listEntries() {
const stdout = this._runHelperSync(['list']);
if (!stdout)
return [];
try {
return JSON.parse(stdout);
} catch (e) {
logError(e, 'Erreur JSON listEntries');
return [];
}
}
addEntry(entry) {
const payload = JSON.stringify(entry);
this._runHelperSync(['add'], payload);
}
_runHelperSync(args, stdinText = '') {
const argv = ['python3', this._helperPath, ...args];
const proc = new Gio.Subprocess({
argv,
flags: Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_PIPE,
});
try {
proc.init(null);
} catch (e) {
logError(e, 'Impossible de lancer password_db.py');
return '';
}
const [ok, stdout, stderr] = proc.communicate_utf8(stdinText, null);
if (!ok || !proc.get_successful()) {
log(`password_db.py error: ${stderr}`);
return '';
}
return stdout.trim();
}
}

View File

@@ -0,0 +1,32 @@
// extension.js
// Point dentrée de lextension GNOME Shell Password Applet.
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
import { PasswordRepository } from './dbRepository.js';
import { PasswordApplet } from './uiComponents.js';
let applet;
let repository;
export function init() {
// Initialisation minimale
}
export function enable() {
repository = new PasswordRepository();
repository.init();
applet = new PasswordApplet(repository);
Main.panel.addToStatusArea('password-applet', applet, 1, 'right');
}
export function disable() {
if (applet) {
applet.destroy();
applet = null;
}
repository = null;
}

View File

@@ -0,0 +1,9 @@
{
"uuid": "password-applet@gilles",
"name": "Password Applet",
"description": "Mini gestionnaire de mots de passe dans la barre supérieure.",
"shell-version": ["45", "46"],
"version": 1,
"settings-schema": "org.gnome.shell.extensions.password-applet",
"stylesheet": "stylesheet.css"
}

122
password-applet@gilles/password_db.py Normal file → Executable file
View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Helper SQLite pour lextension Password Applet.
Commandes :
- init : initialise la base
- list : renvoie les entrées
- add : ajoute une entrée
Note :
Le champ password_enc stocke le mot de passe en clair (MVP).
"""
import json
import os
import sqlite3
import sys
from datetime import datetime
APP_DATA_DIR = os.path.join(
os.path.expanduser('~'),
'.local', 'share', 'gnome-shell', 'password-applet'
)
DB_PATH = os.path.join(APP_DATA_DIR, 'passwords.db')
SCHEMA = """
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
username TEXT NOT NULL,
password_enc TEXT NOT NULL,
url TEXT,
notes TEXT,
is_favorite INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
"""
def ensure_db():
os.makedirs(APP_DATA_DIR, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
try:
conn.executescript(SCHEMA)
conn.commit()
finally:
conn.close()
def encrypt_password(plain: str) -> str:
return plain # Pour le MVP, rien
def cmd_init():
ensure_db()
def cmd_list():
ensure_db()
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
cur = conn.execute(
"SELECT id, label, username, password_enc, url, notes, "
"is_favorite, created_at, updated_at "
"FROM entries ORDER BY created_at DESC"
)
rows = [dict(row) for row in cur.fetchall()]
finally:
conn.close()
print(json.dumps(rows))
def cmd_add():
ensure_db()
raw = sys.stdin.read()
data = json.loads(raw or '{}')
label = (data.get('label') or '').strip()
username = (data.get('username') or '').strip()
password = (data.get('password') or '').strip()
url = (data.get('url') or '').strip() or None
notes = (data.get('notes') or '').strip() or None
if not label or not username or not password:
print('Missing required fields', file=sys.stderr)
sys.exit(1)
password_enc = encrypt_password(password)
now = datetime.utcnow().isoformat(timespec='seconds') + 'Z'
conn = sqlite3.connect(DB_PATH)
try:
conn.execute(
"INSERT INTO entries (label, username, password_enc, url, notes, "
"is_favorite, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?)",
(label, username, password_enc, url, notes, now, now)
)
conn.commit()
finally:
conn.close()
def main(argv):
if len(argv) < 2:
print("Usage: password_db.py [init|list|add]")
return 1
cmd = argv[1]
if cmd == "init":
cmd_init()
elif cmd == "list":
cmd_list()
elif cmd == "add":
cmd_add()
else:
print(f"Unknown command: {cmd}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))

View File

@@ -0,0 +1,70 @@
/* stylesheet.css */
/* Style du mini gestionnaire de mots de passe GNOME Shell */
/* Lien cliquable (URL présente) */
.password-applet-label-link {
color: #5da5ff;
}
/* Nom dutilisateur */
.password-applet-username {
margin-left: 8px;
}
/* Mot de passe masqué */
.password-applet-password {
margin-left: 8px;
font-family: monospace;
opacity: 0.7;
}
/* Boutons copier (User / Password) */
.password-applet-copy-btn {
margin-left: 6px;
padding: 0 6px;
border-radius: 6px;
background-color: #3a3a3a;
color: white;
}
.password-applet-copy-btn:hover {
background-color: #4d4d4d;
}
/* Champs de formulaire */
.password-applet-entry {
min-width: 220px;
padding: 4px;
border-radius: 6px;
background-color: rgba(255,255,255,0.08);
}
/* Bouton Enregistrer */
.password-applet-save-btn {
padding: 4px 12px;
margin-right: 6px;
background-color: #4caf50;
color: white;
border-radius: 6px;
}
.password-applet-save-btn:hover {
background-color: #45a047;
}
/* Bouton Annuler */
.password-applet-cancel-btn {
padding: 4px 12px;
background-color: #d9534f;
color: white;
border-radius: 6px;
}
.password-applet-cancel-btn:hover {
background-color: #c9302c;
}
/* Ligne + Formulaire */
.password-applet-form-row {
margin-bottom: 6px;
}

View File

@@ -0,0 +1,178 @@
// uiComponents.js
// UI du mini gestionnaire de mots de passe.
import St from 'gi://St';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
const Clutter = imports.gi.Clutter;
const Clipboard = St.Clipboard.get_default();
const ClipboardType = St.ClipboardType.CLIPBOARD;
function copyToClipboard(text) {
if (text)
Clipboard.set_text(ClipboardType, text);
}
export class PasswordEntryRow extends PopupMenu.PopupBaseMenuItem {
constructor(entry) {
super({ reactive: true });
const box = new St.BoxLayout({ vertical: false, x_expand: true });
let labelWidget;
if (entry.url) {
labelWidget = new St.Label({
text: entry.label,
style_class: 'password-applet-label-link',
x_expand: true
});
labelWidget.clutter_text.underline = true;
labelWidget.connect('button-press-event', () => {
Gio.AppInfo.launch_default_for_uri(entry.url, null);
});
} else {
labelWidget = new St.Label({ text: entry.label, x_expand: true });
}
box.add_child(labelWidget);
box.add_child(new St.Label({ text: entry.username, x_expand: true }));
box.add_child(new St.Label({ text: '••••••••' }));
const uBtn = new St.Button({ label: 'U' });
uBtn.connect('clicked', () => copyToClipboard(entry.username));
box.add_child(uBtn);
const pBtn = new St.Button({ label: 'P' });
pBtn.connect('clicked', () => copyToClipboard(entry.password_enc));
box.add_child(pBtn);
this.actor.add_child(box);
}
}
export class PasswordListSection extends PopupMenu.PopupMenuSection {
setEntries(entries) {
this.removeAll();
if (!entries.length) {
this.addMenuItem(new PopupMenu.PopupMenuItem(
'Aucune entrée', { reactive: false }
));
return;
}
for (const e of entries)
this.addMenuItem(new PasswordEntryRow(e));
}
}
export class AddEntrySection extends PopupMenu.PopupMenuSection {
constructor(repo, refreshCb) {
super();
this._repo = repo;
this._refreshCb = refreshCb;
this._showAddButton();
}
_showAddButton() {
this.removeAll();
const item = new PopupMenu.PopupMenuItem(' Ajouter une entrée');
item.connect('activate', () => this._showForm());
this.addMenuItem(item);
}
_showForm() {
this.removeAll();
const wrapper = new PopupMenu.PopupBaseMenuItem({ reactive: false });
const box = new St.BoxLayout({ vertical: true });
const mkEntry = hint =>
new St.Entry({ hint_text: hint, style_class: 'password-applet-entry' });
const label = mkEntry("Label");
const user = mkEntry("User");
const pwd = mkEntry("Password");
const url = mkEntry("URL");
const notes = mkEntry("Notes");
const mkRow = (name, widget) => {
const row = new St.BoxLayout();
row.add_child(new St.Label({ text: name + " : " }));
row.add_child(widget);
return row;
};
box.add_child(mkRow("Label", label));
box.add_child(mkRow("User", user));
box.add_child(mkRow("Password", pwd));
box.add_child(mkRow("URL", url));
box.add_child(mkRow("Notes", notes));
const btnRow = new St.BoxLayout();
const save = new St.Button({ label: "Enregistrer" });
save.connect('clicked', () => {
const entry = {
label: label.get_text().trim(),
username: user.get_text().trim(),
password: pwd.get_text().trim(),
url: url.get_text().trim(),
notes: notes.get_text().trim()
};
if (!entry.label || !entry.username || !entry.password) {
Main.notify("Password Applet", "Champs obligatoires manquants");
return;
}
this._repo.addEntry(entry);
this._refreshCb();
this._showAddButton();
});
const cancel = new St.Button({ label: "Annuler" });
cancel.connect('clicked', () => this._showAddButton());
btnRow.add_child(save);
btnRow.add_child(cancel);
box.add_child(btnRow);
wrapper.actor.add_child(box);
this.addMenuItem(wrapper);
}
}
export class PasswordApplet extends PanelMenu.Button {
constructor(repo) {
super(0.0, 'PasswordApplet', false);
this._repo = repo;
this.add_child(new St.Icon({
icon_name: 'dialog-password-symbolic',
style_class: 'system-status-icon'
}));
this._list = new PasswordListSection();
this.menu.addMenuItem(this._list);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._add = new AddEntrySection(repo, () => this._reload());
this.menu.addMenuItem(this._add);
this._reload();
}
_reload() {
const entries = this._repo.listEntries();
this._list.setEntries(entries);
}
}