feat: migliora l'interfaccia e l'avvio di Pi da KDE

This commit is contained in:
Matteo Benedetto
2026-08-21 18:53:57 +02:00
parent 6cf2c9ea80
commit ee7bf5ed58
+85 -22
View File
@@ -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. Requisiti: python3-gi (GTK3), konsole (o xterm come fallback), pi installato.
""" """
import glob
import json import json
import os import os
import re import re
@@ -216,6 +217,22 @@ def display_text(info: dict, limit: int = 100) -> str:
return first or "(sessione senza messaggi)" 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"<b>{title}</b>"
if len(preview) > 96:
preview = preview[:95] + ""
return (
f"<b>{title}</b>\n"
f"<span size='small' foreground='#9aa0aa'>{GLib.markup_escape_text(preview)}</span>"
)
def icon_path(): def icon_path():
"""Percorso dell'icona: dati bundlati (PyInstaller) o cartella assets del sorgente.""" """Percorso dell'icona: dati bundlati (PyInstaller) o cartella assets del sorgente."""
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) 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.store = Gtk.ListStore(str, str, str, str, float, str, int, str)
self.by_path = {} 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) 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 = self.store.filter_new()
self.filter.set_visible_func(self._filter_visible) self.filter.set_visible_func(self._filter_visible)
self.tree = Gtk.TreeView(model=self.filter) self.tree = Gtk.TreeView(model=self.filter)
self.tree.set_search_column(0) self.tree.set_search_column(0)
self.tree.set_headers_clickable(True) 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 = Gtk.TreeViewColumn("Sessione", rend, markup=0)
col.set_expand(True) col.set_expand(True)
col.set_min_width(420) col.set_min_width(470)
col.set_sort_column_id(7) col.set_sort_column_id(7)
self.tree.append_column(col) self.tree.append_column(col)
rend2 = Gtk.CellRendererText(ellipsize="middle") rend2 = Gtk.CellRendererText(ellipsize="middle", ypad=4)
col2 = Gtk.TreeViewColumn("Cartella", rend2, text=1) rend2.set_property("foreground", "#9aa0aa")
col2 = Gtk.TreeViewColumn("Cartella di lavoro", rend2, text=1)
col2.set_expand(True) col2.set_expand(True)
col2.set_min_width(280) col2.set_min_width(280)
col2.set_sort_column_id(1) col2.set_sort_column_id(1)
self.tree.append_column(col2) 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 = Gtk.TreeViewColumn("Modificata", rend3, text=2)
col3.set_sort_column_id(4) col3.set_sort_column_id(4)
col3.set_alignment(1.0) col3.set_alignment(1.0)
self.tree.append_column(col3) 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("row-activated", self._on_row_activated)
self.tree.connect("key-press-event", self._on_key_press) 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.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled.add(self.tree) scrolled.add(self.tree)
# barra pulsanti # toolbar: azioni secondarie a sinistra, ripresa evidenziata a destra.
btn_refresh = Gtk.Button(label="Aggiorna", tooltip_text="Ricarica l'elenco (Ctrl+R)") 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_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_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_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.get_style_context().add_class("suggested-action")
btn_launch.connect("clicked", lambda *_: self.launch()) 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()) btn_quit.connect("clicked", lambda *_: self.close())
hbox = Gtk.Box(spacing=8) hbox = Gtk.Box(spacing=8)
@@ -331,10 +362,7 @@ class LauncherWindow(Gtk.ApplicationWindow):
self.by_path.clear() self.by_path.clear()
for s in sessions: for s in sessions:
text = display_text(s) text = display_text(s)
markup = GLib.markup_escape_text(text) self.store.append([session_markup(s), s["cwd"], fmt_relative(s["mtime"]),
if s["name"]:
markup = f"<b>{markup}</b>"
self.store.append([markup, s["cwd"], fmt_relative(s["mtime"]),
fmt_absolute(s["mtime"]), s["mtime"], s["path"], s["size"], text.lower()]) fmt_absolute(s["mtime"]), s["mtime"], s["path"], s["size"], text.lower()])
self.by_path[s["path"]] = s self.by_path[s["path"]] = s
if selected_path in self.by_path: if selected_path in self.by_path:
@@ -343,6 +371,14 @@ class LauncherWindow(Gtk.ApplicationWindow):
def _update_status(self): def _update_status(self):
n = len(self.store) n = len(self.store)
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( self.status.set_text(
f"{n} session{'i' if n != 1 else 'e'} · {SESSIONS_DIR}" f"{n} session{'i' if n != 1 else 'e'} · {SESSIONS_DIR}"
) )
@@ -406,26 +442,53 @@ class LauncherWindow(Gtk.ApplicationWindow):
if not os.path.isdir(cwd): if not os.path.isdir(cwd):
cwd = os.path.expanduser("~") 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") konsole = shutil.which("konsole")
if konsole: if konsole:
# wrapper bash: più affidabile del passaggio diretto di -e a 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", 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: else:
term = shutil.which("xterm") term = shutil.which("xterm")
if not term: if not term:
self._flash_status("Nessun terminale trovato (installa konsole o xterm).") self._flash_status("Nessun terminale trovato (installa konsole o xterm).")
return return
cmd = [term, "-hold", "-e", "pi", "--session", path] cmd = [term, "-hold", "-e", pi_bin, "--session", path]
try: try:
subprocess.Popen(cmd, start_new_session=True) subprocess.Popen(cmd, start_new_session=True, env=child_env)
except OSError as e: except OSError as e:
self._flash_status(f"Impossibile aprire il terminale: {e}") self._flash_status(f"Impossibile aprire il terminale: {e}")
return return
self._flash_status(f"Avviato: pi --session {os.path.basename(path)} in {cwd}") 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): def rename(self):
path = self.selected_path() path = self.selected_path()
if not path: if not path: