From 9935d0ef8d1cfee877a661edc623059c96ed0077 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 21 May 2026 17:01:07 -0700 Subject: [PATCH] Linting and more file refactoring for the browse action. --- borgmatic/actions/browse/app.py | 14 +- borgmatic/actions/browse/archive.py | 7 +- borgmatic/actions/browse/carousel.py | 117 +++++++++++++++++ borgmatic/actions/browse/loading.py | 8 +- borgmatic/actions/browse/logs.py | 6 +- .../actions/browse/{widgets.py => panels.py} | 121 +----------------- borgmatic/actions/browse/run.py | 4 +- borgmatic/actions/browse/workers.py | 13 +- 8 files changed, 148 insertions(+), 142 deletions(-) create mode 100644 borgmatic/actions/browse/carousel.py rename borgmatic/actions/browse/{widgets.py => panels.py} (57%) diff --git a/borgmatic/actions/browse/app.py b/borgmatic/actions/browse/app.py index d3ef6c3c..ee860288 100644 --- a/borgmatic/actions/browse/app.py +++ b/borgmatic/actions/browse/app.py @@ -2,18 +2,18 @@ import textual.app import textual.binding import textual.widgets +import borgmatic.actions.browse.carousel import borgmatic.actions.browse.logs -import borgmatic.actions.browse.widgets class Browse_app(textual.app.App): - BINDINGS = [ + BINDINGS = ( textual.binding.Binding(key='q', action='quit', description='quit'), textual.binding.Binding(key='v', action='toggle_logs', description='view logs'), textual.binding.Binding( key='c', action='command_palette', description='commands', show=False ), - ] + ) COMMAND_PALETTE_BINDING = 'c' CSS = ''' .panel { @@ -37,15 +37,15 @@ class Browse_app(textual.app.App): def compose(self): yield textual.widgets.Header() - yield borgmatic.actions.browse.widgets.Carousel( - [borgmatic.actions.browse.widgets.Configuration_files_list(self.configs)] + yield borgmatic.actions.browse.carousel.Carousel( + [borgmatic.actions.browse.panels.Configuration_files_list(self.configs)] if len(self.configs) > 1 else [ - borgmatic.actions.browse.widgets.Repositories_list(tuple(self.configs.values())[0]) + borgmatic.actions.browse.panels.Repositories_list(next(iter(self.configs.values()))) ] ) - logs_widget = borgmatic.actions.browse.widgets.Logs() + logs_widget = borgmatic.actions.browse.panels.Logs() yield logs_widget yield textual.widgets.Footer() diff --git a/borgmatic/actions/browse/archive.py b/borgmatic/actions/browse/archive.py index 834c34b9..d188915a 100644 --- a/borgmatic/actions/browse/archive.py +++ b/borgmatic/actions/browse/archive.py @@ -1,13 +1,12 @@ -import os import argparse import json import logging +import os import borgmatic.borg.extract import borgmatic.borg.repo_list import borgmatic.borg.version - logger = logging.getLogger(__name__) @@ -46,9 +45,9 @@ def get_repository_archives(config, repository): def get_archive_files(config, repository, archive_name, list_path=None): with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])): if list_path: - logger.info(f"Listing archive {archive_name} at path {list_path}") + logger.info(f'Listing archive {archive_name} at path {list_path}') else: - logger.info(f"Listing archive {archive_name}") + logger.info(f'Listing archive {archive_name}') global_arguments = argparse.Namespace() local_path = config.get('local_path', 'borg') diff --git a/borgmatic/actions/browse/carousel.py b/borgmatic/actions/browse/carousel.py new file mode 100644 index 00000000..5b4b3849 --- /dev/null +++ b/borgmatic/actions/browse/carousel.py @@ -0,0 +1,117 @@ +import os + +import textual.binding +import textual.containers + +import borgmatic.actions.browse.panels +import borgmatic.actions.browse.paths + + +def make_next_panel(focused_panel, option_id): + if isinstance(focused_panel, borgmatic.actions.browse.panels.Configuration_files_list): + return borgmatic.actions.browse.panels.Repositories_list( + config=focused_panel.configs[option_id] + ) + + if isinstance(focused_panel, borgmatic.actions.browse.panels.Repositories_list): + return borgmatic.actions.browse.panels.Archives_list( + config=focused_panel.config, repository=focused_panel.repositories[option_id] + ) + + if isinstance(focused_panel, borgmatic.actions.browse.panels.Archives_list): + return borgmatic.actions.browse.panels.Directory_list( + config=focused_panel.config, repository=focused_panel.repository, archive_name=option_id + ) + + if isinstance(focused_panel, borgmatic.actions.browse.panels.Directory_list): + option = focused_panel.get_option(option_id) + + if option.prompt.startswith( + borgmatic.actions.browse.paths.PATH_TYPE_ICONS[ + borgmatic.actions.browse.paths.Path_type.DIRECTORY.value + ] + ): + return borgmatic.actions.browse.panels.Directory_list( + focused_panel.config, + focused_panel.repository, + focused_panel.archive_name, + path_components=(*focused_panel.path_components, option_id), + ) + + return borgmatic.actions.browse.panels.File_preview( + focused_panel.config, + focused_panel.repository, + focused_panel.archive_name, + file_path=os.path.sep.join((*focused_panel.path_components, option_id)), + ) + + return None + + +class Carousel(textual.containers.Horizontal): + BINDINGS = ( + textual.binding.Binding( + key='left,h', action='previous', description='previous', priority=True + ), + ) + + def __init__(self, panels): + self.panels = panels + self.focused_panel = panels[0] + + super().__init__() + + def compose(self): + yield from self.panels + + self.focused_panel.focus() + + def action_previous(self): + ''' + Make the previous panel into the focused panel. + ''' + previous_panel_index = self.panels.index(self.focused_panel) - 1 + + if previous_panel_index < 0: + return + + self.focused_panel.styles.display = 'none' + + self.focused_panel = self.panels[previous_panel_index] + self.focused_panel.styles.display = 'block' + self.focused_panel.focus() + + def action_next(self, option_id): + ''' + Hide the current focused panel and create the next one. + ''' + self.focused_panel.styles.display = 'none' + next_panel_index = self.panels.index(self.focused_panel) + 1 + + if next_panel_index < len(self.panels): + self.focused_panel = self.panels[next_panel_index] + self.focused_panel.styles.display = 'block' + else: + self.focused_panel = make_next_panel(self.focused_panel, option_id) + self.panels.append(self.focused_panel) + self.focused_panel.highlighted = 0 + + self.focused_panel.focus() + self.refresh(recompose=True) + + def on_option_list_option_highlighted(self, event): + ''' + The highlighted option has changed, so truncate any next panels. + ''' + next_panel_index = self.panels.index(self.focused_panel) + 1 + + del self.panels[next_panel_index:] + + def on_option_list_option_selected(self, event): + if event.option_list != self.focused_panel or event.option_id == 'loading-indicator': + return + + if event.option_id == '..': + self.action_previous() + else: + self.action_next(event.option_id) diff --git a/borgmatic/actions/browse/loading.py b/borgmatic/actions/browse/loading.py index c7ba7435..60438553 100644 --- a/borgmatic/actions/browse/loading.py +++ b/borgmatic/actions/browse/loading.py @@ -1,21 +1,19 @@ +import contextlib import functools import textual.widgets import textual.widgets.option_list - LOADING_DOT_INTERVAL_SECONDS = 0.3 def update_inline_loading_indicator(widget): if isinstance(widget, textual.widgets.OptionList): - try: + with contextlib.suppress(textual.widgets.option_list.OptionDoesNotExist): widget.replace_option_prompt( 'loading-indicator', (str(widget.get_option('loading-indicator').prompt) + '.').replace('....', ''), ) - except textual.widgets.option_list.OptionDoesNotExist: - pass elif isinstance(widget, textual.widgets.Static): widget.update((str(widget.content) + '.').replace('....', '')) else: @@ -23,7 +21,7 @@ def update_inline_loading_indicator(widget): def add_inline_loading_indicator(widget): - loading_message = f'⏳ loading...' + loading_message = '⏳ loading...' if isinstance(widget, textual.widgets.OptionList): widget.clear_options() diff --git a/borgmatic/actions/browse/logs.py b/borgmatic/actions/browse/logs.py index dbe6231f..aa4bd962 100644 --- a/borgmatic/actions/browse/logs.py +++ b/borgmatic/actions/browse/logs.py @@ -1,11 +1,11 @@ import contextlib import logging -import borgmatic.logger - import textual._context import textual.worker +import borgmatic.logger + class Rich_color_formatter(logging.Formatter): def __init__(self, *args, **kwargs): @@ -43,7 +43,7 @@ class Browse_log_handler(logging.Handler): message = self.format(record) try: - worker = textual.worker.get_current_worker() + textual.worker.get_current_worker() self.logs_widget.app.call_from_thread(self.logs_widget.write, message) except (RuntimeError, textual.worker.NoActiveWorker): with contextlib.suppress(textual._context.NoActiveAppError): diff --git a/borgmatic/actions/browse/widgets.py b/borgmatic/actions/browse/panels.py similarity index 57% rename from borgmatic/actions/browse/widgets.py rename to borgmatic/actions/browse/panels.py index db722e55..024cde1c 100644 --- a/borgmatic/actions/browse/widgets.py +++ b/borgmatic/actions/browse/panels.py @@ -7,8 +7,8 @@ import borgmatic.actions.browse.loading import borgmatic.actions.browse.paths import borgmatic.actions.browse.workers - -OPTION_LIST_BINDINGS = textual.widgets.OptionList.BINDINGS + [ +OPTION_LIST_BINDINGS = ( + *textual.widgets.OptionList.BINDINGS, textual.binding.Binding( key='up,k', action='cursor_up', description='up', show=True, priority=True ), @@ -19,7 +19,7 @@ OPTION_LIST_BINDINGS = textual.widgets.OptionList.BINDINGS + [ key='enter', action='select', description='select', show=True, priority=True ), textual.binding.Binding(key='right,l', action='select', description='select', show=False), -] +) class Configuration_files_list(textual.widgets.OptionList): @@ -32,7 +32,7 @@ class Configuration_files_list(textual.widgets.OptionList): super().__init__( *( textual.widgets.option_list.Option(f'{unexpanded_path}', id=config_path) - for config_path in configs.keys() + for config_path in configs for unexpanded_path in (config_path.replace(home_directory, '~'),) ), classes='panel', @@ -80,7 +80,7 @@ class Archives_list(textual.widgets.OptionList): ) def on_option_list_option_highlighted(self, event): - if self.highlighted not in (None, 0): + if self.highlighted not in {None, 0}: self.highlighted_option_changed = True @@ -120,7 +120,7 @@ class Directory_list(textual.widgets.OptionList): ) def on_option_list_option_highlighted(self, event): - if self.highlighted not in (None, 0): + if self.highlighted not in {None, 0}: self.highlighted_option_changed = True @@ -156,115 +156,6 @@ class File_preview(textual.widgets.Static): ) -def make_next_panel(focused_panel, option_id): - if isinstance(focused_panel, Configuration_files_list): - return Repositories_list(config=focused_panel.configs[option_id]) - - if isinstance(focused_panel, Repositories_list): - return Archives_list( - config=focused_panel.config, repository=focused_panel.repositories[option_id] - ) - - if isinstance(focused_panel, Archives_list): - return Directory_list( - config=focused_panel.config, repository=focused_panel.repository, archive_name=option_id - ) - - if isinstance(focused_panel, Directory_list): - option = focused_panel.get_option(option_id) - - if option.prompt.startswith( - borgmatic.actions.browse.paths.PATH_TYPE_ICONS[ - borgmatic.actions.browse.paths.Path_type.DIRECTORY.value - ] - ): - return Directory_list( - focused_panel.config, - focused_panel.repository, - focused_panel.archive_name, - path_components=focused_panel.path_components + (option_id,), - ) - - return File_preview( - focused_panel.config, - focused_panel.repository, - focused_panel.archive_name, - file_path=os.path.sep.join(focused_panel.path_components + (option_id,)), - ) - - return None - - -class Carousel(textual.containers.Horizontal): - BINDINGS = [ - textual.binding.Binding( - key='left,h', action='previous', description='previous', priority=True - ), - ] - - def __init__(self, panels): - self.panels = panels - self.focused_panel = panels[0] - - super().__init__() - - def compose(self): - for panel in self.panels: - yield panel - - self.focused_panel.focus() - - def action_previous(self): - ''' - Make the previous panel into the focused panel. - ''' - previous_panel_index = self.panels.index(self.focused_panel) - 1 - - if previous_panel_index < 0: - return - - self.focused_panel.styles.display = 'none' - - self.focused_panel = self.panels[previous_panel_index] - self.focused_panel.styles.display = 'block' - self.focused_panel.focus() - - def action_next(self, option_id): - ''' - Hide the current focused panel and create the next one. - ''' - self.focused_panel.styles.display = 'none' - next_panel_index = self.panels.index(self.focused_panel) + 1 - - if next_panel_index < len(self.panels): - self.focused_panel = self.panels[next_panel_index] - self.focused_panel.styles.display = 'block' - else: - self.focused_panel = make_next_panel(self.focused_panel, option_id) - self.panels.append(self.focused_panel) - self.focused_panel.highlighted = 0 - - self.focused_panel.focus() - self.refresh(recompose=True) - - def on_option_list_option_highlighted(self, event): - ''' - The highlighted option has changed, so truncate any next panels. - ''' - next_panel_index = self.panels.index(self.focused_panel) + 1 - - del self.panels[next_panel_index:] - - def on_option_list_option_selected(self, event): - if event.option_list != self.focused_panel or event.option_id == 'loading-indicator': - return - - if event.option_id == '..': - self.action_previous() - else: - self.action_next(event.option_id) - - class Logs(textual.widgets.RichLog): def __init__(self): super().__init__(markup=True, id='logs', classes='panel') diff --git a/borgmatic/actions/browse/run.py b/borgmatic/actions/browse/run.py index d781399c..984cb4e2 100644 --- a/borgmatic/actions/browse/run.py +++ b/borgmatic/actions/browse/run.py @@ -15,13 +15,13 @@ def run_browse( logging.getLogger('asyncio').setLevel(logging.WARNING) try: - import textual + import textual # noqa: F401, PLC0415 except ImportError: # pragma: no cover raise ValueError( 'Unable to import the Textual library for the browse action; try installing "borgmatic[browse]"' ) - import borgmatic.actions.browse.app + import borgmatic.actions.browse.app # noqa: PLC0415 app = borgmatic.actions.browse.app.Browse_app(configs) app.run() diff --git a/borgmatic/actions/browse/workers.py b/borgmatic/actions/browse/workers.py index 1de36bbb..26267123 100644 --- a/borgmatic/actions/browse/workers.py +++ b/borgmatic/actions/browse/workers.py @@ -1,13 +1,12 @@ -import borgmatic.actions.browse.archive - import rich.syntax -import rich.text import textual import textual.widgets.option_list +import borgmatic.actions.browse.archive + @textual.work(thread=True) -async def add_repository_archives(browse_app, archives_list, config, repository, timer): +def add_repository_archives(browse_app, archives_list, config, repository, timer): archives_data = borgmatic.actions.browse.archive.get_repository_archives(config, repository) loading_option = archives_list.get_option('loading-indicator') @@ -65,8 +64,10 @@ def add_archive_files( ) highlighted_option = directory_list.highlighted_option sorted_options = sorted( - directory_list.options - + [textual.widgets.option_list.Option(' '.join(pieces), id=file_path)], + [ + *directory_list.options, + textual.widgets.option_list.Option(' '.join(pieces), id=file_path), + ], key=lambda option: ((option.id == 'loading-indicator'), option.prompt), ) browse_app.call_from_thread(directory_list.set_options, sorted_options)