Pi Session Launcher: GUI GTK3 per riprendere le sessioni di Pi Agent
- elenca sessioni da ~/.pi/agent/sessions/**/*.jsonl (cwd, primo messaggio, nome, ultima modifica) - ricerca/filtro, ordina per recente, nomi in grassetto - doppio clic/Invio: apre Konsole nella cwd della sessione con pi --session <file> - rinomina sessione (append session_info, ultima entry vince come in pi) - install.sh: voce menu KDE
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
.venv/
|
||||
@@ -0,0 +1,47 @@
|
||||
# Pi Session Launcher
|
||||
|
||||
Piccola applicazione GUI (GTK3) per **riprendere le sessioni di Pi Agent**.
|
||||
|
||||
Elenco tutte le sessioni salvate in `~/.pi/agent/sessions/` e, selezionandone una:
|
||||
|
||||
- apre un terminale (Konsole) **nella cartella di lavoro** (`cwd`) in cui la sessione era stata avviata;
|
||||
- riprende la sessione con `pi --session <file sessione>`.
|
||||
|
||||
## Requisiti
|
||||
|
||||
- Python 3 + PyGObject GTK3 (`python3-gi`, `gir1.2-gtk-3.0`)
|
||||
- Konsole (KDE) — fallback: `xterm`
|
||||
- `pi` nel PATH
|
||||
|
||||
## Installazione
|
||||
|
||||
```bash
|
||||
./install.sh # copia la voce nel menu applicazioni KDE (.desktop)
|
||||
```
|
||||
|
||||
Avvio manuale:
|
||||
|
||||
```bash
|
||||
python3 pi-session-launcher.py
|
||||
```
|
||||
|
||||
## Uso
|
||||
|
||||
| Azione | Come |
|
||||
|---|---|
|
||||
| Selezionare una sessione | clic sulla riga |
|
||||
| **Riprendere** la sessione | doppio clic, Invio o pulsante **Riprendi sessione** |
|
||||
| Cercare | digitare nel campo di ricerca (filtra per messaggio e cartella) |
|
||||
| Aprire la cartella della sessione | pulsante **Apri cartella** (Dolphin) |
|
||||
| Aggiornare l'elenco | pulsante **Aggiorna** o `Ctrl+R` |
|
||||
|
||||
Le sessioni **con nome** (`/name` in pi) sono mostrate in **grassetto**.
|
||||
|
||||
## Come funziona
|
||||
|
||||
- Ogni sessione è un file JSONL in `~/.pi/agent/sessions/--<percorso>--/<timestamp>_<uuid>.jsonl`.
|
||||
- L'header della prima riga contiene il campo `cwd` (cartella di avvio).
|
||||
- Il primo messaggio utente e il nome (`session_info`) vengono letti dal file.
|
||||
- Il lancio usa:
|
||||
`konsole --workdir <cwd> --hold --separate -e bash -c 'pi --session "$1"; exec bash' _ <file>`
|
||||
— `exec bash` mantiene la shell aperta quando pi termina.
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — Pi Session Launcher
|
||||
# Crea la voce nel menu applicazioni KDE (.desktop) e rende eseguibile lo script.
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
APP="$DIR/pi-session-launcher.py"
|
||||
DESKTOP="$HOME/.local/share/applications/pi-session-launcher.desktop"
|
||||
|
||||
# sintassi: ./install.sh [--uninstall]
|
||||
if [[ "${1:-}" == "--uninstall" ]]; then
|
||||
rm -f "$DESKTOP"
|
||||
echo "Rimossa la voce di menu: $DESKTOP"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
chmod +x "$APP"
|
||||
|
||||
mkdir -p "$(dirname "$DESKTOP")"
|
||||
cat > "$DESKTOP" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Pi Session Launcher
|
||||
GenericName=Riprendi sessioni di Pi Agent
|
||||
Comment=Elenca le sessioni di Pi Agent e le riprende nella loro cartella
|
||||
Exec=python3 $APP
|
||||
Icon=utilities-terminal
|
||||
Terminal=false
|
||||
Categories=Development;
|
||||
Keywords=pi;agent;session;resume;
|
||||
EOF
|
||||
|
||||
echo "✓ Installato: $DESKTOP"
|
||||
echo " Cerca 'Pi Session Launcher' nel menu applicazioni KDE."
|
||||
echo " (Per rimuovere: $0 --uninstall)"
|
||||
Executable
+483
@@ -0,0 +1,483 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Pi Session Launcher
|
||||
===================
|
||||
GUI (GTK3) che elenca tutte le sessioni salvate di Pi Agent
|
||||
(~/.pi/agent/sessions/**/*.jsonl) e, selezionandone una:
|
||||
|
||||
* apre un terminale (Konsole) nella cartella di lavoro della sessione (cwd)
|
||||
* riprende la sessione con: pi --session <file di sessione>
|
||||
|
||||
Requisiti: python3-gi (GTK3), konsole (o xterm come fallback), pi installato.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk, GLib, Gtk, Pango
|
||||
|
||||
APP_TITLE = "Pi Session Launcher"
|
||||
SESSIONS_DIR = os.path.expanduser("~/.pi/agent/sessions/")
|
||||
MAX_NAME_SCAN = 16 * 1024 * 1024 # oltre questa dimensione, scandisci solo inizio/fine
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lettura sessioni
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _decode(data: bytes) -> str:
|
||||
return data.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def _first_user_message(text: str) -> str:
|
||||
"""Cerca il primo messaggio con role=user nei primi chunk del file."""
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if entry.get("type") != "message":
|
||||
continue
|
||||
msg = entry.get("message") or {}
|
||||
if msg.get("role") != "user":
|
||||
continue
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
return " ".join(parts).strip()
|
||||
if isinstance(content, str):
|
||||
return content.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _session_name(full_text: str) -> str:
|
||||
"""Nome mostrato della sessione: come pi, l'ULTIMA entry session_info vince
|
||||
(nome vuoto cancella il titolo)."""
|
||||
name = ""
|
||||
for line in reversed(full_text.splitlines()):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if '"type":"session_info"' not in line and '"type": "session_info"' not in line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if entry.get("type") == "session_info":
|
||||
return entry.get("name") or ""
|
||||
return name
|
||||
|
||||
|
||||
def parse_session(path: str):
|
||||
"""Legge un file .jsonl di sessione e ne estrae le info utili."""
|
||||
try:
|
||||
stat = os.stat(path)
|
||||
except OSError:
|
||||
return None
|
||||
if stat.st_size == 0:
|
||||
return None
|
||||
|
||||
info = {
|
||||
"path": path,
|
||||
"cwd": "",
|
||||
"sid": "",
|
||||
"name": "",
|
||||
"first": "",
|
||||
"mtime": stat.st_mtime,
|
||||
"size": stat.st_size,
|
||||
}
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
header_raw = f.readline().decode("utf-8", "replace")
|
||||
header = json.loads(header_raw)
|
||||
if header.get("type") != "session":
|
||||
return None
|
||||
info["cwd"] = header.get("cwd", "") or ""
|
||||
info["sid"] = header.get("id", "") or ""
|
||||
|
||||
# primo messaggio utente: leggiamo i primi ~64 KB (righe iniziali)
|
||||
f.seek(0)
|
||||
head = _decode(f.read(65536))
|
||||
info["first"] = _first_user_message(head)
|
||||
|
||||
# nome sessione: scandisci tutto il file (con limite prudente)
|
||||
if stat.st_size <= MAX_NAME_SCAN:
|
||||
full = head + _decode(f.read())
|
||||
else:
|
||||
f.seek(max(0, stat.st_size - 1024 * 1024))
|
||||
tail = _decode(f.read())
|
||||
full = head + tail
|
||||
info["name"] = _session_name(full)
|
||||
except (OSError, json.JSONDecodeError, ValueError):
|
||||
return None
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def rename_session(path, new_name):
|
||||
"""Imposta (o cancella, con '') il nome visualizzato di una sessione,
|
||||
appendendo una entry session_info al file JSONL (stesso meccanismo di pi).
|
||||
pi usa il nome dell'ultima entry session_info presente nel file."""
|
||||
last_id = None
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
e = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if e.get("type") != "session" and e.get("id"):
|
||||
last_id = e["id"]
|
||||
entry = {
|
||||
"type": "session_info",
|
||||
"id": os.urandom(4).hex(),
|
||||
"parentId": last_id,
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
"name": new_name,
|
||||
}
|
||||
with open(path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
|
||||
|
||||
def scan_sessions():
|
||||
"""Elenca tutte le sessioni, ordinate per ultima modifica (più recente prima)."""
|
||||
sessions = []
|
||||
if not os.path.isdir(SESSIONS_DIR):
|
||||
return sessions
|
||||
for root, _dirs, files in os.walk(SESSIONS_DIR):
|
||||
for fn in files:
|
||||
if not fn.endswith(".jsonl"):
|
||||
continue
|
||||
info = parse_session(os.path.join(root, fn))
|
||||
if info:
|
||||
sessions.append(info)
|
||||
sessions.sort(key=lambda s: s["mtime"], reverse=True)
|
||||
return sessions
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Formattazione
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fmt_relative(ts: float) -> str:
|
||||
d = time.time() - ts
|
||||
if d < 0:
|
||||
return "adesso"
|
||||
if d < 60:
|
||||
return f"{int(d)}s fa"
|
||||
if d < 3600:
|
||||
return f"{int(d // 60)}min fa"
|
||||
if d < 86400:
|
||||
return f"{int(d // 3600)}h fa"
|
||||
if d < 172800:
|
||||
return "ieri"
|
||||
return f"{int(d // 86400)}g fa"
|
||||
|
||||
|
||||
def fmt_absolute(ts: float) -> str:
|
||||
return time.strftime("%d/%m/%Y %H:%M", time.localtime(ts))
|
||||
|
||||
|
||||
def fmt_size(n: int) -> str:
|
||||
if n < 1024:
|
||||
return f"{n} B"
|
||||
if n < 1024 * 1024:
|
||||
return f"{n / 1024:.0f} KB"
|
||||
return f"{n / (1024 * 1024):.1f} MB"
|
||||
|
||||
|
||||
def display_text(info: dict, limit: int = 100) -> str:
|
||||
"""Testo principale mostrato per la sessione: nome se presente, altrimenti primo messaggio."""
|
||||
if info["name"]:
|
||||
return info["name"]
|
||||
first = info["first"].replace("\n", " ")
|
||||
if len(first) > limit:
|
||||
first = first[: limit - 1] + "…"
|
||||
return first or "(sessione senza messaggi)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GUI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LauncherWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self, app):
|
||||
super().__init__(application=app, title=APP_TITLE)
|
||||
self.set_default_size(1000, 600)
|
||||
|
||||
# modello: markup_sessione, cwd, rel_time, abs_time, mtime(float), path, size(int), search_text
|
||||
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.connect("search-changed", self._on_search_changed)
|
||||
|
||||
# vista lista
|
||||
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)
|
||||
|
||||
rend = Gtk.CellRendererText(ellipsize="end")
|
||||
col = Gtk.TreeViewColumn("Sessione", rend, markup=0)
|
||||
col.set_expand(True)
|
||||
col.set_min_width(420)
|
||||
col.set_sort_column_id(7)
|
||||
self.tree.append_column(col)
|
||||
|
||||
rend2 = Gtk.CellRendererText(ellipsize="middle")
|
||||
col2 = Gtk.TreeViewColumn("Cartella", 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()
|
||||
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)
|
||||
self.tree.connect("row-activated", self._on_row_activated)
|
||||
self.tree.connect("key-press-event", self._on_key_press)
|
||||
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
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)")
|
||||
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.connect("clicked", lambda *_: self.open_folder())
|
||||
btn_rename = Gtk.Button(label="Rinomina", tooltip_text="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.get_style_context().add_class("suggested-action")
|
||||
btn_launch.connect("clicked", lambda *_: self.launch())
|
||||
btn_quit = Gtk.Button(label="Esci")
|
||||
btn_quit.connect("clicked", lambda *_: self.close())
|
||||
|
||||
hbox = Gtk.Box(spacing=8)
|
||||
hbox.pack_start(btn_refresh, False, False, 0)
|
||||
hbox.pack_start(btn_open_dir, False, False, 0)
|
||||
hbox.pack_start(btn_rename, False, False, 0)
|
||||
hbox.pack_start(Gtk.Box(), True, True, 0) # spaziatore
|
||||
hbox.pack_start(btn_launch, False, False, 0)
|
||||
hbox.pack_start(btn_quit, False, False, 0)
|
||||
|
||||
# status bar
|
||||
self.status = Gtk.Label(label="", xalign=0.0, halign=Gtk.Align.START)
|
||||
self.status.set_ellipsize(Pango.EllipsizeMode.END)
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
box.set_border_width(8)
|
||||
box.pack_start(self.search_entry, False, False, 0)
|
||||
box.pack_start(scrolled, True, True, 0)
|
||||
box.pack_start(hbox, False, False, 0)
|
||||
sep = Gtk.Separator()
|
||||
box.pack_start(sep, False, False, 0)
|
||||
box.pack_start(self.status, False, False, 0)
|
||||
self.add(box)
|
||||
|
||||
self.refresh()
|
||||
self.show_all()
|
||||
self.search_entry.grab_focus()
|
||||
|
||||
# -- popolamento ------------------------------------------------------
|
||||
|
||||
def refresh(self):
|
||||
sessions = scan_sessions()
|
||||
selected_path = self.selected_path()
|
||||
self.store.clear()
|
||||
self.by_path.clear()
|
||||
for s in sessions:
|
||||
text = display_text(s)
|
||||
markup = GLib.markup_escape_text(text)
|
||||
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()])
|
||||
self.by_path[s["path"]] = s
|
||||
if selected_path in self.by_path:
|
||||
self.select_path(selected_path)
|
||||
self._update_status()
|
||||
|
||||
def _update_status(self):
|
||||
n = len(self.store)
|
||||
self.status.set_text(
|
||||
f"{n} session{'i' if n != 1 else 'e'} · {SESSIONS_DIR}"
|
||||
)
|
||||
|
||||
# -- selezione --------------------------------------------------------
|
||||
|
||||
def selected_iter(self):
|
||||
model, it = self.tree.get_selection().get_selected()
|
||||
return model, it
|
||||
|
||||
def selected_path(self):
|
||||
model, it = self.selected_iter()
|
||||
if it is None:
|
||||
return None
|
||||
return model[it][5]
|
||||
|
||||
def select_path(self, path):
|
||||
it = self.store.get_iter_first()
|
||||
while it is not None:
|
||||
if self.store[it][5] == path:
|
||||
self.tree.get_selection().select_iter(it)
|
||||
self.tree.scroll_to_cell(self.store.get_path(it))
|
||||
return
|
||||
it = self.store.iter_next(it)
|
||||
|
||||
# -- filtri -----------------------------------------------------------
|
||||
|
||||
def _filter_visible(self, model, it, _data=None):
|
||||
q = self.search_entry.get_text().strip().lower()
|
||||
if not q:
|
||||
return True
|
||||
return q in model[it][7] or q in model[it][1].lower()
|
||||
|
||||
def _on_search_changed(self, _entry):
|
||||
self.filter.refilter()
|
||||
|
||||
# -- azioni -----------------------------------------------------------
|
||||
|
||||
def _on_row_activated(self, _tree, _path, _col):
|
||||
self.launch()
|
||||
|
||||
def _on_key_press(self, _widget, event):
|
||||
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
|
||||
self.launch()
|
||||
return True
|
||||
if event.keyval == Gdk.KEY_F2:
|
||||
self.rename()
|
||||
return True
|
||||
if event.keyval == Gdk.KEY_r and event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
self.refresh()
|
||||
return True
|
||||
return False
|
||||
|
||||
def launch(self):
|
||||
path = self.selected_path()
|
||||
if not path:
|
||||
self._flash_status("Seleziona prima una sessione dalla lista.")
|
||||
return
|
||||
info = self.by_path.get(path, {})
|
||||
cwd = info.get("cwd") or os.path.expanduser("~")
|
||||
if not os.path.isdir(cwd):
|
||||
cwd = os.path.expanduser("~")
|
||||
|
||||
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)
|
||||
cmd = [konsole, "--workdir", cwd, "--hold", "--separate",
|
||||
"-e", "bash", "-c", 'pi --session "$1"; exec bash', "_", 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]
|
||||
|
||||
try:
|
||||
subprocess.Popen(cmd, start_new_session=True)
|
||||
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}")
|
||||
|
||||
def rename(self):
|
||||
path = self.selected_path()
|
||||
if not path:
|
||||
self._flash_status("Seleziona prima una sessione dalla lista.")
|
||||
return
|
||||
info = self.by_path.get(path, {})
|
||||
current = info.get("name", "")
|
||||
|
||||
dialog = Gtk.Dialog(title="Rinomina sessione", transient_for=self, modal=True)
|
||||
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OK, Gtk.ResponseType.OK)
|
||||
entry = Gtk.Entry(text=current)
|
||||
entry.set_activates_default(True)
|
||||
lbl = Gtk.Label(label="Nome mostrato (vuoto = cancella il nome):")
|
||||
lbl.set_xalign(0.0)
|
||||
box = dialog.get_content_area()
|
||||
box.set_border_width(12)
|
||||
box.set_spacing(8)
|
||||
box.pack_start(lbl, False, False, 0)
|
||||
box.pack_start(entry, False, False, 0)
|
||||
dialog.set_default_response(Gtk.ResponseType.OK)
|
||||
dialog.show_all()
|
||||
response = dialog.run()
|
||||
name = entry.get_text().strip()
|
||||
dialog.destroy()
|
||||
|
||||
if response != Gtk.ResponseType.OK:
|
||||
return
|
||||
try:
|
||||
rename_session(path, name)
|
||||
except OSError as e:
|
||||
self._flash_status(f"Errore nella rinomina: {e}")
|
||||
return
|
||||
self.refresh()
|
||||
self._flash_status(f"Sessione rinominata: {name or '(nome cancellato)'}")
|
||||
|
||||
def open_folder(self):
|
||||
path = self.selected_path()
|
||||
if not path:
|
||||
return
|
||||
info = self.by_path.get(path, {})
|
||||
cwd = info.get("cwd") or os.path.expanduser("~")
|
||||
if not os.path.isdir(cwd):
|
||||
cwd = os.path.expanduser("~")
|
||||
dolphin = shutil.which("dolphin")
|
||||
if dolphin:
|
||||
subprocess.Popen([dolphin, cwd], start_new_session=True)
|
||||
|
||||
def _flash_status(self, text):
|
||||
self.status.set_text(text)
|
||||
GLib.timeout_add(5000, lambda: self._update_status() or False)
|
||||
|
||||
|
||||
class LauncherApp(Gtk.Application):
|
||||
def __init__(self):
|
||||
super().__init__(application_id="it.enne2.pi-session-launcher")
|
||||
self.win = None
|
||||
|
||||
def do_activate(self):
|
||||
if self.win is None:
|
||||
self.win = LauncherWindow(self)
|
||||
self.win.present()
|
||||
|
||||
|
||||
def main():
|
||||
app = LauncherApp()
|
||||
app.run(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user