Get snapshot tests passing :)

I did TDD with screenshots.
This commit is contained in:
Isaiah Odhner 2023-09-08 14:55:35 -04:00
parent 5a66037592
commit 3c2a03ac82
4 changed files with 140 additions and 76 deletions

View File

@ -1,80 +1,117 @@
"""Provides ASCII borders for older terminals."""
"""Provides ASCII alternatives for older terminals."""
from textual._border import BORDER_CHARS, BORDER_LOCATIONS
from textual._border import BORDER_CHARS, BORDER_LOCATIONS, get_box
from textual.scrollbar import ScrollBar
from textual.widgets import RadioButton
from textual_paint.scrollbars import ASCIIScrollBarRender
from textual_paint.windows import WindowTitleBar
replacements: list[tuple[object, str, object, object]] = []
def replace(obj: object, attr: str, ascii_only_value: object) -> None:
"""Replace an attribute with a value for --ascii-only mode."""
if isinstance(obj, dict):
replacements.append((obj, attr, ascii_only_value, obj[attr]))
else:
replacements.append((obj, attr, ascii_only_value, getattr(obj, attr)))
def set_ascii_only_mode(ascii_only: bool) -> None:
"""Set the --ascii-only mode for all replacements."""
for obj, attr, ascii_only_value, non_ascii_value in replacements:
value = ascii_only_value if ascii_only else non_ascii_value
if isinstance(obj, dict):
obj[attr] = value
else:
setattr(obj, attr, value)
get_box.cache_clear()
replace(RadioButton, "BUTTON_INNER", "*") # "*", "o", "O", "@"
# Defined on internal superclass ToggleButton
replace(RadioButton, "BUTTON_LEFT", "(")
replace(RadioButton, "BUTTON_RIGHT", ")")
replace(ScrollBar, "renderer", ASCIIScrollBarRender)
replace(WindowTitleBar, "MINIMIZE_ICON", "_")
replace(WindowTitleBar, "MAXIMIZE_ICON", "[]")
replace(WindowTitleBar, "RESTORE_ICON", "\\[/]" )
replace(WindowTitleBar, "CLOSE_ICON", "X")
def force_ascii_borders() -> None:
"""Force all borders to use ASCII characters."""
def replace_borders() -> None:
"""Conditionally force all borders to use ASCII characters."""
# replace all with ascii border style
for key in BORDER_CHARS:
if key not in ("ascii", "none", "hidden", "blank", ""):
BORDER_CHARS[key] = (
replace(BORDER_CHARS, key, (
("+", "-", "+"),
("|", " ", "|"),
("+", "-", "+"),
)
))
# BORDER_CHARS[""] = (
# replace(BORDER_CHARS, "", (
# (" ", " ", " "),
# (" ", " ", " "),
# (" ", " ", " "),
# )
# ))
# # was originally: (
# # (" ", " ", " "),
# # (" ", " ", " "),
# # (" ", " ", " "),
# # )
# BORDER_CHARS["ascii"] = (
# replace(BORDER_CHARS, "ascii", (
# ("+", "-", "+"),
# ("|", " ", "|"),
# ("+", "-", "+"),
# )
# ))
# # was originally: (
# # ("+", "-", "+"),
# # ("|", " ", "|"),
# # ("+", "-", "+"),
# # )
# BORDER_CHARS["none"] = (
# replace(BORDER_CHARS, "none", (
# (" ", " ", " "),
# (" ", " ", " "),
# (" ", " ", " "),
# )
# ))
# # was originally: (
# # (" ", " ", " "),
# # (" ", " ", " "),
# # (" ", " ", " "),
# # )
# BORDER_CHARS["hidden"] = (
# replace(BORDER_CHARS, "hidden", (
# (" ", " ", " "),
# (" ", " ", " "),
# (" ", " ", " "),
# )
# ))
# # was originally: (
# # (" ", " ", " "),
# # (" ", " ", " "),
# # (" ", " ", " "),
# # )
# BORDER_CHARS["blank"] = (
# replace(BORDER_CHARS, "blank", (
# (" ", " ", " "),
# (" ", " ", " "),
# (" ", " ", " "),
# )
# ))
# # was originally: (
# # (" ", " ", " "),
# # (" ", " ", " "),
# # (" ", " ", " "),
# # )
BORDER_CHARS["round"] = (
replace(BORDER_CHARS, "round", (
(".", "-", "."),
("|", " ", "|"),
("'", "-", "'"),
)
))
# was originally: (
# ("╭", "─", "╮"),
# ("│", " ", "│"),
@ -82,132 +119,132 @@ def force_ascii_borders() -> None:
# )
# This is actually supported in at least some old terminals; it's part of CP437, but not ASCII.
# BORDER_CHARS["solid"] = (
# replace(BORDER_CHARS, "solid", (
# ("┌", "─", "┐"),
# ("│", " ", "│"),
# ("└", "─", "┘"),
# )
# ))
# # was originally: (
# # ("┌", "─", "┐"),
# # ("│", " ", "│"),
# # ("└", "─", "┘"),
# # )
BORDER_CHARS["double"] = (
replace(BORDER_CHARS, "double", (
("#", "=", "#"),
("#", " ", "#"),
("#", "=", "#"),
)
))
# was originally: (
# ("╔", "═", "╗"),
# ("║", " ", "║"),
# ("╚", "═", "╝"),
# )
BORDER_CHARS["dashed"] = (
replace(BORDER_CHARS, "dashed", (
(":", '"', ":"),
(":", " ", ":"),
("'", '"', "'"),
)
))
# was originally: (
# ("┏", "╍", "┓"),
# ("╏", " ", "╏"),
# ("┗", "╍", "┛"),
# )
BORDER_CHARS["heavy"] = (
replace(BORDER_CHARS, "heavy", (
("#", "=", "#"),
("#", " ", "#"),
("#", "=", "#"),
)
))
# was originally: (
# ("┏", "━", "┓"),
# ("┃", " ", "┃"),
# ("┗", "━", "┛"),
# )
BORDER_CHARS["inner"] = (
replace(BORDER_CHARS, "inner", (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
)
))
# was originally: (
# ("▗", "▄", "▖"),
# ("▐", " ", "▌"),
# ("▝", "▀", "▘"),
# )
BORDER_CHARS["outer"] = (
replace(BORDER_CHARS, "outer", (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
)
))
# was originally: (
# ("▛", "▀", "▜"),
# ("▌", " ", "▐"),
# ("▙", "▄", "▟"),
# )
BORDER_CHARS["thick"] = (
replace(BORDER_CHARS, "thick", (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
)
))
# was originally: (
# ("█", "▀", "█"),
# ("█", " ", "█"),
# ("█", "▄", "█"),
# )
BORDER_CHARS["hkey"] = (
replace(BORDER_CHARS, "hkey", (
(" ", " ", " "),
(" ", " ", " "),
("_", "_", "_"),
)
))
# was originally: (
# ("▔", "▔", "▔"),
# (" ", " ", " "),
# ("▁", "▁", "▁"),
# )
BORDER_CHARS["vkey"] = (
replace(BORDER_CHARS, "vkey", (
("[", " ", "]"),
("[", " ", "]"),
("[", " ", "]"),
)
))
# was originally: (
# ("▏", " ", "▕"),
# ("▏", " ", "▕"),
# ("▏", " ", "▕"),
# )
BORDER_CHARS["tall"] = (
replace(BORDER_CHARS, "tall", (
("[", " ", "]"),
("[", " ", "]"),
("[", "_", "]"),
)
))
# was originally: (
# ("▊", "▔", "▎"),
# ("▊", " ", "▎"),
# ("▊", "▁", "▎"),
# )
BORDER_CHARS["panel"] = (
replace(BORDER_CHARS, "panel", (
("[", " ", "]"),
("|", " ", "|"),
("|", "_", "|"),
)
))
# was originally: (
# ("▊", "█", "▎"),
# ("▊", " ", "▎"),
# ("▊", "▁", "▎"),
# )
BORDER_CHARS["wide"] = (
replace(BORDER_CHARS, "wide", (
("_", "_", "_"),
("[", " ", "]"),
(" ", " ", " "),
)
))
# was originally: (
# ("▁", "▁", "▁"),
# ("▎", " ", "▊"),
@ -216,40 +253,43 @@ def force_ascii_borders() -> None:
# Prevent inverse colors
for key in BORDER_LOCATIONS:
BORDER_LOCATIONS[key] = tuple(
replace(BORDER_LOCATIONS, key, tuple(
tuple(value % 2 for value in row)
for row in BORDER_LOCATIONS[key]
)
))
# Prevent imbalanced borders
BORDER_LOCATIONS["tall"] = (
replace(BORDER_LOCATIONS, "tall", (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
)
BORDER_LOCATIONS["wide"] = (
))
replace(BORDER_LOCATIONS, "wide", (
(1, 1, 1),
(0, 1, 0),
(1, 1, 1),
)
BORDER_LOCATIONS["panel"] = (
))
replace(BORDER_LOCATIONS, "panel", (
(3, 3, 3), # invert colors
(0, 0, 0),
(0, 0, 0),
)
))
for key in ("thick", "inner", "outer"):
BORDER_LOCATIONS[key] = (
replace(BORDER_LOCATIONS, key, (
(3, 3, 3), # invert colors
(3, 0, 3), # invert colors except middle
(3, 3, 3), # invert colors
)
))
replace_borders()
if __name__ == "__main__":
force_ascii_borders()
replace_borders()
set_ascii_only_mode(True)
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
from textual.widgets import Label, Switch
class AllBordersApp(App[None]):
"""Demo app for ASCII borders. Based on https://textual.textualize.io/styles/border/#all-border-types"""
@ -346,6 +386,22 @@ if __name__ == "__main__":
Label("vkey", id="vkey"),
Label("wide", id="wide"),
)
yield Switch(True, id="ascii_only_switch")
def on_switch_changed(self, event: Switch.Changed) -> None:
# event.switch.styles.background = "red"
set_ascii_only_mode(event.value)
# event.switch.styles.background = "yellow"
# Refreshing each widget separately seems to be necessary
for widget in self.query("*"):
widget.refresh()
# Or clearing each widget's caches manually and then refreshing the screen:
# for widget in self.query("*"):
# widget._styles_cache.clear()
# # widget._rich_style_cache = {}
# self.refresh()
app = AllBordersApp()
app.run()

