Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7bf5ed58 | ||
|
|
6cf2c9ea80 | ||
|
|
2fe75f29d7 |
@@ -1,24 +1,49 @@
|
|||||||
# Pi Session Launcher
|
# Pi Session Launcher
|
||||||
|
|
||||||
Piccola applicazione GUI (GTK3) per **riprendere le sessioni di Pi Agent**.
|
Piccola applicazione GUI (GTK3) per **elencare, riprendere e rinominare le sessioni di [Pi Agent](https://github.com/earendil-works/pi-mono)**.
|
||||||
|
|
||||||
Elenco tutte le sessioni salvate in `~/.pi/agent/sessions/` e, selezionandone una:
|
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;
|
- apre un terminale (Konsole) **nella cartella di lavoro** (`cwd`) in cui la sessione era stata avviata;
|
||||||
- riprende la sessione con `pi --session <file sessione>`.
|
- riprende la sessione con `pi --session <file sessione>`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Funzionalità
|
||||||
|
|
||||||
|
- Elenco di tutte le sessioni, **ordinate per ultima modifica** (più recenti prima)
|
||||||
|
- Per ogni sessione: primo messaggio (o **nome** se impostato, in grassetto), cartella di avvio, ultima modifica (tempo relativo)
|
||||||
|
- **Ripresa** con doppio clic / Invio / pulsante: apre Konsole nella cwd della sessione e lancia `pi --session`
|
||||||
|
- **Rinomina** sessione (F2): il nome è quello che pi mostra nel suo selettore (`/resume`)
|
||||||
|
- **Ricerca** per messaggio o cartella
|
||||||
|
- Apertura della cartella della sessione in Dolphin
|
||||||
|
- Aggiornamento dell'elenco (Ctrl+R)
|
||||||
|
- Eseguibile portabile singolo (vedi sotto) con icona integrata
|
||||||
|
|
||||||
## Requisiti
|
## Requisiti
|
||||||
|
|
||||||
- Python 3 + PyGObject GTK3 (`python3-gi`, `gir1.2-gtk-3.0`)
|
- Python 3 + PyGObject GTK3 (`python3-gi`, `gir1.2-gtk-3.0`)
|
||||||
- Konsole (KDE) — fallback: `xterm`
|
- Konsole (KDE) — fallback: `xterm`
|
||||||
- `pi` nel PATH
|
- `pi` nel PATH
|
||||||
|
- Per il menu applicazioni: un ambiente desktop con supporto `.desktop` (es. KDE Plasma)
|
||||||
|
|
||||||
## Installazione
|
## Installazione
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./install.sh # copia la voce nel menu applicazioni KDE (.desktop)
|
./install.sh # installa la voce di menu KDE + icone hicolor (48–512 px)
|
||||||
|
./install.sh --uninstall # rimuove la voce di menu
|
||||||
```
|
```
|
||||||
|
|
||||||
|
L'installer crea:
|
||||||
|
|
||||||
|
- `~/.local/share/applications/it.enne2.pi-session-launcher.desktop` (con `StartupWMClass`)
|
||||||
|
- `~/.local/share/icons/hicolor/{48,64,128,256,512}x*/apps/it.enne2.pi-session-launcher.png`
|
||||||
|
|
||||||
|
> **KDE Plasma/Wayland**: l'icona di finestra/taskbar viene associata tramite
|
||||||
|
> l'**app-id** GTK, che deve coincidere con lo stem del file `.desktop` e con il
|
||||||
|
> nome dell'icona hicolor (`it.enne2.pi-session-launcher`). `set_icon_from_file()`
|
||||||
|
> da solo non basta su Wayland. Vedere `main()` in `pi-session-launcher.py`.
|
||||||
|
|
||||||
Avvio manuale:
|
Avvio manuale:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -40,31 +65,52 @@ Le sessioni **con nome** (`/name` in pi) sono mostrate in **grassetto**.
|
|||||||
|
|
||||||
## Eseguibile portabile
|
## Eseguibile portabile
|
||||||
|
|
||||||
Build dell'eseguibile singolo (PyInstaller, non richiede `python3-dev`):
|
È disponibile un eseguibile singolo Linux x86-64 (~33 MB, GTK/Python inclusi)
|
||||||
|
nella [release v0.1.0](https://git.enne2.net/enne2/pi-session-launcher/releases/tag/v0.1.0):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -L -o pi-session-launcher \
|
||||||
|
https://git.enne2.net/attachments/513a5f44-378e-4fc7-9f9e-89f25a082d66
|
||||||
|
chmod +x pi-session-launcher
|
||||||
|
./pi-session-launcher
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build da sorgente
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install --user --break-system-packages pyinstaller
|
python3 -m pip install --user --break-system-packages pyinstaller
|
||||||
python3 -m PyInstaller --onefile --windowed --name pi-session-launcher \
|
python3 -m PyInstaller --clean --noconfirm pi-session-launcher.spec
|
||||||
--collect-all gi --collect-all cairo \
|
|
||||||
--hidden-import gi.repository.Gtk --hidden-import gi.repository.Gdk \
|
|
||||||
--hidden-import gi.repository.Gio --hidden-import gi.repository.GLib \
|
|
||||||
--hidden-import gi.repository.Pango \
|
|
||||||
pi-session-launcher.py
|
|
||||||
./dist/pi-session-launcher
|
./dist/pi-session-launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Il file `pi-session-launcher.spec` include la **potatura post-analisi**, che riduce
|
||||||
|
il binario da ~244 MB a ~33 MB (−86%):
|
||||||
|
|
||||||
|
- rimuove `share/icons/*`: l'hook PyGObject raccoglie **tutti** i temi icona
|
||||||
|
installati sul sistema (Papirus, Breeze, Adwaita…), ma a runtime GTK usa i temi
|
||||||
|
di sistema — i bundlati sono peso morto;
|
||||||
|
- mantiene solo le lingue `it`/`en` in `share/locale/` (122 → 2).
|
||||||
|
|
||||||
> Nota: Nuitka richiede gli header `python3-dev` (non installabili senza sudo);
|
> Nota: Nuitka richiede gli header `python3-dev` (non installabili senza sudo);
|
||||||
> PyInstaller è la via raccomandata dalla documentazione PyGObject per app GTK.
|
> PyInstaller è la via raccomandata dalla documentazione PyGObject per app GTK.
|
||||||
|
|
||||||
## Repository
|
|
||||||
|
|
||||||
Mirror pubblico su Gitea: https://git.enne2.net/enne2/pi-session-launcher
|
|
||||||
|
|
||||||
## Come funziona
|
## Come funziona
|
||||||
|
|
||||||
- Ogni sessione è un file JSONL in `~/.pi/agent/sessions/--<percorso>--/<timestamp>_<uuid>.jsonl`.
|
- 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).
|
- 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 primo messaggio utente e il nome (`session_info`) vengono letti dal file.
|
||||||
|
- La **rinomina** appende una entry `session_info` al file: pi legge l'**ultima**
|
||||||
|
entry (walk inverso), quindi l'ultima rinomina vince e un nome vuoto cancella.
|
||||||
- Il lancio usa:
|
- Il lancio usa:
|
||||||
`konsole --workdir <cwd> --hold --separate -e bash -c 'pi --session "$1"; exec bash' _ <file>`
|
`konsole --workdir <cwd> --hold --separate -e bash -c 'pi --session "$1"; exec bash' _ <file>`
|
||||||
— `exec bash` mantiene la shell aperta quando pi termina.
|
— il wrapper `bash -c` è più affidabile del passaggio diretto degli argomenti a
|
||||||
|
Konsole; `exec bash` mantiene la shell aperta quando pi termina.
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
- Gitea (pubblico): https://git.enne2.net/enne2/pi-session-launcher
|
||||||
|
- Release: https://git.enne2.net/enne2/pi-session-launcher/releases/tag/v0.1.0
|
||||||
|
- Clone SSH: `git clone ssh://git@git.enne2.net:222/enne2/pi-session-launcher.git`
|
||||||
|
|
||||||
|
L'icona (terminale + freccia di ripristino) è stata generata con AI e verificata;
|
||||||
|
è usata come icona dell'app, icona hicolor e avatar del repository.
|
||||||
|
|||||||
+88
-25
@@ -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,9 +371,17 @@ class LauncherWindow(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
def _update_status(self):
|
def _update_status(self):
|
||||||
n = len(self.store)
|
n = len(self.store)
|
||||||
self.status.set_text(
|
selected = self.selected_path()
|
||||||
f"{n} session{'i' if n != 1 else 'e'} · {SESSIONS_DIR}"
|
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 --------------------------------------------------------
|
# -- selezione --------------------------------------------------------
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user