From eed7a565f698ce9deaf1f3ff01480d41c8ba4438 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Jan 2024 23:42:56 +0000 Subject: [PATCH] Deployed 30fff7c with MkDocs version: 1.5.3 --- index.html | 2 +- reference/browsr/index.html | 1052 ++++++++++++++++++----------------- reference/index.html | 1052 ++++++++++++++++++----------------- search/search_index.json | 2 +- sitemap.xml.gz | Bin 127 -> 127 bytes 5 files changed, 1066 insertions(+), 1042 deletions(-) diff --git a/index.html b/index.html index 0da413f..db3c8a6 100644 --- a/index.html +++ b/index.html @@ -692,7 +692,7 @@

Installation

Extra Installation#

If you're looking to use browsr on remote file systems, like GitHub or AWS S3, you'll need to install the remote extra. -If you'd like to browse parquet files, you'll need to install the parquet extra. Or, even simpler, +If you'd like to browse parquet / feather files, you'll need to install the data extra. Or, even simpler, you can install the all extra to get all the extras.

pipx install "browsr[all]"
 
diff --git a/reference/browsr/index.html b/reference/browsr/index.html index 719fc13..2bd8931 100644 --- a/reference/browsr/index.html +++ b/reference/browsr/index.html @@ -963,7 +963,10 @@

400 401 402 -403
class Browsr(BrowsrTextualApp):
+403
+404
+405
+406
class Browsr(BrowsrTextualApp):
     """
     Textual code browser app.
     """
@@ -1070,247 +1073,250 @@ 

elif document.suffix == ".parquet": df = pd.read_parquet(document)[:1000] return self.df_to_table(pandas_dataframe=df, table=self.table_view) - elif document.suffix.lower() in image_file_extensions: - screen_width = self.app.size.width / 4 - content = open_image(document=document, screen_width=screen_width) - return content - elif document.suffix.lower() in [".json"]: - code_str = render_file_to_string(file_info=file_info) - try: - code_obj = json.loads(code_str) - code_lines = json.dumps(code_obj, indent=2).splitlines() - except json.JSONDecodeError: - code_lines = code_str.splitlines() - else: - code_str = render_file_to_string(file_info=file_info) - code_lines = code_str.splitlines() - code = "\n".join(code_lines[:1000]) - lexer = Syntax.guess_lexer(str(document), code=code) - return Syntax( - code=code, - lexer=lexer, - line_numbers=self.linenos, - word_wrap=False, - indent_guides=False, - theme=self.rich_themes[self.theme_index], - ) - - def _handle_file_size(self, file_info: FileInfo) -> None: - """ - Handle a File Size - """ - file_size_mb = file_info.size / 1000 / 1000 - too_large = file_size_mb >= self.config_object.max_file_size - exception = ( - True - if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes - else False - ) - if too_large is True and exception is not True: - raise FileSizeError("File too large") - - def _render_file( - self, file_path: pathlib.Path, code_view: Static, font: str - ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]: - """ - Render a File - """ - try: - file_info = get_file_info(file_path=file_path) - self._handle_file_size(file_info=file_info) - element = self.render_document(file_info=file_info) - return element - except FileSizeError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except PermissionError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("PERMISSION", font=font) - + "\n\n" - + text2art("ERROR", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except UnicodeError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except ArchiveFileError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except Exception: - self.table_view.display = False - self.code_view.display = True - code_view.update( - Traceback(theme=self.rich_themes[self.theme_index], width=None) - ) - self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]" - return None - - def render_code_page( - self, - file_path: pathlib.Path, - scroll_home: bool = True, - content: Optional[Any] = None, - ) -> None: - """ - Render the Code Page with Rich Syntax - """ - code_view = self.query_one("#code", Static) - font = "univers" - if content is not None: - code_view.update(text2art(content, font=font)) - return - element = self._render_file(file_path=file_path, code_view=code_view, font=font) - if isinstance(element, DataTable): - self.code_view.display = False - self.table_view.display = True - if self.code_view.has_focus: - self.table_view.focus() - if scroll_home is True: - self.query_one(DataTable).scroll_home(animate=False) - elif element is not None: - self.table_view.display = False - self.code_view.display = True - if self.table_view.has_focus: - self.code_view.focus() - code_view.update(element) - if scroll_home is True: - self.query_one("#code-view").scroll_home(animate=False) - self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]" - - @on(Mount) - def start_up_app(self) -> None: - """ - On Application Mount - See If a File Should be Displayed - """ - if self.selected_file_path is not None: - self.show_tree = self.force_show_tree - self.render_code_page(file_path=self.selected_file_path) - if self.show_tree is False and self.code_view.display is True: - self.code_view.focus() - elif self.show_tree is False and self.table_view.display is True: - self.table_view.focus() - else: - self.show_tree = True - self.render_code_page( - file_path=pathlib.Path.cwd(), content=__application__.upper() - ) - - @on(BrowsrDirectoryTree.FileSelected) - def handle_file_selected( - self, - message: BrowsrDirectoryTree.FileSelected, # type: ignore[name-defined] - ) -> None: - """ - Called when the user click a file in the directory tree. - """ - self.selected_file_path = upath.UPath(message.path) - file_info = get_file_info(file_path=self.selected_file_path) - self.render_code_page(file_path=upath.UPath(message.path)) - self.file_information.file_info = file_info - - def action_toggle_files(self) -> None: - """ - Called in response to key binding. - """ - self.show_tree = not self.show_tree - - def action_theme(self) -> None: - """ - An action to toggle rich theme. - """ - if self.selected_file_path is None: - return - elif self.theme_index < len(self.rich_themes) - 1: - self.theme_index += 1 - else: - self.theme_index = 0 - self.render_code_page(file_path=self.selected_file_path, scroll_home=False) - - def action_linenos(self) -> None: - """ - An action to toggle line numbers. - """ - if self.selected_file_path is None: - return - self.linenos = not self.linenos - self.render_code_page(file_path=self.selected_file_path, scroll_home=False) - - def _get_download_file_name(self) -> pathlib.Path: - """ - Get the download file name. - """ - download_dir = pathlib.Path.home() / "Downloads" - if not download_dir.exists(): - msg = f"Download directory {download_dir} not found" - raise FileNotFoundError(msg) - download_path = download_dir / self.selected_file_path.name # type: ignore[union-attr] - handled_download_path = handle_duplicate_filenames(file_path=download_path) - return handled_download_path - - @work(thread=True) - def download_selected_file(self) -> None: - """ - Download the selected file. - """ - if self.selected_file_path is None: - return - elif self.selected_file_path.is_dir(): - return - elif is_remote_path(self.selected_file_path): - handled_download_path = self._get_download_file_name() - with self.selected_file_path.open("rb") as file_handle: - with handled_download_path.open("wb") as download_handle: - shutil.copyfileobj(file_handle, download_handle) - - def action_download_file(self) -> None: - """ - Download the selected file. - """ - if self.selected_file_path is None: - return - elif self.selected_file_path.is_dir(): - return - elif is_remote_path(self.selected_file_path): - handled_download_path = self._get_download_file_name() - prompt_message: str = dedent( - f""" - ## File Download - - **Are you sure you want to download that file?** - - **File:** `{self.selected_file_path}` - - **Path:** `{handled_download_path}` - """ - ) - self.confirmation.download_message.update(Markdown(prompt_message)) - self.confirmation.refresh() - self.hidden_table_view = self.table_view.display - self.table_view.display = False - self.confirmation_window.display = True - - def action_parent_dir(self) -> None: - """ - Go to the parent directory - """ - new_path = self.config_object.path.parent.resolve() - if new_path != self.config_object.path: - self.config_object.file_path = str(new_path) - self.directory_tree.path = new_path + elif document.suffix.lower() in [".feather", ".fea"]: + df = pd.read_feather(document)[:1000] + return self.df_to_table(pandas_dataframe=df, table=self.table_view) + elif document.suffix.lower() in image_file_extensions: + screen_width = self.app.size.width / 4 + content = open_image(document=document, screen_width=screen_width) + return content + elif document.suffix.lower() in [".json"]: + code_str = render_file_to_string(file_info=file_info) + try: + code_obj = json.loads(code_str) + code_lines = json.dumps(code_obj, indent=2).splitlines() + except json.JSONDecodeError: + code_lines = code_str.splitlines() + else: + code_str = render_file_to_string(file_info=file_info) + code_lines = code_str.splitlines() + code = "\n".join(code_lines[:1000]) + lexer = Syntax.guess_lexer(str(document), code=code) + return Syntax( + code=code, + lexer=lexer, + line_numbers=self.linenos, + word_wrap=False, + indent_guides=False, + theme=self.rich_themes[self.theme_index], + ) + + def _handle_file_size(self, file_info: FileInfo) -> None: + """ + Handle a File Size + """ + file_size_mb = file_info.size / 1000 / 1000 + too_large = file_size_mb >= self.config_object.max_file_size + exception = ( + True + if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes + else False + ) + if too_large is True and exception is not True: + raise FileSizeError("File too large") + + def _render_file( + self, file_path: pathlib.Path, code_view: Static, font: str + ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]: + """ + Render a File + """ + try: + file_info = get_file_info(file_path=file_path) + self._handle_file_size(file_info=file_info) + element = self.render_document(file_info=file_info) + return element + except FileSizeError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except PermissionError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("PERMISSION", font=font) + + "\n\n" + + text2art("ERROR", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except UnicodeError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except ArchiveFileError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except Exception: + self.table_view.display = False + self.code_view.display = True + code_view.update( + Traceback(theme=self.rich_themes[self.theme_index], width=None) + ) + self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]" + return None + + def render_code_page( + self, + file_path: pathlib.Path, + scroll_home: bool = True, + content: Optional[Any] = None, + ) -> None: + """ + Render the Code Page with Rich Syntax + """ + code_view = self.query_one("#code", Static) + font = "univers" + if content is not None: + code_view.update(text2art(content, font=font)) + return + element = self._render_file(file_path=file_path, code_view=code_view, font=font) + if isinstance(element, DataTable): + self.code_view.display = False + self.table_view.display = True + if self.code_view.has_focus: + self.table_view.focus() + if scroll_home is True: + self.query_one(DataTable).scroll_home(animate=False) + elif element is not None: + self.table_view.display = False + self.code_view.display = True + if self.table_view.has_focus: + self.code_view.focus() + code_view.update(element) + if scroll_home is True: + self.query_one("#code-view").scroll_home(animate=False) + self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]" + + @on(Mount) + def start_up_app(self) -> None: + """ + On Application Mount - See If a File Should be Displayed + """ + if self.selected_file_path is not None: + self.show_tree = self.force_show_tree + self.render_code_page(file_path=self.selected_file_path) + if self.show_tree is False and self.code_view.display is True: + self.code_view.focus() + elif self.show_tree is False and self.table_view.display is True: + self.table_view.focus() + else: + self.show_tree = True + self.render_code_page( + file_path=pathlib.Path.cwd(), content=__application__.upper() + ) + + @on(BrowsrDirectoryTree.FileSelected) + def handle_file_selected( + self, + message: BrowsrDirectoryTree.FileSelected, # type: ignore[name-defined] + ) -> None: + """ + Called when the user click a file in the directory tree. + """ + self.selected_file_path = upath.UPath(message.path) + file_info = get_file_info(file_path=self.selected_file_path) + self.render_code_page(file_path=upath.UPath(message.path)) + self.file_information.file_info = file_info + + def action_toggle_files(self) -> None: + """ + Called in response to key binding. + """ + self.show_tree = not self.show_tree + + def action_theme(self) -> None: + """ + An action to toggle rich theme. + """ + if self.selected_file_path is None: + return + elif self.theme_index < len(self.rich_themes) - 1: + self.theme_index += 1 + else: + self.theme_index = 0 + self.render_code_page(file_path=self.selected_file_path, scroll_home=False) + + def action_linenos(self) -> None: + """ + An action to toggle line numbers. + """ + if self.selected_file_path is None: + return + self.linenos = not self.linenos + self.render_code_page(file_path=self.selected_file_path, scroll_home=False) + + def _get_download_file_name(self) -> pathlib.Path: + """ + Get the download file name. + """ + download_dir = pathlib.Path.home() / "Downloads" + if not download_dir.exists(): + msg = f"Download directory {download_dir} not found" + raise FileNotFoundError(msg) + download_path = download_dir / self.selected_file_path.name # type: ignore[union-attr] + handled_download_path = handle_duplicate_filenames(file_path=download_path) + return handled_download_path + + @work(thread=True) + def download_selected_file(self) -> None: + """ + Download the selected file. + """ + if self.selected_file_path is None: + return + elif self.selected_file_path.is_dir(): + return + elif is_remote_path(self.selected_file_path): + handled_download_path = self._get_download_file_name() + with self.selected_file_path.open("rb") as file_handle: + with handled_download_path.open("wb") as download_handle: + shutil.copyfileobj(file_handle, download_handle) + + def action_download_file(self) -> None: + """ + Download the selected file. + """ + if self.selected_file_path is None: + return + elif self.selected_file_path.is_dir(): + return + elif is_remote_path(self.selected_file_path): + handled_download_path = self._get_download_file_name() + prompt_message: str = dedent( + f""" + ## File Download + + **Are you sure you want to download that file?** + + **File:** `{self.selected_file_path}` + + **Path:** `{handled_download_path}` + """ + ) + self.confirmation.download_message.update(Markdown(prompt_message)) + self.confirmation.refresh() + self.hidden_table_view = self.table_view.display + self.table_view.display = False + self.confirmation_window.display = True + + def action_parent_dir(self) -> None: + """ + Go to the parent directory + """ + new_path = self.config_object.path.parent.resolve() + if new_path != self.config_object.path: + self.config_object.file_path = str(new_path) + self.directory_tree.path = new_path

@@ -1344,27 +1350,27 @@

Source code in browsr/browsr.py -
342
-343
-344
-345
+            
def _get_download_file_name(self) -> pathlib.Path:
-    """
-    Get the download file name.
-    """
-    download_dir = pathlib.Path.home() / "Downloads"
-    if not download_dir.exists():
-        msg = f"Download directory {download_dir} not found"
-        raise FileNotFoundError(msg)
-    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]
-    handled_download_path = handle_duplicate_filenames(file_path=download_path)
-    return handled_download_path
+352
+353
+354
+355
def _get_download_file_name(self) -> pathlib.Path:
+    """
+    Get the download file name.
+    """
+    download_dir = pathlib.Path.home() / "Downloads"
+    if not download_dir.exists():
+        msg = f"Download directory {download_dir} not found"
+        raise FileNotFoundError(msg)
+    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]
+    handled_download_path = handle_duplicate_filenames(file_path=download_path)
+    return handled_download_path
 
@@ -1389,10 +1395,7 @@

Source code in browsr/browsr.py -
188
-189
-190
-191
+            
191
 192
 193
 194
@@ -1401,19 +1404,22 @@ 

197 198 199 -200

def _handle_file_size(self, file_info: FileInfo) -> None:
-    """
-    Handle a File Size
-    """
-    file_size_mb = file_info.size / 1000 / 1000
-    too_large = file_size_mb >= self.config_object.max_file_size
-    exception = (
-        True
-        if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes
-        else False
-    )
-    if too_large is True and exception is not True:
-        raise FileSizeError("File too large")
+200
+201
+202
+203
def _handle_file_size(self, file_info: FileInfo) -> None:
+    """
+    Handle a File Size
+    """
+    file_size_mb = file_info.size / 1000 / 1000
+    too_large = file_size_mb >= self.config_object.max_file_size
+    exception = (
+        True
+        if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes
+        else False
+    )
+    if too_large is True and exception is not True:
+        raise FileSizeError("File too large")
 
@@ -1438,10 +1444,7 @@

Source code in browsr/browsr.py -
202
-203
-204
-205
+            
205
 206
 207
 208
@@ -1486,55 +1489,58 @@ 

247 248 249 -250

def _render_file(
-    self, file_path: pathlib.Path, code_view: Static, font: str
-) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:
-    """
-    Render a File
-    """
-    try:
-        file_info = get_file_info(file_path=file_path)
-        self._handle_file_size(file_info=file_info)
-        element = self.render_document(file_info=file_info)
-        return element
-    except FileSizeError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except PermissionError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("PERMISSION", font=font)
-            + "\n\n"
-            + text2art("ERROR", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except UnicodeError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except ArchiveFileError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except Exception:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            Traceback(theme=self.rich_themes[self.theme_index], width=None)
-        )
-        self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]"
-    return None
+250
+251
+252
+253
def _render_file(
+    self, file_path: pathlib.Path, code_view: Static, font: str
+) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:
+    """
+    Render a File
+    """
+    try:
+        file_info = get_file_info(file_path=file_path)
+        self._handle_file_size(file_info=file_info)
+        element = self.render_document(file_info=file_info)
+        return element
+    except FileSizeError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except PermissionError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("PERMISSION", font=font)
+            + "\n\n"
+            + text2art("ERROR", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except UnicodeError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except ArchiveFileError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except Exception:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            Traceback(theme=self.rich_themes[self.theme_index], width=None)
+        )
+        self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]"
+    return None
 
@@ -1559,10 +1565,7 @@

Source code in browsr/browsr.py -
369
-370
-371
-372
+            
372
 373
 374
 375
@@ -1584,32 +1587,35 @@ 

391 392 393 -394

def action_download_file(self) -> None:
-    """
-    Download the selected file.
-    """
-    if self.selected_file_path is None:
-        return
-    elif self.selected_file_path.is_dir():
-        return
-    elif is_remote_path(self.selected_file_path):
-        handled_download_path = self._get_download_file_name()
-        prompt_message: str = dedent(
-            f"""
-            ## File Download
-
-            **Are you sure you want to download that file?**
-
-            **File:** `{self.selected_file_path}`
-
-            **Path:** `{handled_download_path}`
-            """
-        )
-        self.confirmation.download_message.update(Markdown(prompt_message))
-        self.confirmation.refresh()
-        self.hidden_table_view = self.table_view.display
-        self.table_view.display = False
-        self.confirmation_window.display = True
+394
+395
+396
+397
def action_download_file(self) -> None:
+    """
+    Download the selected file.
+    """
+    if self.selected_file_path is None:
+        return
+    elif self.selected_file_path.is_dir():
+        return
+    elif is_remote_path(self.selected_file_path):
+        handled_download_path = self._get_download_file_name()
+        prompt_message: str = dedent(
+            f"""
+            ## File Download
+
+            **Are you sure you want to download that file?**
+
+            **File:** `{self.selected_file_path}`
+
+            **Path:** `{handled_download_path}`
+            """
+        )
+        self.confirmation.download_message.update(Markdown(prompt_message))
+        self.confirmation.refresh()
+        self.hidden_table_view = self.table_view.display
+        self.table_view.display = False
+        self.confirmation_window.display = True
 
@@ -1634,21 +1640,21 @@

Source code in browsr/browsr.py -
333
-334
-335
-336
+            
def action_linenos(self) -> None:
-    """
-    An action to toggle line numbers.
-    """
-    if self.selected_file_path is None:
-        return
-    self.linenos = not self.linenos
-    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
+340
+341
+342
+343
def action_linenos(self) -> None:
+    """
+    An action to toggle line numbers.
+    """
+    if self.selected_file_path is None:
+        return
+    self.linenos = not self.linenos
+    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
 
@@ -1673,21 +1679,21 @@

Source code in browsr/browsr.py -
396
-397
-398
-399
+            
def action_parent_dir(self) -> None:
-    """
-    Go to the parent directory
-    """
-    new_path = self.config_object.path.parent.resolve()
-    if new_path != self.config_object.path:
-        self.config_object.file_path = str(new_path)
-        self.directory_tree.path = new_path
+403
+404
+405
+406
def action_parent_dir(self) -> None:
+    """
+    Go to the parent directory
+    """
+    new_path = self.config_object.path.parent.resolve()
+    if new_path != self.config_object.path:
+        self.config_object.file_path = str(new_path)
+        self.directory_tree.path = new_path
 
@@ -1712,27 +1718,27 @@

Source code in browsr/browsr.py -
321
-322
-323
-324
+            
def action_theme(self) -> None:
-    """
-    An action to toggle rich theme.
-    """
-    if self.selected_file_path is None:
-        return
-    elif self.theme_index < len(self.rich_themes) - 1:
-        self.theme_index += 1
-    else:
-        self.theme_index = 0
-    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
+331
+332
+333
+334
def action_theme(self) -> None:
+    """
+    An action to toggle rich theme.
+    """
+    if self.selected_file_path is None:
+        return
+    elif self.theme_index < len(self.rich_themes) - 1:
+        self.theme_index += 1
+    else:
+        self.theme_index = 0
+    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
 
@@ -1757,15 +1763,15 @@

Source code in browsr/browsr.py -
def action_toggle_files(self) -> None:
-    """
-    Called in response to key binding.
-    """
-    self.show_tree = not self.show_tree
+            
def action_toggle_files(self) -> None:
+    """
+    Called in response to key binding.
+    """
+    self.show_tree = not self.show_tree
 
@@ -1907,10 +1913,7 @@

Source code in browsr/browsr.py -
354
-355
-356
-357
+            
357
 358
 359
 360
@@ -1920,20 +1923,23 @@ 

364 365 366 -367

@work(thread=True)
-def download_selected_file(self) -> None:
-    """
-    Download the selected file.
-    """
-    if self.selected_file_path is None:
-        return
-    elif self.selected_file_path.is_dir():
-        return
-    elif is_remote_path(self.selected_file_path):
-        handled_download_path = self._get_download_file_name()
-        with self.selected_file_path.open("rb") as file_handle:
-            with handled_download_path.open("wb") as download_handle:
-                shutil.copyfileobj(file_handle, download_handle)
+367
+368
+369
+370
@work(thread=True)
+def download_selected_file(self) -> None:
+    """
+    Download the selected file.
+    """
+    if self.selected_file_path is None:
+        return
+    elif self.selected_file_path.is_dir():
+        return
+    elif is_remote_path(self.selected_file_path):
+        handled_download_path = self._get_download_file_name()
+        with self.selected_file_path.open("rb") as file_handle:
+            with handled_download_path.open("wb") as download_handle:
+                shutil.copyfileobj(file_handle, download_handle)
 
@@ -1958,10 +1964,7 @@

Source code in browsr/browsr.py -
302
-303
-304
-305
+            
305
 306
 307
 308
@@ -1969,18 +1972,21 @@ 

310 311 312 -313

@on(BrowsrDirectoryTree.FileSelected)
-def handle_file_selected(
-    self,
-    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]
-) -> None:
-    """
-    Called when the user click a file in the directory tree.
-    """
-    self.selected_file_path = upath.UPath(message.path)
-    file_info = get_file_info(file_path=self.selected_file_path)
-    self.render_code_page(file_path=upath.UPath(message.path))
-    self.file_information.file_info = file_info
+313
+314
+315
+316
@on(BrowsrDirectoryTree.FileSelected)
+def handle_file_selected(
+    self,
+    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]
+) -> None:
+    """
+    Called when the user click a file in the directory tree.
+    """
+    self.selected_file_path = upath.UPath(message.path)
+    file_info = get_file_info(file_path=self.selected_file_path)
+    self.render_code_page(file_path=upath.UPath(message.path))
+    self.file_information.file_info = file_info
 
@@ -2005,10 +2011,7 @@

Source code in browsr/browsr.py -
252
-253
-254
-255
+            
255
 256
 257
 258
@@ -2035,37 +2038,40 @@ 

279 280 281 -282

def render_code_page(
-    self,
-    file_path: pathlib.Path,
-    scroll_home: bool = True,
-    content: Optional[Any] = None,
-) -> None:
-    """
-    Render the Code Page with Rich Syntax
-    """
-    code_view = self.query_one("#code", Static)
-    font = "univers"
-    if content is not None:
-        code_view.update(text2art(content, font=font))
-        return
-    element = self._render_file(file_path=file_path, code_view=code_view, font=font)
-    if isinstance(element, DataTable):
-        self.code_view.display = False
-        self.table_view.display = True
-        if self.code_view.has_focus:
-            self.table_view.focus()
-        if scroll_home is True:
-            self.query_one(DataTable).scroll_home(animate=False)
-    elif element is not None:
-        self.table_view.display = False
-        self.code_view.display = True
-        if self.table_view.has_focus:
-            self.code_view.focus()
-        code_view.update(element)
-        if scroll_home is True:
-            self.query_one("#code-view").scroll_home(animate=False)
-        self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]"
+282
+283
+284
+285
def render_code_page(
+    self,
+    file_path: pathlib.Path,
+    scroll_home: bool = True,
+    content: Optional[Any] = None,
+) -> None:
+    """
+    Render the Code Page with Rich Syntax
+    """
+    code_view = self.query_one("#code", Static)
+    font = "univers"
+    if content is not None:
+        code_view.update(text2art(content, font=font))
+        return
+    element = self._render_file(file_path=file_path, code_view=code_view, font=font)
+    if isinstance(element, DataTable):
+        self.code_view.display = False
+        self.table_view.display = True
+        if self.code_view.has_focus:
+            self.table_view.focus()
+        if scroll_home is True:
+            self.query_one(DataTable).scroll_home(animate=False)
+    elif element is not None:
+        self.table_view.display = False
+        self.code_view.display = True
+        if self.table_view.has_focus:
+            self.code_view.focus()
+        code_view.update(element)
+        if scroll_home is True:
+            self.query_one("#code-view").scroll_home(animate=False)
+        self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]"
 
@@ -2196,7 +2202,10 @@

183 184 185 -186