View File

@ -10,16 +10,26 @@ from textual.widgets import DirectoryTree, Tree
from textual.widgets._directory_tree import DirEntry
from textual.widgets._tree import TOGGLE_STYLE, TreeNode
from textual_paint.args import args
# from textual_paint.args import args
from textual_paint.ascii_borders import replace
# Vague skeuomorphism
# FILE_ICON = Text.from_markup("[#aaaaaa on #ffffff]=[/] " if args.ascii_only else "📄 ")
# FOLDER_OPEN_ICON = Text.from_markup("[rgb(128,128,64)]L[/] " if args.ascii_only else "📂 ")
# FOLDER_CLOSED_ICON = Text.from_markup("[rgb(128,128,64)]V[/] " if args.ascii_only else "📁 ")
# Simple generic tree style
FILE_ICON = Text.from_markup("" if args.ascii_only else "📄 ")
FOLDER_OPEN_ICON = Text.from_markup("[blue]-[/] " if args.ascii_only else "📂 ")
FOLDER_CLOSED_ICON = Text.from_markup("[blue]+[/] " if args.ascii_only else "📁 ")
# FILE_ICON = Text.from_markup("" if args.ascii_only else "📄 ")
# FOLDER_OPEN_ICON = Text.from_markup("[blue]-[/] " if args.ascii_only else "📂 ")
# FOLDER_CLOSED_ICON = Text.from_markup("[blue]+[/] " if args.ascii_only else "📁 ")
# Simple generic tree style + new way of handling --ascii-only mode
FILE_ICON = Text("📄 ")
FOLDER_OPEN_ICON = Text("📂 ")
FOLDER_CLOSED_ICON = Text("📁 ")
icons = locals()
replace(icons, "FILE_ICON", Text.from_markup(""))
replace(icons, "FOLDER_OPEN_ICON", Text.from_markup("[blue]-[/] "))
replace(icons, "FOLDER_CLOSED_ICON", Text.from_markup("[blue]+[/] "))
class EnhancedDirectoryTree(DirectoryTree):
"""A DirectoryTree with auto-expansion, filtering of hidden files, and ASCII icon replacements."""

