Set up development features for gallery app

This commit is contained in:
Isaiah Odhner 2023-09-06 01:35:06 -04:00
parent 937dabb73c
commit 9140d14405
3 changed files with 69 additions and 8 deletions

View File

@ -9,6 +9,7 @@ from textual.app import ScreenStackError
if TYPE_CHECKING: if TYPE_CHECKING:
from .paint import PaintApp from .paint import PaintApp
from .gallery import GalleryApp
def restart_program() -> None: def restart_program() -> None:
"""Restarts the current program, after resetting terminal state, and cleaning up file objects and descriptors.""" """Restarts the current program, after resetting terminal state, and cleaning up file objects and descriptors."""
@ -57,7 +58,7 @@ def restart_program() -> None:
# os.execl(python, python, *sys.argv) # os.execl(python, python, *sys.argv)
os.execl(sys.executable, *sys.orig_argv) os.execl(sys.executable, *sys.orig_argv)
def restart_on_changes(app: PaintApp) -> None: def restart_on_changes(app: PaintApp|GalleryApp) -> None:
"""Restarts the current program when a file is changed""" """Restarts the current program when a file is changed"""
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED
@ -80,7 +81,7 @@ def restart_on_changes(app: PaintApp) -> None:
# or else nothing happens. # or else nothing happens.
# However, when _app.action_reload is called from the key binding, # However, when _app.action_reload is called from the key binding,
# it seems to work fine with or without unsaved changes. # it seems to work fine with or without unsaved changes.
if _app.is_document_modified(): if isinstance(_app, PaintApp) and _app.is_document_modified():
_app.call_from_thread(_app.action_reload) _app.call_from_thread(_app.action_reload)
else: else:
restart_program() restart_program()

View File

@ -0,0 +1,5 @@
GalleryItem {
layout: vertical;
width: 100%;
content-align: center middle;
}

View File

@ -2,20 +2,29 @@
import argparse import argparse
import os import os
import re
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, Static from textual.binding import Binding
from textual.containers import HorizontalScroll, Vertical from textual.containers import HorizontalScroll, Vertical
from textual.widgets import Footer, Header, Static
from .paint import AnsiArtDocument
from .__init__ import __version__ from .__init__ import __version__
from .auto_restart import restart_on_changes, restart_program
from .paint import AnsiArtDocument
parser = argparse.ArgumentParser(description='ANSI art gallery', usage='%(prog)s [folder]', prog="python -m src.textual_paint.gallery") parser = argparse.ArgumentParser(description='ANSI art gallery', usage='%(prog)s [folder]', prog="python -m src.textual_paint.gallery")
parser.add_argument('folder', nargs='?', default=None, help='Path to a folder containing ANSI art.') parser.add_argument('folder', nargs='?', default=None, help='Path to a folder containing ANSI art.')
dev_options = parser.add_argument_group('development options')
dev_options.add_argument('--inspect-layout', action='store_true', help='Enables DOM inspector (F12) and middle click highlight')
dev_options.add_argument('--restart-on-changes', action='store_true', help='Restart the app when the source code is changed')
args = parser.parse_args() args = parser.parse_args()
def _(text: str) -> str:
"""Placeholder for localization function."""
return text
class GalleryItem(Vertical): class GalleryItem(Vertical):
"""An image with a caption.""" """An image with a caption."""
@ -32,11 +41,31 @@ class GalleryItem(Vertical):
yield Static(text) yield Static(text)
yield Static(self.caption) yield Static(self.caption)
class Gallery(App[None]): class GalleryApp(App[None]):
"""ANSI art gallery TUI""" """ANSI art gallery TUI"""
TITLE = f"ANSI art gallery v{__version__}" TITLE = f"ANSI art gallery v{__version__}"
CSS_PATH = "gallery.css"
BINDINGS = [
Binding("ctrl+q", "exit", _("Quit")),
# dev helper
# f5 would be more traditional, but I need something not bound to anything
# in the context of the terminal in VS Code, and not used by this app, like Ctrl+R, and detectable in the terminal.
# This isn't as important now that I have automatic reloading,
# but I still use it regularly.
Binding("f2", "reload", _("Reload")),
# Temporary quick access to work on a specific dialog.
# Can be used together with `--press f3` when using `textual run` to open the dialog at startup.
# Would be better if all dialogs were accessible from the keyboard.
# Binding("f3", "custom_zoom", _("Custom Zoom")),
# Dev tool to inspect the widget tree.
Binding("f12", "toggle_inspector", _("Toggle Inspector")),
# Update screenshot on readme.
# Binding("ctrl+j", "update_screenshot", _("Update Screenshot")),
]
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Add widgets to the layout.""" """Add widgets to the layout."""
yield Header(show_clock=True) yield Header(show_clock=True)
@ -46,6 +75,15 @@ class Gallery(App[None]):
yield Footer() yield Footer()
if not args.inspect_layout:
return
# importing the inspector adds instrumentation which can slow down startup
from .inspector import Inspector
inspector = Inspector()
inspector.display = False
yield inspector
def on_mount(self) -> None: def on_mount(self) -> None:
"""Called when the app is mounted to the DOM.""" """Called when the app is mounted to the DOM."""
self._load() self._load()
@ -57,7 +95,7 @@ class Gallery(App[None]):
folder = os.path.join(os.path.dirname(__file__), "../../samples") folder = os.path.join(os.path.dirname(__file__), "../../samples")
if not os.path.isdir(folder): if not os.path.isdir(folder):
raise Exception(f"Folder not found: {folder}") raise Exception(f"Folder not found: {folder}")
for filename in os.listdir(folder): for filename in os.listdir(folder):
if not filename.endswith(".ans"): if not filename.endswith(".ans"):
continue continue
@ -68,8 +106,25 @@ class Gallery(App[None]):
self.scroll.mount(GalleryItem(image, caption=filename)) self.scroll.mount(GalleryItem(image, caption=filename))
def action_reload(self) -> None:
"""Reload the program."""
restart_program()
app = Gallery() def action_toggle_inspector(self) -> None:
if not args.inspect_layout:
return
# importing the inspector adds instrumentation which can slow down startup
from .inspector import Inspector
inspector = self.query_one(Inspector)
inspector.display = not inspector.display
if not inspector.display:
inspector.picking = False
app = GalleryApp()
if args.restart_on_changes:
restart_on_changes(app)
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()