def render_document(
+186
+187
+188
+189
def render_document(
     self,
     file_info: FileInfo,
 ) -> Union[Syntax, Markdown, DataTable[str], Pixels]:
@@ -2225,30 +2234,33 @@ 

elif document.suffix == ".parquet": df = pd.read_parquet(document)[:1000] return self.df_to_table(pandas_dataframe=df, table=self.table_view) - elif document.suffix.lower() in image_file_extensions: - screen_width = self.app.size.width / 4 - content = open_image(document=document, screen_width=screen_width) - return content - elif document.suffix.lower() in [".json"]: - code_str = render_file_to_string(file_info=file_info) - try: - code_obj = json.loads(code_str) - code_lines = json.dumps(code_obj, indent=2).splitlines() - except json.JSONDecodeError: - code_lines = code_str.splitlines() - else: - code_str = render_file_to_string(file_info=file_info) - code_lines = code_str.splitlines() - code = "\n".join(code_lines[:1000]) - lexer = Syntax.guess_lexer(str(document), code=code) - return Syntax( - code=code, - lexer=lexer, - line_numbers=self.linenos, - word_wrap=False, - indent_guides=False, - theme=self.rich_themes[self.theme_index], - ) + elif document.suffix.lower() in [".feather", ".fea"]: + df = pd.read_feather(document)[:1000] + return self.df_to_table(pandas_dataframe=df, table=self.table_view) + elif document.suffix.lower() in image_file_extensions: + screen_width = self.app.size.width / 4 + content = open_image(document=document, screen_width=screen_width) + return content + elif document.suffix.lower() in [".json"]: + code_str = render_file_to_string(file_info=file_info) + try: + code_obj = json.loads(code_str) + code_lines = json.dumps(code_obj, indent=2).splitlines() + except json.JSONDecodeError: + code_lines = code_str.splitlines() + else: + code_str = render_file_to_string(file_info=file_info) + code_lines = code_str.splitlines() + code = "\n".join(code_lines[:1000]) + lexer = Syntax.guess_lexer(str(document), code=code) + return Syntax( + code=code, + lexer=lexer, + line_numbers=self.linenos, + word_wrap=False, + indent_guides=False, + theme=self.rich_themes[self.theme_index], + )

@@ -2273,10 +2285,7 @@

Source code in browsr/browsr.py -
284
-285
-286
-287
+            
287
 288
 289
 290
@@ -2289,23 +2298,26 @@ 

297 298 299 -300

@on(Mount)
-def start_up_app(self) -> None:
-    """
-    On Application Mount - See If a File Should be Displayed
-    """
-    if self.selected_file_path is not None:
-        self.show_tree = self.force_show_tree
-        self.render_code_page(file_path=self.selected_file_path)
-        if self.show_tree is False and self.code_view.display is True:
-            self.code_view.focus()
-        elif self.show_tree is False and self.table_view.display is True:
-            self.table_view.focus()
-    else:
-        self.show_tree = True
-        self.render_code_page(
-            file_path=pathlib.Path.cwd(), content=__application__.upper()
-        )
+300
+301
+302
+303
@on(Mount)
+def start_up_app(self) -> None:
+    """
+    On Application Mount - See If a File Should be Displayed
+    """
+    if self.selected_file_path is not None:
+        self.show_tree = self.force_show_tree
+        self.render_code_page(file_path=self.selected_file_path)
+        if self.show_tree is False and self.code_view.display is True:
+            self.code_view.focus()
+        elif self.show_tree is False and self.table_view.display is True:
+            self.table_view.focus()
+    else:
+        self.show_tree = True
+        self.render_code_page(
+            file_path=pathlib.Path.cwd(), content=__application__.upper()
+        )
 
diff --git a/reference/index.html b/reference/index.html index 8dbeaa3..4422b7c 100644 --- a/reference/index.html +++ b/reference/index.html @@ -994,7 +994,10 @@

400 401 402 -403

class Browsr(BrowsrTextualApp):
+403
+404
+405
+406
class Browsr(BrowsrTextualApp):
     """
     Textual code browser app.
     """
@@ -1101,247 +1104,250 @@ 

elif document.suffix == ".parquet": df = pd.read_parquet(document)[:1000] return self.df_to_table(pandas_dataframe=df, table=self.table_view) - elif document.suffix.lower() in image_file_extensions: - screen_width = self.app.size.width / 4 - content = open_image(document=document, screen_width=screen_width) - return content - elif document.suffix.lower() in [".json"]: - code_str = render_file_to_string(file_info=file_info) - try: - code_obj = json.loads(code_str) - code_lines = json.dumps(code_obj, indent=2).splitlines() - except json.JSONDecodeError: - code_lines = code_str.splitlines() - else: - code_str = render_file_to_string(file_info=file_info) - code_lines = code_str.splitlines() - code = "\n".join(code_lines[:1000]) - lexer = Syntax.guess_lexer(str(document), code=code) - return Syntax( - code=code, - lexer=lexer, - line_numbers=self.linenos, - word_wrap=False, - indent_guides=False, - theme=self.rich_themes[self.theme_index], - ) - - def _handle_file_size(self, file_info: FileInfo) -> None: - """ - Handle a File Size - """ - file_size_mb = file_info.size / 1000 / 1000 - too_large = file_size_mb >= self.config_object.max_file_size - exception = ( - True - if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes - else False - ) - if too_large is True and exception is not True: - raise FileSizeError("File too large") - - def _render_file( - self, file_path: pathlib.Path, code_view: Static, font: str - ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]: - """ - Render a File - """ - try: - file_info = get_file_info(file_path=file_path) - self._handle_file_size(file_info=file_info) - element = self.render_document(file_info=file_info) - return element - except FileSizeError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except PermissionError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("PERMISSION", font=font) - + "\n\n" - + text2art("ERROR", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except UnicodeError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except ArchiveFileError: - self.table_view.display = False - self.code_view.display = True - code_view.update( - text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font) - ) - self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" - except Exception: - self.table_view.display = False - self.code_view.display = True - code_view.update( - Traceback(theme=self.rich_themes[self.theme_index], width=None) - ) - self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]" - return None - - def render_code_page( - self, - file_path: pathlib.Path, - scroll_home: bool = True, - content: Optional[Any] = None, - ) -> None: - """ - Render the Code Page with Rich Syntax - """ - code_view = self.query_one("#code", Static) - font = "univers" - if content is not None: - code_view.update(text2art(content, font=font)) - return - element = self._render_file(file_path=file_path, code_view=code_view, font=font) - if isinstance(element, DataTable): - self.code_view.display = False - self.table_view.display = True - if self.code_view.has_focus: - self.table_view.focus() - if scroll_home is True: - self.query_one(DataTable).scroll_home(animate=False) - elif element is not None: - self.table_view.display = False - self.code_view.display = True - if self.table_view.has_focus: - self.code_view.focus() - code_view.update(element) - if scroll_home is True: - self.query_one("#code-view").scroll_home(animate=False) - self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]" - - @on(Mount) - def start_up_app(self) -> None: - """ - On Application Mount - See If a File Should be Displayed - """ - if self.selected_file_path is not None: - self.show_tree = self.force_show_tree - self.render_code_page(file_path=self.selected_file_path) - if self.show_tree is False and self.code_view.display is True: - self.code_view.focus() - elif self.show_tree is False and self.table_view.display is True: - self.table_view.focus() - else: - self.show_tree = True - self.render_code_page( - file_path=pathlib.Path.cwd(), content=__application__.upper() - ) - - @on(BrowsrDirectoryTree.FileSelected) - def handle_file_selected( - self, - message: BrowsrDirectoryTree.FileSelected, # type: ignore[name-defined] - ) -> None: - """ - Called when the user click a file in the directory tree. - """ - self.selected_file_path = upath.UPath(message.path) - file_info = get_file_info(file_path=self.selected_file_path) - self.render_code_page(file_path=upath.UPath(message.path)) - self.file_information.file_info = file_info - - def action_toggle_files(self) -> None: - """ - Called in response to key binding. - """ - self.show_tree = not self.show_tree - - def action_theme(self) -> None: - """ - An action to toggle rich theme. - """ - if self.selected_file_path is None: - return - elif self.theme_index < len(self.rich_themes) - 1: - self.theme_index += 1 - else: - self.theme_index = 0 - self.render_code_page(file_path=self.selected_file_path, scroll_home=False) - - def action_linenos(self) -> None: - """ - An action to toggle line numbers. - """ - if self.selected_file_path is None: - return - self.linenos = not self.linenos - self.render_code_page(file_path=self.selected_file_path, scroll_home=False) - - def _get_download_file_name(self) -> pathlib.Path: - """ - Get the download file name. - """ - download_dir = pathlib.Path.home() / "Downloads" - if not download_dir.exists(): - msg = f"Download directory {download_dir} not found" - raise FileNotFoundError(msg) - download_path = download_dir / self.selected_file_path.name # type: ignore[union-attr] - handled_download_path = handle_duplicate_filenames(file_path=download_path) - return handled_download_path - - @work(thread=True) - def download_selected_file(self) -> None: - """ - Download the selected file. - """ - if self.selected_file_path is None: - return - elif self.selected_file_path.is_dir(): - return - elif is_remote_path(self.selected_file_path): - handled_download_path = self._get_download_file_name() - with self.selected_file_path.open("rb") as file_handle: - with handled_download_path.open("wb") as download_handle: - shutil.copyfileobj(file_handle, download_handle) - - def action_download_file(self) -> None: - """ - Download the selected file. - """ - if self.selected_file_path is None: - return - elif self.selected_file_path.is_dir(): - return - elif is_remote_path(self.selected_file_path): - handled_download_path = self._get_download_file_name() - prompt_message: str = dedent( - f""" - ## File Download - - **Are you sure you want to download that file?** - - **File:** `{self.selected_file_path}` - - **Path:** `{handled_download_path}` - """ - ) - self.confirmation.download_message.update(Markdown(prompt_message)) - self.confirmation.refresh() - self.hidden_table_view = self.table_view.display - self.table_view.display = False - self.confirmation_window.display = True - - def action_parent_dir(self) -> None: - """ - Go to the parent directory - """ - new_path = self.config_object.path.parent.resolve() - if new_path != self.config_object.path: - self.config_object.file_path = str(new_path) - self.directory_tree.path = new_path + elif document.suffix.lower() in [".feather", ".fea"]: + df = pd.read_feather(document)[:1000] + return self.df_to_table(pandas_dataframe=df, table=self.table_view) + elif document.suffix.lower() in image_file_extensions: + screen_width = self.app.size.width / 4 + content = open_image(document=document, screen_width=screen_width) + return content + elif document.suffix.lower() in [".json"]: + code_str = render_file_to_string(file_info=file_info) + try: + code_obj = json.loads(code_str) + code_lines = json.dumps(code_obj, indent=2).splitlines() + except json.JSONDecodeError: + code_lines = code_str.splitlines() + else: + code_str = render_file_to_string(file_info=file_info) + code_lines = code_str.splitlines() + code = "\n".join(code_lines[:1000]) + lexer = Syntax.guess_lexer(str(document), code=code) + return Syntax( + code=code, + lexer=lexer, + line_numbers=self.linenos, + word_wrap=False, + indent_guides=False, + theme=self.rich_themes[self.theme_index], + ) + + def _handle_file_size(self, file_info: FileInfo) -> None: + """ + Handle a File Size + """ + file_size_mb = file_info.size / 1000 / 1000 + too_large = file_size_mb >= self.config_object.max_file_size + exception = ( + True + if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes + else False + ) + if too_large is True and exception is not True: + raise FileSizeError("File too large") + + def _render_file( + self, file_path: pathlib.Path, code_view: Static, font: str + ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]: + """ + Render a File + """ + try: + file_info = get_file_info(file_path=file_path) + self._handle_file_size(file_info=file_info) + element = self.render_document(file_info=file_info) + return element + except FileSizeError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except PermissionError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("PERMISSION", font=font) + + "\n\n" + + text2art("ERROR", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except UnicodeError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except ArchiveFileError: + self.table_view.display = False + self.code_view.display = True + code_view.update( + text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font) + ) + self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]" + except Exception: + self.table_view.display = False + self.code_view.display = True + code_view.update( + Traceback(theme=self.rich_themes[self.theme_index], width=None) + ) + self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]" + return None + + def render_code_page( + self, + file_path: pathlib.Path, + scroll_home: bool = True, + content: Optional[Any] = None, + ) -> None: + """ + Render the Code Page with Rich Syntax + """ + code_view = self.query_one("#code", Static) + font = "univers" + if content is not None: + code_view.update(text2art(content, font=font)) + return + element = self._render_file(file_path=file_path, code_view=code_view, font=font) + if isinstance(element, DataTable): + self.code_view.display = False + self.table_view.display = True + if self.code_view.has_focus: + self.table_view.focus() + if scroll_home is True: + self.query_one(DataTable).scroll_home(animate=False) + elif element is not None: + self.table_view.display = False + self.code_view.display = True + if self.table_view.has_focus: + self.code_view.focus() + code_view.update(element) + if scroll_home is True: + self.query_one("#code-view").scroll_home(animate=False) + self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]" + + @on(Mount) + def start_up_app(self) -> None: + """ + On Application Mount - See If a File Should be Displayed + """ + if self.selected_file_path is not None: + self.show_tree = self.force_show_tree + self.render_code_page(file_path=self.selected_file_path) + if self.show_tree is False and self.code_view.display is True: + self.code_view.focus() + elif self.show_tree is False and self.table_view.display is True: + self.table_view.focus() + else: + self.show_tree = True + self.render_code_page( + file_path=pathlib.Path.cwd(), content=__application__.upper() + ) + + @on(BrowsrDirectoryTree.FileSelected) + def handle_file_selected( + self, + message: BrowsrDirectoryTree.FileSelected, # type: ignore[name-defined] + ) -> None: + """ + Called when the user click a file in the directory tree. + """ + self.selected_file_path = upath.UPath(message.path) + file_info = get_file_info(file_path=self.selected_file_path) + self.render_code_page(file_path=upath.UPath(message.path)) + self.file_information.file_info = file_info + + def action_toggle_files(self) -> None: + """ + Called in response to key binding. + """ + self.show_tree = not self.show_tree + + def action_theme(self) -> None: + """ + An action to toggle rich theme. + """ + if self.selected_file_path is None: + return + elif self.theme_index < len(self.rich_themes) - 1: + self.theme_index += 1 + else: + self.theme_index = 0 + self.render_code_page(file_path=self.selected_file_path, scroll_home=False) + + def action_linenos(self) -> None: + """ + An action to toggle line numbers. + """ + if self.selected_file_path is None: + return + self.linenos = not self.linenos + self.render_code_page(file_path=self.selected_file_path, scroll_home=False) + + def _get_download_file_name(self) -> pathlib.Path: + """ + Get the download file name. + """ + download_dir = pathlib.Path.home() / "Downloads" + if not download_dir.exists(): + msg = f"Download directory {download_dir} not found" + raise FileNotFoundError(msg) + download_path = download_dir / self.selected_file_path.name # type: ignore[union-attr] + handled_download_path = handle_duplicate_filenames(file_path=download_path) + return handled_download_path + + @work(thread=True) + def download_selected_file(self) -> None: + """ + Download the selected file. + """ + if self.selected_file_path is None: + return + elif self.selected_file_path.is_dir(): + return + elif is_remote_path(self.selected_file_path): + handled_download_path = self._get_download_file_name() + with self.selected_file_path.open("rb") as file_handle: + with handled_download_path.open("wb") as download_handle: + shutil.copyfileobj(file_handle, download_handle) + + def action_download_file(self) -> None: + """ + Download the selected file. + """ + if self.selected_file_path is None: + return + elif self.selected_file_path.is_dir(): + return + elif is_remote_path(self.selected_file_path): + handled_download_path = self._get_download_file_name() + prompt_message: str = dedent( + f""" + ## File Download + + **Are you sure you want to download that file?** + + **File:** `{self.selected_file_path}` + + **Path:** `{handled_download_path}` + """ + ) + self.confirmation.download_message.update(Markdown(prompt_message)) + self.confirmation.refresh() + self.hidden_table_view = self.table_view.display + self.table_view.display = False + self.confirmation_window.display = True + + def action_parent_dir(self) -> None: + """ + Go to the parent directory + """ + new_path = self.config_object.path.parent.resolve() + if new_path != self.config_object.path: + self.config_object.file_path = str(new_path) + self.directory_tree.path = new_path

@@ -1375,27 +1381,27 @@

Source code in browsr/browsr.py -
342
-343
-344
-345
+            
def _get_download_file_name(self) -> pathlib.Path:
-    """
-    Get the download file name.
-    """
-    download_dir = pathlib.Path.home() / "Downloads"
-    if not download_dir.exists():
-        msg = f"Download directory {download_dir} not found"
-        raise FileNotFoundError(msg)
-    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]
-    handled_download_path = handle_duplicate_filenames(file_path=download_path)
-    return handled_download_path
+352
+353
+354
+355
def _get_download_file_name(self) -> pathlib.Path:
+    """
+    Get the download file name.
+    """
+    download_dir = pathlib.Path.home() / "Downloads"
+    if not download_dir.exists():
+        msg = f"Download directory {download_dir} not found"
+        raise FileNotFoundError(msg)
+    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]
+    handled_download_path = handle_duplicate_filenames(file_path=download_path)
+    return handled_download_path
 
@@ -1420,10 +1426,7 @@

Source code in browsr/browsr.py -
188
-189
-190
-191
+            
191
 192
 193
 194
@@ -1432,19 +1435,22 @@ 

197 198 199 -200

def _handle_file_size(self, file_info: FileInfo) -> None:
-    """
-    Handle a File Size
-    """
-    file_size_mb = file_info.size / 1000 / 1000
-    too_large = file_size_mb >= self.config_object.max_file_size
-    exception = (
-        True
-        if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes
-        else False
-    )
-    if too_large is True and exception is not True:
-        raise FileSizeError("File too large")
+200
+201
+202
+203
def _handle_file_size(self, file_info: FileInfo) -> None:
+    """
+    Handle a File Size
+    """
+    file_size_mb = file_info.size / 1000 / 1000
+    too_large = file_size_mb >= self.config_object.max_file_size
+    exception = (
+        True
+        if is_local_path(file_info.file) and ".csv" in file_info.file.suffixes
+        else False
+    )
+    if too_large is True and exception is not True:
+        raise FileSizeError("File too large")
 
@@ -1469,10 +1475,7 @@

Source code in browsr/browsr.py -
202
-203
-204
-205
+            
205
 206
 207
 208
@@ -1517,55 +1520,58 @@ 

247 248 249 -250

def _render_file(
-    self, file_path: pathlib.Path, code_view: Static, font: str
-) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:
-    """
-    Render a File
-    """
-    try:
-        file_info = get_file_info(file_path=file_path)
-        self._handle_file_size(file_info=file_info)
-        element = self.render_document(file_info=file_info)
-        return element
-    except FileSizeError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except PermissionError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("PERMISSION", font=font)
-            + "\n\n"
-            + text2art("ERROR", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except UnicodeError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except ArchiveFileError:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font)
-        )
-        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
-    except Exception:
-        self.table_view.display = False
-        self.code_view.display = True
-        code_view.update(
-            Traceback(theme=self.rich_themes[self.theme_index], width=None)
-        )
-        self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]"
-    return None
+250
+251
+252
+253
def _render_file(
+    self, file_path: pathlib.Path, code_view: Static, font: str
+) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:
+    """
+    Render a File
+    """
+    try:
+        file_info = get_file_info(file_path=file_path)
+        self._handle_file_size(file_info=file_info)
+        element = self.render_document(file_info=file_info)
+        return element
+    except FileSizeError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except PermissionError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("PERMISSION", font=font)
+            + "\n\n"
+            + text2art("ERROR", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except UnicodeError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except ArchiveFileError:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font)
+        )
+        self.sub_title = f"ERROR [{self.rich_themes[self.theme_index]}]"
+    except Exception:
+        self.table_view.display = False
+        self.code_view.display = True
+        code_view.update(
+            Traceback(theme=self.rich_themes[self.theme_index], width=None)
+        )
+        self.sub_title = "ERROR" + f" [{self.rich_themes[self.theme_index]}]"
+    return None
 
@@ -1590,10 +1596,7 @@

Source code in browsr/browsr.py -
369
-370
-371
-372
+            
372
 373
 374
 375
@@ -1615,32 +1618,35 @@ 

391 392 393 -394

def action_download_file(self) -> None:
-    """
-    Download the selected file.
-    """
-    if self.selected_file_path is None:
-        return
-    elif self.selected_file_path.is_dir():
-        return
-    elif is_remote_path(self.selected_file_path):
-        handled_download_path = self._get_download_file_name()
-        prompt_message: str = dedent(
-            f"""
-            ## File Download
-
-            **Are you sure you want to download that file?**
-
-            **File:** `{self.selected_file_path}`
-
-            **Path:** `{handled_download_path}`
-            """
-        )
-        self.confirmation.download_message.update(Markdown(prompt_message))
-        self.confirmation.refresh()
-        self.hidden_table_view = self.table_view.display
-        self.table_view.display = False
-        self.confirmation_window.display = True
+394
+395
+396
+397
def action_download_file(self) -> None:
+    """
+    Download the selected file.
+    """
+    if self.selected_file_path is None:
+        return
+    elif self.selected_file_path.is_dir():
+        return
+    elif is_remote_path(self.selected_file_path):
+        handled_download_path = self._get_download_file_name()
+        prompt_message: str = dedent(
+            f"""
+            ## File Download
+
+            **Are you sure you want to download that file?**
+
+            **File:** `{self.selected_file_path}`
+
+            **Path:** `{handled_download_path}`
+            """
+        )
+        self.confirmation.download_message.update(Markdown(prompt_message))
+        self.confirmation.refresh()
+        self.hidden_table_view = self.table_view.display
+        self.table_view.display = False
+        self.confirmation_window.display = True
 
@@ -1665,21 +1671,21 @@

Source code in browsr/browsr.py -
333
-334
-335
-336
+            
def action_linenos(self) -> None:
-    """
-    An action to toggle line numbers.
-    """
-    if self.selected_file_path is None:
-        return
-    self.linenos = not self.linenos
-    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
+340
+341
+342
+343
def action_linenos(self) -> None:
+    """
+    An action to toggle line numbers.
+    """
+    if self.selected_file_path is None:
+        return
+    self.linenos = not self.linenos
+    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
 
@@ -1704,21 +1710,21 @@

Source code in browsr/browsr.py -
396
-397
-398
-399
+            
def action_parent_dir(self) -> None:
-    """
-    Go to the parent directory
-    """
-    new_path = self.config_object.path.parent.resolve()
-    if new_path != self.config_object.path:
-        self.config_object.file_path = str(new_path)
-        self.directory_tree.path = new_path
+403
+404
+405
+406
def action_parent_dir(self) -> None:
+    """
+    Go to the parent directory
+    """
+    new_path = self.config_object.path.parent.resolve()
+    if new_path != self.config_object.path:
+        self.config_object.file_path = str(new_path)
+        self.directory_tree.path = new_path
 
@@ -1743,27 +1749,27 @@

Source code in browsr/browsr.py -
321
-322
-323
-324
+            
def action_theme(self) -> None:
-    """
-    An action to toggle rich theme.
-    """
-    if self.selected_file_path is None:
-        return
-    elif self.theme_index < len(self.rich_themes) - 1:
-        self.theme_index += 1
-    else:
-        self.theme_index = 0
-    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
+331
+332
+333
+334
def action_theme(self) -> None:
+    """
+    An action to toggle rich theme.
+    """
+    if self.selected_file_path is None:
+        return
+    elif self.theme_index < len(self.rich_themes) - 1:
+        self.theme_index += 1
+    else:
+        self.theme_index = 0
+    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)
 
@@ -1788,15 +1794,15 @@

Source code in browsr/browsr.py -
def action_toggle_files(self) -> None:
-    """
-    Called in response to key binding.
-    """
-    self.show_tree = not self.show_tree
+            
def action_toggle_files(self) -> None:
+    """
+    Called in response to key binding.
+    """
+    self.show_tree = not self.show_tree
 
@@ -1938,10 +1944,7 @@

Source code in browsr/browsr.py -
354
-355
-356
-357
+            
357
 358
 359
 360
@@ -1951,20 +1954,23 @@ 

364 365 366 -367

@work(thread=True)
-def download_selected_file(self) -> None:
-    """
-    Download the selected file.
-    """
-    if self.selected_file_path is None:
-        return
-    elif self.selected_file_path.is_dir():
-        return
-    elif is_remote_path(self.selected_file_path):
-        handled_download_path = self._get_download_file_name()
-        with self.selected_file_path.open("rb") as file_handle:
-            with handled_download_path.open("wb") as download_handle:
-                shutil.copyfileobj(file_handle, download_handle)
+367
+368
+369
+370
@work(thread=True)
+def download_selected_file(self) -> None:
+    """
+    Download the selected file.
+    """
+    if self.selected_file_path is None:
+        return
+    elif self.selected_file_path.is_dir():
+        return
+    elif is_remote_path(self.selected_file_path):
+        handled_download_path = self._get_download_file_name()
+        with self.selected_file_path.open("rb") as file_handle:
+            with handled_download_path.open("wb") as download_handle:
+                shutil.copyfileobj(file_handle, download_handle)
 
@@ -1989,10 +1995,7 @@

Source code in browsr/browsr.py -
302
-303
-304
-305
+            
305
 306
 307
 308
@@ -2000,18 +2003,21 @@ 

310 311 312 -313

@on(BrowsrDirectoryTree.FileSelected)
-def handle_file_selected(
-    self,
-    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]
-) -> None:
-    """
-    Called when the user click a file in the directory tree.
-    """
-    self.selected_file_path = upath.UPath(message.path)
-    file_info = get_file_info(file_path=self.selected_file_path)
-    self.render_code_page(file_path=upath.UPath(message.path))
-    self.file_information.file_info = file_info
+313
+314
+315
+316
@on(BrowsrDirectoryTree.FileSelected)
+def handle_file_selected(
+    self,
+    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]
+) -> None:
+    """
+    Called when the user click a file in the directory tree.
+    """
+    self.selected_file_path = upath.UPath(message.path)
+    file_info = get_file_info(file_path=self.selected_file_path)
+    self.render_code_page(file_path=upath.UPath(message.path))
+    self.file_information.file_info = file_info
 
@@ -2036,10 +2042,7 @@

Source code in browsr/browsr.py -
252
-253
-254
-255
+            
255
 256
 257
 258
@@ -2066,37 +2069,40 @@ 

279 280 281 -282

def render_code_page(
-    self,
-    file_path: pathlib.Path,
-    scroll_home: bool = True,
-    content: Optional[Any] = None,
-) -> None:
-    """
-    Render the Code Page with Rich Syntax
-    """
-    code_view = self.query_one("#code", Static)
-    font = "univers"
-    if content is not None:
-        code_view.update(text2art(content, font=font))
-        return
-    element = self._render_file(file_path=file_path, code_view=code_view, font=font)
-    if isinstance(element, DataTable):
-        self.code_view.display = False
-        self.table_view.display = True
-        if self.code_view.has_focus:
-            self.table_view.focus()
-        if scroll_home is True:
-            self.query_one(DataTable).scroll_home(animate=False)
-    elif element is not None:
-        self.table_view.display = False
-        self.code_view.display = True
-        if self.table_view.has_focus:
-            self.code_view.focus()
-        code_view.update(element)
-        if scroll_home is True:
-            self.query_one("#code-view").scroll_home(animate=False)
-        self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]"
+282
+283
+284
+285
def render_code_page(
+    self,
+    file_path: pathlib.Path,
+    scroll_home: bool = True,
+    content: Optional[Any] = None,
+) -> None:
+    """
+    Render the Code Page with Rich Syntax
+    """
+    code_view = self.query_one("#code", Static)
+    font = "univers"
+    if content is not None:
+        code_view.update(text2art(content, font=font))
+        return
+    element = self._render_file(file_path=file_path, code_view=code_view, font=font)
+    if isinstance(element, DataTable):
+        self.code_view.display = False
+        self.table_view.display = True
+        if self.code_view.has_focus:
+            self.table_view.focus()
+        if scroll_home is True:
+            self.query_one(DataTable).scroll_home(animate=False)
+    elif element is not None:
+        self.table_view.display = False
+        self.code_view.display = True
+        if self.table_view.has_focus:
+            self.code_view.focus()
+        code_view.update(element)
+        if scroll_home is True:
+            self.query_one("#code-view").scroll_home(animate=False)
+        self.sub_title = f"{file_path} [{self.rich_themes[self.theme_index]}]"
 
@@ -2227,7 +2233,10 @@

183 184 185 -186

