From d1aa6bac6894905b53d192c5345c88697b295e2b Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Sat, 8 Jun 2024 20:51:37 +0100 Subject: [PATCH 1/7] refac: tui: Reduce load time of `.tui` top-level - Change: Localize all non-stdlib imports. - Change: Define `Loop` within `init()`. --- src/termvisage/tui/__init__.py | 56 ++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/termvisage/tui/__init__.py b/src/termvisage/tui/__init__.py index 9cb2260..2c3b65f 100644 --- a/src/termvisage/tui/__init__.py +++ b/src/termvisage/tui/__init__.py @@ -2,23 +2,15 @@ from __future__ import annotations -import argparse import logging as _logging import os from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, Tuple, Union -import urwid -from term_image.image import GraphicsImage -from term_image.utils import get_cell_size, lock_tty -from term_image.widget import UrwidImageScreen +if TYPE_CHECKING: + import argparse -from .. import notify -from ..config import config_options -from . import main, render -from .keys import adjust_footer, update_footer_expand_collapse_icon -from .main import process_input, scan_dir_grid, scan_dir_menu, sort_key_lexi -from .widgets import Image, info_bar, main as main_widget + from .widgets import Image def init( @@ -29,12 +21,36 @@ def init( ImageClass: type, ) -> None: """Initializes the TUI""" + + import urwid + from term_image.image import GraphicsImage + from term_image.utils import get_cell_size, lock_tty + from term_image.widget import UrwidImageScreen + + from .. import notify from ..__main__ import TEMP_DIR + from ..config import config_options from ..logging import LoggingThread, log - from . import keys + from . import keys, main, render + from .keys import adjust_footer, update_footer_expand_collapse_icon + from .main import process_input, scan_dir_grid, scan_dir_menu, sort_key_lexi + from .widgets import Image, info_bar, main as main_widget global active, initialized + class Loop(urwid.MainLoop): + def start(self): + update_footer_expand_collapse_icon() + adjust_footer() + return super().start() + + def process_input(self, keys): + if "window resize" in keys: + # "window resize" never reaches `.unhandled_input()`. + # Adjust the footer and clear grid cache. + keys.append("resized") + return super().process_input(keys) + if args.debug: main_widget.contents.insert( 1, (urwid.AttrMap(urwid.Filler(info_bar), "reverse"), ("given", 1)) @@ -173,20 +189,6 @@ def init( os.close(main.update_pipe) -class Loop(urwid.MainLoop): - def start(self): - update_footer_expand_collapse_icon() - adjust_footer() - return super().start() - - def process_input(self, keys): - if "window resize" in keys: - # "window resize" never reaches `.unhandled_input()`. - # Adjust the footer and clear grid cache. - keys.append("resized") - return super().process_input(keys) - - active = initialized = False palette = [ ("default", "", "", "", "", ""), From 0ef97b791d7771a07660b93fe145d1b722681914 Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Sun, 16 Jun 2024 21:53:08 +0100 Subject: [PATCH 2/7] refac: cli: Lazily import `.tui` - Change: Import `.tui` if and only of launching the TUI. - Change: Use `BaseImage` instances in image lists instead of `Image` widget instances. --- src/termvisage/__main__.py | 4 ++-- src/termvisage/cli.py | 40 +++++++++++++++++++++------------- src/termvisage/tui/__init__.py | 9 +++++--- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/termvisage/__main__.py b/src/termvisage/__main__.py index 8ed7408..b31c535 100644 --- a/src/termvisage/__main__.py +++ b/src/termvisage/__main__.py @@ -120,8 +120,8 @@ def finish_multi_logging(): write_tty(RESTORE_WINDOW_TITLE_b) # Explicit cleanup is necessary since the top-level `Image` widgets # will still hold references to the `BaseImage` instances - for _, image_w in cli.url_images: - image_w._ti_image.close() + for _, image in cli.url_images: + image.close() # Session-specific temporary data directory. diff --git a/src/termvisage/cli.py b/src/termvisage/cli.py index 5d4d0f6..90345ed 100644 --- a/src/termvisage/cli.py +++ b/src/termvisage/cli.py @@ -16,7 +16,17 @@ from tempfile import mkdtemp from threading import current_thread from time import sleep -from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + List, + Optional, + Tuple, + Union, +) from urllib.parse import urlparse import PIL @@ -36,12 +46,11 @@ from term_image.image import BlockImage, ITerm2Image, KittyImage, Size, auto_image_class from term_image.utils import get_terminal_name_version, get_terminal_size, write_tty -from . import logging, notify, tui +from . import logging, notify from .config import config_options, init_config from .ctlseqs import ERASE_IN_LINE_LEFT_b from .exit_codes import FAILURE, INVALID_ARG, NO_VALID_SOURCE, SUCCESS from .logging import LoggingThread, init_log, log, log_exception -from .tui.widgets import Image try: import fcntl # noqa: F401 @@ -50,6 +59,9 @@ else: OS_HAS_FCNTL = True +if TYPE_CHECKING: + from term_image.image import BaseImage + # Checks for CL arguments that have possible invalid values and don't have corresponding # config options. See `check_arg()`. @@ -539,7 +551,7 @@ def update_dict(base: dict, update: dict): def get_urls( url_queue: Queue, - images: List[Tuple[str, Image]], + images: list[tuple[str, BaseImage]], ImageClass: type, ) -> None: """Processes URL sources from a/some separate thread(s)""" @@ -547,7 +559,7 @@ def get_urls( while source: log(f"Getting image from {source!r}", logger, verbose=True) try: - images.append((basename(source), Image(ImageClass.from_url(source)))) + images.append((basename(source), ImageClass.from_url(source))) # Also handles `ConnectionTimeout` except requests.exceptions.ConnectionError: log(f"Unable to get {source!r}", logger, _logging.ERROR) @@ -564,14 +576,14 @@ def get_urls( def open_files( file_queue: Queue, - images: List[Tuple[str, Image]], + images: list[tuple[str, BaseImage]], ImageClass: type, ) -> None: source = file_queue.get() while source: log(f"Opening {source!r}", logger, verbose=True) try: - images.append((source, Image(ImageClass.from_file(source)))) + images.append((source, ImageClass.from_file(source))) except PIL.UnidentifiedImageError as e: log(str(e), logger, _logging.ERROR) except OSError as e: @@ -904,13 +916,9 @@ def main() -> None: log("No valid source!", logger) return NO_VALID_SOURCE # Sort entries by order on the command line - images.sort( - key=lambda x: unique_sources[x[0] if x[1] is ... else x[1]._ti_image.source] - ) + images.sort(key=lambda x: unique_sources[x[0] if x[1] is ... else x[1].source]) - if args.cli or ( - not args.tui and len(images) == 1 and isinstance(images[0][1], Image) - ): + if args.cli or not args.tui and len(images) == 1 and images[0][1] is not ...: log("Running in CLI mode", logger, direct=False) if style_args.get("native") and len(images) > 1: @@ -918,7 +926,7 @@ def main() -> None: show_name = len(args.sources) > 1 for entry in images: - image = entry[1]._ti_image + image = entry[1] if 0 < args.max_pixels < mul(*image._original_size): log( f"Has more than the maximum pixel-count, skipping: {entry[0]!r}", @@ -1015,6 +1023,8 @@ def main() -> None: sys.stdout.close() break elif OS_HAS_FCNTL: + from . import tui + tui.init(args, style_args, images, contents, ImageClass) else: log( @@ -1043,4 +1053,4 @@ def main() -> None: SHOW_HIDDEN: bool # # Used in other modules args: argparse.Namespace | None = None -url_images: list[tuple[str, Image]] = [] +url_images: list[tuple[str, BaseImage]] = [] diff --git a/src/termvisage/tui/__init__.py b/src/termvisage/tui/__init__.py index 2c3b65f..3ba0171 100644 --- a/src/termvisage/tui/__init__.py +++ b/src/termvisage/tui/__init__.py @@ -5,18 +5,18 @@ import logging as _logging import os from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict if TYPE_CHECKING: import argparse - from .widgets import Image + from term_image.image import BaseImage def init( args: argparse.Namespace, style_args: Dict[str, Any], - images: Iterable[Tuple[str, Union[Image, Iterator]]], + images: list[tuple[str, BaseImage | Ellipsis]], contents: dict, ImageClass: type, ) -> None: @@ -76,6 +76,9 @@ def process_input(self, keys): render.REPEAT = args.repeat render.THUMBNAIL_CACHE_SIZE = config_options.thumbnail_cache + images = [ + entry if entry[1] is ... else (entry[0], Image(entry[1])) for entry in images + ] images.sort( key=lambda x: sort_key_lexi( Path(x[0] if x[1] is ... else x[1]._ti_image.source), From 46c92da98d77ebe1668bf733c3d2ffb732670930 Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Wed, 19 Jun 2024 23:38:31 +0100 Subject: [PATCH 3/7] refac: tui: Defer TUI reconfigure till its init - Change: Reconfigure the TUI when initializing it instead of immediately after loading user config. --- src/termvisage/config.py | 2 -- src/termvisage/tui/__init__.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/termvisage/config.py b/src/termvisage/config.py index 9df9e5c..b3824c3 100644 --- a/src/termvisage/config.py +++ b/src/termvisage/config.py @@ -128,8 +128,6 @@ def init_config() -> None: context_keys["global"]["Config"][3] = False # Till the config menu is implemented expand_key[3] = False # "Expand/Collapse Footer" action should be hidden - reconfigure_tui(_context_keys) - def load_config(config_file: str) -> None: """Loads a user config file.""" diff --git a/src/termvisage/tui/__init__.py b/src/termvisage/tui/__init__.py index 3ba0171..284a260 100644 --- a/src/termvisage/tui/__init__.py +++ b/src/termvisage/tui/__init__.py @@ -29,7 +29,7 @@ def init( from .. import notify from ..__main__ import TEMP_DIR - from ..config import config_options + from ..config import _context_keys, config_options, reconfigure_tui from ..logging import LoggingThread, log from . import keys, main, render from .keys import adjust_footer, update_footer_expand_collapse_icon @@ -51,6 +51,8 @@ def process_input(self, keys): keys.append("resized") return super().process_input(keys) + reconfigure_tui(_context_keys) + if args.debug: main_widget.contents.insert( 1, (urwid.AttrMap(urwid.Filler(info_bar), "reverse"), ("given", 1)) From 926027299607f91bc38ca2ba3cc46f3b0fb35883 Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Thu, 20 Jun 2024 13:11:39 +0100 Subject: [PATCH 4/7] refac: tui: Move quitting status to top-level - Change: `.tui.main.quitting` -> `.tui.quitting`. Eliminates the need to load sub-modules of `.tui` in order to access the quitting status. --- src/termvisage/notify.py | 2 +- src/termvisage/tui/__init__.py | 6 +++--- src/termvisage/tui/keys.py | 4 ++-- src/termvisage/tui/main.py | 1 - src/termvisage/tui/render.py | 28 ++++++++++++++-------------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/termvisage/notify.py b/src/termvisage/notify.py index 19269d5..447b844 100644 --- a/src/termvisage/notify.py +++ b/src/termvisage/notify.py @@ -188,7 +188,7 @@ def start_loading() -> None: """Signals the start of a progressive operation.""" global _n_loading - if not (QUIET or __main__.interrupted or main.quitting): + if not (QUIET or __main__.interrupted or tui.quitting): _n_loading += 1 _loading.set() diff --git a/src/termvisage/tui/__init__.py b/src/termvisage/tui/__init__.py index 284a260..e60669e 100644 --- a/src/termvisage/tui/__init__.py +++ b/src/termvisage/tui/__init__.py @@ -36,7 +36,7 @@ def init( from .main import process_input, scan_dir_grid, scan_dir_menu, sort_key_lexi from .widgets import Image, info_bar, main as main_widget - global active, initialized + global active, initialized, quitting class Loop(urwid.MainLoop): def start(self): @@ -182,7 +182,7 @@ def process_input(self, keys): anim_render_manager.join() log("Exited TUI normally", logger, direct=False) except Exception: - main.quitting = True + quitting = True render.image_render_queue.put((None,) * 3) image_render_manager.join() render.anim_render_queue.put((None,) * 3) @@ -194,7 +194,7 @@ def process_input(self, keys): os.close(main.update_pipe) -active = initialized = False +active = initialized = quitting = False palette = [ ("default", "", "", "", "", ""), ("default bold", "", "", "", "bold", ""), diff --git a/src/termvisage/tui/keys.py b/src/termvisage/tui/keys.py index 1ea43bd..7d6bd44 100644 --- a/src/termvisage/tui/keys.py +++ b/src/termvisage/tui/keys.py @@ -14,7 +14,7 @@ from term_image.image import GraphicsImage from term_image.utils import get_cell_size, get_terminal_size -from .. import __version__, logging +from .. import __version__, logging, tui from ..config import config_options, context_keys, expand_key from . import main from .render import resync_grid_rendering @@ -312,7 +312,7 @@ def update_footer_expand_collapse_icon(): # global @register_key(("global", "Quit")) def quit(): - main.quitting = True + tui.quitting = True raise urwid.ExitMainLoop() diff --git a/src/termvisage/tui/main.py b/src/termvisage/tui/main.py index 5193179..2082bb0 100644 --- a/src/termvisage/tui/main.py +++ b/src/termvisage/tui/main.py @@ -678,7 +678,6 @@ def update_screen(): logger = _logging.getLogger(__name__) -quitting = False # For grid scanning/display grid_acknowledge = Event() diff --git a/src/termvisage/tui/render.py b/src/termvisage/tui/render.py index 0125ef6..eb91c85 100644 --- a/src/termvisage/tui/render.py +++ b/src/termvisage/tui/render.py @@ -12,7 +12,7 @@ from term_image.image import Size -from .. import logging, notify +from .. import logging, notify, tui from ..utils import clear_queue, clear_queue_and_stop_loading from . import main @@ -477,14 +477,14 @@ def mark_thumbnail_rendered(source: str, thumbnail: str) -> None: try: while True: while not ( - main.quitting + tui.quitting or grid_active.wait(0.1) - or main.quitting + or tui.quitting or not grid_render_out.empty() ): pass - if main.quitting: + if tui.quitting: break if not in_sync.is_set(): @@ -503,7 +503,7 @@ def mark_thumbnail_rendered(source: str, thumbnail: str) -> None: canvas_size = (grid_cell_width - 2, grid_cell_width // 2 - 2) del grid_cell_width - if main.quitting or not in_sync.is_set(): + if tui.quitting or not in_sync.is_set(): continue if grid_active.is_set(): @@ -517,7 +517,7 @@ def mark_thumbnail_rendered(source: str, thumbnail: str) -> None: ) notify.start_loading() - if main.quitting or not in_sync.is_set(): + if tui.quitting or not in_sync.is_set(): continue try: @@ -527,7 +527,7 @@ def mark_thumbnail_rendered(source: str, thumbnail: str) -> None: except Empty: pass else: - if batch_no == grid_batch_no and in_sync.is_set() and not main.quitting: + if batch_no == grid_batch_no and in_sync.is_set() and not tui.quitting: grid_cache[basename(source)] = ( ImageCanvas( render.encode().split(b"\n"), canvas_size, rendered_size @@ -653,15 +653,15 @@ def cache_thumbnail(source: str, thumbnail: str, deduplicated: str | None) -> No try: while True: while not ( - main.quitting + tui.quitting or grid_active.wait(0.1) - or main.quitting + or tui.quitting or not thumbnail_out.empty() or thumbnails_to_be_deleted ): pass - if main.quitting: + if tui.quitting: break if not in_sync.is_set(): @@ -709,7 +709,7 @@ def cache_thumbnail(source: str, thumbnail: str, deduplicated: str | None) -> No while grid_thumbnail_queue.get(): pass - if main.quitting or not in_sync.is_set(): + if tui.quitting or not in_sync.is_set(): continue if thumbnails_to_be_deleted: @@ -724,7 +724,7 @@ def cache_thumbnail(source: str, thumbnail: str, deduplicated: str | None) -> No delete_thumbnail(thumbnail) thumbnails_to_be_deleted -= thumbnails_to_delete - if main.quitting or not in_sync.is_set(): + if tui.quitting or not in_sync.is_set(): continue if grid_active.is_set(): @@ -741,7 +741,7 @@ def cache_thumbnail(source: str, thumbnail: str, deduplicated: str | None) -> No thumbnail_in.put(source) notify.start_loading() - if main.quitting or not in_sync.is_set(): + if tui.quitting or not in_sync.is_set(): continue try: @@ -749,7 +749,7 @@ def cache_thumbnail(source: str, thumbnail: str, deduplicated: str | None) -> No except Empty: pass else: - if in_sync.is_set() and not main.quitting: + if in_sync.is_set() and not tui.quitting: if thumbnail: with thumbnail_render_lock: thumbnails_being_rendered[thumbnail].add(source) From b09901e2c057cf4cbf9901a76cb560cae8687abc Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Mon, 24 Jun 2024 22:36:58 +0100 Subject: [PATCH 5/7] refac: notify: Lazily load `.tui` - Change: Import from `.tui` in `load()`, if and only if not skipping the TUI loading indication phase. - Change: Do not import submodules of `.tui` at module level. --- src/termvisage/notify.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/termvisage/notify.py b/src/termvisage/notify.py index 447b844..a5b8b4b 100644 --- a/src/termvisage/notify.py +++ b/src/termvisage/notify.py @@ -14,7 +14,6 @@ from . import __main__, logging, tui from .config import config_options from .ctlseqs import SGR_FG_BLUE, SGR_FG_DEFAULT, SGR_FG_RED, SGR_FG_YELLOW -from .tui import main, widgets DEBUG = INFO = 0 WARNING = 1 @@ -25,16 +24,16 @@ def add_notification(msg: Union[str, Tuple[str, str]]) -> None: """Adds a message to the TUI notification bar.""" if _alarms.full(): - clear_notification(main.loop, None) - widgets.notifications.contents.insert( + clear_notification(tui.main.loop, None) + tui.widgets.notifications.contents.insert( 0, (urwid.Filler(urwid.Text(msg, wrap="ellipsis")), ("given", 1)) ) - _alarms.put(main.loop.set_alarm_in(5, clear_notification)) + _alarms.put(tui.main.loop.set_alarm_in(5, clear_notification)) def clear_notification(loop: urwid.MainLoop, data: Any) -> None: """Removes the oldest message in the TUI notification bar.""" - widgets.notifications.contents.pop() + tui.widgets.notifications.contents.pop() loop.remove_alarm(_alarms.get()) @@ -70,9 +69,6 @@ def load() -> None: - elipsis-style for the CLI - braille-style for the TUI """ - from .tui.main import update_screen - from .tui.widgets import loading - global _n_loading stream = stdout if stdout.isatty() else stderr @@ -108,6 +104,10 @@ def load() -> None: _loading.clear() # Signal "not loading" _loading.wait() # Wait for a loading operation + if _n_loading > -1: # Not skipping TUI phase? + from .tui.main import update_screen + from .tui.widgets import loading + while _n_loading > -1: # TUI phase hasn't ended? while _n_loading > 0: # Anything loading? # Animate the TUI loading indicator From 33fda6232f44c5cb7546f45d532a1dd2e10a3488 Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Mon, 24 Jun 2024 22:41:17 +0100 Subject: [PATCH 6/7] fix: tui: Fix circular imports - Fix: Resolve circular imports caused by interdependencies between submodules of `.tui`. --- src/termvisage/tui/__init__.py | 3 ++- src/termvisage/tui/render.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/termvisage/tui/__init__.py b/src/termvisage/tui/__init__.py index e60669e..ebdc868 100644 --- a/src/termvisage/tui/__init__.py +++ b/src/termvisage/tui/__init__.py @@ -31,7 +31,8 @@ def init( from ..__main__ import TEMP_DIR from ..config import _context_keys, config_options, reconfigure_tui from ..logging import LoggingThread, log - from . import keys, main, render + from . import main # Loaded before `.tui.keys` to prevent circular import + from . import keys, render from .keys import adjust_footer, update_footer_expand_collapse_icon from .main import process_input, scan_dir_grid, scan_dir_menu, sort_key_lexi from .widgets import Image, info_bar, main as main_widget diff --git a/src/termvisage/tui/render.py b/src/termvisage/tui/render.py index eb91c85..ab50875 100644 --- a/src/termvisage/tui/render.py +++ b/src/termvisage/tui/render.py @@ -14,7 +14,6 @@ from .. import logging, notify, tui from ..utils import clear_queue, clear_queue_and_stop_loading -from . import main def delete_thumbnail(thumbnail: str) -> bool: @@ -32,13 +31,15 @@ def resync_grid_rendering() -> None: # worse, races. See the resync blocks in `manage_grid_renders()` and # `manage_grid_thumbnails()` especially their beginnings and ends. + from .main import THUMBNAIL + # Signal `GridRenderManager` and `GridThumbnailManager` to **start** resync. # # `GridThumbnailManager` waits for `GridRenderManager` to **start** resync # **before** it does because it modifies (clears) shared data. Hence, # `grid_renderer_in_sync` must be cleared **before** `grid_thumbnailer_in_sync`. grid_renderer_in_sync.clear() - if main.THUMBNAIL: + if THUMBNAIL: grid_thumbnailer_in_sync.clear() # Wait for `GridRenderManager` and `GridThumbnailManager` to **start** resync. @@ -46,7 +47,7 @@ def resync_grid_rendering() -> None: # The order within this set of operations is not necessarily important. However, # the order of this set important with respect to the other sets. grid_renderer_in_sync.wait() - if main.THUMBNAIL: + if THUMBNAIL: grid_thumbnailer_in_sync.wait() # Send the batch delimiter, without which each thread cannot **end** resync. @@ -62,7 +63,7 @@ def resync_grid_rendering() -> None: # forward any jobs from the **new** batch to `GridRenderManager` **before** the # delimiter. grid_render_queue.put(None) - if main.THUMBNAIL: + if THUMBNAIL: grid_thumbnail_queue.put(None) From 0945d00b9f3f81aaa3cc0742b82bc1916b8303e8 Mon Sep 17 00:00:00 2001 From: AnonymouX47 Date: Mon, 24 Jun 2024 22:54:11 +0100 Subject: [PATCH 7/7] fix: Correct type annotation of `.tui.main.loop` - Fix: Change annotation of `.tui.main.loop` from `Loop` to `urwid.MainLoop`, since `Loop` is now defined within `.tui.init()`. - Change: Remove unnecessary import of `.tui`. Partially a consequence of d1aa6bac6894905b53d192c5345c88697b295e2b. --- src/termvisage/tui/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/termvisage/tui/main.py b/src/termvisage/tui/main.py index 2082bb0..a0f713c 100644 --- a/src/termvisage/tui/main.py +++ b/src/termvisage/tui/main.py @@ -17,7 +17,7 @@ from term_image.image import BaseImage from term_image.utils import write_tty -from .. import logging, notify, tui +from .. import logging, notify from ..config import context_keys, expand_key from ..ctlseqs import BEL_b from .keys import ( @@ -707,7 +707,7 @@ def update_screen(): # Set from `.tui.init()` ImageClass: BaseImage displayer: Generator[None, int, bool] -loop: tui.Loop +loop: urwid.MainLoop update_pipe: int # # Corresponding to (or derived directly from) command-line args and/or config options