mirror of
https://github.com/1j01/textual-paint.git
synced 2024-12-22 14:21:33 +03:00
Get snapshot tests passing :)
I did TDD with screenshots.
This commit is contained in:
parent
5a66037592
commit
3c2a03ac82
@ -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()
|
||||
|
@ -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."""
|
||||
|
@ -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)]\\[_][/]"
|
||||
|
@ -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("")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user