View File

@ -45,6 +45,7 @@ from textual_paint.ansi_art_document import (SAVE_DISABLED_FORMATS,
FormatWriteNotSupported,
Selection)
from textual_paint.args import args, get_help_text
from textual_paint.ascii_borders import set_ascii_only_mode
from textual_paint.auto_restart import restart_on_changes, restart_program
from textual_paint.edit_colors import EditColorsDialogWindow
from textual_paint.file_dialogs import OpenDialogWindow, SaveAsDialogWindow
@ -163,7 +164,7 @@ class Tool(Enum):
# - Ellipse: ⬭⭕🔴🟠🟡🟢🔵🟣🟤⚫⚪🔘🫧🕳️🥚💫💊🛞
# - Rounded Rectangle: ▢⬜⬛𓋰⌨️⏺️💳📺🧫
if args.ascii_only_icons:
if args.ascii_only or args.ascii_only_icons:
enum_to_icon = {
Tool.free_form_select: "'::.", # "*" "<^>" "<[u]^[/]7" "'::." ".::." "<%>"
Tool.select: "::", # "#" "::" ":_:" ":[u]:[/]:" ":[u]'[/]:"
@ -1058,6 +1059,10 @@ class PaintApp(App[None]):
TITLE = _("Paint")
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
set_ascii_only_mode(args.ascii_only)
def watch_file_path(self, file_path: Optional[str]) -> None:
"""Called when file_path changes."""
if file_path is None:
@ -4034,19 +4039,6 @@ Columns: {len(palette) // 2}
widget.styles.border = ("round", Color.from_hsl(i / 10, 1, 0.5))
widget.border_title = widget.css_identifier_styled # type: ignore
if args.ascii_only:
args.ascii_only_icons = True
from textual_paint.ascii_borders import force_ascii_borders
force_ascii_borders()
RadioButton.BUTTON_INNER = "*" # "*", "o", "O", "@"
# Defined on internal superclass ToggleButton
RadioButton.BUTTON_LEFT = "("
RadioButton.BUTTON_RIGHT = ")"
ScrollBar.renderer = ASCIIScrollBarRender
# header_icon_markup = "[on white][blue]\\\\[/][red]|[/][yellow]/[/][/]"
# header_icon_markup = "[black]..,[/]\n[blue]\\\\[/][on white][red]|[/][yellow]/[/][/]\n[black on rgb(192,192,192)]\\[_][/]"

View File

@ -21,10 +21,16 @@ from textual_paint.localization.i18n import get as _
class WindowTitleBar(Container):
"""A title bar widget."""
MINIMIZE_ICON = "_" if args.ascii_only else "🗕" # "_", "-"
MAXIMIZE_ICON = "[]" if args.ascii_only else "🗖" # "+", "^", "[]", (non-ASCII) "□"
RESTORE_ICON = "\\[/]" if args.ascii_only else "🗗" # "+", "^", "%", "#", "-", "=", (needs escaping) "[/]"
CLOSE_ICON = "X" if args.ascii_only else "🗙" # "X", "x"
# --ascii-only replacements are now handled in ascii_borders.py (which should be renamed.)
# MINIMIZE_ICON = "_" if args.ascii_only else "🗕" # "_", "-"
# MAXIMIZE_ICON = "[]" if args.ascii_only else "🗖" # "+", "^", "[]", (non-ASCII) "□"
# RESTORE_ICON = "\\[/]" if args.ascii_only else "🗗" # "+", "^", "%", "#", "-", "=", (needs escaping) "[/]"
# CLOSE_ICON = "X" if args.ascii_only else "🗙" # "X", "x"
MINIMIZE_ICON = "🗕"
MAXIMIZE_ICON = "🗖"
RESTORE_ICON = "🗗"
CLOSE_ICON = "🗙"
title = var("")