From ee7bf5ed58c8637f3cd1a8c477435850c9c337eb Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Fri, 21 Aug 2026 18:53:57 +0200 Subject: [PATCH] feat: migliora l'interfaccia e l'avvio di Pi da KDE --- pi-session-launcher.py | 113 ++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 25 deletions(-) diff --git a/pi-session-launcher.py b/pi-session-launcher.py index 2ceffee..c4c77f4 100755 --- a/pi-session-launcher.py +++ b/pi-session-launcher.py @@ -12,6 +12,7 @@ GUI (GTK3) che elenca tutte le sessioni salvate di Pi Agent Requisiti: python3-gi (GTK3), konsole (o xterm come fallback), pi installato. """ +import glob import json import os import re @@ -216,6 +217,22 @@ def display_text(info: dict, limit: int = 100) -> str: return first or "(sessione senza messaggi)" +def session_markup(info: dict) -> str: + """Titolo leggibile, con anteprima secondaria per le sessioni rinominate.""" + title = GLib.markup_escape_text(display_text(info)) + if not info["name"]: + return title + preview = info["first"].replace("\n", " ").strip() + if not preview or preview == info["name"]: + return f"{title}" + if len(preview) > 96: + preview = preview[:95] + "…" + return ( + f"{title}\n" + f"{GLib.markup_escape_text(preview)}" + ) + + def icon_path(): """Percorso dell'icona: dati bundlati (PyInstaller) o cartella assets del sorgente.""" base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) @@ -245,37 +262,45 @@ class LauncherWindow(Gtk.ApplicationWindow): self.store = Gtk.ListStore(str, str, str, str, float, str, int, str) self.by_path = {} - self.search_entry = Gtk.SearchEntry(placeholder_text="Cerca sessione o cartella…") + self.search_entry = Gtk.SearchEntry( + placeholder_text="Cerca per nome, messaggio o cartella…" + ) + self.search_entry.set_tooltip_text("Filtra le sessioni mentre scrivi") self.search_entry.connect("search-changed", self._on_search_changed) - # vista lista + # vista lista: righe ariose, griglia orizzontale e metadati attenuati. self.filter = self.store.filter_new() self.filter.set_visible_func(self._filter_visible) self.tree = Gtk.TreeView(model=self.filter) self.tree.set_search_column(0) self.tree.set_headers_clickable(True) + self.tree.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL) - rend = Gtk.CellRendererText(ellipsize="end") + rend = Gtk.CellRendererText(ellipsize="end", ypad=4) col = Gtk.TreeViewColumn("Sessione", rend, markup=0) col.set_expand(True) - col.set_min_width(420) + col.set_min_width(470) col.set_sort_column_id(7) self.tree.append_column(col) - rend2 = Gtk.CellRendererText(ellipsize="middle") - col2 = Gtk.TreeViewColumn("Cartella", rend2, text=1) + rend2 = Gtk.CellRendererText(ellipsize="middle", ypad=4) + rend2.set_property("foreground", "#9aa0aa") + col2 = Gtk.TreeViewColumn("Cartella di lavoro", rend2, text=1) col2.set_expand(True) col2.set_min_width(280) col2.set_sort_column_id(1) self.tree.append_column(col2) - rend3 = Gtk.CellRendererText() + rend3 = Gtk.CellRendererText(ypad=4) + rend3.set_property("foreground", "#9aa0aa") col3 = Gtk.TreeViewColumn("Modificata", rend3, text=2) col3.set_sort_column_id(4) col3.set_alignment(1.0) self.tree.append_column(col3) - self.tree.get_selection().set_mode(Gtk.SelectionMode.SINGLE) + selection = self.tree.get_selection() + selection.set_mode(Gtk.SelectionMode.SINGLE) + selection.connect("changed", lambda *_: self._update_status()) self.tree.connect("row-activated", self._on_row_activated) self.tree.connect("key-press-event", self._on_key_press) @@ -283,17 +308,23 @@ class LauncherWindow(Gtk.ApplicationWindow): scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled.add(self.tree) - # barra pulsanti - btn_refresh = Gtk.Button(label="Aggiorna", tooltip_text="Ricarica l'elenco (Ctrl+R)") + # toolbar: azioni secondarie a sinistra, ripresa evidenziata a destra. + def button(label, icon_name, tooltip): + b = Gtk.Button(label=label, tooltip_text=tooltip) + b.set_image(Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)) + b.set_always_show_image(True) + return b + + btn_refresh = button("Aggiorna", "view-refresh-symbolic", "Ricarica l'elenco (Ctrl+R)") btn_refresh.connect("clicked", lambda *_: self.refresh()) - btn_open_dir = Gtk.Button(label="Apri cartella", tooltip_text="Apre la cartella della sessione in Dolphin") + btn_open_dir = button("Apri cartella", "folder-open-symbolic", "Apre la cartella della sessione in Dolphin") btn_open_dir.connect("clicked", lambda *_: self.open_folder()) - btn_rename = Gtk.Button(label="Rinomina", tooltip_text="Rinomina la sessione selezionata (F2)") + btn_rename = button("Rinomina", "edit-rename-symbolic", "Rinomina la sessione selezionata (F2)") btn_rename.connect("clicked", lambda *_: self.rename()) - btn_launch = Gtk.Button(label="Riprendi sessione", tooltip_text="Apre Konsole nella cartella e riprende la sessione (Invio)") + btn_launch = button("Riprendi sessione", "media-playback-start-symbolic", "Apre Konsole nella cartella e riprende la sessione (Invio)") btn_launch.get_style_context().add_class("suggested-action") btn_launch.connect("clicked", lambda *_: self.launch()) - btn_quit = Gtk.Button(label="Esci") + btn_quit = button("Esci", "application-exit-symbolic", "Chiude il launcher") btn_quit.connect("clicked", lambda *_: self.close()) hbox = Gtk.Box(spacing=8) @@ -331,10 +362,7 @@ class LauncherWindow(Gtk.ApplicationWindow): self.by_path.clear() for s in sessions: text = display_text(s) - markup = GLib.markup_escape_text(text) - if s["name"]: - markup = f"{markup}" - self.store.append([markup, s["cwd"], fmt_relative(s["mtime"]), + self.store.append([session_markup(s), s["cwd"], fmt_relative(s["mtime"]), fmt_absolute(s["mtime"]), s["mtime"], s["path"], s["size"], text.lower()]) self.by_path[s["path"]] = s if selected_path in self.by_path: @@ -343,9 +371,17 @@ class LauncherWindow(Gtk.ApplicationWindow): def _update_status(self): n = len(self.store) - self.status.set_text( - f"{n} session{'i' if n != 1 else 'e'} · {SESSIONS_DIR}" - ) + selected = self.selected_path() + if selected: + info = self.by_path.get(selected, {}) + label = display_text(info) if info else os.path.basename(selected) + self.status.set_text( + f"{n} session{'i' if n != 1 else 'e'} · Selezionata: {label}" + ) + else: + self.status.set_text( + f"{n} session{'i' if n != 1 else 'e'} · {SESSIONS_DIR}" + ) # -- selezione -------------------------------------------------------- @@ -406,26 +442,53 @@ class LauncherWindow(Gtk.ApplicationWindow): if not os.path.isdir(cwd): cwd = os.path.expanduser("~") + # Le app avviate dal menu KDE non ereditano ~/.bashrc: individua Pi e + # antepone la sua directory a PATH, così il suo shebang usa il Node.js + # compatibile incluso in ~/.local/share/pi-node/ invece del Node di sistema. + pi_bin = self._find_pi() + if not pi_bin: + self._flash_status("Pi Agent non trovato: installalo oppure aggiungilo al PATH.") + return + child_env = os.environ.copy() + pi_dir = os.path.dirname(pi_bin) + child_env["PATH"] = pi_dir + os.pathsep + child_env.get("PATH", "") + konsole = shutil.which("konsole") if konsole: # wrapper bash: più affidabile del passaggio diretto di -e a Konsole - # ("exec bash" mantiene la shell aperta quando pi esce) + # ("exec bash" mantiene la shell aperta quando Pi esce) cmd = [konsole, "--workdir", cwd, "--hold", "--separate", - "-e", "bash", "-c", 'pi --session "$1"; exec bash', "_", path] + "-e", "bash", "-c", '"$1" --session "$2"; exec bash', "_", pi_bin, path] else: term = shutil.which("xterm") if not term: self._flash_status("Nessun terminale trovato (installa konsole o xterm).") return - cmd = [term, "-hold", "-e", "pi", "--session", path] + cmd = [term, "-hold", "-e", pi_bin, "--session", path] try: - subprocess.Popen(cmd, start_new_session=True) + subprocess.Popen(cmd, start_new_session=True, env=child_env) except OSError as e: self._flash_status(f"Impossibile aprire il terminale: {e}") return self._flash_status(f"Avviato: pi --session {os.path.basename(path)} in {cwd}") + @staticmethod + def _find_pi(): + """Trova Pi privilegiando il runtime portable con il suo Node.js.""" + candidates = sorted( + glob.glob(os.path.expanduser("~/.local/share/pi-node/node-*/bin/pi")), + reverse=True, + ) + found_on_path = shutil.which("pi") + if found_on_path: + candidates.append(found_on_path) + for candidate in candidates: + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + # Conserva il symlink bin/pi: il suo parent contiene anche node. + return os.path.abspath(candidate) + return None + def rename(self): path = self.selected_path() if not path: