Four papercuts met while using the CLI: the test menu returned a verdict nobody could find at the end of the output, the file browser never closed once a file was picked, a wrong KeePass password killed the whole CLI, and a configuration key had drifted from the code that reads it. --- FR --- Quatre irritants rencontrés à l'usage : le menu Test rendait un verdict introuvable en fin de sortie, le navigateur de fichiers ne se fermait jamais une fois le fichier choisi, un mauvais mot de passe KeePass tuait tout le CLI, et une clé de configuration avait dérivé du code qui la lit. Assisted-by: Claude Opus 5
120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import os
|
|
|
|
import urwid
|
|
|
|
|
|
class FileBrowser(urwid.WidgetWrap):
|
|
def __init__(self, initial_path, callback, open_dir=False):
|
|
self.callback = callback
|
|
self.current_path = os.path.abspath(initial_path)
|
|
self.list_walker = urwid.SimpleFocusListWalker([])
|
|
self.listbox = urwid.ListBox(self.list_walker)
|
|
super().__init__(self.listbox)
|
|
self.open_dir = open_dir
|
|
self.refresh_list()
|
|
|
|
def refresh_list(self):
|
|
"""Updates the list of files and directories."""
|
|
self.list_walker.clear()
|
|
self.list_walker.append(
|
|
urwid.Button("..", on_press=self.go_up_directory)
|
|
)
|
|
if self.open_dir:
|
|
self.list_walker.append(
|
|
urwid.Button(".", on_press=self.select_directory)
|
|
)
|
|
|
|
try:
|
|
entries = os.listdir(self.current_path)
|
|
entries.sort(key=lambda r: r.lower())
|
|
for entry in entries:
|
|
full_path = os.path.join(self.current_path, entry)
|
|
if os.path.isdir(full_path):
|
|
self.list_walker.append(
|
|
urwid.Button(f"{entry}/", on_press=self.open_directory)
|
|
)
|
|
elif not self.open_dir:
|
|
self.list_walker.append(
|
|
urwid.Button(entry, on_press=self.select_file)
|
|
)
|
|
except OSError as e:
|
|
# Handle directory access errors
|
|
self.list_walker.append(urwid.Text(f"Access Error: {e}"))
|
|
|
|
def go_up_directory(self, button):
|
|
"""Moves up one level in the directory hierarchy."""
|
|
parent_path = os.path.dirname(self.current_path)
|
|
if parent_path != self.current_path:
|
|
self.current_path = parent_path
|
|
self.refresh_list()
|
|
|
|
def open_directory(self, button):
|
|
"""Moves into a subdirectory."""
|
|
dirname = button.label[:-1]
|
|
new_path = os.path.join(self.current_path, dirname)
|
|
if os.path.isdir(new_path):
|
|
self.current_path = new_path
|
|
self.refresh_list()
|
|
|
|
def select_directory(self, button):
|
|
"""Selects a directory, and closes the browser."""
|
|
self.callback(self.current_path)
|
|
exit_program()
|
|
|
|
def select_file(self, button):
|
|
"""Selects a file, and closes the browser.
|
|
|
|
The callback records the choice; leaving the loop is what ENDS the
|
|
browser. Without it the selection worked and nothing seemed to happen:
|
|
the screen stayed up, no key closed it, and only Ctrl+C got out — so
|
|
the browser looked frozen at the exact moment it had done its job.
|
|
"""
|
|
filename = button.label
|
|
selected_file_path = os.path.join(self.current_path, filename)
|
|
self.callback(selected_file_path)
|
|
exit_program()
|
|
|
|
def unhandled_input(self, key):
|
|
"""Leaving without choosing has to be possible.
|
|
|
|
Every other way out of this browser selects something. A caller that
|
|
offers an alternative — typing a path — can only be reached by
|
|
cancelling, so cancelling has to exist.
|
|
"""
|
|
if key in ("q", "Q", "esc"):
|
|
exit_program()
|
|
|
|
def run_main_frame(self):
|
|
main_frame = urwid.Frame(
|
|
body=self,
|
|
header=urwid.Text(("header", f"Navigate: {self.current_path}")),
|
|
footer=urwid.Text(
|
|
(
|
|
"footer",
|
|
"Arrow keys to navigate, Enter to select, q to cancel.",
|
|
)
|
|
),
|
|
)
|
|
|
|
palette = [
|
|
("header", "dark cyan", "black"),
|
|
("footer", "dark cyan", "black"),
|
|
("body", "white", "black"),
|
|
("button", "black", "dark cyan", "standout"),
|
|
("focus", "white", "dark green", "bold"),
|
|
("bold", "bold", "black"),
|
|
]
|
|
|
|
loop = urwid.MainLoop(
|
|
main_frame, palette, unhandled_input=self.unhandled_input
|
|
)
|
|
loop.run()
|
|
|
|
|
|
def exit_program():
|
|
"""Exits the program."""
|
|
raise urwid.ExitMainLoop()
|