diff --git a/binary_requirements.in b/binary_requirements.in index 5df75d93..af6f4163 100644 --- a/binary_requirements.in +++ b/binary_requirements.in @@ -1,6 +1,7 @@ . apprise attrs +binaryornot certifi charset-normalizer click diff --git a/binary_requirements.txt b/binary_requirements.txt index d1ef3175..e6567ccc 100644 --- a/binary_requirements.txt +++ b/binary_requirements.txt @@ -2,6 +2,7 @@ # uv pip compile --annotation-style line binary_requirements.in -o binary_requirements.txt apprise==1.10.0 # via -r binary_requirements.in attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in +binaryornot==0.6.0 # via -r binary_requirements.in . # via -r binary_requirements.in certifi==2026.5.20 # via apprise, requests, -r binary_requirements.in charset-normalizer==3.4.7 # via requests, -r binary_requirements.in diff --git a/borgmatic/actions/browse/archive.py b/borgmatic/actions/browse/archive.py index c31a8018..e68f0933 100644 --- a/borgmatic/actions/browse/archive.py +++ b/borgmatic/actions/browse/archive.py @@ -7,6 +7,9 @@ import borgmatic.borg.extract import borgmatic.borg.repo_list import borgmatic.borg.version +import binaryornot.helpers + + logger = logging.getLogger(__name__) @@ -82,6 +85,10 @@ READLINES_HINT_BYTES = 100000 def get_archive_file_content(config, repository, archive_name, file_path): + ''' + Given a configuration dict, a repository dict, an archive name in that repository, and a file + path in that archive, return the file's contents or None if the file can't be loaded. + ''' with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])): logger.info(f'Getting archive content of file {file_path}') local_path = config.get('local_path', 'borg') @@ -102,10 +109,16 @@ def get_archive_file_content(config, repository, archive_name, file_path): extract_to_stdout=True, ).stdout.readlines(READLINES_HINT_BYTES) - content = ''.join(line.decode() for line in lines) + content = b''.join(lines) - return ( - content - if len(content) < READLINES_HINT_BYTES - else f'{content}[... truncated for display ...]' - ) + if binaryornot.helpers.is_binary_string(content): + return None + + try: + return ( + content.decode() + if len(content) < READLINES_HINT_BYTES + else f'{content.decode()}\n[... truncated for display ...]' + ) + except UnicodeDecodeError: + return None diff --git a/borgmatic/actions/browse/panels.py b/borgmatic/actions/browse/panels.py index aad66e5e..00cbada0 100644 --- a/borgmatic/actions/browse/panels.py +++ b/borgmatic/actions/browse/panels.py @@ -15,6 +15,12 @@ OPTION_LIST_BINDINGS = ( textual.binding.Binding( key='down,j', action='cursor_down', description='scroll down', show=True, priority=True ), + textual.binding.Binding( + key='pageup', action='page_up', description='page up', show=True, priority=True + ), + textual.binding.Binding( + key='pagedown', action='page_down', description='page down', show=True, priority=True + ), textual.binding.Binding( key='enter', action='select', description='select', show=True, priority=True ), @@ -55,7 +61,7 @@ class Repositories_list(textual.widgets.OptionList): ), classes='panel', ) - self.border_title = 'repositories' + self.border_title = '📦 repositories' class Archives_list(textual.widgets.OptionList): @@ -66,7 +72,7 @@ class Archives_list(textual.widgets.OptionList): self.repository = repository super().__init__(classes='panel') - self.border_title = 'archives' + self.border_title = '📚 archives' self.highlighted_option_changed = False timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self) @@ -128,10 +134,10 @@ class File_preview(textual.widgets.RichLog): BINDINGS = [ *textual.widgets.RichLog.BINDINGS, textual.binding.Binding( - key='up', action='scroll_up', description='scroll up', show=True, priority=True + key='up,k', action='scroll_up', description='scroll up', show=True, priority=True ), textual.binding.Binding( - key='down', action='scroll_down', description='scroll down', show=True, priority=True + key='down,j', action='scroll_down', description='scroll down', show=True, priority=True ), textual.binding.Binding( key='pageup', action='page_up', description='page up', show=True, priority=True @@ -175,4 +181,4 @@ class File_preview(textual.widgets.RichLog): class Logs(textual.widgets.RichLog): def __init__(self): super().__init__(markup=True, id='logs', classes='panel') - self.border_title = 'logs' + self.border_title = '🪵 logs' diff --git a/borgmatic/actions/browse/paths.py b/borgmatic/actions/browse/paths.py index 08ebf996..b458ebde 100644 --- a/borgmatic/actions/browse/paths.py +++ b/borgmatic/actions/browse/paths.py @@ -4,11 +4,13 @@ import enum class Path_type(enum.Enum): DIRECTORY = 'd' LINK = 'l' + PIPE = 'p' FILE = '-' PATH_TYPE_ICONS = { Path_type.DIRECTORY.value: '📁', Path_type.LINK.value: '🔗', + Path_type.PIPE.value: '🚰', Path_type.FILE.value: '📄', } diff --git a/borgmatic/actions/browse/workers.py b/borgmatic/actions/browse/workers.py index 210009ff..4f8e9808 100644 --- a/borgmatic/actions/browse/workers.py +++ b/borgmatic/actions/browse/workers.py @@ -59,7 +59,7 @@ def add_archive_files( ) for path_type, file_path, link_target in file_type_paths: - pieces = (borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, '?'), file_path) + ( + pieces = (borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, '❓'), file_path) + ( ('→', link_target) if link_target else () ) highlighted_option = directory_list.highlighted_option @@ -88,6 +88,10 @@ def load_file_preview(browse_app, file_preview, config, repository, archive_name ) browse_app.call_from_thread(timer.stop) - syntax_lexer = rich.syntax.Syntax.guess_lexer(file_path, file_content) browse_app.call_from_thread(file_preview.clear) - browse_app.call_from_thread(file_preview.write, rich.syntax.Syntax(file_content, syntax_lexer)) + + if file_content is None: + browse_app.call_from_thread(file_preview.write, 'Cannot load a preview for this file') + else: + syntax_lexer = rich.syntax.Syntax.guess_lexer(file_path, file_content) + browse_app.call_from_thread(file_preview.write, rich.syntax.Syntax(file_content, syntax_lexer)) diff --git a/pyproject.toml b/pyproject.toml index 8f028eec..a2d76607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ validate-borgmatic-config = "borgmatic.commands.validate_config:main" [project.optional-dependencies] Apprise = ["apprise"] -browse = ["textual"] +browse = ["textual", "binaryornot"] browse-dev = ["textual-dev"] [project.urls] diff --git a/test_requirements.in b/test_requirements.in index caeda01c..8db5916e 100644 --- a/test_requirements.in +++ b/test_requirements.in @@ -1,5 +1,6 @@ apprise attrs +binaryornot certifi charset-normalizer click>=8.1.8 diff --git a/test_requirements.txt b/test_requirements.txt index 8fc78811..4d73938c 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,6 +2,7 @@ # uv pip compile --annotation-style line test_requirements.in -o test_requirements.txt apprise==1.10.0 # via -r test_requirements.in attrs==26.1.0 # via jsonschema, referencing, -r test_requirements.in +binaryornot==0.6.0 # via -r test_requirements.in certifi==2026.5.20 # via apprise, requests, -r test_requirements.in charset-normalizer==3.4.7 # via requests, -r test_requirements.in click>=8.1.8