def render_document(
+186
+187
+188
+189
def render_document(
     self,
     file_info: FileInfo,
 ) -> Union[Syntax, Markdown, DataTable[str], Pixels]:
@@ -2256,30 +2265,33 @@ 

elif document.suffix == ".parquet": df = pd.read_parquet(document)[:1000] return self.df_to_table(pandas_dataframe=df, table=self.table_view) - elif document.suffix.lower() in image_file_extensions: - screen_width = self.app.size.width / 4 - content = open_image(document=document, screen_width=screen_width) - return content - elif document.suffix.lower() in [".json"]: - code_str = render_file_to_string(file_info=file_info) - try: - code_obj = json.loads(code_str) - code_lines = json.dumps(code_obj, indent=2).splitlines() - except json.JSONDecodeError: - code_lines = code_str.splitlines() - else: - code_str = render_file_to_string(file_info=file_info) - code_lines = code_str.splitlines() - code = "\n".join(code_lines[:1000]) - lexer = Syntax.guess_lexer(str(document), code=code) - return Syntax( - code=code, - lexer=lexer, - line_numbers=self.linenos, - word_wrap=False, - indent_guides=False, - theme=self.rich_themes[self.theme_index], - ) + elif document.suffix.lower() in [".feather", ".fea"]: + df = pd.read_feather(document)[:1000] + return self.df_to_table(pandas_dataframe=df, table=self.table_view) + elif document.suffix.lower() in image_file_extensions: + screen_width = self.app.size.width / 4 + content = open_image(document=document, screen_width=screen_width) + return content + elif document.suffix.lower() in [".json"]: + code_str = render_file_to_string(file_info=file_info) + try: + code_obj = json.loads(code_str) + code_lines = json.dumps(code_obj, indent=2).splitlines() + except json.JSONDecodeError: + code_lines = code_str.splitlines() + else: + code_str = render_file_to_string(file_info=file_info) + code_lines = code_str.splitlines() + code = "\n".join(code_lines[:1000]) + lexer = Syntax.guess_lexer(str(document), code=code) + return Syntax( + code=code, + lexer=lexer, + line_numbers=self.linenos, + word_wrap=False, + indent_guides=False, + theme=self.rich_themes[self.theme_index], + )

@@ -2304,10 +2316,7 @@

Source code in browsr/browsr.py -
284
-285
-286
-287
+            
287
 288
 289
 290
@@ -2320,23 +2329,26 @@ 

297 298 299 -300

@on(Mount)
-def start_up_app(self) -> None:
-    """
-    On Application Mount - See If a File Should be Displayed
-    """
-    if self.selected_file_path is not None:
-        self.show_tree = self.force_show_tree
-        self.render_code_page(file_path=self.selected_file_path)
-        if self.show_tree is False and self.code_view.display is True:
-            self.code_view.focus()
-        elif self.show_tree is False and self.table_view.display is True:
-            self.table_view.focus()
-    else:
-        self.show_tree = True
-        self.render_code_page(
-            file_path=pathlib.Path.cwd(), content=__application__.upper()
-        )
+300
+301
+302
+303
@on(Mount)
+def start_up_app(self) -> None:
+    """
+    On Application Mount - See If a File Should be Displayed
+    """
+    if self.selected_file_path is not None:
+        self.show_tree = self.force_show_tree
+        self.render_code_page(file_path=self.selected_file_path)
+        if self.show_tree is False and self.code_view.display is True:
+            self.code_view.focus()
+        elif self.show_tree is False and self.table_view.display is True:
+            self.table_view.focus()
+    else:
+        self.show_tree = True
+        self.render_code_page(
+            file_path=pathlib.Path.cwd(), content=__application__.upper()
+        )
 
diff --git a/search/search_index.json b/search/search_index.json index 38f6e59..9ffdda8 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"browsr","text":"

browsr \ud83d\uddc2\ufe0f is a pleasant file explorer in your terminal. It's a command line TUI (text-based user interface) application that empowers you to browse the contents of local and remote filesystems with your keyboard or mouse.

You can quickly navigate through directories and peek at files whether they're hosted locally, in GitHub, over SSH, in AWS S3, Google Cloud Storage, or Azure Blob Storage. View code files with syntax highlighting, format JSON files, render images, convert data files to navigable datatables, and more.

Screen Recording Your browser does not support the video tag."},{"location":"#installation","title":"Installation","text":"

It's recommended to use pipx instead of pip. pipx installs the package in an isolated environment and makes it available everywhere. If you'd like to use pip instead, just replace pipx with pip in the below command.

pipx install browsr\n
"},{"location":"#extra-installation","title":"Extra Installation","text":"

If you're looking to use browsr on remote file systems, like GitHub or AWS S3, you'll need to install the remote extra. If you'd like to browse parquet files, you'll need to install the parquet extra. Or, even simpler, you can install the all extra to get all the extras.

pipx install \"browsr[all]\"\n
"},{"location":"#usage","title":"Usage","text":"

Simply give browsr a path to a local or remote file / directory. Check out the Documentation for more information about the file systems supported.

"},{"location":"#local","title":"Local","text":"
browsr ~/Downloads/\n
"},{"location":"#github","title":"GitHub","text":"
browsr github://juftin:browsr\n
export GITHUB_TOKEN=\"ghp_1234567890\"\nbrowsr github://juftin:browsr-private@main\n
"},{"location":"#cloud","title":"Cloud","text":"
browsr s3://my-bucket\n

** Currently AWS S3, Google Cloud Storage, and Azure Blob Storage are supported.

"},{"location":"#ssh-sftp","title":"SSH / SFTP","text":"
browsr ssh://username@example.com:22\n
"},{"location":"cli/","title":"Command Line Interface","text":""},{"location":"cli/#browsr","title":"browsr","text":"

browsr \ud83d\uddc2\ufe0f a pleasant file explorer in your terminal

Navigate through directories and peek at files whether they're hosted locally, over SSH, in GitHub, AWS S3, Google Cloud Storage, or Azure Blob Storage. View code files with syntax highlighting, format JSON files, render images, convert data files to navigable datatables, and more.

"},{"location":"cli/#installation","title":"Installation","text":"

It's recommended to install browsr via pipx with all optional dependencies, this enables browsr to access remote cloud storage buckets and open parquet files.

pipx install \"browsr[all]\"\n
"},{"location":"cli/#usage-examples","title":"Usage Examples","text":""},{"location":"cli/#local","title":"Local","text":""},{"location":"cli/#browse-your-current-working-directory","title":"Browse your current working directory","text":"
browsr\n
"},{"location":"cli/#browse-a-local-directory","title":"Browse a local directory","text":"
browsr/path/to/directory\n
"},{"location":"cli/#cloud-storage","title":"Cloud Storage","text":""},{"location":"cli/#browse-an-s3-bucket","title":"Browse an S3 bucket","text":"
browsr s3://bucket-name\n
"},{"location":"cli/#browse-a-gcs-bucket","title":"Browse a GCS bucket","text":"
browsr gs://bucket-name\n
"},{"location":"cli/#browse-azure-services","title":"Browse Azure Services","text":"
browsr adl://bucket-name\nbrowsr az://bucket-name\n
"},{"location":"cli/#pass-extra-arguments-to-cloud-storage","title":"Pass Extra Arguments to Cloud Storage","text":"

Some cloud storage providers require extra arguments to be passed to the filesystem. For example, to browse an anonymous S3 bucket, you need to pass the anon=True argument to the filesystem. This can be done with the -k/--kwargs argument.

browsr s3://anonymous-bucket -k anon=True\n
"},{"location":"cli/#github","title":"GitHub","text":""},{"location":"cli/#browse-a-github-repository","title":"Browse a GitHub repository","text":"
browsr github://juftin:browsr\n
"},{"location":"cli/#browse-a-github-repository-branch","title":"Browse a GitHub Repository Branch","text":"
browsr github://juftin:browsr@main\n
"},{"location":"cli/#browse-a-private-github-repository","title":"Browse a Private GitHub Repository","text":"
export GITHUB_TOKEN=\"ghp_1234567890\"\nbrowsr github://juftin:browsr-private@main\n
"},{"location":"cli/#browse-a-github-repository-subdirectory","title":"Browse a GitHub Repository Subdirectory","text":"
browsr github://juftin:browsr@main/tests\n
"},{"location":"cli/#browse-a-github-url","title":"Browse a GitHub URL","text":"
browsr https://github.com/juftin/browsr\n
"},{"location":"cli/#browse-a-filesystem-over-ssh","title":"Browse a Filesystem over SSH","text":"
browsr ssh://user@host:22\n
"},{"location":"cli/#browse-a-sftp-server","title":"Browse a SFTP Server","text":"
browsr sftp://user@host:22/path/to/directory\n
"},{"location":"cli/#key-bindings","title":"Key Bindings","text":"
  • Q - Quit the application
  • F - Toggle the file tree sidebar
  • T - Toggle the rich theme for code formatting
  • N - Toggle line numbers for code formatting
  • D - Toggle dark mode for the application
  • X - Download the file from cloud storage

Usage:

browsr [OPTIONS] PATH\n

Options:

Name Type Description Default -m, --max-file-size integer Maximum file size in MB for the application to open 20 --version boolean Show the version and exit. False --debug / --no-debug boolean Enable extra debugging output False -k, --kwargs text Key=Value pairs to pass to the filesystem None --help boolean Show this message and exit. False"},{"location":"contributing/","title":"Contributing","text":""},{"location":"contributing/#environment-setup","title":"Environment Setup","text":"

pipx

This documentaion uses pipx to install and manage non-project command line tools like hatch and pre-commit. If you don't already have pipx installed, make sure to see their documentation. If you prefer not to use pipx, you can use pip instead.

  1. Install hatch

    pipx install hatch\n

    pre-commit

    Hatch will attempt to set up pre-commit hooks for you using pre-commit. If you don't already, make sure to install pre-commit as well: pipx install pre-commit

  2. Build the Virtual Environment

    hatch env create\n
  3. If you need to, you can link hatch's virtual environment to your IDE. It's located in the .venv directory at the root of the project.

  4. Activate the Virtual Environment

    hatch shell\n
"},{"location":"contributing/#using-hatch","title":"Using Hatch","text":""},{"location":"contributing/#hatch-cheat-sheet","title":"Hatch Cheat Sheet","text":"Command Description Command Notes Run Tests hatch run cov Runs tests with pytest and coverage Run Formatting hatch run lint:fmt Runs ruff code formatter Run Linting hatch run lint:all Runs ruff and mypy linters / type checkers Run Type Checking hatch run lint:typing Runs mypy type checker Update Requirements Lock Files hatch run gen:reqs Updating lock file using pip-compile Upgrade Dependencies hatch run gen:reqs-update Updating lock file using pip-compile and --update flag Serve the Documentation hatch run docs:serve Serve the documentation using MkDocs Run the pre-commit Hooks hatch run lint:precommit Runs the pre-commit hooks on all files"},{"location":"contributing/#hatch-explanation","title":"Hatch Explanation","text":"

Hatch is a Python package manager. Its most basic use is as a standardized build-system. However, hatch also has some extra features which this project takes advantage of. These features include virtual environment management and the organization of common scripts like linting and testing. All the operations in hatch take place in one of its managed virtual environments.

Hatch has a variety of environments, to see them simply ask hatch:

hatch CLIOutput
hatch env show\n
                                   Standalone                                   \n\u250f\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Na\u2026 \u2503 Type     \u2503 Fea\u2026 \u2503 Dependencies         \u2503 Environment variabl\u2026 \u2503 Scrip\u2026 \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 de\u2026 \u2502 pip-com\u2026 \u2502 all  \u2502                      \u2502 GITHUB_TOKEN=placeh\u2026 \u2502 cov    \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 test   \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 do\u2026 \u2502 pip-com\u2026 \u2502      \u2502 markdown-callouts    \u2502                      \u2502 build  \u2502\n\u2502     \u2502          \u2502      \u2502 markdown-exec        \u2502                      \u2502 gh-de\u2026 \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs               \u2502                      \u2502 serve  \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-autorefs      \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-click         \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-gen-files     \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-literate-nav  \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-material      \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-section-index \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocstrings         \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocstrings-python  \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 pymdown-extensions   \u2502                      \u2502        \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 gen \u2502 virtual  \u2502      \u2502                      \u2502                      \u2502 relea\u2026 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 li\u2026 \u2502 pip-com\u2026 \u2502      \u2502 mypy>=1.6.1          \u2502                      \u2502 all    \u2502\n\u2502     \u2502          \u2502      \u2502 ruff~=0.1.7          \u2502                      \u2502 fmt    \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 preco\u2026 \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 style  \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 typing \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 te\u2026 \u2502 pip-com\u2026 \u2502 all  \u2502 pytest               \u2502 GITHUB_TOKEN=placeh\u2026 \u2502 cov    \u2502\n\u2502     \u2502          \u2502      \u2502 pytest-cov           \u2502                      \u2502 test   \u2502\n\u2502     \u2502          \u2502      \u2502 pytest-textual-snap\u2026 \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 pytest-vcr~=1.0.2    \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 textual-dev~=1.0.1   \u2502                      \u2502        \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                    Matrices                                    \n\u250f\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2513\n\u2503\u2503 Type \u2503 En\u2026 \u2503 \u2026 \u2503 Dependencies     \u2503 Environment variables                \u2503  \u2503\n\u2521\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2529\n\u2502\u2502 pip\u2026 \u2502 al\u2026 \u2502 \u2026 \u2502 pytest           \u2502 GITHUB_TOKEN={env:GITHUB_TOKEN:plac\u2026 \u2502  \u2502\n\u2502\u2502      \u2502 al\u2026 \u2502   \u2502 pytest-cov       \u2502                                      \u2502  \u2502\n\u2502\u2502      \u2502 al\u2026 \u2502   \u2502 pytest-textual-\u2026 \u2502                                      \u2502  \u2502\n\u2502\u2502      \u2502 al\u2026 \u2502   \u2502 pytest-vcr~=1.0\u2026 \u2502                                      \u2502  \u2502\n\u2502\u2502      \u2502     \u2502   \u2502 textual-dev~=1.\u2026 \u2502                                      \u2502  \u2502\n\u2514\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2518\n

That above command will tell you that there are five environments that you can use:

  • default
  • docs
  • gen
  • lint
  • test

Each of these environments has a set of commands that you can run. To see the commands for a specific environment, run:

hatch CLIOutput
hatch env show default\n
                               Standalone                                \n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Name    \u2503 Type        \u2503 Features \u2503 Environment variables    \u2503 Scripts \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 default \u2502 pip-compile \u2502 all      \u2502 GITHUB_TOKEN=placeholder \u2502 cov     \u2502\n\u2502         \u2502             \u2502          \u2502                          \u2502 test    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n

Here we can see that the default environment has the following commands:

  • cov
  • test

The one that we're interested in is cov, which will run the tests for the project.

hatch run cov\n

Since cov is in the default environment, we can run it without specifying the environment. However, to run the serve command in the docs environment, we need to specify the environment:

hatch run docs:serve\n

You can see what scripts are available using the env show command

hatch CLIOutput
hatch env show docs\n
                       Standalone                        \n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Name \u2503 Type        \u2503 Dependencies         \u2503 Scripts   \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 docs \u2502 pip-compile \u2502 markdown-callouts    \u2502 build     \u2502\n\u2502      \u2502             \u2502 markdown-exec        \u2502 gh-deploy \u2502\n\u2502      \u2502             \u2502 mkdocs               \u2502 serve     \u2502\n\u2502      \u2502             \u2502 mkdocs-autorefs      \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-click         \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-gen-files     \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-literate-nav  \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-material      \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-section-index \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocstrings         \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocstrings-python  \u2502           \u2502\n\u2502      \u2502             \u2502 pymdown-extensions   \u2502           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"contributing/#committing-code","title":"Committing Code","text":"

This project uses pre-commit to run a set of checks on the code before it is committed. The pre-commit hooks are installed by hatch automatically when you run it for the first time.

This project uses semantic-versioning standards, managed by semantic-release. Releases for this project are handled entirely by CI/CD via pull requests being merged into the main branch. Contributions follow the gitmoji standards with conventional commits.

While you can denote other changes on your commit messages with gitmoji, the following commit message emoji prefixes are the only ones to trigger new releases:

Emoji Shortcode Description Semver \ud83d\udca5 :boom: Introduce breaking changes. Major \u2728 :sparkles: Introduce new features. Minor \ud83d\udc1b :bug: Fix a bug. Patch \ud83d\ude91 :ambulance: Critical hotfix. Patch \ud83d\udd12 :lock: Fix security issues. Patch

Most features can be squash merged into a single commit on a pull-request. When merging multiple commits, they will be summarized into a single release.

If you're working on a new feature, your commit message might look like:

\u2728 New Feature Description\n

Bug fix commits would look like this:

\ud83d\udc1b Bug Fix Description\n

If you're working on a feature that introduces breaking changes, your commit message might look like:

\ud83d\udca5 Breaking Change Description\n

Other commits that don't trigger a release might look like this:

\ud83d\udcdd Documentation Update Description\n\ud83d\udc77 CI/CD Update Description\n\ud83e\uddea Testing Changes Description\n\ud83d\ude9a Moving/Renaming Description\n\u2b06\ufe0f Dependency Upgrade Description\n
"},{"location":"contributing/#pre-releases","title":"Pre-Releases","text":"

semantic-release supports pre-releases. To trigger a pre-release, you would merge your pull request into an alpha or beta branch.

"},{"location":"contributing/#specific-release-versions","title":"Specific Release Versions","text":"

In some cases you need more advanced control around what kind of release you need to create. If you need to release a specific version, you can do so by creating a new branch with the version number as the branch name. For example, if the current version is 2.3.2, but you need to release a fix as 1.2.5, you would create a branch named 1.2.x and merge your changes into that branch.

See the semantic-release documentation for more information about branch based releases and other advanced release cases.

"},{"location":"reference/","title":"browsr","text":"

browsr

"},{"location":"reference/#browsr.Browsr","title":"Browsr","text":"

Bases: BrowsrTextualApp

Textual code browser app.

Source code in browsr/browsr.py
class Browsr(BrowsrTextualApp):\n    \"\"\"\n    Textual code browser app.\n    \"\"\"\n\n    TITLE = __application__\n    CSS_PATH = \"browsr.css\"\n    BINDINGS: ClassVar[List[BindingType]] = [\n        Binding(key=\"q\", action=\"quit\", description=\"Quit\"),\n        Binding(key=\"f\", action=\"toggle_files\", description=\"Toggle Files\"),\n        Binding(key=\"t\", action=\"theme\", description=\"Toggle Theme\"),\n        Binding(key=\"n\", action=\"linenos\", description=\"Toggle Line Numbers\"),\n        Binding(key=\"d\", action=\"toggle_dark\", description=\"Toggle Dark Mode\"),\n        Binding(key=\".\", action=\"parent_dir\", description=\"Parent Directory\"),\n    ]\n\n    show_tree = var(True)\n    theme_index = var(0)\n    linenos = var(False)\n    rich_themes = favorite_themes\n    selected_file_path: Union[upath.UPath, pathlib.Path, None, var[None]] = var(None)\n    force_show_tree = var(False)\n    hidden_table_view = var(False)\n\n    def watch_show_tree(self, show_tree: bool) -> None:\n        \"\"\"\n        Called when show_tree is modified.\n        \"\"\"\n        self.set_class(show_tree, \"-show-tree\")\n\n    def compose(self) -> Iterable[Widget]:\n        \"\"\"\n        Compose our UI.\n        \"\"\"\n        file_path = self.config_object.path\n        if is_remote_path(file_path):\n            self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n        if file_path.is_file():\n            self.selected_file_path = file_path\n            file_path = file_path.parent\n        elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n            if TYPE_CHECKING:\n                assert isinstance(file_path, pathlib.Path)\n            self.selected_file_path = file_path.joinpath(\"README.md\")\n            self.force_show_tree = True\n        self.header = Header()\n        yield self.header\n        self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n        self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n        self.table_view: DataTable[str] = VimDataTable(\n            zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n        )\n        self.table_view.display = False\n        self.confirmation = ConfirmationPopUp()\n        self.confirmation_window = Container(\n            self.confirmation, id=\"confirmation-container\"\n        )\n        self.confirmation_window.display = False\n        self.container = Container(\n            self.directory_tree,\n            self.code_view,\n            self.table_view,\n            self.confirmation_window,\n        )\n        yield self.container\n        self.file_information = CurrentFileInfoBar()\n        self.info_bar = Horizontal(\n            self.file_information,\n            id=\"file-info-bar\",\n        )\n        if self.selected_file_path is not None:\n            self.file_information.file_info = get_file_info(\n                file_path=self.selected_file_path\n            )\n        yield self.info_bar\n        self.footer = Footer()\n        yield self.footer\n\n    def render_document(\n        self,\n        file_info: FileInfo,\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n        \"\"\"\n        Render a Code Doc Given Its Extension\n\n        Parameters\n        ----------\n        file_info: FileInfo\n            The file info object for the file to render.\n\n        Returns\n        -------\n        Union[Syntax, Markdown, DataTable[str], Pixels]\n        \"\"\"\n        document = file_info.file\n        if document.suffix == \".md\":\n            return Markdown(\n                document.read_text(encoding=\"utf-8\"),\n                code_theme=self.rich_themes[self.theme_index],\n                hyperlinks=True,\n            )\n        elif \".csv\" in document.suffixes:\n            df = pd.read_csv(document, nrows=1000)\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix == \".parquet\":\n            df = pd.read_parquet(document)[:1000]\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix.lower() in image_file_extensions:\n            screen_width = self.app.size.width / 4\n            content = open_image(document=document, screen_width=screen_width)\n            return content\n        elif document.suffix.lower() in [\".json\"]:\n            code_str = render_file_to_string(file_info=file_info)\n            try:\n                code_obj = json.loads(code_str)\n                code_lines = json.dumps(code_obj, indent=2).splitlines()\n            except json.JSONDecodeError:\n                code_lines = code_str.splitlines()\n        else:\n            code_str = render_file_to_string(file_info=file_info)\n            code_lines = code_str.splitlines()\n        code = \"\\n\".join(code_lines[:1000])\n        lexer = Syntax.guess_lexer(str(document), code=code)\n        return Syntax(\n            code=code,\n            lexer=lexer,\n            line_numbers=self.linenos,\n            word_wrap=False,\n            indent_guides=False,\n            theme=self.rich_themes[self.theme_index],\n        )\n\n    def _handle_file_size(self, file_info: FileInfo) -> None:\n        \"\"\"\n        Handle a File Size\n        \"\"\"\n        file_size_mb = file_info.size / 1000 / 1000\n        too_large = file_size_mb >= self.config_object.max_file_size\n        exception = (\n            True\n            if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n            else False\n        )\n        if too_large is True and exception is not True:\n            raise FileSizeError(\"File too large\")\n\n    def _render_file(\n        self, file_path: pathlib.Path, code_view: Static, font: str\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n        \"\"\"\n        Render a File\n        \"\"\"\n        try:\n            file_info = get_file_info(file_path=file_path)\n            self._handle_file_size(file_info=file_info)\n            element = self.render_document(file_info=file_info)\n            return element\n        except FileSizeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except PermissionError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"PERMISSION\", font=font)\n                + \"\\n\\n\"\n                + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except UnicodeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except ArchiveFileError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except Exception:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                Traceback(theme=self.rich_themes[self.theme_index], width=None)\n            )\n            self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n        return None\n\n    def render_code_page(\n        self,\n        file_path: pathlib.Path,\n        scroll_home: bool = True,\n        content: Optional[Any] = None,\n    ) -> None:\n        \"\"\"\n        Render the Code Page with Rich Syntax\n        \"\"\"\n        code_view = self.query_one(\"#code\", Static)\n        font = \"univers\"\n        if content is not None:\n            code_view.update(text2art(content, font=font))\n            return\n        element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n        if isinstance(element, DataTable):\n            self.code_view.display = False\n            self.table_view.display = True\n            if self.code_view.has_focus:\n                self.table_view.focus()\n            if scroll_home is True:\n                self.query_one(DataTable).scroll_home(animate=False)\n        elif element is not None:\n            self.table_view.display = False\n            self.code_view.display = True\n            if self.table_view.has_focus:\n                self.code_view.focus()\n            code_view.update(element)\n            if scroll_home is True:\n                self.query_one(\"#code-view\").scroll_home(animate=False)\n            self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n\n    @on(Mount)\n    def start_up_app(self) -> None:\n        \"\"\"\n        On Application Mount - See If a File Should be Displayed\n        \"\"\"\n        if self.selected_file_path is not None:\n            self.show_tree = self.force_show_tree\n            self.render_code_page(file_path=self.selected_file_path)\n            if self.show_tree is False and self.code_view.display is True:\n                self.code_view.focus()\n            elif self.show_tree is False and self.table_view.display is True:\n                self.table_view.focus()\n        else:\n            self.show_tree = True\n            self.render_code_page(\n                file_path=pathlib.Path.cwd(), content=__application__.upper()\n            )\n\n    @on(BrowsrDirectoryTree.FileSelected)\n    def handle_file_selected(\n        self,\n        message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n    ) -> None:\n        \"\"\"\n        Called when the user click a file in the directory tree.\n        \"\"\"\n        self.selected_file_path = upath.UPath(message.path)\n        file_info = get_file_info(file_path=self.selected_file_path)\n        self.render_code_page(file_path=upath.UPath(message.path))\n        self.file_information.file_info = file_info\n\n    def action_toggle_files(self) -> None:\n        \"\"\"\n        Called in response to key binding.\n        \"\"\"\n        self.show_tree = not self.show_tree\n\n    def action_theme(self) -> None:\n        \"\"\"\n        An action to toggle rich theme.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.theme_index < len(self.rich_themes) - 1:\n            self.theme_index += 1\n        else:\n            self.theme_index = 0\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def action_linenos(self) -> None:\n        \"\"\"\n        An action to toggle line numbers.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        self.linenos = not self.linenos\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def _get_download_file_name(self) -> pathlib.Path:\n        \"\"\"\n        Get the download file name.\n        \"\"\"\n        download_dir = pathlib.Path.home() / \"Downloads\"\n        if not download_dir.exists():\n            msg = f\"Download directory {download_dir} not found\"\n            raise FileNotFoundError(msg)\n        download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n        handled_download_path = handle_duplicate_filenames(file_path=download_path)\n        return handled_download_path\n\n    @work(thread=True)\n    def download_selected_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            with self.selected_file_path.open(\"rb\") as file_handle:\n                with handled_download_path.open(\"wb\") as download_handle:\n                    shutil.copyfileobj(file_handle, download_handle)\n\n    def action_download_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            prompt_message: str = dedent(\n                f\"\"\"\n                ## File Download\n\n                **Are you sure you want to download that file?**\n\n                **File:** `{self.selected_file_path}`\n\n                **Path:** `{handled_download_path}`\n                \"\"\"\n            )\n            self.confirmation.download_message.update(Markdown(prompt_message))\n            self.confirmation.refresh()\n            self.hidden_table_view = self.table_view.display\n            self.table_view.display = False\n            self.confirmation_window.display = True\n\n    def action_parent_dir(self) -> None:\n        \"\"\"\n        Go to the parent directory\n        \"\"\"\n        new_path = self.config_object.path.parent.resolve()\n        if new_path != self.config_object.path:\n            self.config_object.file_path = str(new_path)\n            self.directory_tree.path = new_path\n
"},{"location":"reference/#browsr.Browsr._get_download_file_name","title":"_get_download_file_name()","text":"

Get the download file name.

Source code in browsr/browsr.py
def _get_download_file_name(self) -> pathlib.Path:\n    \"\"\"\n    Get the download file name.\n    \"\"\"\n    download_dir = pathlib.Path.home() / \"Downloads\"\n    if not download_dir.exists():\n        msg = f\"Download directory {download_dir} not found\"\n        raise FileNotFoundError(msg)\n    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n    handled_download_path = handle_duplicate_filenames(file_path=download_path)\n    return handled_download_path\n
"},{"location":"reference/#browsr.Browsr._handle_file_size","title":"_handle_file_size(file_info)","text":"

Handle a File Size

Source code in browsr/browsr.py
def _handle_file_size(self, file_info: FileInfo) -> None:\n    \"\"\"\n    Handle a File Size\n    \"\"\"\n    file_size_mb = file_info.size / 1000 / 1000\n    too_large = file_size_mb >= self.config_object.max_file_size\n    exception = (\n        True\n        if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n        else False\n    )\n    if too_large is True and exception is not True:\n        raise FileSizeError(\"File too large\")\n
"},{"location":"reference/#browsr.Browsr._render_file","title":"_render_file(file_path, code_view, font)","text":"

Render a File

Source code in browsr/browsr.py
def _render_file(\n    self, file_path: pathlib.Path, code_view: Static, font: str\n) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n    \"\"\"\n    Render a File\n    \"\"\"\n    try:\n        file_info = get_file_info(file_path=file_path)\n        self._handle_file_size(file_info=file_info)\n        element = self.render_document(file_info=file_info)\n        return element\n    except FileSizeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except PermissionError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"PERMISSION\", font=font)\n            + \"\\n\\n\"\n            + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except UnicodeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except ArchiveFileError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except Exception:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            Traceback(theme=self.rich_themes[self.theme_index], width=None)\n        )\n        self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n    return None\n
"},{"location":"reference/#browsr.Browsr.action_download_file","title":"action_download_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
def action_download_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        prompt_message: str = dedent(\n            f\"\"\"\n            ## File Download\n\n            **Are you sure you want to download that file?**\n\n            **File:** `{self.selected_file_path}`\n\n            **Path:** `{handled_download_path}`\n            \"\"\"\n        )\n        self.confirmation.download_message.update(Markdown(prompt_message))\n        self.confirmation.refresh()\n        self.hidden_table_view = self.table_view.display\n        self.table_view.display = False\n        self.confirmation_window.display = True\n
"},{"location":"reference/#browsr.Browsr.action_linenos","title":"action_linenos()","text":"

An action to toggle line numbers.

Source code in browsr/browsr.py
def action_linenos(self) -> None:\n    \"\"\"\n    An action to toggle line numbers.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    self.linenos = not self.linenos\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/#browsr.Browsr.action_parent_dir","title":"action_parent_dir()","text":"

Go to the parent directory

Source code in browsr/browsr.py
def action_parent_dir(self) -> None:\n    \"\"\"\n    Go to the parent directory\n    \"\"\"\n    new_path = self.config_object.path.parent.resolve()\n    if new_path != self.config_object.path:\n        self.config_object.file_path = str(new_path)\n        self.directory_tree.path = new_path\n
"},{"location":"reference/#browsr.Browsr.action_theme","title":"action_theme()","text":"

An action to toggle rich theme.

Source code in browsr/browsr.py
def action_theme(self) -> None:\n    \"\"\"\n    An action to toggle rich theme.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.theme_index < len(self.rich_themes) - 1:\n        self.theme_index += 1\n    else:\n        self.theme_index = 0\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/#browsr.Browsr.action_toggle_files","title":"action_toggle_files()","text":"

Called in response to key binding.

Source code in browsr/browsr.py
def action_toggle_files(self) -> None:\n    \"\"\"\n    Called in response to key binding.\n    \"\"\"\n    self.show_tree = not self.show_tree\n
"},{"location":"reference/#browsr.Browsr.compose","title":"compose()","text":"

Compose our UI.

Source code in browsr/browsr.py
def compose(self) -> Iterable[Widget]:\n    \"\"\"\n    Compose our UI.\n    \"\"\"\n    file_path = self.config_object.path\n    if is_remote_path(file_path):\n        self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n    if file_path.is_file():\n        self.selected_file_path = file_path\n        file_path = file_path.parent\n    elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n        if TYPE_CHECKING:\n            assert isinstance(file_path, pathlib.Path)\n        self.selected_file_path = file_path.joinpath(\"README.md\")\n        self.force_show_tree = True\n    self.header = Header()\n    yield self.header\n    self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n    self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n    self.table_view: DataTable[str] = VimDataTable(\n        zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n    )\n    self.table_view.display = False\n    self.confirmation = ConfirmationPopUp()\n    self.confirmation_window = Container(\n        self.confirmation, id=\"confirmation-container\"\n    )\n    self.confirmation_window.display = False\n    self.container = Container(\n        self.directory_tree,\n        self.code_view,\n        self.table_view,\n        self.confirmation_window,\n    )\n    yield self.container\n    self.file_information = CurrentFileInfoBar()\n    self.info_bar = Horizontal(\n        self.file_information,\n        id=\"file-info-bar\",\n    )\n    if self.selected_file_path is not None:\n        self.file_information.file_info = get_file_info(\n            file_path=self.selected_file_path\n        )\n    yield self.info_bar\n    self.footer = Footer()\n    yield self.footer\n
"},{"location":"reference/#browsr.Browsr.download_selected_file","title":"download_selected_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
@work(thread=True)\ndef download_selected_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        with self.selected_file_path.open(\"rb\") as file_handle:\n            with handled_download_path.open(\"wb\") as download_handle:\n                shutil.copyfileobj(file_handle, download_handle)\n
"},{"location":"reference/#browsr.Browsr.handle_file_selected","title":"handle_file_selected(message)","text":"

Called when the user click a file in the directory tree.

Source code in browsr/browsr.py
@on(BrowsrDirectoryTree.FileSelected)\ndef handle_file_selected(\n    self,\n    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n) -> None:\n    \"\"\"\n    Called when the user click a file in the directory tree.\n    \"\"\"\n    self.selected_file_path = upath.UPath(message.path)\n    file_info = get_file_info(file_path=self.selected_file_path)\n    self.render_code_page(file_path=upath.UPath(message.path))\n    self.file_information.file_info = file_info\n
"},{"location":"reference/#browsr.Browsr.render_code_page","title":"render_code_page(file_path, scroll_home=True, content=None)","text":"

Render the Code Page with Rich Syntax

Source code in browsr/browsr.py
def render_code_page(\n    self,\n    file_path: pathlib.Path,\n    scroll_home: bool = True,\n    content: Optional[Any] = None,\n) -> None:\n    \"\"\"\n    Render the Code Page with Rich Syntax\n    \"\"\"\n    code_view = self.query_one(\"#code\", Static)\n    font = \"univers\"\n    if content is not None:\n        code_view.update(text2art(content, font=font))\n        return\n    element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n    if isinstance(element, DataTable):\n        self.code_view.display = False\n        self.table_view.display = True\n        if self.code_view.has_focus:\n            self.table_view.focus()\n        if scroll_home is True:\n            self.query_one(DataTable).scroll_home(animate=False)\n    elif element is not None:\n        self.table_view.display = False\n        self.code_view.display = True\n        if self.table_view.has_focus:\n            self.code_view.focus()\n        code_view.update(element)\n        if scroll_home is True:\n            self.query_one(\"#code-view\").scroll_home(animate=False)\n        self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n
"},{"location":"reference/#browsr.Browsr.render_document","title":"render_document(file_info)","text":"

Render a Code Doc Given Its Extension

Parameters:

Name Type Description Default file_info FileInfo

The file info object for the file to render.

required

Returns:

Type Description Union[Syntax, Markdown, DataTable[str], Pixels] Source code in browsr/browsr.py
def render_document(\n    self,\n    file_info: FileInfo,\n) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n    \"\"\"\n    Render a Code Doc Given Its Extension\n\n    Parameters\n    ----------\n    file_info: FileInfo\n        The file info object for the file to render.\n\n    Returns\n    -------\n    Union[Syntax, Markdown, DataTable[str], Pixels]\n    \"\"\"\n    document = file_info.file\n    if document.suffix == \".md\":\n        return Markdown(\n            document.read_text(encoding=\"utf-8\"),\n            code_theme=self.rich_themes[self.theme_index],\n            hyperlinks=True,\n        )\n    elif \".csv\" in document.suffixes:\n        df = pd.read_csv(document, nrows=1000)\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix == \".parquet\":\n        df = pd.read_parquet(document)[:1000]\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix.lower() in image_file_extensions:\n        screen_width = self.app.size.width / 4\n        content = open_image(document=document, screen_width=screen_width)\n        return content\n    elif document.suffix.lower() in [\".json\"]:\n        code_str = render_file_to_string(file_info=file_info)\n        try:\n            code_obj = json.loads(code_str)\n            code_lines = json.dumps(code_obj, indent=2).splitlines()\n        except json.JSONDecodeError:\n            code_lines = code_str.splitlines()\n    else:\n        code_str = render_file_to_string(file_info=file_info)\n        code_lines = code_str.splitlines()\n    code = \"\\n\".join(code_lines[:1000])\n    lexer = Syntax.guess_lexer(str(document), code=code)\n    return Syntax(\n        code=code,\n        lexer=lexer,\n        line_numbers=self.linenos,\n        word_wrap=False,\n        indent_guides=False,\n        theme=self.rich_themes[self.theme_index],\n    )\n
"},{"location":"reference/#browsr.Browsr.start_up_app","title":"start_up_app()","text":"

On Application Mount - See If a File Should be Displayed

Source code in browsr/browsr.py
@on(Mount)\ndef start_up_app(self) -> None:\n    \"\"\"\n    On Application Mount - See If a File Should be Displayed\n    \"\"\"\n    if self.selected_file_path is not None:\n        self.show_tree = self.force_show_tree\n        self.render_code_page(file_path=self.selected_file_path)\n        if self.show_tree is False and self.code_view.display is True:\n            self.code_view.focus()\n        elif self.show_tree is False and self.table_view.display is True:\n            self.table_view.focus()\n    else:\n        self.show_tree = True\n        self.render_code_page(\n            file_path=pathlib.Path.cwd(), content=__application__.upper()\n        )\n
"},{"location":"reference/#browsr.Browsr.watch_show_tree","title":"watch_show_tree(show_tree)","text":"

Called when show_tree is modified.

Source code in browsr/browsr.py
def watch_show_tree(self, show_tree: bool) -> None:\n    \"\"\"\n    Called when show_tree is modified.\n    \"\"\"\n    self.set_class(show_tree, \"-show-tree\")\n
"},{"location":"reference/#browsr.BrowsrDirectoryTree","title":"BrowsrDirectoryTree","text":"

Bases: UniversalDirectoryTree

A DirectoryTree that can handle any filesystem.

Source code in browsr/universal_directory_tree.py
class BrowsrDirectoryTree(UniversalDirectoryTree):\n    \"\"\"\n    A DirectoryTree that can handle any filesystem.\n    \"\"\"\n\n    PATH: type[BrowsrPath] = BrowsrPath\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *UniversalDirectoryTree.BINDINGS,\n        *vim_cursor_bindings,\n    ]\n\n    @classmethod\n    def _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n        \"\"\"\n        Handle scenarios when someone wants to browse all of s3\n\n        This is because S3FS handles the root directory differently\n        than other filesystems\n        \"\"\"\n        if str(dir_path) == \"s3:/\":\n            sub_buckets = sorted(\n                Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n            )\n            return sub_buckets\n        return None\n\n    def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n        \"\"\"\n        Populate the given tree node with the given directory content.\n\n        This function overrides the original textual method to handle root level\n        cloud buckets.\n        \"\"\"\n        top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n        if top_level_buckets is not None:\n            content = top_level_buckets\n        node.remove_children()\n        for path in content:\n            if top_level_buckets is not None:\n                path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n            else:\n                path_name = path.name\n            node.add(\n                path_name,\n                data=DirEntry(path),\n                allow_expand=self._safe_is_dir(path),\n            )\n        node.expand()\n
"},{"location":"reference/#browsr.BrowsrDirectoryTree._handle_top_level_bucket","title":"_handle_top_level_bucket(dir_path) classmethod","text":"

Handle scenarios when someone wants to browse all of s3

This is because S3FS handles the root directory differently than other filesystems

Source code in browsr/universal_directory_tree.py
@classmethod\ndef _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n    \"\"\"\n    Handle scenarios when someone wants to browse all of s3\n\n    This is because S3FS handles the root directory differently\n    than other filesystems\n    \"\"\"\n    if str(dir_path) == \"s3:/\":\n        sub_buckets = sorted(\n            Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n        )\n        return sub_buckets\n    return None\n
"},{"location":"reference/#browsr.BrowsrDirectoryTree._populate_node","title":"_populate_node(node, content)","text":"

Populate the given tree node with the given directory content.

This function overrides the original textual method to handle root level cloud buckets.

Source code in browsr/universal_directory_tree.py
def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n    \"\"\"\n    Populate the given tree node with the given directory content.\n\n    This function overrides the original textual method to handle root level\n    cloud buckets.\n    \"\"\"\n    top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n    if top_level_buckets is not None:\n        content = top_level_buckets\n    node.remove_children()\n    for path in content:\n        if top_level_buckets is not None:\n            path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n        else:\n            path_name = path.name\n        node.add(\n            path_name,\n            data=DirEntry(path),\n            allow_expand=self._safe_is_dir(path),\n        )\n    node.expand()\n
"},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
  • browsr
    • _base
    • _cli
    • _config
    • _utils
    • _version
    • browsr
    • universal_directory_tree
"},{"location":"reference/_base/","title":"_base","text":"

Extension Classes

"},{"location":"reference/_base/#browsr._base.BrowsrPath","title":"BrowsrPath","text":"

Bases: UPath

A UPath object that can be extended with persisted kwargs

Source code in browsr/_base.py
class BrowsrPath(UPath):\n    \"\"\"\n    A UPath object that can be extended with persisted kwargs\n    \"\"\"\n\n    __path_kwargs__: ClassVar[dict[str, Any]] = {}\n\n    def __new__(cls, *args: str | PathLike[Any], **kwargs: Any) -> BrowsrPath:\n        \"\"\"\n        Create a new BrowsrPath object\n        \"\"\"\n        return super().__new__(cls, *args, **kwargs, **cls.__path_kwargs__)\n
"},{"location":"reference/_base/#browsr._base.BrowsrPath.__new__","title":"__new__(*args, **kwargs)","text":"

Create a new BrowsrPath object

Source code in browsr/_base.py
def __new__(cls, *args: str | PathLike[Any], **kwargs: Any) -> BrowsrPath:\n    \"\"\"\n    Create a new BrowsrPath object\n    \"\"\"\n    return super().__new__(cls, *args, **kwargs, **cls.__path_kwargs__)\n
"},{"location":"reference/_base/#browsr._base.BrowsrTextualApp","title":"BrowsrTextualApp","text":"

Bases: App[str]

textual.app.App Extension

Source code in browsr/_base.py
class BrowsrTextualApp(App[str]):\n    \"\"\"\n    textual.app.App Extension\n    \"\"\"\n\n    show_tree = var(True)\n    theme_index = var(0)\n    linenos = var(False)\n    rich_themes = favorite_themes\n    selected_file_path: upath.UPath | pathlib.Path | None | var[None] = var(None)\n    force_show_tree = var(False)\n    hidden_table_view = var(False)\n\n    def __init__(\n        self,\n        config_object: TextualAppContext | None = None,\n    ):\n        \"\"\"\n        Like the textual.app.App class, but with an extra config_object property\n\n        Parameters\n        ----------\n        config_object: Optional[TextualAppContext]\n            A configuration object. This is an optional python object,\n            like a dictionary to pass into an application\n        \"\"\"\n        super().__init__()\n        self.config_object = config_object or TextualAppContext()\n        traceback.install(show_locals=True)\n\n    @staticmethod\n    def df_to_table(\n        pandas_dataframe: DataFrame,\n        table: DataTable[str],\n        show_index: bool = True,\n        index_name: str | None = None,\n    ) -> DataTable[str]:\n        \"\"\"\n        Convert a pandas.DataFrame obj into a rich.Table obj.\n\n        Parameters\n        ----------\n        pandas_dataframe: DataFrame\n            A Pandas DataFrame to be converted to a rich Table.\n        table: DataTable[str]\n            A DataTable that should be populated by the DataFrame values.\n        show_index: bool\n            Add a column with a row count to the table. Defaults to True.\n        index_name: Optional[str]\n            The column name to give to the index column.\n            Defaults to None, showing no value.\n\n        Returns\n        -------\n        DataTable[str]\n            The DataTable instance passed, populated with the DataFrame values.\n        \"\"\"\n        table.clear(columns=True)\n        if show_index:\n            index_name = str(index_name) if index_name else \"\"\n            table.add_column(index_name)\n        for column in pandas_dataframe.columns:\n            table.add_column(str(column))\n        pandas_dataframe.replace([np.NaN], [\"\"], inplace=True)\n        for index, value_list in enumerate(pandas_dataframe.values.tolist()):\n            row = [str(index)] if show_index else []\n            row += [str(x) for x in value_list]\n            table.add_row(*row)\n        return table\n
"},{"location":"reference/_base/#browsr._base.BrowsrTextualApp.__init__","title":"__init__(config_object=None)","text":"

Like the textual.app.App class, but with an extra config_object property

Parameters:

Name Type Description Default config_object TextualAppContext | None

A configuration object. This is an optional python object, like a dictionary to pass into an application

None Source code in browsr/_base.py
def __init__(\n    self,\n    config_object: TextualAppContext | None = None,\n):\n    \"\"\"\n    Like the textual.app.App class, but with an extra config_object property\n\n    Parameters\n    ----------\n    config_object: Optional[TextualAppContext]\n        A configuration object. This is an optional python object,\n        like a dictionary to pass into an application\n    \"\"\"\n    super().__init__()\n    self.config_object = config_object or TextualAppContext()\n    traceback.install(show_locals=True)\n
"},{"location":"reference/_base/#browsr._base.BrowsrTextualApp.df_to_table","title":"df_to_table(pandas_dataframe, table, show_index=True, index_name=None) staticmethod","text":"

Convert a pandas.DataFrame obj into a rich.Table obj.

Parameters:

Name Type Description Default pandas_dataframe DataFrame

A Pandas DataFrame to be converted to a rich Table.

required table DataTable[str]

A DataTable that should be populated by the DataFrame values.

required show_index bool

Add a column with a row count to the table. Defaults to True.

True index_name str | None

The column name to give to the index column. Defaults to None, showing no value.

None

Returns:

Type Description DataTable[str]

The DataTable instance passed, populated with the DataFrame values.

Source code in browsr/_base.py
@staticmethod\ndef df_to_table(\n    pandas_dataframe: DataFrame,\n    table: DataTable[str],\n    show_index: bool = True,\n    index_name: str | None = None,\n) -> DataTable[str]:\n    \"\"\"\n    Convert a pandas.DataFrame obj into a rich.Table obj.\n\n    Parameters\n    ----------\n    pandas_dataframe: DataFrame\n        A Pandas DataFrame to be converted to a rich Table.\n    table: DataTable[str]\n        A DataTable that should be populated by the DataFrame values.\n    show_index: bool\n        Add a column with a row count to the table. Defaults to True.\n    index_name: Optional[str]\n        The column name to give to the index column.\n        Defaults to None, showing no value.\n\n    Returns\n    -------\n    DataTable[str]\n        The DataTable instance passed, populated with the DataFrame values.\n    \"\"\"\n    table.clear(columns=True)\n    if show_index:\n        index_name = str(index_name) if index_name else \"\"\n        table.add_column(index_name)\n    for column in pandas_dataframe.columns:\n        table.add_column(str(column))\n    pandas_dataframe.replace([np.NaN], [\"\"], inplace=True)\n    for index, value_list in enumerate(pandas_dataframe.values.tolist()):\n        row = [str(index)] if show_index else []\n        row += [str(x) for x in value_list]\n        table.add_row(*row)\n    return table\n
"},{"location":"reference/_base/#browsr._base.ConfirmationPopUp","title":"ConfirmationPopUp","text":"

Bases: Container

A Pop Up that asks for confirmation

Source code in browsr/_base.py
class ConfirmationPopUp(Container):\n    \"\"\"\n    A Pop Up that asks for confirmation\n    \"\"\"\n\n    __confirmation_message__: str = dedent(\n        \"\"\"\n        ## File Download\n\n        Are you sure you want to download that file?\n        \"\"\"\n    )\n\n    def compose(self) -> ComposeResult:\n        \"\"\"\n        Compose the Confirmation Pop Up\n        \"\"\"\n        self.download_message = Static(Markdown(\"\"))\n        yield self.download_message\n        yield Button(\"Yes\", variant=\"success\")\n        yield Button(\"No\", variant=\"error\")\n\n    @on(Button.Pressed)\n    def handle_download_selection(self, message: Button.Pressed) -> None:\n        \"\"\"\n        Handle Button Presses\n        \"\"\"\n        self.app.confirmation_window.display = False\n        if message.button.variant == \"success\":\n            self.app.download_selected_file()\n        self.app.table_view.display = self.app.hidden_table_view\n
"},{"location":"reference/_base/#browsr._base.ConfirmationPopUp.compose","title":"compose()","text":"

Compose the Confirmation Pop Up

Source code in browsr/_base.py
def compose(self) -> ComposeResult:\n    \"\"\"\n    Compose the Confirmation Pop Up\n    \"\"\"\n    self.download_message = Static(Markdown(\"\"))\n    yield self.download_message\n    yield Button(\"Yes\", variant=\"success\")\n    yield Button(\"No\", variant=\"error\")\n
"},{"location":"reference/_base/#browsr._base.ConfirmationPopUp.handle_download_selection","title":"handle_download_selection(message)","text":"

Handle Button Presses

Source code in browsr/_base.py
@on(Button.Pressed)\ndef handle_download_selection(self, message: Button.Pressed) -> None:\n    \"\"\"\n    Handle Button Presses\n    \"\"\"\n    self.app.confirmation_window.display = False\n    if message.button.variant == \"success\":\n        self.app.download_selected_file()\n    self.app.table_view.display = self.app.hidden_table_view\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar","title":"CurrentFileInfoBar","text":"

Bases: Widget

A Widget that displays information about the currently selected file

Thanks, Kupo. https://github.com/darrenburns/kupo

Source code in browsr/_base.py
class CurrentFileInfoBar(Widget):\n    \"\"\"\n    A Widget that displays information about the currently selected file\n\n    Thanks, Kupo. https://github.com/darrenburns/kupo\n    \"\"\"\n\n    file_info: FileInfo | var[None] = reactive(None)\n\n    def watch_file_info(self, new_file: FileInfo | None) -> None:\n        \"\"\"\n        Watch the file_info property for changes\n        \"\"\"\n        if new_file is None:\n            self.display = False\n        else:\n            self.display = True\n\n    @classmethod\n    def _convert_size(cls, size_bytes: int) -> str:\n        \"\"\"\n        Convert Bytes to Human Readable String\n        \"\"\"\n        if size_bytes == 0:\n            return \" 0B\"\n        size_name = (\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\")\n        index = int(math.floor(math.log(size_bytes, 1024)))\n        p = math.pow(1024, index)\n        number = round(size_bytes / p, 2)\n        unit = size_name[index]\n        return f\"{number:.0f}{unit}\"\n\n    def render(self) -> RenderableType:\n        \"\"\"\n        Render the Current File Info Bar\n        \"\"\"\n        if self.file_info is None or not self.file_info.is_file:\n            return Text(\"\")\n        status_string = \"\ud83d\uddc4\ufe0f\ufe0f\ufe0f  \" + self._convert_size(self.file_info.size)\n        if self.file_info.last_modified is not None:\n            modify_time = self.file_info.last_modified.strftime(\"%b %d, %Y %I:%M %p\")\n            status_string += \"  \ud83d\udcc5  \" + modify_time\n        status_string += (\n            \"  \ud83d\udcbe  \"\n            + self.file_info.file.name\n            + \"  \ud83d\udcc2  \"\n            + self.file_info.file.parent.name\n        )\n        if self.file_info.owner not in [\"\", None]:\n            status_string += \"  \ud83d\udc64  \" + self.file_info.owner\n        if self.file_info.group.strip() not in [\"\", None]:\n            status_string += \"  \ud83c\udfe0  \" + self.file_info.group\n        return Text(status_string, style=\"dim\")\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar._convert_size","title":"_convert_size(size_bytes) classmethod","text":"

Convert Bytes to Human Readable String

Source code in browsr/_base.py
@classmethod\ndef _convert_size(cls, size_bytes: int) -> str:\n    \"\"\"\n    Convert Bytes to Human Readable String\n    \"\"\"\n    if size_bytes == 0:\n        return \" 0B\"\n    size_name = (\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\")\n    index = int(math.floor(math.log(size_bytes, 1024)))\n    p = math.pow(1024, index)\n    number = round(size_bytes / p, 2)\n    unit = size_name[index]\n    return f\"{number:.0f}{unit}\"\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar.render","title":"render()","text":"

Render the Current File Info Bar

Source code in browsr/_base.py
def render(self) -> RenderableType:\n    \"\"\"\n    Render the Current File Info Bar\n    \"\"\"\n    if self.file_info is None or not self.file_info.is_file:\n        return Text(\"\")\n    status_string = \"\ud83d\uddc4\ufe0f\ufe0f\ufe0f  \" + self._convert_size(self.file_info.size)\n    if self.file_info.last_modified is not None:\n        modify_time = self.file_info.last_modified.strftime(\"%b %d, %Y %I:%M %p\")\n        status_string += \"  \ud83d\udcc5  \" + modify_time\n    status_string += (\n        \"  \ud83d\udcbe  \"\n        + self.file_info.file.name\n        + \"  \ud83d\udcc2  \"\n        + self.file_info.file.parent.name\n    )\n    if self.file_info.owner not in [\"\", None]:\n        status_string += \"  \ud83d\udc64  \" + self.file_info.owner\n    if self.file_info.group.strip() not in [\"\", None]:\n        status_string += \"  \ud83c\udfe0  \" + self.file_info.group\n    return Text(status_string, style=\"dim\")\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar.watch_file_info","title":"watch_file_info(new_file)","text":"

Watch the file_info property for changes

Source code in browsr/_base.py
def watch_file_info(self, new_file: FileInfo | None) -> None:\n    \"\"\"\n    Watch the file_info property for changes\n    \"\"\"\n    if new_file is None:\n        self.display = False\n    else:\n        self.display = True\n
"},{"location":"reference/_base/#browsr._base.FileSizeError","title":"FileSizeError","text":"

Bases: Exception

File Too Large Error

Source code in browsr/_base.py
class FileSizeError(Exception):\n    \"\"\"\n    File Too Large Error\n    \"\"\"\n
"},{"location":"reference/_base/#browsr._base.TextualAppContext","title":"TextualAppContext dataclass","text":"

App Context Object

Source code in browsr/_base.py
@dataclass\nclass TextualAppContext:\n    \"\"\"\n    App Context Object\n    \"\"\"\n\n    file_path: str = field(default_factory=os.getcwd)\n    config: dict[str, Any] | None = None\n    debug: bool = False\n    max_file_size: int = 20\n    kwargs: dict[str, Any] | None = None\n\n    @property\n    def path(self) -> pathlib.Path:\n        \"\"\"\n        Resolve `file_path` to a upath.UPath object\n        \"\"\"\n        if \"github\" in str(self.file_path).lower():\n            file_path = str(self.file_path)\n            file_path = file_path.lstrip(\"https://\")  # noqa: B005\n            file_path = file_path.lstrip(\"http://\")  # noqa: B005\n            file_path = file_path.lstrip(\"www.\")  # noqa: B005\n            if file_path.endswith(\".git\"):\n                file_path = file_path[:-4]\n            file_path = handle_github_url(url=str(file_path))\n            self.file_path = file_path\n        if str(self.file_path).endswith(\"/\") and len(str(self.file_path)) > 1:\n            self.file_path = str(self.file_path)[:-1]\n        kwargs = self.kwargs or {}\n        PathClass = copy(BrowsrPath)  # noqa: N806\n        PathClass.__path_kwargs__ = kwargs\n        return (\n            PathClass(self.file_path).resolve()\n            if self.file_path\n            else pathlib.Path.cwd().resolve()\n        )\n
"},{"location":"reference/_base/#browsr._base.TextualAppContext.path","title":"path: pathlib.Path property","text":"

Resolve file_path to a upath.UPath object

"},{"location":"reference/_base/#browsr._base.VimDataTable","title":"VimDataTable","text":"

Bases: DataTable[str]

A DataTable with Vim Keybindings

Source code in browsr/_base.py
class VimDataTable(DataTable[str]):\n    \"\"\"\n    A DataTable with Vim Keybindings\n    \"\"\"\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *DataTable.BINDINGS,\n        *vim_cursor_bindings,\n    ]\n
"},{"location":"reference/_base/#browsr._base.VimScroll","title":"VimScroll","text":"

Bases: VerticalScroll

A VerticalScroll with Vim Keybindings

Source code in browsr/_base.py
class VimScroll(VerticalScroll):\n    \"\"\"\n    A VerticalScroll with Vim Keybindings\n    \"\"\"\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *VerticalScroll.BINDINGS,\n        *vim_scroll_bindings,\n    ]\n
"},{"location":"reference/_cli/","title":"_cli","text":"

browsr command line interface

"},{"location":"reference/_cli/#browsr._cli.browsr","title":"browsr(path, debug, max_file_size, kwargs)","text":"

browsr \ud83d\uddc2\ufe0f a pleasant file explorer in your terminal

Navigate through directories and peek at files whether they're hosted locally, over SSH, in GitHub, AWS S3, Google Cloud Storage, or Azure Blob Storage. View code files with syntax highlighting, format JSON files, render images, convert data files to navigable datatables, and more.

"},{"location":"reference/_cli/#browsr._cli.browsr--installation","title":"Installation","text":"

It's recommended to install browsr via pipx with all optional dependencies, this enables browsr to access remote cloud storage buckets and open parquet files.

pipx install \"browsr[all]\"\n
"},{"location":"reference/_cli/#browsr._cli.browsr--usage-examples","title":"Usage Examples","text":""},{"location":"reference/_cli/#browsr._cli.browsr--local","title":"Local","text":""},{"location":"reference/_cli/#browsr._cli.browsr--browse-your-current-working-directory","title":"Browse your current working directory","text":"
browsr\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-local-directory","title":"Browse a local directory","text":"
browsr/path/to/directory\n
"},{"location":"reference/_cli/#browsr._cli.browsr--cloud-storage","title":"Cloud Storage","text":""},{"location":"reference/_cli/#browsr._cli.browsr--browse-an-s3-bucket","title":"Browse an S3 bucket","text":"
browsr s3://bucket-name\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-gcs-bucket","title":"Browse a GCS bucket","text":"
browsr gs://bucket-name\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-azure-services","title":"Browse Azure Services","text":"
browsr adl://bucket-name\nbrowsr az://bucket-name\n
"},{"location":"reference/_cli/#browsr._cli.browsr--pass-extra-arguments-to-cloud-storage","title":"Pass Extra Arguments to Cloud Storage","text":"

Some cloud storage providers require extra arguments to be passed to the filesystem. For example, to browse an anonymous S3 bucket, you need to pass the anon=True argument to the filesystem. This can be done with the -k/--kwargs argument.

browsr s3://anonymous-bucket -k anon=True\n
"},{"location":"reference/_cli/#browsr._cli.browsr--github","title":"GitHub","text":""},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-repository","title":"Browse a GitHub repository","text":"
browsr github://juftin:browsr\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-repository-branch","title":"Browse a GitHub Repository Branch","text":"
browsr github://juftin:browsr@main\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-private-github-repository","title":"Browse a Private GitHub Repository","text":"
export GITHUB_TOKEN=\"ghp_1234567890\"\nbrowsr github://juftin:browsr-private@main\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-repository-subdirectory","title":"Browse a GitHub Repository Subdirectory","text":"
browsr github://juftin:browsr@main/tests\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-url","title":"Browse a GitHub URL","text":"
browsr https://github.com/juftin/browsr\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-filesystem-over-ssh","title":"Browse a Filesystem over SSH","text":"
browsr ssh://user@host:22\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-sftp-server","title":"Browse a SFTP Server","text":"
browsr sftp://user@host:22/path/to/directory\n
"},{"location":"reference/_cli/#browsr._cli.browsr--key-bindings","title":"Key Bindings","text":"
  • Q - Quit the application
  • F - Toggle the file tree sidebar
  • T - Toggle the rich theme for code formatting
  • N - Toggle line numbers for code formatting
  • D - Toggle dark mode for the application
  • X - Download the file from cloud storage
Source code in browsr/_cli.py
@click.command(name=\"browsr\", cls=rich_click.rich_command.RichCommand)\n@click.argument(\"path\", default=None, required=False, metavar=\"PATH\")\n@click.option(\n    \"-m\",\n    \"--max-file-size\",\n    default=20,\n    type=int,\n    help=\"Maximum file size in MB for the application to open\",\n)\n@click.version_option(version=__version__, prog_name=__application__)\n@click.option(\n    \"--debug/--no-debug\",\n    default=False,\n    help=\"Enable extra debugging output\",\n    type=click.BOOL,\n)\n@click.option(\n    \"-k\", \"--kwargs\", multiple=True, help=\"Key=Value pairs to pass to the filesystem\"\n)\ndef browsr(\n    path: Optional[str],\n    debug: bool,\n    max_file_size: int,\n    kwargs: Tuple[str, ...],\n) -> None:\n    \"\"\"\n    browsr \ud83d\uddc2\ufe0f  a pleasant file explorer in your terminal\n\n    Navigate through directories and peek at files whether they're hosted locally,\n    over SSH, in GitHub, AWS S3, Google Cloud Storage, or Azure Blob Storage.\n    View code files with syntax highlighting, format JSON files, render images,\n    convert data files to navigable datatables, and more.\n\n    \\f\n\n    ![browsr](https://raw.githubusercontent.com/juftin/browsr/main/docs/_static/screenshot_utils.png)\n\n    ## Installation\n\n    It's recommended to install **`browsr`** via [pipx](https://pypa.github.io/pipx/)\n    with **`all`** optional dependencies, this enables **`browsr`** to access\n    remote cloud storage buckets and open parquet files.\n\n    ```shell\n    pipx install \"browsr[all]\"\n    ```\n\n    ## Usage Examples\n\n    ### Local\n\n    #### Browse your current working directory\n\n    ```shell\n    browsr\n    ```\n\n    #### Browse a local directory\n\n    ```shell\n    browsr/path/to/directory\n    ```\n\n    ### Cloud Storage\n\n    #### Browse an S3 bucket\n\n    ```shell\n    browsr s3://bucket-name\n    ```\n\n    #### Browse a GCS bucket\n\n    ```shell\n    browsr gs://bucket-name\n    ```\n\n    #### Browse Azure Services\n\n    ```shell\n    browsr adl://bucket-name\n    browsr az://bucket-name\n    ```\n\n    #### Pass Extra Arguments to Cloud Storage\n\n    Some cloud storage providers require extra arguments to be passed to the\n    filesystem. For example, to browse an anonymous S3 bucket, you need to pass\n    the `anon=True` argument to the filesystem. This can be done with the `-k/--kwargs`\n    argument.\n\n    ```shell\n    browsr s3://anonymous-bucket -k anon=True\n    ```\n\n    ### GitHub\n\n    #### Browse a GitHub repository\n\n    ```shell\n    browsr github://juftin:browsr\n    ```\n\n    #### Browse a GitHub Repository Branch\n\n    ```shell\n    browsr github://juftin:browsr@main\n    ```\n\n    #### Browse a Private GitHub Repository\n\n    ```shell\n    export GITHUB_TOKEN=\"ghp_1234567890\"\n    browsr github://juftin:browsr-private@main\n    ```\n\n    #### Browse a GitHub Repository Subdirectory\n\n    ```shell\n    browsr github://juftin:browsr@main/tests\n    ```\n\n    #### Browse a GitHub URL\n\n    ```shell\n    browsr https://github.com/juftin/browsr\n    ```\n\n    #### Browse a Filesystem over SSH\n\n    ```\n    browsr ssh://user@host:22\n    ```\n\n    #### Browse a SFTP Server\n\n    ```\n    browsr sftp://user@host:22/path/to/directory\n    ```\n\n    ## Key Bindings\n    - **`Q`** - Quit the application\n    - **`F`** - Toggle the file tree sidebar\n    - **`T`** - Toggle the rich theme for code formatting\n    - **`N`** - Toggle line numbers for code formatting\n    - **`D`** - Toggle dark mode for the application\n    - **`X`** - Download the file from cloud storage\n    \"\"\"\n    extra_kwargs = {}\n    if kwargs:\n        for kwarg in kwargs:\n            try:\n                key, value = kwarg.split(\"=\")\n                extra_kwargs[key] = value\n            except ValueError as ve:\n                raise click.BadParameter(\n                    message=(\n                        f\"Invalid Key/Value pair: `{kwarg}` \"\n                        \"- must be in the format Key=Value\"\n                    ),\n                    param_hint=\"kwargs\",\n                ) from ve\n    file_path = path or os.getcwd()\n    config = TextualAppContext(\n        file_path=file_path,\n        debug=debug,\n        max_file_size=max_file_size,\n        kwargs=extra_kwargs,\n    )\n    app = Browsr(config_object=config)\n    app.run()\n
"},{"location":"reference/_config/","title":"_config","text":"

browsr configuration file

"},{"location":"reference/_utils/","title":"_utils","text":"

Code Browsr Utility Functions

"},{"location":"reference/_utils/#browsr._utils.ArchiveFileError","title":"ArchiveFileError","text":"

Bases: Exception

Archive File Error

Source code in browsr/_utils.py
class ArchiveFileError(Exception):\n    \"\"\"\n    Archive File Error\n    \"\"\"\n
"},{"location":"reference/_utils/#browsr._utils.FileInfo","title":"FileInfo dataclass","text":"

File Information Object

Source code in browsr/_utils.py
@dataclass\nclass FileInfo:\n    \"\"\"\n    File Information Object\n    \"\"\"\n\n    file: pathlib.Path\n    size: int\n    last_modified: Optional[datetime.datetime]\n    stat: Union[Dict[str, Any], os.stat_result]\n    is_local: bool\n    is_file: bool\n    owner: str\n    group: str\n    is_cloudpath: bool\n
"},{"location":"reference/_utils/#browsr._utils._open_pdf_as_image","title":"_open_pdf_as_image(buf)","text":"

Open a PDF file and return a PIL.Image object

Source code in browsr/_utils.py
def _open_pdf_as_image(buf: BinaryIO) -> Image.Image:\n    \"\"\"\n    Open a PDF file and return a PIL.Image object\n    \"\"\"\n    doc = fitz.open(stream=buf.read(), filetype=\"pdf\")\n    pix: Pixmap = doc[0].get_pixmap()\n    if pix.colorspace is None:\n        mode = \"L\"\n    elif pix.colorspace.n == 1:\n        mode = \"L\" if pix.alpha == 0 else \"LA\"\n    elif pix.colorspace.n == 3:  # noqa: PLR2004\n        mode = \"RGB\" if pix.alpha == 0 else \"RGBA\"\n    else:\n        mode = \"CMYK\"\n    return Image.frombytes(size=(pix.width, pix.height), data=pix.samples, mode=mode)\n
"},{"location":"reference/_utils/#browsr._utils.get_file_info","title":"get_file_info(file_path)","text":"

Get File Information, Regardless of the FileSystem

Source code in browsr/_utils.py
def get_file_info(file_path: pathlib.Path) -> FileInfo:\n    \"\"\"\n    Get File Information, Regardless of the FileSystem\n    \"\"\"\n    try:\n        stat: Union[Dict[str, Any], os.stat_result] = file_path.stat()\n        is_file = file_path.is_file()\n    except PermissionError:\n        stat = {\"size\": 0}\n        is_file = True\n    is_cloudpath = is_remote_path(file_path)\n    if isinstance(stat, dict):\n        lower_dict = {key.lower(): value for key, value in stat.items()}\n        file_size = lower_dict[\"size\"]\n        modified_keys = [\"lastmodified\", \"updated\", \"mtime\"]\n        last_modified = None\n        for modified_key in modified_keys:\n            if modified_key in lower_dict:\n                last_modified = lower_dict[modified_key]\n                break\n        if isinstance(last_modified, str):\n            last_modified = datetime.datetime.fromisoformat(last_modified[:-1])\n        return FileInfo(\n            file=file_path,\n            size=file_size,\n            last_modified=last_modified,\n            stat=stat,\n            is_local=False,\n            is_file=is_file,\n            owner=\"\",\n            group=\"\",\n            is_cloudpath=is_cloudpath,\n        )\n    else:\n        last_modified = datetime.datetime.fromtimestamp(\n            stat.st_mtime, tz=datetime.timezone.utc\n        )\n        try:\n            owner = file_path.owner()\n            group = file_path.group()\n        except NotImplementedError:\n            owner = \"\"\n            group = \"\"\n        return FileInfo(\n            file=file_path,\n            size=stat.st_size,\n            last_modified=last_modified,\n            stat=stat,\n            is_local=True,\n            is_file=is_file,\n            owner=owner,\n            group=group,\n            is_cloudpath=is_cloudpath,\n        )\n
"},{"location":"reference/_utils/#browsr._utils.handle_duplicate_filenames","title":"handle_duplicate_filenames(file_path)","text":"

Handle Duplicate Filenames

Duplicate filenames are handled by appending a number to the filename in the form of \"filename (1).ext\", \"filename (2).ext\", etc.

Source code in browsr/_utils.py
def handle_duplicate_filenames(file_path: pathlib.Path) -> pathlib.Path:\n    \"\"\"\n    Handle Duplicate Filenames\n\n    Duplicate filenames are handled by appending a number to the filename\n    in the form of \"filename (1).ext\", \"filename (2).ext\", etc.\n    \"\"\"\n    if not file_path.exists():\n        return file_path\n    else:\n        i = 1\n        while True:\n            new_file_stem = f\"{file_path.stem} ({i})\"\n            new_file_path = file_path.with_stem(new_file_stem)\n            if not new_file_path.exists():\n                return new_file_path\n            i += 1\n
"},{"location":"reference/_utils/#browsr._utils.handle_github_url","title":"handle_github_url(url)","text":"

Handle GitHub URLs

GitHub URLs are handled by converting them to the raw URL.

Source code in browsr/_utils.py
def handle_github_url(url: str) -> str:\n    \"\"\"\n    Handle GitHub URLs\n\n    GitHub URLs are handled by converting them to the raw URL.\n    \"\"\"\n    try:\n        import requests\n    except ImportError as e:\n        raise ImportError(\n            \"The requests library is required to browse GitHub files. \"\n            \"Install browsr with the `remote` extra to install requests.\"\n        ) from e\n\n    gitub_prefix = \"github://\"\n    if gitub_prefix in url and \"@\" not in url:\n        _, user_password = url.split(\"github://\")\n        org, repo_str = user_password.split(\":\")\n        repo, *args = repo_str.split(\"/\")\n    elif gitub_prefix in url and \"@\" in url:\n        return url\n    elif \"github.com\" in url.lower():\n        _, org, repo, *args = url.split(\"/\")\n    else:\n        msg = f\"Invalid GitHub URL: {url}\"\n        raise ValueError(msg)\n    token = os.getenv(\"GITHUB_TOKEN\")\n    auth = {\"auth\": (\"Bearer\", token)} if token is not None else {}\n    resp = requests.get(\n        f\"https://api.github.com/repos/{org}/{repo}\",\n        headers={\"Accept\": \"application/vnd.github.v3+json\"},\n        timeout=10,\n        **auth,  # type: ignore[arg-type]\n    )\n    resp.raise_for_status()\n    default_branch = resp.json()[\"default_branch\"]\n    arg_str = \"/\".join(args)\n    github_uri = f\"{gitub_prefix}{org}:{repo}@{default_branch}/{arg_str}\".rstrip(\"/\")\n    return github_uri\n
"},{"location":"reference/_utils/#browsr._utils.open_image","title":"open_image(document, screen_width)","text":"

Open an image file and return a rich_pixels.Pixels object

Source code in browsr/_utils.py
def open_image(document: pathlib.Path, screen_width: float) -> Pixels:\n    \"\"\"\n    Open an image file and return a rich_pixels.Pixels object\n    \"\"\"\n    with document.open(\"rb\") as buf:\n        if document.suffix.lower() == \".pdf\":\n            image = _open_pdf_as_image(buf=buf)\n        else:\n            image = Image.open(buf)\n        image_width = image.width\n        image_height = image.height\n        size_ratio = image_width / screen_width\n        new_width = min(int(image_width / size_ratio), image_width)\n        new_height = min(int(image_height / size_ratio), image_height)\n        resized = image.resize((new_width, new_height))\n        return rich_pixels.Pixels.from_image(resized)\n
"},{"location":"reference/_utils/#browsr._utils.render_file_to_string","title":"render_file_to_string(file_info)","text":"

Render File to String

Parameters:

Name Type Description Default file_info FileInfo

The file to render.

required

Returns:

Type Description str

The rendered file as a string.

Source code in browsr/_utils.py
def render_file_to_string(file_info: FileInfo) -> str:\n    \"\"\"\n    Render File to String\n\n    Parameters\n    ----------\n    file_info : FileInfo\n        The file to render.\n\n    Returns\n    -------\n    str\n        The rendered file as a string.\n    \"\"\"\n    try:\n        return file_info.file.read_text(encoding=\"utf-8\")\n    except UnicodeDecodeError as e:\n        if file_info.file.suffix.lower() in [\".tar\", \".gz\", \".zip\", \".tgz\"]:\n            msg = f\"Cannot render archive file {file_info.file}.\"\n            raise ArchiveFileError(msg) from e\n        else:\n            raise e\n
"},{"location":"reference/_version/","title":"_version","text":"

browsr version file.

"},{"location":"reference/browsr/","title":"browsr","text":"

Browsr TUI App

This module contains the code browser app for the browsr package. This app was inspired by the CodeBrowser example from textual

"},{"location":"reference/browsr/#browsr.browsr.Browsr","title":"Browsr","text":"

Bases: BrowsrTextualApp

Textual code browser app.

Source code in browsr/browsr.py
class Browsr(BrowsrTextualApp):\n    \"\"\"\n    Textual code browser app.\n    \"\"\"\n\n    TITLE = __application__\n    CSS_PATH = \"browsr.css\"\n    BINDINGS: ClassVar[List[BindingType]] = [\n        Binding(key=\"q\", action=\"quit\", description=\"Quit\"),\n        Binding(key=\"f\", action=\"toggle_files\", description=\"Toggle Files\"),\n        Binding(key=\"t\", action=\"theme\", description=\"Toggle Theme\"),\n        Binding(key=\"n\", action=\"linenos\", description=\"Toggle Line Numbers\"),\n        Binding(key=\"d\", action=\"toggle_dark\", description=\"Toggle Dark Mode\"),\n        Binding(key=\".\", action=\"parent_dir\", description=\"Parent Directory\"),\n    ]\n\n    show_tree = var(True)\n    theme_index = var(0)\n    linenos = var(False)\n    rich_themes = favorite_themes\n    selected_file_path: Union[upath.UPath, pathlib.Path, None, var[None]] = var(None)\n    force_show_tree = var(False)\n    hidden_table_view = var(False)\n\n    def watch_show_tree(self, show_tree: bool) -> None:\n        \"\"\"\n        Called when show_tree is modified.\n        \"\"\"\n        self.set_class(show_tree, \"-show-tree\")\n\n    def compose(self) -> Iterable[Widget]:\n        \"\"\"\n        Compose our UI.\n        \"\"\"\n        file_path = self.config_object.path\n        if is_remote_path(file_path):\n            self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n        if file_path.is_file():\n            self.selected_file_path = file_path\n            file_path = file_path.parent\n        elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n            if TYPE_CHECKING:\n                assert isinstance(file_path, pathlib.Path)\n            self.selected_file_path = file_path.joinpath(\"README.md\")\n            self.force_show_tree = True\n        self.header = Header()\n        yield self.header\n        self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n        self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n        self.table_view: DataTable[str] = VimDataTable(\n            zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n        )\n        self.table_view.display = False\n        self.confirmation = ConfirmationPopUp()\n        self.confirmation_window = Container(\n            self.confirmation, id=\"confirmation-container\"\n        )\n        self.confirmation_window.display = False\n        self.container = Container(\n            self.directory_tree,\n            self.code_view,\n            self.table_view,\n            self.confirmation_window,\n        )\n        yield self.container\n        self.file_information = CurrentFileInfoBar()\n        self.info_bar = Horizontal(\n            self.file_information,\n            id=\"file-info-bar\",\n        )\n        if self.selected_file_path is not None:\n            self.file_information.file_info = get_file_info(\n                file_path=self.selected_file_path\n            )\n        yield self.info_bar\n        self.footer = Footer()\n        yield self.footer\n\n    def render_document(\n        self,\n        file_info: FileInfo,\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n        \"\"\"\n        Render a Code Doc Given Its Extension\n\n        Parameters\n        ----------\n        file_info: FileInfo\n            The file info object for the file to render.\n\n        Returns\n        -------\n        Union[Syntax, Markdown, DataTable[str], Pixels]\n        \"\"\"\n        document = file_info.file\n        if document.suffix == \".md\":\n            return Markdown(\n                document.read_text(encoding=\"utf-8\"),\n                code_theme=self.rich_themes[self.theme_index],\n                hyperlinks=True,\n            )\n        elif \".csv\" in document.suffixes:\n            df = pd.read_csv(document, nrows=1000)\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix == \".parquet\":\n            df = pd.read_parquet(document)[:1000]\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix.lower() in image_file_extensions:\n            screen_width = self.app.size.width / 4\n            content = open_image(document=document, screen_width=screen_width)\n            return content\n        elif document.suffix.lower() in [\".json\"]:\n            code_str = render_file_to_string(file_info=file_info)\n            try:\n                code_obj = json.loads(code_str)\n                code_lines = json.dumps(code_obj, indent=2).splitlines()\n            except json.JSONDecodeError:\n                code_lines = code_str.splitlines()\n        else:\n            code_str = render_file_to_string(file_info=file_info)\n            code_lines = code_str.splitlines()\n        code = \"\\n\".join(code_lines[:1000])\n        lexer = Syntax.guess_lexer(str(document), code=code)\n        return Syntax(\n            code=code,\n            lexer=lexer,\n            line_numbers=self.linenos,\n            word_wrap=False,\n            indent_guides=False,\n            theme=self.rich_themes[self.theme_index],\n        )\n\n    def _handle_file_size(self, file_info: FileInfo) -> None:\n        \"\"\"\n        Handle a File Size\n        \"\"\"\n        file_size_mb = file_info.size / 1000 / 1000\n        too_large = file_size_mb >= self.config_object.max_file_size\n        exception = (\n            True\n            if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n            else False\n        )\n        if too_large is True and exception is not True:\n            raise FileSizeError(\"File too large\")\n\n    def _render_file(\n        self, file_path: pathlib.Path, code_view: Static, font: str\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n        \"\"\"\n        Render a File\n        \"\"\"\n        try:\n            file_info = get_file_info(file_path=file_path)\n            self._handle_file_size(file_info=file_info)\n            element = self.render_document(file_info=file_info)\n            return element\n        except FileSizeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except PermissionError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"PERMISSION\", font=font)\n                + \"\\n\\n\"\n                + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except UnicodeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except ArchiveFileError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except Exception:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                Traceback(theme=self.rich_themes[self.theme_index], width=None)\n            )\n            self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n        return None\n\n    def render_code_page(\n        self,\n        file_path: pathlib.Path,\n        scroll_home: bool = True,\n        content: Optional[Any] = None,\n    ) -> None:\n        \"\"\"\n        Render the Code Page with Rich Syntax\n        \"\"\"\n        code_view = self.query_one(\"#code\", Static)\n        font = \"univers\"\n        if content is not None:\n            code_view.update(text2art(content, font=font))\n            return\n        element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n        if isinstance(element, DataTable):\n            self.code_view.display = False\n            self.table_view.display = True\n            if self.code_view.has_focus:\n                self.table_view.focus()\n            if scroll_home is True:\n                self.query_one(DataTable).scroll_home(animate=False)\n        elif element is not None:\n            self.table_view.display = False\n            self.code_view.display = True\n            if self.table_view.has_focus:\n                self.code_view.focus()\n            code_view.update(element)\n            if scroll_home is True:\n                self.query_one(\"#code-view\").scroll_home(animate=False)\n            self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n\n    @on(Mount)\n    def start_up_app(self) -> None:\n        \"\"\"\n        On Application Mount - See If a File Should be Displayed\n        \"\"\"\n        if self.selected_file_path is not None:\n            self.show_tree = self.force_show_tree\n            self.render_code_page(file_path=self.selected_file_path)\n            if self.show_tree is False and self.code_view.display is True:\n                self.code_view.focus()\n            elif self.show_tree is False and self.table_view.display is True:\n                self.table_view.focus()\n        else:\n            self.show_tree = True\n            self.render_code_page(\n                file_path=pathlib.Path.cwd(), content=__application__.upper()\n            )\n\n    @on(BrowsrDirectoryTree.FileSelected)\n    def handle_file_selected(\n        self,\n        message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n    ) -> None:\n        \"\"\"\n        Called when the user click a file in the directory tree.\n        \"\"\"\n        self.selected_file_path = upath.UPath(message.path)\n        file_info = get_file_info(file_path=self.selected_file_path)\n        self.render_code_page(file_path=upath.UPath(message.path))\n        self.file_information.file_info = file_info\n\n    def action_toggle_files(self) -> None:\n        \"\"\"\n        Called in response to key binding.\n        \"\"\"\n        self.show_tree = not self.show_tree\n\n    def action_theme(self) -> None:\n        \"\"\"\n        An action to toggle rich theme.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.theme_index < len(self.rich_themes) - 1:\n            self.theme_index += 1\n        else:\n            self.theme_index = 0\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def action_linenos(self) -> None:\n        \"\"\"\n        An action to toggle line numbers.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        self.linenos = not self.linenos\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def _get_download_file_name(self) -> pathlib.Path:\n        \"\"\"\n        Get the download file name.\n        \"\"\"\n        download_dir = pathlib.Path.home() / \"Downloads\"\n        if not download_dir.exists():\n            msg = f\"Download directory {download_dir} not found\"\n            raise FileNotFoundError(msg)\n        download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n        handled_download_path = handle_duplicate_filenames(file_path=download_path)\n        return handled_download_path\n\n    @work(thread=True)\n    def download_selected_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            with self.selected_file_path.open(\"rb\") as file_handle:\n                with handled_download_path.open(\"wb\") as download_handle:\n                    shutil.copyfileobj(file_handle, download_handle)\n\n    def action_download_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            prompt_message: str = dedent(\n                f\"\"\"\n                ## File Download\n\n                **Are you sure you want to download that file?**\n\n                **File:** `{self.selected_file_path}`\n\n                **Path:** `{handled_download_path}`\n                \"\"\"\n            )\n            self.confirmation.download_message.update(Markdown(prompt_message))\n            self.confirmation.refresh()\n            self.hidden_table_view = self.table_view.display\n            self.table_view.display = False\n            self.confirmation_window.display = True\n\n    def action_parent_dir(self) -> None:\n        \"\"\"\n        Go to the parent directory\n        \"\"\"\n        new_path = self.config_object.path.parent.resolve()\n        if new_path != self.config_object.path:\n            self.config_object.file_path = str(new_path)\n            self.directory_tree.path = new_path\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr._get_download_file_name","title":"_get_download_file_name()","text":"

Get the download file name.

Source code in browsr/browsr.py
def _get_download_file_name(self) -> pathlib.Path:\n    \"\"\"\n    Get the download file name.\n    \"\"\"\n    download_dir = pathlib.Path.home() / \"Downloads\"\n    if not download_dir.exists():\n        msg = f\"Download directory {download_dir} not found\"\n        raise FileNotFoundError(msg)\n    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n    handled_download_path = handle_duplicate_filenames(file_path=download_path)\n    return handled_download_path\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr._handle_file_size","title":"_handle_file_size(file_info)","text":"

Handle a File Size

Source code in browsr/browsr.py
def _handle_file_size(self, file_info: FileInfo) -> None:\n    \"\"\"\n    Handle a File Size\n    \"\"\"\n    file_size_mb = file_info.size / 1000 / 1000\n    too_large = file_size_mb >= self.config_object.max_file_size\n    exception = (\n        True\n        if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n        else False\n    )\n    if too_large is True and exception is not True:\n        raise FileSizeError(\"File too large\")\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr._render_file","title":"_render_file(file_path, code_view, font)","text":"

Render a File

Source code in browsr/browsr.py
def _render_file(\n    self, file_path: pathlib.Path, code_view: Static, font: str\n) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n    \"\"\"\n    Render a File\n    \"\"\"\n    try:\n        file_info = get_file_info(file_path=file_path)\n        self._handle_file_size(file_info=file_info)\n        element = self.render_document(file_info=file_info)\n        return element\n    except FileSizeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except PermissionError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"PERMISSION\", font=font)\n            + \"\\n\\n\"\n            + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except UnicodeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except ArchiveFileError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except Exception:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            Traceback(theme=self.rich_themes[self.theme_index], width=None)\n        )\n        self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n    return None\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_download_file","title":"action_download_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
def action_download_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        prompt_message: str = dedent(\n            f\"\"\"\n            ## File Download\n\n            **Are you sure you want to download that file?**\n\n            **File:** `{self.selected_file_path}`\n\n            **Path:** `{handled_download_path}`\n            \"\"\"\n        )\n        self.confirmation.download_message.update(Markdown(prompt_message))\n        self.confirmation.refresh()\n        self.hidden_table_view = self.table_view.display\n        self.table_view.display = False\n        self.confirmation_window.display = True\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_linenos","title":"action_linenos()","text":"

An action to toggle line numbers.

Source code in browsr/browsr.py
def action_linenos(self) -> None:\n    \"\"\"\n    An action to toggle line numbers.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    self.linenos = not self.linenos\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_parent_dir","title":"action_parent_dir()","text":"

Go to the parent directory

Source code in browsr/browsr.py
def action_parent_dir(self) -> None:\n    \"\"\"\n    Go to the parent directory\n    \"\"\"\n    new_path = self.config_object.path.parent.resolve()\n    if new_path != self.config_object.path:\n        self.config_object.file_path = str(new_path)\n        self.directory_tree.path = new_path\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_theme","title":"action_theme()","text":"

An action to toggle rich theme.

Source code in browsr/browsr.py
def action_theme(self) -> None:\n    \"\"\"\n    An action to toggle rich theme.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.theme_index < len(self.rich_themes) - 1:\n        self.theme_index += 1\n    else:\n        self.theme_index = 0\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_toggle_files","title":"action_toggle_files()","text":"

Called in response to key binding.

Source code in browsr/browsr.py
def action_toggle_files(self) -> None:\n    \"\"\"\n    Called in response to key binding.\n    \"\"\"\n    self.show_tree = not self.show_tree\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.compose","title":"compose()","text":"

Compose our UI.

Source code in browsr/browsr.py
def compose(self) -> Iterable[Widget]:\n    \"\"\"\n    Compose our UI.\n    \"\"\"\n    file_path = self.config_object.path\n    if is_remote_path(file_path):\n        self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n    if file_path.is_file():\n        self.selected_file_path = file_path\n        file_path = file_path.parent\n    elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n        if TYPE_CHECKING:\n            assert isinstance(file_path, pathlib.Path)\n        self.selected_file_path = file_path.joinpath(\"README.md\")\n        self.force_show_tree = True\n    self.header = Header()\n    yield self.header\n    self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n    self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n    self.table_view: DataTable[str] = VimDataTable(\n        zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n    )\n    self.table_view.display = False\n    self.confirmation = ConfirmationPopUp()\n    self.confirmation_window = Container(\n        self.confirmation, id=\"confirmation-container\"\n    )\n    self.confirmation_window.display = False\n    self.container = Container(\n        self.directory_tree,\n        self.code_view,\n        self.table_view,\n        self.confirmation_window,\n    )\n    yield self.container\n    self.file_information = CurrentFileInfoBar()\n    self.info_bar = Horizontal(\n        self.file_information,\n        id=\"file-info-bar\",\n    )\n    if self.selected_file_path is not None:\n        self.file_information.file_info = get_file_info(\n            file_path=self.selected_file_path\n        )\n    yield self.info_bar\n    self.footer = Footer()\n    yield self.footer\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.download_selected_file","title":"download_selected_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
@work(thread=True)\ndef download_selected_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        with self.selected_file_path.open(\"rb\") as file_handle:\n            with handled_download_path.open(\"wb\") as download_handle:\n                shutil.copyfileobj(file_handle, download_handle)\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.handle_file_selected","title":"handle_file_selected(message)","text":"

Called when the user click a file in the directory tree.

Source code in browsr/browsr.py
@on(BrowsrDirectoryTree.FileSelected)\ndef handle_file_selected(\n    self,\n    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n) -> None:\n    \"\"\"\n    Called when the user click a file in the directory tree.\n    \"\"\"\n    self.selected_file_path = upath.UPath(message.path)\n    file_info = get_file_info(file_path=self.selected_file_path)\n    self.render_code_page(file_path=upath.UPath(message.path))\n    self.file_information.file_info = file_info\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.render_code_page","title":"render_code_page(file_path, scroll_home=True, content=None)","text":"

Render the Code Page with Rich Syntax

Source code in browsr/browsr.py
def render_code_page(\n    self,\n    file_path: pathlib.Path,\n    scroll_home: bool = True,\n    content: Optional[Any] = None,\n) -> None:\n    \"\"\"\n    Render the Code Page with Rich Syntax\n    \"\"\"\n    code_view = self.query_one(\"#code\", Static)\n    font = \"univers\"\n    if content is not None:\n        code_view.update(text2art(content, font=font))\n        return\n    element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n    if isinstance(element, DataTable):\n        self.code_view.display = False\n        self.table_view.display = True\n        if self.code_view.has_focus:\n            self.table_view.focus()\n        if scroll_home is True:\n            self.query_one(DataTable).scroll_home(animate=False)\n    elif element is not None:\n        self.table_view.display = False\n        self.code_view.display = True\n        if self.table_view.has_focus:\n            self.code_view.focus()\n        code_view.update(element)\n        if scroll_home is True:\n            self.query_one(\"#code-view\").scroll_home(animate=False)\n        self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.render_document","title":"render_document(file_info)","text":"

Render a Code Doc Given Its Extension

Parameters:

Name Type Description Default file_info FileInfo

The file info object for the file to render.

required

Returns:

Type Description Union[Syntax, Markdown, DataTable[str], Pixels] Source code in browsr/browsr.py
def render_document(\n    self,\n    file_info: FileInfo,\n) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n    \"\"\"\n    Render a Code Doc Given Its Extension\n\n    Parameters\n    ----------\n    file_info: FileInfo\n        The file info object for the file to render.\n\n    Returns\n    -------\n    Union[Syntax, Markdown, DataTable[str], Pixels]\n    \"\"\"\n    document = file_info.file\n    if document.suffix == \".md\":\n        return Markdown(\n            document.read_text(encoding=\"utf-8\"),\n            code_theme=self.rich_themes[self.theme_index],\n            hyperlinks=True,\n        )\n    elif \".csv\" in document.suffixes:\n        df = pd.read_csv(document, nrows=1000)\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix == \".parquet\":\n        df = pd.read_parquet(document)[:1000]\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix.lower() in image_file_extensions:\n        screen_width = self.app.size.width / 4\n        content = open_image(document=document, screen_width=screen_width)\n        return content\n    elif document.suffix.lower() in [\".json\"]:\n        code_str = render_file_to_string(file_info=file_info)\n        try:\n            code_obj = json.loads(code_str)\n            code_lines = json.dumps(code_obj, indent=2).splitlines()\n        except json.JSONDecodeError:\n            code_lines = code_str.splitlines()\n    else:\n        code_str = render_file_to_string(file_info=file_info)\n        code_lines = code_str.splitlines()\n    code = \"\\n\".join(code_lines[:1000])\n    lexer = Syntax.guess_lexer(str(document), code=code)\n    return Syntax(\n        code=code,\n        lexer=lexer,\n        line_numbers=self.linenos,\n        word_wrap=False,\n        indent_guides=False,\n        theme=self.rich_themes[self.theme_index],\n    )\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.start_up_app","title":"start_up_app()","text":"

On Application Mount - See If a File Should be Displayed

Source code in browsr/browsr.py
@on(Mount)\ndef start_up_app(self) -> None:\n    \"\"\"\n    On Application Mount - See If a File Should be Displayed\n    \"\"\"\n    if self.selected_file_path is not None:\n        self.show_tree = self.force_show_tree\n        self.render_code_page(file_path=self.selected_file_path)\n        if self.show_tree is False and self.code_view.display is True:\n            self.code_view.focus()\n        elif self.show_tree is False and self.table_view.display is True:\n            self.table_view.focus()\n    else:\n        self.show_tree = True\n        self.render_code_page(\n            file_path=pathlib.Path.cwd(), content=__application__.upper()\n        )\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.watch_show_tree","title":"watch_show_tree(show_tree)","text":"

Called when show_tree is modified.

Source code in browsr/browsr.py
def watch_show_tree(self, show_tree: bool) -> None:\n    \"\"\"\n    Called when show_tree is modified.\n    \"\"\"\n    self.set_class(show_tree, \"-show-tree\")\n
"},{"location":"reference/universal_directory_tree/","title":"universal_directory_tree","text":"

A universal directory tree widget for Textual.

"},{"location":"reference/universal_directory_tree/#browsr.universal_directory_tree.BrowsrDirectoryTree","title":"BrowsrDirectoryTree","text":"

Bases: UniversalDirectoryTree

A DirectoryTree that can handle any filesystem.

Source code in browsr/universal_directory_tree.py
class BrowsrDirectoryTree(UniversalDirectoryTree):\n    \"\"\"\n    A DirectoryTree that can handle any filesystem.\n    \"\"\"\n\n    PATH: type[BrowsrPath] = BrowsrPath\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *UniversalDirectoryTree.BINDINGS,\n        *vim_cursor_bindings,\n    ]\n\n    @classmethod\n    def _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n        \"\"\"\n        Handle scenarios when someone wants to browse all of s3\n\n        This is because S3FS handles the root directory differently\n        than other filesystems\n        \"\"\"\n        if str(dir_path) == \"s3:/\":\n            sub_buckets = sorted(\n                Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n            )\n            return sub_buckets\n        return None\n\n    def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n        \"\"\"\n        Populate the given tree node with the given directory content.\n\n        This function overrides the original textual method to handle root level\n        cloud buckets.\n        \"\"\"\n        top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n        if top_level_buckets is not None:\n            content = top_level_buckets\n        node.remove_children()\n        for path in content:\n            if top_level_buckets is not None:\n                path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n            else:\n                path_name = path.name\n            node.add(\n                path_name,\n                data=DirEntry(path),\n                allow_expand=self._safe_is_dir(path),\n            )\n        node.expand()\n
"},{"location":"reference/universal_directory_tree/#browsr.universal_directory_tree.BrowsrDirectoryTree._handle_top_level_bucket","title":"_handle_top_level_bucket(dir_path) classmethod","text":"

Handle scenarios when someone wants to browse all of s3

This is because S3FS handles the root directory differently than other filesystems

Source code in browsr/universal_directory_tree.py
@classmethod\ndef _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n    \"\"\"\n    Handle scenarios when someone wants to browse all of s3\n\n    This is because S3FS handles the root directory differently\n    than other filesystems\n    \"\"\"\n    if str(dir_path) == \"s3:/\":\n        sub_buckets = sorted(\n            Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n        )\n        return sub_buckets\n    return None\n
"},{"location":"reference/universal_directory_tree/#browsr.universal_directory_tree.BrowsrDirectoryTree._populate_node","title":"_populate_node(node, content)","text":"

Populate the given tree node with the given directory content.

This function overrides the original textual method to handle root level cloud buckets.

Source code in browsr/universal_directory_tree.py
def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n    \"\"\"\n    Populate the given tree node with the given directory content.\n\n    This function overrides the original textual method to handle root level\n    cloud buckets.\n    \"\"\"\n    top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n    if top_level_buckets is not None:\n        content = top_level_buckets\n    node.remove_children()\n    for path in content:\n        if top_level_buckets is not None:\n            path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n        else:\n            path_name = path.name\n        node.add(\n            path_name,\n            data=DirEntry(path),\n            allow_expand=self._safe_is_dir(path),\n        )\n    node.expand()\n
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"browsr","text":"

browsr \ud83d\uddc2\ufe0f is a pleasant file explorer in your terminal. It's a command line TUI (text-based user interface) application that empowers you to browse the contents of local and remote filesystems with your keyboard or mouse.

You can quickly navigate through directories and peek at files whether they're hosted locally, in GitHub, over SSH, in AWS S3, Google Cloud Storage, or Azure Blob Storage. View code files with syntax highlighting, format JSON files, render images, convert data files to navigable datatables, and more.

Screen Recording Your browser does not support the video tag."},{"location":"#installation","title":"Installation","text":"

It's recommended to use pipx instead of pip. pipx installs the package in an isolated environment and makes it available everywhere. If you'd like to use pip instead, just replace pipx with pip in the below command.

pipx install browsr\n
"},{"location":"#extra-installation","title":"Extra Installation","text":"

If you're looking to use browsr on remote file systems, like GitHub or AWS S3, you'll need to install the remote extra. If you'd like to browse parquet / feather files, you'll need to install the data extra. Or, even simpler, you can install the all extra to get all the extras.

pipx install \"browsr[all]\"\n
"},{"location":"#usage","title":"Usage","text":"

Simply give browsr a path to a local or remote file / directory. Check out the Documentation for more information about the file systems supported.

"},{"location":"#local","title":"Local","text":"
browsr ~/Downloads/\n
"},{"location":"#github","title":"GitHub","text":"
browsr github://juftin:browsr\n
export GITHUB_TOKEN=\"ghp_1234567890\"\nbrowsr github://juftin:browsr-private@main\n
"},{"location":"#cloud","title":"Cloud","text":"
browsr s3://my-bucket\n

** Currently AWS S3, Google Cloud Storage, and Azure Blob Storage are supported.

"},{"location":"#ssh-sftp","title":"SSH / SFTP","text":"
browsr ssh://username@example.com:22\n
"},{"location":"cli/","title":"Command Line Interface","text":""},{"location":"cli/#browsr","title":"browsr","text":"

browsr \ud83d\uddc2\ufe0f a pleasant file explorer in your terminal

Navigate through directories and peek at files whether they're hosted locally, over SSH, in GitHub, AWS S3, Google Cloud Storage, or Azure Blob Storage. View code files with syntax highlighting, format JSON files, render images, convert data files to navigable datatables, and more.

"},{"location":"cli/#installation","title":"Installation","text":"

It's recommended to install browsr via pipx with all optional dependencies, this enables browsr to access remote cloud storage buckets and open parquet files.

pipx install \"browsr[all]\"\n
"},{"location":"cli/#usage-examples","title":"Usage Examples","text":""},{"location":"cli/#local","title":"Local","text":""},{"location":"cli/#browse-your-current-working-directory","title":"Browse your current working directory","text":"
browsr\n
"},{"location":"cli/#browse-a-local-directory","title":"Browse a local directory","text":"
browsr/path/to/directory\n
"},{"location":"cli/#cloud-storage","title":"Cloud Storage","text":""},{"location":"cli/#browse-an-s3-bucket","title":"Browse an S3 bucket","text":"
browsr s3://bucket-name\n
"},{"location":"cli/#browse-a-gcs-bucket","title":"Browse a GCS bucket","text":"
browsr gs://bucket-name\n
"},{"location":"cli/#browse-azure-services","title":"Browse Azure Services","text":"
browsr adl://bucket-name\nbrowsr az://bucket-name\n
"},{"location":"cli/#pass-extra-arguments-to-cloud-storage","title":"Pass Extra Arguments to Cloud Storage","text":"

Some cloud storage providers require extra arguments to be passed to the filesystem. For example, to browse an anonymous S3 bucket, you need to pass the anon=True argument to the filesystem. This can be done with the -k/--kwargs argument.

browsr s3://anonymous-bucket -k anon=True\n
"},{"location":"cli/#github","title":"GitHub","text":""},{"location":"cli/#browse-a-github-repository","title":"Browse a GitHub repository","text":"
browsr github://juftin:browsr\n
"},{"location":"cli/#browse-a-github-repository-branch","title":"Browse a GitHub Repository Branch","text":"
browsr github://juftin:browsr@main\n
"},{"location":"cli/#browse-a-private-github-repository","title":"Browse a Private GitHub Repository","text":"
export GITHUB_TOKEN=\"ghp_1234567890\"\nbrowsr github://juftin:browsr-private@main\n
"},{"location":"cli/#browse-a-github-repository-subdirectory","title":"Browse a GitHub Repository Subdirectory","text":"
browsr github://juftin:browsr@main/tests\n
"},{"location":"cli/#browse-a-github-url","title":"Browse a GitHub URL","text":"
browsr https://github.com/juftin/browsr\n
"},{"location":"cli/#browse-a-filesystem-over-ssh","title":"Browse a Filesystem over SSH","text":"
browsr ssh://user@host:22\n
"},{"location":"cli/#browse-a-sftp-server","title":"Browse a SFTP Server","text":"
browsr sftp://user@host:22/path/to/directory\n
"},{"location":"cli/#key-bindings","title":"Key Bindings","text":"
  • Q - Quit the application
  • F - Toggle the file tree sidebar
  • T - Toggle the rich theme for code formatting
  • N - Toggle line numbers for code formatting
  • D - Toggle dark mode for the application
  • X - Download the file from cloud storage

Usage:

browsr [OPTIONS] PATH\n

Options:

Name Type Description Default -m, --max-file-size integer Maximum file size in MB for the application to open 20 --version boolean Show the version and exit. False --debug / --no-debug boolean Enable extra debugging output False -k, --kwargs text Key=Value pairs to pass to the filesystem None --help boolean Show this message and exit. False"},{"location":"contributing/","title":"Contributing","text":""},{"location":"contributing/#environment-setup","title":"Environment Setup","text":"

pipx

This documentaion uses pipx to install and manage non-project command line tools like hatch and pre-commit. If you don't already have pipx installed, make sure to see their documentation. If you prefer not to use pipx, you can use pip instead.

  1. Install hatch

    pipx install hatch\n

    pre-commit

    Hatch will attempt to set up pre-commit hooks for you using pre-commit. If you don't already, make sure to install pre-commit as well: pipx install pre-commit

  2. Build the Virtual Environment

    hatch env create\n
  3. If you need to, you can link hatch's virtual environment to your IDE. It's located in the .venv directory at the root of the project.

  4. Activate the Virtual Environment

    hatch shell\n
"},{"location":"contributing/#using-hatch","title":"Using Hatch","text":""},{"location":"contributing/#hatch-cheat-sheet","title":"Hatch Cheat Sheet","text":"Command Description Command Notes Run Tests hatch run cov Runs tests with pytest and coverage Run Formatting hatch run lint:fmt Runs ruff code formatter Run Linting hatch run lint:all Runs ruff and mypy linters / type checkers Run Type Checking hatch run lint:typing Runs mypy type checker Update Requirements Lock Files hatch run gen:reqs Updating lock file using pip-compile Upgrade Dependencies hatch run gen:reqs-update Updating lock file using pip-compile and --update flag Serve the Documentation hatch run docs:serve Serve the documentation using MkDocs Run the pre-commit Hooks hatch run lint:precommit Runs the pre-commit hooks on all files"},{"location":"contributing/#hatch-explanation","title":"Hatch Explanation","text":"

Hatch is a Python package manager. Its most basic use is as a standardized build-system. However, hatch also has some extra features which this project takes advantage of. These features include virtual environment management and the organization of common scripts like linting and testing. All the operations in hatch take place in one of its managed virtual environments.

Hatch has a variety of environments, to see them simply ask hatch:

hatch CLIOutput
hatch env show\n
                                   Standalone                                   \n\u250f\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Na\u2026 \u2503 Type     \u2503 Fea\u2026 \u2503 Dependencies         \u2503 Environment variabl\u2026 \u2503 Scrip\u2026 \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 de\u2026 \u2502 pip-com\u2026 \u2502 all  \u2502                      \u2502 GITHUB_TOKEN=placeh\u2026 \u2502 cov    \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 test   \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 do\u2026 \u2502 pip-com\u2026 \u2502      \u2502 markdown-callouts    \u2502                      \u2502 build  \u2502\n\u2502     \u2502          \u2502      \u2502 markdown-exec        \u2502                      \u2502 gh-de\u2026 \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs               \u2502                      \u2502 serve  \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-autorefs      \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-click         \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-gen-files     \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-literate-nav  \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-material      \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocs-section-index \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocstrings         \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 mkdocstrings-python  \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 pymdown-extensions   \u2502                      \u2502        \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 gen \u2502 virtual  \u2502      \u2502                      \u2502                      \u2502 relea\u2026 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 li\u2026 \u2502 pip-com\u2026 \u2502      \u2502 mypy>=1.6.1          \u2502                      \u2502 all    \u2502\n\u2502     \u2502          \u2502      \u2502 ruff~=0.1.7          \u2502                      \u2502 fmt    \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 preco\u2026 \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 style  \u2502\n\u2502     \u2502          \u2502      \u2502                      \u2502                      \u2502 typing \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 te\u2026 \u2502 pip-com\u2026 \u2502 all  \u2502 pytest               \u2502 GITHUB_TOKEN=placeh\u2026 \u2502 cov    \u2502\n\u2502     \u2502          \u2502      \u2502 pytest-cov           \u2502                      \u2502 test   \u2502\n\u2502     \u2502          \u2502      \u2502 pytest-textual-snap\u2026 \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 pytest-vcr~=1.0.2    \u2502                      \u2502        \u2502\n\u2502     \u2502          \u2502      \u2502 textual-dev~=1.0.1   \u2502                      \u2502        \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                    Matrices                                    \n\u250f\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2513\n\u2503\u2503 Type \u2503 En\u2026 \u2503 \u2026 \u2503 Dependencies     \u2503 Environment variables                \u2503  \u2503\n\u2521\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2529\n\u2502\u2502 pip\u2026 \u2502 al\u2026 \u2502 \u2026 \u2502 pytest           \u2502 GITHUB_TOKEN={env:GITHUB_TOKEN:plac\u2026 \u2502  \u2502\n\u2502\u2502      \u2502 al\u2026 \u2502   \u2502 pytest-cov       \u2502                                      \u2502  \u2502\n\u2502\u2502      \u2502 al\u2026 \u2502   \u2502 pytest-textual-\u2026 \u2502                                      \u2502  \u2502\n\u2502\u2502      \u2502 al\u2026 \u2502   \u2502 pytest-vcr~=1.0\u2026 \u2502                                      \u2502  \u2502\n\u2502\u2502      \u2502     \u2502   \u2502 textual-dev~=1.\u2026 \u2502                                      \u2502  \u2502\n\u2514\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2518\n

That above command will tell you that there are five environments that you can use:

  • default
  • docs
  • gen
  • lint
  • test

Each of these environments has a set of commands that you can run. To see the commands for a specific environment, run:

hatch CLIOutput
hatch env show default\n
                               Standalone                                \n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Name    \u2503 Type        \u2503 Features \u2503 Environment variables    \u2503 Scripts \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 default \u2502 pip-compile \u2502 all      \u2502 GITHUB_TOKEN=placeholder \u2502 cov     \u2502\n\u2502         \u2502             \u2502          \u2502                          \u2502 test    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n

Here we can see that the default environment has the following commands:

  • cov
  • test

The one that we're interested in is cov, which will run the tests for the project.

hatch run cov\n

Since cov is in the default environment, we can run it without specifying the environment. However, to run the serve command in the docs environment, we need to specify the environment:

hatch run docs:serve\n

You can see what scripts are available using the env show command

hatch CLIOutput
hatch env show docs\n
                       Standalone                        \n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Name \u2503 Type        \u2503 Dependencies         \u2503 Scripts   \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 docs \u2502 pip-compile \u2502 markdown-callouts    \u2502 build     \u2502\n\u2502      \u2502             \u2502 markdown-exec        \u2502 gh-deploy \u2502\n\u2502      \u2502             \u2502 mkdocs               \u2502 serve     \u2502\n\u2502      \u2502             \u2502 mkdocs-autorefs      \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-click         \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-gen-files     \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-literate-nav  \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-material      \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocs-section-index \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocstrings         \u2502           \u2502\n\u2502      \u2502             \u2502 mkdocstrings-python  \u2502           \u2502\n\u2502      \u2502             \u2502 pymdown-extensions   \u2502           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"contributing/#committing-code","title":"Committing Code","text":"

This project uses pre-commit to run a set of checks on the code before it is committed. The pre-commit hooks are installed by hatch automatically when you run it for the first time.

This project uses semantic-versioning standards, managed by semantic-release. Releases for this project are handled entirely by CI/CD via pull requests being merged into the main branch. Contributions follow the gitmoji standards with conventional commits.

While you can denote other changes on your commit messages with gitmoji, the following commit message emoji prefixes are the only ones to trigger new releases:

Emoji Shortcode Description Semver \ud83d\udca5 :boom: Introduce breaking changes. Major \u2728 :sparkles: Introduce new features. Minor \ud83d\udc1b :bug: Fix a bug. Patch \ud83d\ude91 :ambulance: Critical hotfix. Patch \ud83d\udd12 :lock: Fix security issues. Patch

Most features can be squash merged into a single commit on a pull-request. When merging multiple commits, they will be summarized into a single release.

If you're working on a new feature, your commit message might look like:

\u2728 New Feature Description\n

Bug fix commits would look like this:

\ud83d\udc1b Bug Fix Description\n

If you're working on a feature that introduces breaking changes, your commit message might look like:

\ud83d\udca5 Breaking Change Description\n

Other commits that don't trigger a release might look like this:

\ud83d\udcdd Documentation Update Description\n\ud83d\udc77 CI/CD Update Description\n\ud83e\uddea Testing Changes Description\n\ud83d\ude9a Moving/Renaming Description\n\u2b06\ufe0f Dependency Upgrade Description\n
"},{"location":"contributing/#pre-releases","title":"Pre-Releases","text":"

semantic-release supports pre-releases. To trigger a pre-release, you would merge your pull request into an alpha or beta branch.

"},{"location":"contributing/#specific-release-versions","title":"Specific Release Versions","text":"

In some cases you need more advanced control around what kind of release you need to create. If you need to release a specific version, you can do so by creating a new branch with the version number as the branch name. For example, if the current version is 2.3.2, but you need to release a fix as 1.2.5, you would create a branch named 1.2.x and merge your changes into that branch.

See the semantic-release documentation for more information about branch based releases and other advanced release cases.

"},{"location":"reference/","title":"browsr","text":"

browsr

"},{"location":"reference/#browsr.Browsr","title":"Browsr","text":"

Bases: BrowsrTextualApp

Textual code browser app.

Source code in browsr/browsr.py
class Browsr(BrowsrTextualApp):\n    \"\"\"\n    Textual code browser app.\n    \"\"\"\n\n    TITLE = __application__\n    CSS_PATH = \"browsr.css\"\n    BINDINGS: ClassVar[List[BindingType]] = [\n        Binding(key=\"q\", action=\"quit\", description=\"Quit\"),\n        Binding(key=\"f\", action=\"toggle_files\", description=\"Toggle Files\"),\n        Binding(key=\"t\", action=\"theme\", description=\"Toggle Theme\"),\n        Binding(key=\"n\", action=\"linenos\", description=\"Toggle Line Numbers\"),\n        Binding(key=\"d\", action=\"toggle_dark\", description=\"Toggle Dark Mode\"),\n        Binding(key=\".\", action=\"parent_dir\", description=\"Parent Directory\"),\n    ]\n\n    show_tree = var(True)\n    theme_index = var(0)\n    linenos = var(False)\n    rich_themes = favorite_themes\n    selected_file_path: Union[upath.UPath, pathlib.Path, None, var[None]] = var(None)\n    force_show_tree = var(False)\n    hidden_table_view = var(False)\n\n    def watch_show_tree(self, show_tree: bool) -> None:\n        \"\"\"\n        Called when show_tree is modified.\n        \"\"\"\n        self.set_class(show_tree, \"-show-tree\")\n\n    def compose(self) -> Iterable[Widget]:\n        \"\"\"\n        Compose our UI.\n        \"\"\"\n        file_path = self.config_object.path\n        if is_remote_path(file_path):\n            self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n        if file_path.is_file():\n            self.selected_file_path = file_path\n            file_path = file_path.parent\n        elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n            if TYPE_CHECKING:\n                assert isinstance(file_path, pathlib.Path)\n            self.selected_file_path = file_path.joinpath(\"README.md\")\n            self.force_show_tree = True\n        self.header = Header()\n        yield self.header\n        self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n        self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n        self.table_view: DataTable[str] = VimDataTable(\n            zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n        )\n        self.table_view.display = False\n        self.confirmation = ConfirmationPopUp()\n        self.confirmation_window = Container(\n            self.confirmation, id=\"confirmation-container\"\n        )\n        self.confirmation_window.display = False\n        self.container = Container(\n            self.directory_tree,\n            self.code_view,\n            self.table_view,\n            self.confirmation_window,\n        )\n        yield self.container\n        self.file_information = CurrentFileInfoBar()\n        self.info_bar = Horizontal(\n            self.file_information,\n            id=\"file-info-bar\",\n        )\n        if self.selected_file_path is not None:\n            self.file_information.file_info = get_file_info(\n                file_path=self.selected_file_path\n            )\n        yield self.info_bar\n        self.footer = Footer()\n        yield self.footer\n\n    def render_document(\n        self,\n        file_info: FileInfo,\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n        \"\"\"\n        Render a Code Doc Given Its Extension\n\n        Parameters\n        ----------\n        file_info: FileInfo\n            The file info object for the file to render.\n\n        Returns\n        -------\n        Union[Syntax, Markdown, DataTable[str], Pixels]\n        \"\"\"\n        document = file_info.file\n        if document.suffix == \".md\":\n            return Markdown(\n                document.read_text(encoding=\"utf-8\"),\n                code_theme=self.rich_themes[self.theme_index],\n                hyperlinks=True,\n            )\n        elif \".csv\" in document.suffixes:\n            df = pd.read_csv(document, nrows=1000)\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix == \".parquet\":\n            df = pd.read_parquet(document)[:1000]\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix.lower() in [\".feather\", \".fea\"]:\n            df = pd.read_feather(document)[:1000]\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix.lower() in image_file_extensions:\n            screen_width = self.app.size.width / 4\n            content = open_image(document=document, screen_width=screen_width)\n            return content\n        elif document.suffix.lower() in [\".json\"]:\n            code_str = render_file_to_string(file_info=file_info)\n            try:\n                code_obj = json.loads(code_str)\n                code_lines = json.dumps(code_obj, indent=2).splitlines()\n            except json.JSONDecodeError:\n                code_lines = code_str.splitlines()\n        else:\n            code_str = render_file_to_string(file_info=file_info)\n            code_lines = code_str.splitlines()\n        code = \"\\n\".join(code_lines[:1000])\n        lexer = Syntax.guess_lexer(str(document), code=code)\n        return Syntax(\n            code=code,\n            lexer=lexer,\n            line_numbers=self.linenos,\n            word_wrap=False,\n            indent_guides=False,\n            theme=self.rich_themes[self.theme_index],\n        )\n\n    def _handle_file_size(self, file_info: FileInfo) -> None:\n        \"\"\"\n        Handle a File Size\n        \"\"\"\n        file_size_mb = file_info.size / 1000 / 1000\n        too_large = file_size_mb >= self.config_object.max_file_size\n        exception = (\n            True\n            if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n            else False\n        )\n        if too_large is True and exception is not True:\n            raise FileSizeError(\"File too large\")\n\n    def _render_file(\n        self, file_path: pathlib.Path, code_view: Static, font: str\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n        \"\"\"\n        Render a File\n        \"\"\"\n        try:\n            file_info = get_file_info(file_path=file_path)\n            self._handle_file_size(file_info=file_info)\n            element = self.render_document(file_info=file_info)\n            return element\n        except FileSizeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except PermissionError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"PERMISSION\", font=font)\n                + \"\\n\\n\"\n                + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except UnicodeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except ArchiveFileError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except Exception:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                Traceback(theme=self.rich_themes[self.theme_index], width=None)\n            )\n            self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n        return None\n\n    def render_code_page(\n        self,\n        file_path: pathlib.Path,\n        scroll_home: bool = True,\n        content: Optional[Any] = None,\n    ) -> None:\n        \"\"\"\n        Render the Code Page with Rich Syntax\n        \"\"\"\n        code_view = self.query_one(\"#code\", Static)\n        font = \"univers\"\n        if content is not None:\n            code_view.update(text2art(content, font=font))\n            return\n        element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n        if isinstance(element, DataTable):\n            self.code_view.display = False\n            self.table_view.display = True\n            if self.code_view.has_focus:\n                self.table_view.focus()\n            if scroll_home is True:\n                self.query_one(DataTable).scroll_home(animate=False)\n        elif element is not None:\n            self.table_view.display = False\n            self.code_view.display = True\n            if self.table_view.has_focus:\n                self.code_view.focus()\n            code_view.update(element)\n            if scroll_home is True:\n                self.query_one(\"#code-view\").scroll_home(animate=False)\n            self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n\n    @on(Mount)\n    def start_up_app(self) -> None:\n        \"\"\"\n        On Application Mount - See If a File Should be Displayed\n        \"\"\"\n        if self.selected_file_path is not None:\n            self.show_tree = self.force_show_tree\n            self.render_code_page(file_path=self.selected_file_path)\n            if self.show_tree is False and self.code_view.display is True:\n                self.code_view.focus()\n            elif self.show_tree is False and self.table_view.display is True:\n                self.table_view.focus()\n        else:\n            self.show_tree = True\n            self.render_code_page(\n                file_path=pathlib.Path.cwd(), content=__application__.upper()\n            )\n\n    @on(BrowsrDirectoryTree.FileSelected)\n    def handle_file_selected(\n        self,\n        message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n    ) -> None:\n        \"\"\"\n        Called when the user click a file in the directory tree.\n        \"\"\"\n        self.selected_file_path = upath.UPath(message.path)\n        file_info = get_file_info(file_path=self.selected_file_path)\n        self.render_code_page(file_path=upath.UPath(message.path))\n        self.file_information.file_info = file_info\n\n    def action_toggle_files(self) -> None:\n        \"\"\"\n        Called in response to key binding.\n        \"\"\"\n        self.show_tree = not self.show_tree\n\n    def action_theme(self) -> None:\n        \"\"\"\n        An action to toggle rich theme.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.theme_index < len(self.rich_themes) - 1:\n            self.theme_index += 1\n        else:\n            self.theme_index = 0\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def action_linenos(self) -> None:\n        \"\"\"\n        An action to toggle line numbers.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        self.linenos = not self.linenos\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def _get_download_file_name(self) -> pathlib.Path:\n        \"\"\"\n        Get the download file name.\n        \"\"\"\n        download_dir = pathlib.Path.home() / \"Downloads\"\n        if not download_dir.exists():\n            msg = f\"Download directory {download_dir} not found\"\n            raise FileNotFoundError(msg)\n        download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n        handled_download_path = handle_duplicate_filenames(file_path=download_path)\n        return handled_download_path\n\n    @work(thread=True)\n    def download_selected_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            with self.selected_file_path.open(\"rb\") as file_handle:\n                with handled_download_path.open(\"wb\") as download_handle:\n                    shutil.copyfileobj(file_handle, download_handle)\n\n    def action_download_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            prompt_message: str = dedent(\n                f\"\"\"\n                ## File Download\n\n                **Are you sure you want to download that file?**\n\n                **File:** `{self.selected_file_path}`\n\n                **Path:** `{handled_download_path}`\n                \"\"\"\n            )\n            self.confirmation.download_message.update(Markdown(prompt_message))\n            self.confirmation.refresh()\n            self.hidden_table_view = self.table_view.display\n            self.table_view.display = False\n            self.confirmation_window.display = True\n\n    def action_parent_dir(self) -> None:\n        \"\"\"\n        Go to the parent directory\n        \"\"\"\n        new_path = self.config_object.path.parent.resolve()\n        if new_path != self.config_object.path:\n            self.config_object.file_path = str(new_path)\n            self.directory_tree.path = new_path\n
"},{"location":"reference/#browsr.Browsr._get_download_file_name","title":"_get_download_file_name()","text":"

Get the download file name.

Source code in browsr/browsr.py
def _get_download_file_name(self) -> pathlib.Path:\n    \"\"\"\n    Get the download file name.\n    \"\"\"\n    download_dir = pathlib.Path.home() / \"Downloads\"\n    if not download_dir.exists():\n        msg = f\"Download directory {download_dir} not found\"\n        raise FileNotFoundError(msg)\n    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n    handled_download_path = handle_duplicate_filenames(file_path=download_path)\n    return handled_download_path\n
"},{"location":"reference/#browsr.Browsr._handle_file_size","title":"_handle_file_size(file_info)","text":"

Handle a File Size

Source code in browsr/browsr.py
def _handle_file_size(self, file_info: FileInfo) -> None:\n    \"\"\"\n    Handle a File Size\n    \"\"\"\n    file_size_mb = file_info.size / 1000 / 1000\n    too_large = file_size_mb >= self.config_object.max_file_size\n    exception = (\n        True\n        if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n        else False\n    )\n    if too_large is True and exception is not True:\n        raise FileSizeError(\"File too large\")\n
"},{"location":"reference/#browsr.Browsr._render_file","title":"_render_file(file_path, code_view, font)","text":"

Render a File

Source code in browsr/browsr.py
def _render_file(\n    self, file_path: pathlib.Path, code_view: Static, font: str\n) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n    \"\"\"\n    Render a File\n    \"\"\"\n    try:\n        file_info = get_file_info(file_path=file_path)\n        self._handle_file_size(file_info=file_info)\n        element = self.render_document(file_info=file_info)\n        return element\n    except FileSizeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except PermissionError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"PERMISSION\", font=font)\n            + \"\\n\\n\"\n            + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except UnicodeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except ArchiveFileError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except Exception:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            Traceback(theme=self.rich_themes[self.theme_index], width=None)\n        )\n        self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n    return None\n
"},{"location":"reference/#browsr.Browsr.action_download_file","title":"action_download_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
def action_download_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        prompt_message: str = dedent(\n            f\"\"\"\n            ## File Download\n\n            **Are you sure you want to download that file?**\n\n            **File:** `{self.selected_file_path}`\n\n            **Path:** `{handled_download_path}`\n            \"\"\"\n        )\n        self.confirmation.download_message.update(Markdown(prompt_message))\n        self.confirmation.refresh()\n        self.hidden_table_view = self.table_view.display\n        self.table_view.display = False\n        self.confirmation_window.display = True\n
"},{"location":"reference/#browsr.Browsr.action_linenos","title":"action_linenos()","text":"

An action to toggle line numbers.

Source code in browsr/browsr.py
def action_linenos(self) -> None:\n    \"\"\"\n    An action to toggle line numbers.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    self.linenos = not self.linenos\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/#browsr.Browsr.action_parent_dir","title":"action_parent_dir()","text":"

Go to the parent directory

Source code in browsr/browsr.py
def action_parent_dir(self) -> None:\n    \"\"\"\n    Go to the parent directory\n    \"\"\"\n    new_path = self.config_object.path.parent.resolve()\n    if new_path != self.config_object.path:\n        self.config_object.file_path = str(new_path)\n        self.directory_tree.path = new_path\n
"},{"location":"reference/#browsr.Browsr.action_theme","title":"action_theme()","text":"

An action to toggle rich theme.

Source code in browsr/browsr.py
def action_theme(self) -> None:\n    \"\"\"\n    An action to toggle rich theme.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.theme_index < len(self.rich_themes) - 1:\n        self.theme_index += 1\n    else:\n        self.theme_index = 0\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/#browsr.Browsr.action_toggle_files","title":"action_toggle_files()","text":"

Called in response to key binding.

Source code in browsr/browsr.py
def action_toggle_files(self) -> None:\n    \"\"\"\n    Called in response to key binding.\n    \"\"\"\n    self.show_tree = not self.show_tree\n
"},{"location":"reference/#browsr.Browsr.compose","title":"compose()","text":"

Compose our UI.

Source code in browsr/browsr.py
def compose(self) -> Iterable[Widget]:\n    \"\"\"\n    Compose our UI.\n    \"\"\"\n    file_path = self.config_object.path\n    if is_remote_path(file_path):\n        self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n    if file_path.is_file():\n        self.selected_file_path = file_path\n        file_path = file_path.parent\n    elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n        if TYPE_CHECKING:\n            assert isinstance(file_path, pathlib.Path)\n        self.selected_file_path = file_path.joinpath(\"README.md\")\n        self.force_show_tree = True\n    self.header = Header()\n    yield self.header\n    self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n    self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n    self.table_view: DataTable[str] = VimDataTable(\n        zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n    )\n    self.table_view.display = False\n    self.confirmation = ConfirmationPopUp()\n    self.confirmation_window = Container(\n        self.confirmation, id=\"confirmation-container\"\n    )\n    self.confirmation_window.display = False\n    self.container = Container(\n        self.directory_tree,\n        self.code_view,\n        self.table_view,\n        self.confirmation_window,\n    )\n    yield self.container\n    self.file_information = CurrentFileInfoBar()\n    self.info_bar = Horizontal(\n        self.file_information,\n        id=\"file-info-bar\",\n    )\n    if self.selected_file_path is not None:\n        self.file_information.file_info = get_file_info(\n            file_path=self.selected_file_path\n        )\n    yield self.info_bar\n    self.footer = Footer()\n    yield self.footer\n
"},{"location":"reference/#browsr.Browsr.download_selected_file","title":"download_selected_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
@work(thread=True)\ndef download_selected_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        with self.selected_file_path.open(\"rb\") as file_handle:\n            with handled_download_path.open(\"wb\") as download_handle:\n                shutil.copyfileobj(file_handle, download_handle)\n
"},{"location":"reference/#browsr.Browsr.handle_file_selected","title":"handle_file_selected(message)","text":"

Called when the user click a file in the directory tree.

Source code in browsr/browsr.py
@on(BrowsrDirectoryTree.FileSelected)\ndef handle_file_selected(\n    self,\n    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n) -> None:\n    \"\"\"\n    Called when the user click a file in the directory tree.\n    \"\"\"\n    self.selected_file_path = upath.UPath(message.path)\n    file_info = get_file_info(file_path=self.selected_file_path)\n    self.render_code_page(file_path=upath.UPath(message.path))\n    self.file_information.file_info = file_info\n
"},{"location":"reference/#browsr.Browsr.render_code_page","title":"render_code_page(file_path, scroll_home=True, content=None)","text":"

Render the Code Page with Rich Syntax

Source code in browsr/browsr.py
def render_code_page(\n    self,\n    file_path: pathlib.Path,\n    scroll_home: bool = True,\n    content: Optional[Any] = None,\n) -> None:\n    \"\"\"\n    Render the Code Page with Rich Syntax\n    \"\"\"\n    code_view = self.query_one(\"#code\", Static)\n    font = \"univers\"\n    if content is not None:\n        code_view.update(text2art(content, font=font))\n        return\n    element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n    if isinstance(element, DataTable):\n        self.code_view.display = False\n        self.table_view.display = True\n        if self.code_view.has_focus:\n            self.table_view.focus()\n        if scroll_home is True:\n            self.query_one(DataTable).scroll_home(animate=False)\n    elif element is not None:\n        self.table_view.display = False\n        self.code_view.display = True\n        if self.table_view.has_focus:\n            self.code_view.focus()\n        code_view.update(element)\n        if scroll_home is True:\n            self.query_one(\"#code-view\").scroll_home(animate=False)\n        self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n
"},{"location":"reference/#browsr.Browsr.render_document","title":"render_document(file_info)","text":"

Render a Code Doc Given Its Extension

Parameters:

Name Type Description Default file_info FileInfo

The file info object for the file to render.

required

Returns:

Type Description Union[Syntax, Markdown, DataTable[str], Pixels] Source code in browsr/browsr.py
def render_document(\n    self,\n    file_info: FileInfo,\n) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n    \"\"\"\n    Render a Code Doc Given Its Extension\n\n    Parameters\n    ----------\n    file_info: FileInfo\n        The file info object for the file to render.\n\n    Returns\n    -------\n    Union[Syntax, Markdown, DataTable[str], Pixels]\n    \"\"\"\n    document = file_info.file\n    if document.suffix == \".md\":\n        return Markdown(\n            document.read_text(encoding=\"utf-8\"),\n            code_theme=self.rich_themes[self.theme_index],\n            hyperlinks=True,\n        )\n    elif \".csv\" in document.suffixes:\n        df = pd.read_csv(document, nrows=1000)\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix == \".parquet\":\n        df = pd.read_parquet(document)[:1000]\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix.lower() in [\".feather\", \".fea\"]:\n        df = pd.read_feather(document)[:1000]\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix.lower() in image_file_extensions:\n        screen_width = self.app.size.width / 4\n        content = open_image(document=document, screen_width=screen_width)\n        return content\n    elif document.suffix.lower() in [\".json\"]:\n        code_str = render_file_to_string(file_info=file_info)\n        try:\n            code_obj = json.loads(code_str)\n            code_lines = json.dumps(code_obj, indent=2).splitlines()\n        except json.JSONDecodeError:\n            code_lines = code_str.splitlines()\n    else:\n        code_str = render_file_to_string(file_info=file_info)\n        code_lines = code_str.splitlines()\n    code = \"\\n\".join(code_lines[:1000])\n    lexer = Syntax.guess_lexer(str(document), code=code)\n    return Syntax(\n        code=code,\n        lexer=lexer,\n        line_numbers=self.linenos,\n        word_wrap=False,\n        indent_guides=False,\n        theme=self.rich_themes[self.theme_index],\n    )\n
"},{"location":"reference/#browsr.Browsr.start_up_app","title":"start_up_app()","text":"

On Application Mount - See If a File Should be Displayed

Source code in browsr/browsr.py
@on(Mount)\ndef start_up_app(self) -> None:\n    \"\"\"\n    On Application Mount - See If a File Should be Displayed\n    \"\"\"\n    if self.selected_file_path is not None:\n        self.show_tree = self.force_show_tree\n        self.render_code_page(file_path=self.selected_file_path)\n        if self.show_tree is False and self.code_view.display is True:\n            self.code_view.focus()\n        elif self.show_tree is False and self.table_view.display is True:\n            self.table_view.focus()\n    else:\n        self.show_tree = True\n        self.render_code_page(\n            file_path=pathlib.Path.cwd(), content=__application__.upper()\n        )\n
"},{"location":"reference/#browsr.Browsr.watch_show_tree","title":"watch_show_tree(show_tree)","text":"

Called when show_tree is modified.

Source code in browsr/browsr.py
def watch_show_tree(self, show_tree: bool) -> None:\n    \"\"\"\n    Called when show_tree is modified.\n    \"\"\"\n    self.set_class(show_tree, \"-show-tree\")\n
"},{"location":"reference/#browsr.BrowsrDirectoryTree","title":"BrowsrDirectoryTree","text":"

Bases: UniversalDirectoryTree

A DirectoryTree that can handle any filesystem.

Source code in browsr/universal_directory_tree.py
class BrowsrDirectoryTree(UniversalDirectoryTree):\n    \"\"\"\n    A DirectoryTree that can handle any filesystem.\n    \"\"\"\n\n    PATH: type[BrowsrPath] = BrowsrPath\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *UniversalDirectoryTree.BINDINGS,\n        *vim_cursor_bindings,\n    ]\n\n    @classmethod\n    def _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n        \"\"\"\n        Handle scenarios when someone wants to browse all of s3\n\n        This is because S3FS handles the root directory differently\n        than other filesystems\n        \"\"\"\n        if str(dir_path) == \"s3:/\":\n            sub_buckets = sorted(\n                Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n            )\n            return sub_buckets\n        return None\n\n    def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n        \"\"\"\n        Populate the given tree node with the given directory content.\n\n        This function overrides the original textual method to handle root level\n        cloud buckets.\n        \"\"\"\n        top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n        if top_level_buckets is not None:\n            content = top_level_buckets\n        node.remove_children()\n        for path in content:\n            if top_level_buckets is not None:\n                path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n            else:\n                path_name = path.name\n            node.add(\n                path_name,\n                data=DirEntry(path),\n                allow_expand=self._safe_is_dir(path),\n            )\n        node.expand()\n
"},{"location":"reference/#browsr.BrowsrDirectoryTree._handle_top_level_bucket","title":"_handle_top_level_bucket(dir_path) classmethod","text":"

Handle scenarios when someone wants to browse all of s3

This is because S3FS handles the root directory differently than other filesystems

Source code in browsr/universal_directory_tree.py
@classmethod\ndef _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n    \"\"\"\n    Handle scenarios when someone wants to browse all of s3\n\n    This is because S3FS handles the root directory differently\n    than other filesystems\n    \"\"\"\n    if str(dir_path) == \"s3:/\":\n        sub_buckets = sorted(\n            Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n        )\n        return sub_buckets\n    return None\n
"},{"location":"reference/#browsr.BrowsrDirectoryTree._populate_node","title":"_populate_node(node, content)","text":"

Populate the given tree node with the given directory content.

This function overrides the original textual method to handle root level cloud buckets.

Source code in browsr/universal_directory_tree.py
def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n    \"\"\"\n    Populate the given tree node with the given directory content.\n\n    This function overrides the original textual method to handle root level\n    cloud buckets.\n    \"\"\"\n    top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n    if top_level_buckets is not None:\n        content = top_level_buckets\n    node.remove_children()\n    for path in content:\n        if top_level_buckets is not None:\n            path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n        else:\n            path_name = path.name\n        node.add(\n            path_name,\n            data=DirEntry(path),\n            allow_expand=self._safe_is_dir(path),\n        )\n    node.expand()\n
"},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
  • browsr
    • _base
    • _cli
    • _config
    • _utils
    • _version
    • browsr
    • universal_directory_tree
"},{"location":"reference/_base/","title":"_base","text":"

Extension Classes

"},{"location":"reference/_base/#browsr._base.BrowsrPath","title":"BrowsrPath","text":"

Bases: UPath

A UPath object that can be extended with persisted kwargs

Source code in browsr/_base.py
class BrowsrPath(UPath):\n    \"\"\"\n    A UPath object that can be extended with persisted kwargs\n    \"\"\"\n\n    __path_kwargs__: ClassVar[dict[str, Any]] = {}\n\n    def __new__(cls, *args: str | PathLike[Any], **kwargs: Any) -> BrowsrPath:\n        \"\"\"\n        Create a new BrowsrPath object\n        \"\"\"\n        return super().__new__(cls, *args, **kwargs, **cls.__path_kwargs__)\n
"},{"location":"reference/_base/#browsr._base.BrowsrPath.__new__","title":"__new__(*args, **kwargs)","text":"

Create a new BrowsrPath object

Source code in browsr/_base.py
def __new__(cls, *args: str | PathLike[Any], **kwargs: Any) -> BrowsrPath:\n    \"\"\"\n    Create a new BrowsrPath object\n    \"\"\"\n    return super().__new__(cls, *args, **kwargs, **cls.__path_kwargs__)\n
"},{"location":"reference/_base/#browsr._base.BrowsrTextualApp","title":"BrowsrTextualApp","text":"

Bases: App[str]

textual.app.App Extension

Source code in browsr/_base.py
class BrowsrTextualApp(App[str]):\n    \"\"\"\n    textual.app.App Extension\n    \"\"\"\n\n    show_tree = var(True)\n    theme_index = var(0)\n    linenos = var(False)\n    rich_themes = favorite_themes\n    selected_file_path: upath.UPath | pathlib.Path | None | var[None] = var(None)\n    force_show_tree = var(False)\n    hidden_table_view = var(False)\n\n    def __init__(\n        self,\n        config_object: TextualAppContext | None = None,\n    ):\n        \"\"\"\n        Like the textual.app.App class, but with an extra config_object property\n\n        Parameters\n        ----------\n        config_object: Optional[TextualAppContext]\n            A configuration object. This is an optional python object,\n            like a dictionary to pass into an application\n        \"\"\"\n        super().__init__()\n        self.config_object = config_object or TextualAppContext()\n        traceback.install(show_locals=True)\n\n    @staticmethod\n    def df_to_table(\n        pandas_dataframe: DataFrame,\n        table: DataTable[str],\n        show_index: bool = True,\n        index_name: str | None = None,\n    ) -> DataTable[str]:\n        \"\"\"\n        Convert a pandas.DataFrame obj into a rich.Table obj.\n\n        Parameters\n        ----------\n        pandas_dataframe: DataFrame\n            A Pandas DataFrame to be converted to a rich Table.\n        table: DataTable[str]\n            A DataTable that should be populated by the DataFrame values.\n        show_index: bool\n            Add a column with a row count to the table. Defaults to True.\n        index_name: Optional[str]\n            The column name to give to the index column.\n            Defaults to None, showing no value.\n\n        Returns\n        -------\n        DataTable[str]\n            The DataTable instance passed, populated with the DataFrame values.\n        \"\"\"\n        table.clear(columns=True)\n        if show_index:\n            index_name = str(index_name) if index_name else \"\"\n            table.add_column(index_name)\n        for column in pandas_dataframe.columns:\n            table.add_column(str(column))\n        pandas_dataframe.replace([np.NaN], [\"\"], inplace=True)\n        for index, value_list in enumerate(pandas_dataframe.values.tolist()):\n            row = [str(index)] if show_index else []\n            row += [str(x) for x in value_list]\n            table.add_row(*row)\n        return table\n
"},{"location":"reference/_base/#browsr._base.BrowsrTextualApp.__init__","title":"__init__(config_object=None)","text":"

Like the textual.app.App class, but with an extra config_object property

Parameters:

Name Type Description Default config_object TextualAppContext | None

A configuration object. This is an optional python object, like a dictionary to pass into an application

None Source code in browsr/_base.py
def __init__(\n    self,\n    config_object: TextualAppContext | None = None,\n):\n    \"\"\"\n    Like the textual.app.App class, but with an extra config_object property\n\n    Parameters\n    ----------\n    config_object: Optional[TextualAppContext]\n        A configuration object. This is an optional python object,\n        like a dictionary to pass into an application\n    \"\"\"\n    super().__init__()\n    self.config_object = config_object or TextualAppContext()\n    traceback.install(show_locals=True)\n
"},{"location":"reference/_base/#browsr._base.BrowsrTextualApp.df_to_table","title":"df_to_table(pandas_dataframe, table, show_index=True, index_name=None) staticmethod","text":"

Convert a pandas.DataFrame obj into a rich.Table obj.

Parameters:

Name Type Description Default pandas_dataframe DataFrame

A Pandas DataFrame to be converted to a rich Table.

required table DataTable[str]

A DataTable that should be populated by the DataFrame values.

required show_index bool

Add a column with a row count to the table. Defaults to True.

True index_name str | None

The column name to give to the index column. Defaults to None, showing no value.

None

Returns:

Type Description DataTable[str]

The DataTable instance passed, populated with the DataFrame values.

Source code in browsr/_base.py
@staticmethod\ndef df_to_table(\n    pandas_dataframe: DataFrame,\n    table: DataTable[str],\n    show_index: bool = True,\n    index_name: str | None = None,\n) -> DataTable[str]:\n    \"\"\"\n    Convert a pandas.DataFrame obj into a rich.Table obj.\n\n    Parameters\n    ----------\n    pandas_dataframe: DataFrame\n        A Pandas DataFrame to be converted to a rich Table.\n    table: DataTable[str]\n        A DataTable that should be populated by the DataFrame values.\n    show_index: bool\n        Add a column with a row count to the table. Defaults to True.\n    index_name: Optional[str]\n        The column name to give to the index column.\n        Defaults to None, showing no value.\n\n    Returns\n    -------\n    DataTable[str]\n        The DataTable instance passed, populated with the DataFrame values.\n    \"\"\"\n    table.clear(columns=True)\n    if show_index:\n        index_name = str(index_name) if index_name else \"\"\n        table.add_column(index_name)\n    for column in pandas_dataframe.columns:\n        table.add_column(str(column))\n    pandas_dataframe.replace([np.NaN], [\"\"], inplace=True)\n    for index, value_list in enumerate(pandas_dataframe.values.tolist()):\n        row = [str(index)] if show_index else []\n        row += [str(x) for x in value_list]\n        table.add_row(*row)\n    return table\n
"},{"location":"reference/_base/#browsr._base.ConfirmationPopUp","title":"ConfirmationPopUp","text":"

Bases: Container

A Pop Up that asks for confirmation

Source code in browsr/_base.py
class ConfirmationPopUp(Container):\n    \"\"\"\n    A Pop Up that asks for confirmation\n    \"\"\"\n\n    __confirmation_message__: str = dedent(\n        \"\"\"\n        ## File Download\n\n        Are you sure you want to download that file?\n        \"\"\"\n    )\n\n    def compose(self) -> ComposeResult:\n        \"\"\"\n        Compose the Confirmation Pop Up\n        \"\"\"\n        self.download_message = Static(Markdown(\"\"))\n        yield self.download_message\n        yield Button(\"Yes\", variant=\"success\")\n        yield Button(\"No\", variant=\"error\")\n\n    @on(Button.Pressed)\n    def handle_download_selection(self, message: Button.Pressed) -> None:\n        \"\"\"\n        Handle Button Presses\n        \"\"\"\n        self.app.confirmation_window.display = False\n        if message.button.variant == \"success\":\n            self.app.download_selected_file()\n        self.app.table_view.display = self.app.hidden_table_view\n
"},{"location":"reference/_base/#browsr._base.ConfirmationPopUp.compose","title":"compose()","text":"

Compose the Confirmation Pop Up

Source code in browsr/_base.py
def compose(self) -> ComposeResult:\n    \"\"\"\n    Compose the Confirmation Pop Up\n    \"\"\"\n    self.download_message = Static(Markdown(\"\"))\n    yield self.download_message\n    yield Button(\"Yes\", variant=\"success\")\n    yield Button(\"No\", variant=\"error\")\n
"},{"location":"reference/_base/#browsr._base.ConfirmationPopUp.handle_download_selection","title":"handle_download_selection(message)","text":"

Handle Button Presses

Source code in browsr/_base.py
@on(Button.Pressed)\ndef handle_download_selection(self, message: Button.Pressed) -> None:\n    \"\"\"\n    Handle Button Presses\n    \"\"\"\n    self.app.confirmation_window.display = False\n    if message.button.variant == \"success\":\n        self.app.download_selected_file()\n    self.app.table_view.display = self.app.hidden_table_view\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar","title":"CurrentFileInfoBar","text":"

Bases: Widget

A Widget that displays information about the currently selected file

Thanks, Kupo. https://github.com/darrenburns/kupo

Source code in browsr/_base.py
class CurrentFileInfoBar(Widget):\n    \"\"\"\n    A Widget that displays information about the currently selected file\n\n    Thanks, Kupo. https://github.com/darrenburns/kupo\n    \"\"\"\n\n    file_info: FileInfo | var[None] = reactive(None)\n\n    def watch_file_info(self, new_file: FileInfo | None) -> None:\n        \"\"\"\n        Watch the file_info property for changes\n        \"\"\"\n        if new_file is None:\n            self.display = False\n        else:\n            self.display = True\n\n    @classmethod\n    def _convert_size(cls, size_bytes: int) -> str:\n        \"\"\"\n        Convert Bytes to Human Readable String\n        \"\"\"\n        if size_bytes == 0:\n            return \" 0B\"\n        size_name = (\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\")\n        index = int(math.floor(math.log(size_bytes, 1024)))\n        p = math.pow(1024, index)\n        number = round(size_bytes / p, 2)\n        unit = size_name[index]\n        return f\"{number:.0f}{unit}\"\n\n    def render(self) -> RenderableType:\n        \"\"\"\n        Render the Current File Info Bar\n        \"\"\"\n        if self.file_info is None or not self.file_info.is_file:\n            return Text(\"\")\n        status_string = \"\ud83d\uddc4\ufe0f\ufe0f\ufe0f  \" + self._convert_size(self.file_info.size)\n        if self.file_info.last_modified is not None:\n            modify_time = self.file_info.last_modified.strftime(\"%b %d, %Y %I:%M %p\")\n            status_string += \"  \ud83d\udcc5  \" + modify_time\n        status_string += (\n            \"  \ud83d\udcbe  \"\n            + self.file_info.file.name\n            + \"  \ud83d\udcc2  \"\n            + self.file_info.file.parent.name\n        )\n        if self.file_info.owner not in [\"\", None]:\n            status_string += \"  \ud83d\udc64  \" + self.file_info.owner\n        if self.file_info.group.strip() not in [\"\", None]:\n            status_string += \"  \ud83c\udfe0  \" + self.file_info.group\n        return Text(status_string, style=\"dim\")\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar._convert_size","title":"_convert_size(size_bytes) classmethod","text":"

Convert Bytes to Human Readable String

Source code in browsr/_base.py
@classmethod\ndef _convert_size(cls, size_bytes: int) -> str:\n    \"\"\"\n    Convert Bytes to Human Readable String\n    \"\"\"\n    if size_bytes == 0:\n        return \" 0B\"\n    size_name = (\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\")\n    index = int(math.floor(math.log(size_bytes, 1024)))\n    p = math.pow(1024, index)\n    number = round(size_bytes / p, 2)\n    unit = size_name[index]\n    return f\"{number:.0f}{unit}\"\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar.render","title":"render()","text":"

Render the Current File Info Bar

Source code in browsr/_base.py
def render(self) -> RenderableType:\n    \"\"\"\n    Render the Current File Info Bar\n    \"\"\"\n    if self.file_info is None or not self.file_info.is_file:\n        return Text(\"\")\n    status_string = \"\ud83d\uddc4\ufe0f\ufe0f\ufe0f  \" + self._convert_size(self.file_info.size)\n    if self.file_info.last_modified is not None:\n        modify_time = self.file_info.last_modified.strftime(\"%b %d, %Y %I:%M %p\")\n        status_string += \"  \ud83d\udcc5  \" + modify_time\n    status_string += (\n        \"  \ud83d\udcbe  \"\n        + self.file_info.file.name\n        + \"  \ud83d\udcc2  \"\n        + self.file_info.file.parent.name\n    )\n    if self.file_info.owner not in [\"\", None]:\n        status_string += \"  \ud83d\udc64  \" + self.file_info.owner\n    if self.file_info.group.strip() not in [\"\", None]:\n        status_string += \"  \ud83c\udfe0  \" + self.file_info.group\n    return Text(status_string, style=\"dim\")\n
"},{"location":"reference/_base/#browsr._base.CurrentFileInfoBar.watch_file_info","title":"watch_file_info(new_file)","text":"

Watch the file_info property for changes

Source code in browsr/_base.py
def watch_file_info(self, new_file: FileInfo | None) -> None:\n    \"\"\"\n    Watch the file_info property for changes\n    \"\"\"\n    if new_file is None:\n        self.display = False\n    else:\n        self.display = True\n
"},{"location":"reference/_base/#browsr._base.FileSizeError","title":"FileSizeError","text":"

Bases: Exception

File Too Large Error

Source code in browsr/_base.py
class FileSizeError(Exception):\n    \"\"\"\n    File Too Large Error\n    \"\"\"\n
"},{"location":"reference/_base/#browsr._base.TextualAppContext","title":"TextualAppContext dataclass","text":"

App Context Object

Source code in browsr/_base.py
@dataclass\nclass TextualAppContext:\n    \"\"\"\n    App Context Object\n    \"\"\"\n\n    file_path: str = field(default_factory=os.getcwd)\n    config: dict[str, Any] | None = None\n    debug: bool = False\n    max_file_size: int = 20\n    kwargs: dict[str, Any] | None = None\n\n    @property\n    def path(self) -> pathlib.Path:\n        \"\"\"\n        Resolve `file_path` to a upath.UPath object\n        \"\"\"\n        if \"github\" in str(self.file_path).lower():\n            file_path = str(self.file_path)\n            file_path = file_path.lstrip(\"https://\")  # noqa: B005\n            file_path = file_path.lstrip(\"http://\")  # noqa: B005\n            file_path = file_path.lstrip(\"www.\")  # noqa: B005\n            if file_path.endswith(\".git\"):\n                file_path = file_path[:-4]\n            file_path = handle_github_url(url=str(file_path))\n            self.file_path = file_path\n        if str(self.file_path).endswith(\"/\") and len(str(self.file_path)) > 1:\n            self.file_path = str(self.file_path)[:-1]\n        kwargs = self.kwargs or {}\n        PathClass = copy(BrowsrPath)  # noqa: N806\n        PathClass.__path_kwargs__ = kwargs\n        return (\n            PathClass(self.file_path).resolve()\n            if self.file_path\n            else pathlib.Path.cwd().resolve()\n        )\n
"},{"location":"reference/_base/#browsr._base.TextualAppContext.path","title":"path: pathlib.Path property","text":"

Resolve file_path to a upath.UPath object

"},{"location":"reference/_base/#browsr._base.VimDataTable","title":"VimDataTable","text":"

Bases: DataTable[str]

A DataTable with Vim Keybindings

Source code in browsr/_base.py
class VimDataTable(DataTable[str]):\n    \"\"\"\n    A DataTable with Vim Keybindings\n    \"\"\"\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *DataTable.BINDINGS,\n        *vim_cursor_bindings,\n    ]\n
"},{"location":"reference/_base/#browsr._base.VimScroll","title":"VimScroll","text":"

Bases: VerticalScroll

A VerticalScroll with Vim Keybindings

Source code in browsr/_base.py
class VimScroll(VerticalScroll):\n    \"\"\"\n    A VerticalScroll with Vim Keybindings\n    \"\"\"\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *VerticalScroll.BINDINGS,\n        *vim_scroll_bindings,\n    ]\n
"},{"location":"reference/_cli/","title":"_cli","text":"

browsr command line interface

"},{"location":"reference/_cli/#browsr._cli.browsr","title":"browsr(path, debug, max_file_size, kwargs)","text":"

browsr \ud83d\uddc2\ufe0f a pleasant file explorer in your terminal

Navigate through directories and peek at files whether they're hosted locally, over SSH, in GitHub, AWS S3, Google Cloud Storage, or Azure Blob Storage. View code files with syntax highlighting, format JSON files, render images, convert data files to navigable datatables, and more.

"},{"location":"reference/_cli/#browsr._cli.browsr--installation","title":"Installation","text":"

It's recommended to install browsr via pipx with all optional dependencies, this enables browsr to access remote cloud storage buckets and open parquet files.

pipx install \"browsr[all]\"\n
"},{"location":"reference/_cli/#browsr._cli.browsr--usage-examples","title":"Usage Examples","text":""},{"location":"reference/_cli/#browsr._cli.browsr--local","title":"Local","text":""},{"location":"reference/_cli/#browsr._cli.browsr--browse-your-current-working-directory","title":"Browse your current working directory","text":"
browsr\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-local-directory","title":"Browse a local directory","text":"
browsr/path/to/directory\n
"},{"location":"reference/_cli/#browsr._cli.browsr--cloud-storage","title":"Cloud Storage","text":""},{"location":"reference/_cli/#browsr._cli.browsr--browse-an-s3-bucket","title":"Browse an S3 bucket","text":"
browsr s3://bucket-name\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-gcs-bucket","title":"Browse a GCS bucket","text":"
browsr gs://bucket-name\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-azure-services","title":"Browse Azure Services","text":"
browsr adl://bucket-name\nbrowsr az://bucket-name\n
"},{"location":"reference/_cli/#browsr._cli.browsr--pass-extra-arguments-to-cloud-storage","title":"Pass Extra Arguments to Cloud Storage","text":"

Some cloud storage providers require extra arguments to be passed to the filesystem. For example, to browse an anonymous S3 bucket, you need to pass the anon=True argument to the filesystem. This can be done with the -k/--kwargs argument.

browsr s3://anonymous-bucket -k anon=True\n
"},{"location":"reference/_cli/#browsr._cli.browsr--github","title":"GitHub","text":""},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-repository","title":"Browse a GitHub repository","text":"
browsr github://juftin:browsr\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-repository-branch","title":"Browse a GitHub Repository Branch","text":"
browsr github://juftin:browsr@main\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-private-github-repository","title":"Browse a Private GitHub Repository","text":"
export GITHUB_TOKEN=\"ghp_1234567890\"\nbrowsr github://juftin:browsr-private@main\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-repository-subdirectory","title":"Browse a GitHub Repository Subdirectory","text":"
browsr github://juftin:browsr@main/tests\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-github-url","title":"Browse a GitHub URL","text":"
browsr https://github.com/juftin/browsr\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-filesystem-over-ssh","title":"Browse a Filesystem over SSH","text":"
browsr ssh://user@host:22\n
"},{"location":"reference/_cli/#browsr._cli.browsr--browse-a-sftp-server","title":"Browse a SFTP Server","text":"
browsr sftp://user@host:22/path/to/directory\n
"},{"location":"reference/_cli/#browsr._cli.browsr--key-bindings","title":"Key Bindings","text":"
  • Q - Quit the application
  • F - Toggle the file tree sidebar
  • T - Toggle the rich theme for code formatting
  • N - Toggle line numbers for code formatting
  • D - Toggle dark mode for the application
  • X - Download the file from cloud storage
Source code in browsr/_cli.py
@click.command(name=\"browsr\", cls=rich_click.rich_command.RichCommand)\n@click.argument(\"path\", default=None, required=False, metavar=\"PATH\")\n@click.option(\n    \"-m\",\n    \"--max-file-size\",\n    default=20,\n    type=int,\n    help=\"Maximum file size in MB for the application to open\",\n)\n@click.version_option(version=__version__, prog_name=__application__)\n@click.option(\n    \"--debug/--no-debug\",\n    default=False,\n    help=\"Enable extra debugging output\",\n    type=click.BOOL,\n)\n@click.option(\n    \"-k\", \"--kwargs\", multiple=True, help=\"Key=Value pairs to pass to the filesystem\"\n)\ndef browsr(\n    path: Optional[str],\n    debug: bool,\n    max_file_size: int,\n    kwargs: Tuple[str, ...],\n) -> None:\n    \"\"\"\n    browsr \ud83d\uddc2\ufe0f  a pleasant file explorer in your terminal\n\n    Navigate through directories and peek at files whether they're hosted locally,\n    over SSH, in GitHub, AWS S3, Google Cloud Storage, or Azure Blob Storage.\n    View code files with syntax highlighting, format JSON files, render images,\n    convert data files to navigable datatables, and more.\n\n    \\f\n\n    ![browsr](https://raw.githubusercontent.com/juftin/browsr/main/docs/_static/screenshot_utils.png)\n\n    ## Installation\n\n    It's recommended to install **`browsr`** via [pipx](https://pypa.github.io/pipx/)\n    with **`all`** optional dependencies, this enables **`browsr`** to access\n    remote cloud storage buckets and open parquet files.\n\n    ```shell\n    pipx install \"browsr[all]\"\n    ```\n\n    ## Usage Examples\n\n    ### Local\n\n    #### Browse your current working directory\n\n    ```shell\n    browsr\n    ```\n\n    #### Browse a local directory\n\n    ```shell\n    browsr/path/to/directory\n    ```\n\n    ### Cloud Storage\n\n    #### Browse an S3 bucket\n\n    ```shell\n    browsr s3://bucket-name\n    ```\n\n    #### Browse a GCS bucket\n\n    ```shell\n    browsr gs://bucket-name\n    ```\n\n    #### Browse Azure Services\n\n    ```shell\n    browsr adl://bucket-name\n    browsr az://bucket-name\n    ```\n\n    #### Pass Extra Arguments to Cloud Storage\n\n    Some cloud storage providers require extra arguments to be passed to the\n    filesystem. For example, to browse an anonymous S3 bucket, you need to pass\n    the `anon=True` argument to the filesystem. This can be done with the `-k/--kwargs`\n    argument.\n\n    ```shell\n    browsr s3://anonymous-bucket -k anon=True\n    ```\n\n    ### GitHub\n\n    #### Browse a GitHub repository\n\n    ```shell\n    browsr github://juftin:browsr\n    ```\n\n    #### Browse a GitHub Repository Branch\n\n    ```shell\n    browsr github://juftin:browsr@main\n    ```\n\n    #### Browse a Private GitHub Repository\n\n    ```shell\n    export GITHUB_TOKEN=\"ghp_1234567890\"\n    browsr github://juftin:browsr-private@main\n    ```\n\n    #### Browse a GitHub Repository Subdirectory\n\n    ```shell\n    browsr github://juftin:browsr@main/tests\n    ```\n\n    #### Browse a GitHub URL\n\n    ```shell\n    browsr https://github.com/juftin/browsr\n    ```\n\n    #### Browse a Filesystem over SSH\n\n    ```\n    browsr ssh://user@host:22\n    ```\n\n    #### Browse a SFTP Server\n\n    ```\n    browsr sftp://user@host:22/path/to/directory\n    ```\n\n    ## Key Bindings\n    - **`Q`** - Quit the application\n    - **`F`** - Toggle the file tree sidebar\n    - **`T`** - Toggle the rich theme for code formatting\n    - **`N`** - Toggle line numbers for code formatting\n    - **`D`** - Toggle dark mode for the application\n    - **`X`** - Download the file from cloud storage\n    \"\"\"\n    extra_kwargs = {}\n    if kwargs:\n        for kwarg in kwargs:\n            try:\n                key, value = kwarg.split(\"=\")\n                extra_kwargs[key] = value\n            except ValueError as ve:\n                raise click.BadParameter(\n                    message=(\n                        f\"Invalid Key/Value pair: `{kwarg}` \"\n                        \"- must be in the format Key=Value\"\n                    ),\n                    param_hint=\"kwargs\",\n                ) from ve\n    file_path = path or os.getcwd()\n    config = TextualAppContext(\n        file_path=file_path,\n        debug=debug,\n        max_file_size=max_file_size,\n        kwargs=extra_kwargs,\n    )\n    app = Browsr(config_object=config)\n    app.run()\n
"},{"location":"reference/_config/","title":"_config","text":"

browsr configuration file

"},{"location":"reference/_utils/","title":"_utils","text":"

Code Browsr Utility Functions

"},{"location":"reference/_utils/#browsr._utils.ArchiveFileError","title":"ArchiveFileError","text":"

Bases: Exception

Archive File Error

Source code in browsr/_utils.py
class ArchiveFileError(Exception):\n    \"\"\"\n    Archive File Error\n    \"\"\"\n
"},{"location":"reference/_utils/#browsr._utils.FileInfo","title":"FileInfo dataclass","text":"

File Information Object

Source code in browsr/_utils.py
@dataclass\nclass FileInfo:\n    \"\"\"\n    File Information Object\n    \"\"\"\n\n    file: pathlib.Path\n    size: int\n    last_modified: Optional[datetime.datetime]\n    stat: Union[Dict[str, Any], os.stat_result]\n    is_local: bool\n    is_file: bool\n    owner: str\n    group: str\n    is_cloudpath: bool\n
"},{"location":"reference/_utils/#browsr._utils._open_pdf_as_image","title":"_open_pdf_as_image(buf)","text":"

Open a PDF file and return a PIL.Image object

Source code in browsr/_utils.py
def _open_pdf_as_image(buf: BinaryIO) -> Image.Image:\n    \"\"\"\n    Open a PDF file and return a PIL.Image object\n    \"\"\"\n    doc = fitz.open(stream=buf.read(), filetype=\"pdf\")\n    pix: Pixmap = doc[0].get_pixmap()\n    if pix.colorspace is None:\n        mode = \"L\"\n    elif pix.colorspace.n == 1:\n        mode = \"L\" if pix.alpha == 0 else \"LA\"\n    elif pix.colorspace.n == 3:  # noqa: PLR2004\n        mode = \"RGB\" if pix.alpha == 0 else \"RGBA\"\n    else:\n        mode = \"CMYK\"\n    return Image.frombytes(size=(pix.width, pix.height), data=pix.samples, mode=mode)\n
"},{"location":"reference/_utils/#browsr._utils.get_file_info","title":"get_file_info(file_path)","text":"

Get File Information, Regardless of the FileSystem

Source code in browsr/_utils.py
def get_file_info(file_path: pathlib.Path) -> FileInfo:\n    \"\"\"\n    Get File Information, Regardless of the FileSystem\n    \"\"\"\n    try:\n        stat: Union[Dict[str, Any], os.stat_result] = file_path.stat()\n        is_file = file_path.is_file()\n    except PermissionError:\n        stat = {\"size\": 0}\n        is_file = True\n    is_cloudpath = is_remote_path(file_path)\n    if isinstance(stat, dict):\n        lower_dict = {key.lower(): value for key, value in stat.items()}\n        file_size = lower_dict[\"size\"]\n        modified_keys = [\"lastmodified\", \"updated\", \"mtime\"]\n        last_modified = None\n        for modified_key in modified_keys:\n            if modified_key in lower_dict:\n                last_modified = lower_dict[modified_key]\n                break\n        if isinstance(last_modified, str):\n            last_modified = datetime.datetime.fromisoformat(last_modified[:-1])\n        return FileInfo(\n            file=file_path,\n            size=file_size,\n            last_modified=last_modified,\n            stat=stat,\n            is_local=False,\n            is_file=is_file,\n            owner=\"\",\n            group=\"\",\n            is_cloudpath=is_cloudpath,\n        )\n    else:\n        last_modified = datetime.datetime.fromtimestamp(\n            stat.st_mtime, tz=datetime.timezone.utc\n        )\n        try:\n            owner = file_path.owner()\n            group = file_path.group()\n        except NotImplementedError:\n            owner = \"\"\n            group = \"\"\n        return FileInfo(\n            file=file_path,\n            size=stat.st_size,\n            last_modified=last_modified,\n            stat=stat,\n            is_local=True,\n            is_file=is_file,\n            owner=owner,\n            group=group,\n            is_cloudpath=is_cloudpath,\n        )\n
"},{"location":"reference/_utils/#browsr._utils.handle_duplicate_filenames","title":"handle_duplicate_filenames(file_path)","text":"

Handle Duplicate Filenames

Duplicate filenames are handled by appending a number to the filename in the form of \"filename (1).ext\", \"filename (2).ext\", etc.

Source code in browsr/_utils.py
def handle_duplicate_filenames(file_path: pathlib.Path) -> pathlib.Path:\n    \"\"\"\n    Handle Duplicate Filenames\n\n    Duplicate filenames are handled by appending a number to the filename\n    in the form of \"filename (1).ext\", \"filename (2).ext\", etc.\n    \"\"\"\n    if not file_path.exists():\n        return file_path\n    else:\n        i = 1\n        while True:\n            new_file_stem = f\"{file_path.stem} ({i})\"\n            new_file_path = file_path.with_stem(new_file_stem)\n            if not new_file_path.exists():\n                return new_file_path\n            i += 1\n
"},{"location":"reference/_utils/#browsr._utils.handle_github_url","title":"handle_github_url(url)","text":"

Handle GitHub URLs

GitHub URLs are handled by converting them to the raw URL.

Source code in browsr/_utils.py
def handle_github_url(url: str) -> str:\n    \"\"\"\n    Handle GitHub URLs\n\n    GitHub URLs are handled by converting them to the raw URL.\n    \"\"\"\n    try:\n        import requests\n    except ImportError as e:\n        raise ImportError(\n            \"The requests library is required to browse GitHub files. \"\n            \"Install browsr with the `remote` extra to install requests.\"\n        ) from e\n\n    gitub_prefix = \"github://\"\n    if gitub_prefix in url and \"@\" not in url:\n        _, user_password = url.split(\"github://\")\n        org, repo_str = user_password.split(\":\")\n        repo, *args = repo_str.split(\"/\")\n    elif gitub_prefix in url and \"@\" in url:\n        return url\n    elif \"github.com\" in url.lower():\n        _, org, repo, *args = url.split(\"/\")\n    else:\n        msg = f\"Invalid GitHub URL: {url}\"\n        raise ValueError(msg)\n    token = os.getenv(\"GITHUB_TOKEN\")\n    auth = {\"auth\": (\"Bearer\", token)} if token is not None else {}\n    resp = requests.get(\n        f\"https://api.github.com/repos/{org}/{repo}\",\n        headers={\"Accept\": \"application/vnd.github.v3+json\"},\n        timeout=10,\n        **auth,  # type: ignore[arg-type]\n    )\n    resp.raise_for_status()\n    default_branch = resp.json()[\"default_branch\"]\n    arg_str = \"/\".join(args)\n    github_uri = f\"{gitub_prefix}{org}:{repo}@{default_branch}/{arg_str}\".rstrip(\"/\")\n    return github_uri\n
"},{"location":"reference/_utils/#browsr._utils.open_image","title":"open_image(document, screen_width)","text":"

Open an image file and return a rich_pixels.Pixels object

Source code in browsr/_utils.py
def open_image(document: pathlib.Path, screen_width: float) -> Pixels:\n    \"\"\"\n    Open an image file and return a rich_pixels.Pixels object\n    \"\"\"\n    with document.open(\"rb\") as buf:\n        if document.suffix.lower() == \".pdf\":\n            image = _open_pdf_as_image(buf=buf)\n        else:\n            image = Image.open(buf)\n        image_width = image.width\n        image_height = image.height\n        size_ratio = image_width / screen_width\n        new_width = min(int(image_width / size_ratio), image_width)\n        new_height = min(int(image_height / size_ratio), image_height)\n        resized = image.resize((new_width, new_height))\n        return rich_pixels.Pixels.from_image(resized)\n
"},{"location":"reference/_utils/#browsr._utils.render_file_to_string","title":"render_file_to_string(file_info)","text":"

Render File to String

Parameters:

Name Type Description Default file_info FileInfo

The file to render.

required

Returns:

Type Description str

The rendered file as a string.

Source code in browsr/_utils.py
def render_file_to_string(file_info: FileInfo) -> str:\n    \"\"\"\n    Render File to String\n\n    Parameters\n    ----------\n    file_info : FileInfo\n        The file to render.\n\n    Returns\n    -------\n    str\n        The rendered file as a string.\n    \"\"\"\n    try:\n        return file_info.file.read_text(encoding=\"utf-8\")\n    except UnicodeDecodeError as e:\n        if file_info.file.suffix.lower() in [\".tar\", \".gz\", \".zip\", \".tgz\"]:\n            msg = f\"Cannot render archive file {file_info.file}.\"\n            raise ArchiveFileError(msg) from e\n        else:\n            raise e\n
"},{"location":"reference/_version/","title":"_version","text":"

browsr version file.

"},{"location":"reference/browsr/","title":"browsr","text":"

Browsr TUI App

This module contains the code browser app for the browsr package. This app was inspired by the CodeBrowser example from textual

"},{"location":"reference/browsr/#browsr.browsr.Browsr","title":"Browsr","text":"

Bases: BrowsrTextualApp

Textual code browser app.

Source code in browsr/browsr.py
class Browsr(BrowsrTextualApp):\n    \"\"\"\n    Textual code browser app.\n    \"\"\"\n\n    TITLE = __application__\n    CSS_PATH = \"browsr.css\"\n    BINDINGS: ClassVar[List[BindingType]] = [\n        Binding(key=\"q\", action=\"quit\", description=\"Quit\"),\n        Binding(key=\"f\", action=\"toggle_files\", description=\"Toggle Files\"),\n        Binding(key=\"t\", action=\"theme\", description=\"Toggle Theme\"),\n        Binding(key=\"n\", action=\"linenos\", description=\"Toggle Line Numbers\"),\n        Binding(key=\"d\", action=\"toggle_dark\", description=\"Toggle Dark Mode\"),\n        Binding(key=\".\", action=\"parent_dir\", description=\"Parent Directory\"),\n    ]\n\n    show_tree = var(True)\n    theme_index = var(0)\n    linenos = var(False)\n    rich_themes = favorite_themes\n    selected_file_path: Union[upath.UPath, pathlib.Path, None, var[None]] = var(None)\n    force_show_tree = var(False)\n    hidden_table_view = var(False)\n\n    def watch_show_tree(self, show_tree: bool) -> None:\n        \"\"\"\n        Called when show_tree is modified.\n        \"\"\"\n        self.set_class(show_tree, \"-show-tree\")\n\n    def compose(self) -> Iterable[Widget]:\n        \"\"\"\n        Compose our UI.\n        \"\"\"\n        file_path = self.config_object.path\n        if is_remote_path(file_path):\n            self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n        if file_path.is_file():\n            self.selected_file_path = file_path\n            file_path = file_path.parent\n        elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n            if TYPE_CHECKING:\n                assert isinstance(file_path, pathlib.Path)\n            self.selected_file_path = file_path.joinpath(\"README.md\")\n            self.force_show_tree = True\n        self.header = Header()\n        yield self.header\n        self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n        self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n        self.table_view: DataTable[str] = VimDataTable(\n            zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n        )\n        self.table_view.display = False\n        self.confirmation = ConfirmationPopUp()\n        self.confirmation_window = Container(\n            self.confirmation, id=\"confirmation-container\"\n        )\n        self.confirmation_window.display = False\n        self.container = Container(\n            self.directory_tree,\n            self.code_view,\n            self.table_view,\n            self.confirmation_window,\n        )\n        yield self.container\n        self.file_information = CurrentFileInfoBar()\n        self.info_bar = Horizontal(\n            self.file_information,\n            id=\"file-info-bar\",\n        )\n        if self.selected_file_path is not None:\n            self.file_information.file_info = get_file_info(\n                file_path=self.selected_file_path\n            )\n        yield self.info_bar\n        self.footer = Footer()\n        yield self.footer\n\n    def render_document(\n        self,\n        file_info: FileInfo,\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n        \"\"\"\n        Render a Code Doc Given Its Extension\n\n        Parameters\n        ----------\n        file_info: FileInfo\n            The file info object for the file to render.\n\n        Returns\n        -------\n        Union[Syntax, Markdown, DataTable[str], Pixels]\n        \"\"\"\n        document = file_info.file\n        if document.suffix == \".md\":\n            return Markdown(\n                document.read_text(encoding=\"utf-8\"),\n                code_theme=self.rich_themes[self.theme_index],\n                hyperlinks=True,\n            )\n        elif \".csv\" in document.suffixes:\n            df = pd.read_csv(document, nrows=1000)\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix == \".parquet\":\n            df = pd.read_parquet(document)[:1000]\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix.lower() in [\".feather\", \".fea\"]:\n            df = pd.read_feather(document)[:1000]\n            return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n        elif document.suffix.lower() in image_file_extensions:\n            screen_width = self.app.size.width / 4\n            content = open_image(document=document, screen_width=screen_width)\n            return content\n        elif document.suffix.lower() in [\".json\"]:\n            code_str = render_file_to_string(file_info=file_info)\n            try:\n                code_obj = json.loads(code_str)\n                code_lines = json.dumps(code_obj, indent=2).splitlines()\n            except json.JSONDecodeError:\n                code_lines = code_str.splitlines()\n        else:\n            code_str = render_file_to_string(file_info=file_info)\n            code_lines = code_str.splitlines()\n        code = \"\\n\".join(code_lines[:1000])\n        lexer = Syntax.guess_lexer(str(document), code=code)\n        return Syntax(\n            code=code,\n            lexer=lexer,\n            line_numbers=self.linenos,\n            word_wrap=False,\n            indent_guides=False,\n            theme=self.rich_themes[self.theme_index],\n        )\n\n    def _handle_file_size(self, file_info: FileInfo) -> None:\n        \"\"\"\n        Handle a File Size\n        \"\"\"\n        file_size_mb = file_info.size / 1000 / 1000\n        too_large = file_size_mb >= self.config_object.max_file_size\n        exception = (\n            True\n            if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n            else False\n        )\n        if too_large is True and exception is not True:\n            raise FileSizeError(\"File too large\")\n\n    def _render_file(\n        self, file_path: pathlib.Path, code_view: Static, font: str\n    ) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n        \"\"\"\n        Render a File\n        \"\"\"\n        try:\n            file_info = get_file_info(file_path=file_path)\n            self._handle_file_size(file_info=file_info)\n            element = self.render_document(file_info=file_info)\n            return element\n        except FileSizeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except PermissionError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"PERMISSION\", font=font)\n                + \"\\n\\n\"\n                + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except UnicodeError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except ArchiveFileError:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n            )\n            self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n        except Exception:\n            self.table_view.display = False\n            self.code_view.display = True\n            code_view.update(\n                Traceback(theme=self.rich_themes[self.theme_index], width=None)\n            )\n            self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n        return None\n\n    def render_code_page(\n        self,\n        file_path: pathlib.Path,\n        scroll_home: bool = True,\n        content: Optional[Any] = None,\n    ) -> None:\n        \"\"\"\n        Render the Code Page with Rich Syntax\n        \"\"\"\n        code_view = self.query_one(\"#code\", Static)\n        font = \"univers\"\n        if content is not None:\n            code_view.update(text2art(content, font=font))\n            return\n        element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n        if isinstance(element, DataTable):\n            self.code_view.display = False\n            self.table_view.display = True\n            if self.code_view.has_focus:\n                self.table_view.focus()\n            if scroll_home is True:\n                self.query_one(DataTable).scroll_home(animate=False)\n        elif element is not None:\n            self.table_view.display = False\n            self.code_view.display = True\n            if self.table_view.has_focus:\n                self.code_view.focus()\n            code_view.update(element)\n            if scroll_home is True:\n                self.query_one(\"#code-view\").scroll_home(animate=False)\n            self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n\n    @on(Mount)\n    def start_up_app(self) -> None:\n        \"\"\"\n        On Application Mount - See If a File Should be Displayed\n        \"\"\"\n        if self.selected_file_path is not None:\n            self.show_tree = self.force_show_tree\n            self.render_code_page(file_path=self.selected_file_path)\n            if self.show_tree is False and self.code_view.display is True:\n                self.code_view.focus()\n            elif self.show_tree is False and self.table_view.display is True:\n                self.table_view.focus()\n        else:\n            self.show_tree = True\n            self.render_code_page(\n                file_path=pathlib.Path.cwd(), content=__application__.upper()\n            )\n\n    @on(BrowsrDirectoryTree.FileSelected)\n    def handle_file_selected(\n        self,\n        message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n    ) -> None:\n        \"\"\"\n        Called when the user click a file in the directory tree.\n        \"\"\"\n        self.selected_file_path = upath.UPath(message.path)\n        file_info = get_file_info(file_path=self.selected_file_path)\n        self.render_code_page(file_path=upath.UPath(message.path))\n        self.file_information.file_info = file_info\n\n    def action_toggle_files(self) -> None:\n        \"\"\"\n        Called in response to key binding.\n        \"\"\"\n        self.show_tree = not self.show_tree\n\n    def action_theme(self) -> None:\n        \"\"\"\n        An action to toggle rich theme.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.theme_index < len(self.rich_themes) - 1:\n            self.theme_index += 1\n        else:\n            self.theme_index = 0\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def action_linenos(self) -> None:\n        \"\"\"\n        An action to toggle line numbers.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        self.linenos = not self.linenos\n        self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n\n    def _get_download_file_name(self) -> pathlib.Path:\n        \"\"\"\n        Get the download file name.\n        \"\"\"\n        download_dir = pathlib.Path.home() / \"Downloads\"\n        if not download_dir.exists():\n            msg = f\"Download directory {download_dir} not found\"\n            raise FileNotFoundError(msg)\n        download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n        handled_download_path = handle_duplicate_filenames(file_path=download_path)\n        return handled_download_path\n\n    @work(thread=True)\n    def download_selected_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            with self.selected_file_path.open(\"rb\") as file_handle:\n                with handled_download_path.open(\"wb\") as download_handle:\n                    shutil.copyfileobj(file_handle, download_handle)\n\n    def action_download_file(self) -> None:\n        \"\"\"\n        Download the selected file.\n        \"\"\"\n        if self.selected_file_path is None:\n            return\n        elif self.selected_file_path.is_dir():\n            return\n        elif is_remote_path(self.selected_file_path):\n            handled_download_path = self._get_download_file_name()\n            prompt_message: str = dedent(\n                f\"\"\"\n                ## File Download\n\n                **Are you sure you want to download that file?**\n\n                **File:** `{self.selected_file_path}`\n\n                **Path:** `{handled_download_path}`\n                \"\"\"\n            )\n            self.confirmation.download_message.update(Markdown(prompt_message))\n            self.confirmation.refresh()\n            self.hidden_table_view = self.table_view.display\n            self.table_view.display = False\n            self.confirmation_window.display = True\n\n    def action_parent_dir(self) -> None:\n        \"\"\"\n        Go to the parent directory\n        \"\"\"\n        new_path = self.config_object.path.parent.resolve()\n        if new_path != self.config_object.path:\n            self.config_object.file_path = str(new_path)\n            self.directory_tree.path = new_path\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr._get_download_file_name","title":"_get_download_file_name()","text":"

Get the download file name.

Source code in browsr/browsr.py
def _get_download_file_name(self) -> pathlib.Path:\n    \"\"\"\n    Get the download file name.\n    \"\"\"\n    download_dir = pathlib.Path.home() / \"Downloads\"\n    if not download_dir.exists():\n        msg = f\"Download directory {download_dir} not found\"\n        raise FileNotFoundError(msg)\n    download_path = download_dir / self.selected_file_path.name  # type: ignore[union-attr]\n    handled_download_path = handle_duplicate_filenames(file_path=download_path)\n    return handled_download_path\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr._handle_file_size","title":"_handle_file_size(file_info)","text":"

Handle a File Size

Source code in browsr/browsr.py
def _handle_file_size(self, file_info: FileInfo) -> None:\n    \"\"\"\n    Handle a File Size\n    \"\"\"\n    file_size_mb = file_info.size / 1000 / 1000\n    too_large = file_size_mb >= self.config_object.max_file_size\n    exception = (\n        True\n        if is_local_path(file_info.file) and \".csv\" in file_info.file.suffixes\n        else False\n    )\n    if too_large is True and exception is not True:\n        raise FileSizeError(\"File too large\")\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr._render_file","title":"_render_file(file_path, code_view, font)","text":"

Render a File

Source code in browsr/browsr.py
def _render_file(\n    self, file_path: pathlib.Path, code_view: Static, font: str\n) -> Union[Syntax, Markdown, DataTable[str], Pixels, None]:\n    \"\"\"\n    Render a File\n    \"\"\"\n    try:\n        file_info = get_file_info(file_path=file_path)\n        self._handle_file_size(file_info=file_info)\n        element = self.render_document(file_info=file_info)\n        return element\n    except FileSizeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"FILE TOO\", font=font) + \"\\n\\n\" + text2art(\"LARGE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except PermissionError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"PERMISSION\", font=font)\n            + \"\\n\\n\"\n            + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except UnicodeError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ENCODING\", font=font) + \"\\n\\n\" + text2art(\"ERROR\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except ArchiveFileError:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            text2art(\"ARCHIVE\", font=font) + \"\\n\\n\" + text2art(\"FILE\", font=font)\n        )\n        self.sub_title = f\"ERROR [{self.rich_themes[self.theme_index]}]\"\n    except Exception:\n        self.table_view.display = False\n        self.code_view.display = True\n        code_view.update(\n            Traceback(theme=self.rich_themes[self.theme_index], width=None)\n        )\n        self.sub_title = \"ERROR\" + f\" [{self.rich_themes[self.theme_index]}]\"\n    return None\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_download_file","title":"action_download_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
def action_download_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        prompt_message: str = dedent(\n            f\"\"\"\n            ## File Download\n\n            **Are you sure you want to download that file?**\n\n            **File:** `{self.selected_file_path}`\n\n            **Path:** `{handled_download_path}`\n            \"\"\"\n        )\n        self.confirmation.download_message.update(Markdown(prompt_message))\n        self.confirmation.refresh()\n        self.hidden_table_view = self.table_view.display\n        self.table_view.display = False\n        self.confirmation_window.display = True\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_linenos","title":"action_linenos()","text":"

An action to toggle line numbers.

Source code in browsr/browsr.py
def action_linenos(self) -> None:\n    \"\"\"\n    An action to toggle line numbers.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    self.linenos = not self.linenos\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_parent_dir","title":"action_parent_dir()","text":"

Go to the parent directory

Source code in browsr/browsr.py
def action_parent_dir(self) -> None:\n    \"\"\"\n    Go to the parent directory\n    \"\"\"\n    new_path = self.config_object.path.parent.resolve()\n    if new_path != self.config_object.path:\n        self.config_object.file_path = str(new_path)\n        self.directory_tree.path = new_path\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_theme","title":"action_theme()","text":"

An action to toggle rich theme.

Source code in browsr/browsr.py
def action_theme(self) -> None:\n    \"\"\"\n    An action to toggle rich theme.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.theme_index < len(self.rich_themes) - 1:\n        self.theme_index += 1\n    else:\n        self.theme_index = 0\n    self.render_code_page(file_path=self.selected_file_path, scroll_home=False)\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.action_toggle_files","title":"action_toggle_files()","text":"

Called in response to key binding.

Source code in browsr/browsr.py
def action_toggle_files(self) -> None:\n    \"\"\"\n    Called in response to key binding.\n    \"\"\"\n    self.show_tree = not self.show_tree\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.compose","title":"compose()","text":"

Compose our UI.

Source code in browsr/browsr.py
def compose(self) -> Iterable[Widget]:\n    \"\"\"\n    Compose our UI.\n    \"\"\"\n    file_path = self.config_object.path\n    if is_remote_path(file_path):\n        self.bind(\"x\", \"download_file\", description=\"Download File\", show=True)\n    if file_path.is_file():\n        self.selected_file_path = file_path\n        file_path = file_path.parent\n    elif file_path.is_dir() and file_path.joinpath(\"README.md\").exists():\n        if TYPE_CHECKING:\n            assert isinstance(file_path, pathlib.Path)\n        self.selected_file_path = file_path.joinpath(\"README.md\")\n        self.force_show_tree = True\n    self.header = Header()\n    yield self.header\n    self.directory_tree = BrowsrDirectoryTree(str(file_path), id=\"tree-view\")\n    self.code_view = VimScroll(Static(id=\"code\", expand=True), id=\"code-view\")\n    self.table_view: DataTable[str] = VimDataTable(\n        zebra_stripes=True, show_header=True, show_cursor=True, id=\"table-view\"\n    )\n    self.table_view.display = False\n    self.confirmation = ConfirmationPopUp()\n    self.confirmation_window = Container(\n        self.confirmation, id=\"confirmation-container\"\n    )\n    self.confirmation_window.display = False\n    self.container = Container(\n        self.directory_tree,\n        self.code_view,\n        self.table_view,\n        self.confirmation_window,\n    )\n    yield self.container\n    self.file_information = CurrentFileInfoBar()\n    self.info_bar = Horizontal(\n        self.file_information,\n        id=\"file-info-bar\",\n    )\n    if self.selected_file_path is not None:\n        self.file_information.file_info = get_file_info(\n            file_path=self.selected_file_path\n        )\n    yield self.info_bar\n    self.footer = Footer()\n    yield self.footer\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.download_selected_file","title":"download_selected_file()","text":"

Download the selected file.

Source code in browsr/browsr.py
@work(thread=True)\ndef download_selected_file(self) -> None:\n    \"\"\"\n    Download the selected file.\n    \"\"\"\n    if self.selected_file_path is None:\n        return\n    elif self.selected_file_path.is_dir():\n        return\n    elif is_remote_path(self.selected_file_path):\n        handled_download_path = self._get_download_file_name()\n        with self.selected_file_path.open(\"rb\") as file_handle:\n            with handled_download_path.open(\"wb\") as download_handle:\n                shutil.copyfileobj(file_handle, download_handle)\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.handle_file_selected","title":"handle_file_selected(message)","text":"

Called when the user click a file in the directory tree.

Source code in browsr/browsr.py
@on(BrowsrDirectoryTree.FileSelected)\ndef handle_file_selected(\n    self,\n    message: BrowsrDirectoryTree.FileSelected,  # type: ignore[name-defined]\n) -> None:\n    \"\"\"\n    Called when the user click a file in the directory tree.\n    \"\"\"\n    self.selected_file_path = upath.UPath(message.path)\n    file_info = get_file_info(file_path=self.selected_file_path)\n    self.render_code_page(file_path=upath.UPath(message.path))\n    self.file_information.file_info = file_info\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.render_code_page","title":"render_code_page(file_path, scroll_home=True, content=None)","text":"

Render the Code Page with Rich Syntax

Source code in browsr/browsr.py
def render_code_page(\n    self,\n    file_path: pathlib.Path,\n    scroll_home: bool = True,\n    content: Optional[Any] = None,\n) -> None:\n    \"\"\"\n    Render the Code Page with Rich Syntax\n    \"\"\"\n    code_view = self.query_one(\"#code\", Static)\n    font = \"univers\"\n    if content is not None:\n        code_view.update(text2art(content, font=font))\n        return\n    element = self._render_file(file_path=file_path, code_view=code_view, font=font)\n    if isinstance(element, DataTable):\n        self.code_view.display = False\n        self.table_view.display = True\n        if self.code_view.has_focus:\n            self.table_view.focus()\n        if scroll_home is True:\n            self.query_one(DataTable).scroll_home(animate=False)\n    elif element is not None:\n        self.table_view.display = False\n        self.code_view.display = True\n        if self.table_view.has_focus:\n            self.code_view.focus()\n        code_view.update(element)\n        if scroll_home is True:\n            self.query_one(\"#code-view\").scroll_home(animate=False)\n        self.sub_title = f\"{file_path} [{self.rich_themes[self.theme_index]}]\"\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.render_document","title":"render_document(file_info)","text":"

Render a Code Doc Given Its Extension

Parameters:

Name Type Description Default file_info FileInfo

The file info object for the file to render.

required

Returns:

Type Description Union[Syntax, Markdown, DataTable[str], Pixels] Source code in browsr/browsr.py
def render_document(\n    self,\n    file_info: FileInfo,\n) -> Union[Syntax, Markdown, DataTable[str], Pixels]:\n    \"\"\"\n    Render a Code Doc Given Its Extension\n\n    Parameters\n    ----------\n    file_info: FileInfo\n        The file info object for the file to render.\n\n    Returns\n    -------\n    Union[Syntax, Markdown, DataTable[str], Pixels]\n    \"\"\"\n    document = file_info.file\n    if document.suffix == \".md\":\n        return Markdown(\n            document.read_text(encoding=\"utf-8\"),\n            code_theme=self.rich_themes[self.theme_index],\n            hyperlinks=True,\n        )\n    elif \".csv\" in document.suffixes:\n        df = pd.read_csv(document, nrows=1000)\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix == \".parquet\":\n        df = pd.read_parquet(document)[:1000]\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix.lower() in [\".feather\", \".fea\"]:\n        df = pd.read_feather(document)[:1000]\n        return self.df_to_table(pandas_dataframe=df, table=self.table_view)\n    elif document.suffix.lower() in image_file_extensions:\n        screen_width = self.app.size.width / 4\n        content = open_image(document=document, screen_width=screen_width)\n        return content\n    elif document.suffix.lower() in [\".json\"]:\n        code_str = render_file_to_string(file_info=file_info)\n        try:\n            code_obj = json.loads(code_str)\n            code_lines = json.dumps(code_obj, indent=2).splitlines()\n        except json.JSONDecodeError:\n            code_lines = code_str.splitlines()\n    else:\n        code_str = render_file_to_string(file_info=file_info)\n        code_lines = code_str.splitlines()\n    code = \"\\n\".join(code_lines[:1000])\n    lexer = Syntax.guess_lexer(str(document), code=code)\n    return Syntax(\n        code=code,\n        lexer=lexer,\n        line_numbers=self.linenos,\n        word_wrap=False,\n        indent_guides=False,\n        theme=self.rich_themes[self.theme_index],\n    )\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.start_up_app","title":"start_up_app()","text":"

On Application Mount - See If a File Should be Displayed

Source code in browsr/browsr.py
@on(Mount)\ndef start_up_app(self) -> None:\n    \"\"\"\n    On Application Mount - See If a File Should be Displayed\n    \"\"\"\n    if self.selected_file_path is not None:\n        self.show_tree = self.force_show_tree\n        self.render_code_page(file_path=self.selected_file_path)\n        if self.show_tree is False and self.code_view.display is True:\n            self.code_view.focus()\n        elif self.show_tree is False and self.table_view.display is True:\n            self.table_view.focus()\n    else:\n        self.show_tree = True\n        self.render_code_page(\n            file_path=pathlib.Path.cwd(), content=__application__.upper()\n        )\n
"},{"location":"reference/browsr/#browsr.browsr.Browsr.watch_show_tree","title":"watch_show_tree(show_tree)","text":"

Called when show_tree is modified.

Source code in browsr/browsr.py
def watch_show_tree(self, show_tree: bool) -> None:\n    \"\"\"\n    Called when show_tree is modified.\n    \"\"\"\n    self.set_class(show_tree, \"-show-tree\")\n
"},{"location":"reference/universal_directory_tree/","title":"universal_directory_tree","text":"

A universal directory tree widget for Textual.

"},{"location":"reference/universal_directory_tree/#browsr.universal_directory_tree.BrowsrDirectoryTree","title":"BrowsrDirectoryTree","text":"

Bases: UniversalDirectoryTree

A DirectoryTree that can handle any filesystem.

Source code in browsr/universal_directory_tree.py
class BrowsrDirectoryTree(UniversalDirectoryTree):\n    \"\"\"\n    A DirectoryTree that can handle any filesystem.\n    \"\"\"\n\n    PATH: type[BrowsrPath] = BrowsrPath\n\n    BINDINGS: ClassVar[list[BindingType]] = [\n        *UniversalDirectoryTree.BINDINGS,\n        *vim_cursor_bindings,\n    ]\n\n    @classmethod\n    def _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n        \"\"\"\n        Handle scenarios when someone wants to browse all of s3\n\n        This is because S3FS handles the root directory differently\n        than other filesystems\n        \"\"\"\n        if str(dir_path) == \"s3:/\":\n            sub_buckets = sorted(\n                Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n            )\n            return sub_buckets\n        return None\n\n    def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n        \"\"\"\n        Populate the given tree node with the given directory content.\n\n        This function overrides the original textual method to handle root level\n        cloud buckets.\n        \"\"\"\n        top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n        if top_level_buckets is not None:\n            content = top_level_buckets\n        node.remove_children()\n        for path in content:\n            if top_level_buckets is not None:\n                path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n            else:\n                path_name = path.name\n            node.add(\n                path_name,\n                data=DirEntry(path),\n                allow_expand=self._safe_is_dir(path),\n            )\n        node.expand()\n
"},{"location":"reference/universal_directory_tree/#browsr.universal_directory_tree.BrowsrDirectoryTree._handle_top_level_bucket","title":"_handle_top_level_bucket(dir_path) classmethod","text":"

Handle scenarios when someone wants to browse all of s3

This is because S3FS handles the root directory differently than other filesystems

Source code in browsr/universal_directory_tree.py
@classmethod\ndef _handle_top_level_bucket(cls, dir_path: Path) -> Iterable[Path] | None:\n    \"\"\"\n    Handle scenarios when someone wants to browse all of s3\n\n    This is because S3FS handles the root directory differently\n    than other filesystems\n    \"\"\"\n    if str(dir_path) == \"s3:/\":\n        sub_buckets = sorted(\n            Path(f\"s3://{bucket.name}\") for bucket in dir_path.iterdir()\n        )\n        return sub_buckets\n    return None\n
"},{"location":"reference/universal_directory_tree/#browsr.universal_directory_tree.BrowsrDirectoryTree._populate_node","title":"_populate_node(node, content)","text":"

Populate the given tree node with the given directory content.

This function overrides the original textual method to handle root level cloud buckets.

Source code in browsr/universal_directory_tree.py
def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None:\n    \"\"\"\n    Populate the given tree node with the given directory content.\n\n    This function overrides the original textual method to handle root level\n    cloud buckets.\n    \"\"\"\n    top_level_buckets = self._handle_top_level_bucket(dir_path=node.data.path)\n    if top_level_buckets is not None:\n        content = top_level_buckets\n    node.remove_children()\n    for path in content:\n        if top_level_buckets is not None:\n            path_name = str(path).replace(\"s3://\", \"\").rstrip(\"/\")\n        else:\n            path_name = path.name\n        node.add(\n            path_name,\n            data=DirEntry(path),\n            allow_expand=self._safe_is_dir(path),\n        )\n    node.expand()\n
"}]} \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index be33385e37ded689bf44bbba7721906dc5e0c3b8..10caa7156484923cda4ab8e038e402704ac5385e 100644 GIT binary patch delta 13 Ucmb=gXP58h;P^jz#zgiC03gH!`Tzg` delta 13 Ucmb=gXP58h;JD1)I+48s0365!GXMYp