2025-10-31 01:10:54 -04:00
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2025-10-31 01:10:54 -04:00
|
|
|
# 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):
|
2026-08-10 03:10:50 -04:00
|
|
|
"""Selects a directory, and closes the browser."""
|
2025-10-31 01:10:54 -04:00
|
|
|
self.callback(self.current_path)
|
2026-08-10 03:10:50 -04:00
|
|
|
exit_program()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
def select_file(self, button):
|
2026-08-10 03:10:50 -04:00
|
|
|
"""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.
|
|
|
|
|
"""
|
2025-10-31 01:10:54 -04:00
|
|
|
filename = button.label
|
|
|
|
|
selected_file_path = os.path.join(self.current_path, filename)
|
|
|
|
|
self.callback(selected_file_path)
|
2026-08-10 03:10:50 -04:00
|
|
|
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()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
def run_main_frame(self):
|
|
|
|
|
main_frame = urwid.Frame(
|
|
|
|
|
body=self,
|
|
|
|
|
header=urwid.Text(("header", f"Navigate: {self.current_path}")),
|
|
|
|
|
footer=urwid.Text(
|
2026-08-10 03:10:50 -04:00
|
|
|
(
|
|
|
|
|
"footer",
|
|
|
|
|
"Arrow keys to navigate, Enter to select, q to cancel.",
|
|
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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"),
|
|
|
|
|
]
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
loop = urwid.MainLoop(
|
|
|
|
|
main_frame, palette, unhandled_input=self.unhandled_input
|
|
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
loop.run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def exit_program():
|
|
|
|
|
"""Exits the program."""
|
|
|
|
|
raise urwid.ExitMainLoop()
|