Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy-load the TUI #21

Merged
merged 7 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/termvisage/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
40 changes: 25 additions & 15 deletions src/termvisage/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()`.
Expand Down Expand Up @@ -539,15 +551,15 @@ 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)"""
source = url_queue.get()
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)
Expand All @@ -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:
Expand Down Expand Up @@ -904,21 +916,17 @@ 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:
style_args["stall_native"] = False

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}",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]] = []
2 changes: 0 additions & 2 deletions src/termvisage/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
18 changes: 9 additions & 9 deletions src/termvisage/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
70 changes: 39 additions & 31 deletions src/termvisage/tui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,57 @@

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

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 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:
"""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 _context_keys, config_options, reconfigure_tui
from ..logging import LoggingThread, log
from . import keys
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

global active, initialized, quitting

class Loop(urwid.MainLoop):
def start(self):
update_footer_expand_collapse_icon()
adjust_footer()
return super().start()

global active, initialized
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)

reconfigure_tui(_context_keys)

if args.debug:
main_widget.contents.insert(
Expand All @@ -60,6 +79,9 @@ def init(
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),
Expand Down Expand Up @@ -161,7 +183,7 @@ def init(
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)
Expand All @@ -173,21 +195,7 @@ 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
active = initialized = quitting = False
palette = [
("default", "", "", "", "", ""),
("default bold", "", "", "", "bold", ""),
Expand Down
4 changes: 2 additions & 2 deletions src/termvisage/tui/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()


Expand Down
5 changes: 2 additions & 3 deletions src/termvisage/tui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -678,7 +678,6 @@ def update_screen():


logger = _logging.getLogger(__name__)
quitting = False

# For grid scanning/display
grid_acknowledge = Event()
Expand Down Expand Up @@ -708,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
Expand Down
Loading
Loading