diff --git a/ISSUE.md b/ISSUE.md deleted file mode 100644 index 4733764..0000000 --- a/ISSUE.md +++ /dev/null @@ -1,47 +0,0 @@ -# Feature: WASM terminal should respect theme palette for ANSI colors - -## Summary - -The WASM terminal parser/renderer has a hardcoded color palette (Tomorrow Night) that doesn't respect the theme passed to the Terminal constructor. When an application sends ANSI color codes, they get resolved to Tomorrow Night RGB values regardless of the configured theme. - -## Current Behavior - -When creating a terminal with a custom theme: -```typescript -const terminal = new Terminal({ - theme: { - background: '#002b36', // solarized - foreground: '#839496', - green: '#859900', - // ... - } -}); -``` - -The WASM parser still outputs Tomorrow Night colors (e.g., `#b5bd68` for green instead of `#859900`), because the internal WASM module resolves ANSI codes to its built-in palette. - -The renderer uses the theme for canvas background/cursor/selection, but text colors come pre-resolved from WASM. - -## Expected Behavior - -ANSI color codes should resolve to the user's configured theme palette, not the hardcoded Tomorrow Night palette. - -## Workaround - -We currently patch `renderer.renderCell` to intercept and remap colors from Tomorrow Night → custom theme using a color map. This works but requires knowing the exact Tomorrow Night palette values and adds overhead to every cell render. - -## Proposed Solutions - -1. **Pass theme palette to WASM** - Allow the palette to be configured when initializing the WASM module -2. **Return color indices** - Have WASM return ANSI color indices (0-15) rather than resolved RGB, letting the renderer resolve them -3. **Document the internal palette** - At minimum, document that Tomorrow Night is the hardcoded palette so consumers can build their own remapping - -Option 2 would be cleanest as it separates parsing from rendering. - -## Environment - -- ghostty-web: 0.4.0 -- Browser: All - ---- -*Filed from webterm project where we encountered this while implementing theme support* diff --git a/examples/calculator.css b/examples/calculator.css deleted file mode 100644 index 9b3f6ea..0000000 --- a/examples/calculator.css +++ /dev/null @@ -1,33 +0,0 @@ -Screen { - overflow: auto; -} - -#calculator { - layout: grid; - grid-size: 4; - grid-gutter: 1 2; - grid-columns: 1fr; - grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr; - margin: 1 2; - min-height: 25; - min-width: 26; - height: 100%; -} - -Button { - width: 100%; - height: 100%; -} - -#numbers { - column-span: 4; - content-align: right middle; - padding: 0 1; - height: 100%; - background: $primary-lighten-2; - color: $text; -} - -#number-0 { - column-span: 2; -} diff --git a/examples/calculator.py b/examples/calculator.py deleted file mode 100644 index 3e421f3..0000000 --- a/examples/calculator.py +++ /dev/null @@ -1,145 +0,0 @@ -from contextlib import suppress -from decimal import Decimal -from typing import ClassVar - -from textual import events -from textual.app import App, ComposeResult -from textual.containers import Container -from textual.css.query import NoMatches -from textual.reactive import var -from textual.widgets import Button, Static - - -class CalculatorApp(App): - """A working 'desktop' calculator.""" - - CSS_PATH = "calculator.css" - - numbers = var("0") - show_ac = var(True) - left = var(Decimal("0")) - right = var(Decimal("0")) - value = var("") - operator = var("plus") - - NAME_MAP: ClassVar = { - "asterisk": "multiply", - "slash": "divide", - "underscore": "plus-minus", - "full_stop": "point", - "plus_minus_sign": "plus-minus", - "percent_sign": "percent", - "equals_sign": "equals", - "minus": "minus", - "plus": "plus", - } - - def watch_numbers(self, value: str) -> None: - """Called when numbers is updated.""" - # Update the Numbers widget - self.query_one("#numbers", Static).update(value) - - def compute_show_ac(self) -> bool: - """Compute switch to show AC or C button""" - return self.value in ("", "0") and self.numbers == "0" - - def watch_show_ac(self, show_ac: bool) -> None: - """Called when show_ac changes.""" - self.query_one("#c").display = not show_ac - self.query_one("#ac").display = show_ac - - def compose(self) -> ComposeResult: - """Add our buttons.""" - with Container(id="calculator"): - yield Static(id="numbers") - yield Button("AC", id="ac", variant="primary") - yield Button("C", id="c", variant="primary") - yield Button("+/-", id="plus-minus", variant="primary") - yield Button("%", id="percent", variant="primary") - yield Button("÷", id="divide", variant="warning") - yield Button("7", id="number-7") - yield Button("8", id="number-8") - yield Button("9", id="number-9") - yield Button("x", id="multiply", variant="warning") - yield Button("4", id="number-4") - yield Button("5", id="number-5") - yield Button("6", id="number-6") - yield Button("-", id="minus", variant="warning") - yield Button("1", id="number-1") - yield Button("2", id="number-2") - yield Button("3", id="number-3") - yield Button("+", id="plus", variant="warning") - yield Button("0", id="number-0") - yield Button(".", id="point") - yield Button("=", id="equals", variant="warning") - - def on_key(self, event: events.Key) -> None: - """Called when the user presses a key.""" - - def press(button_id: str) -> None: - with suppress(NoMatches): - self.query_one(f"#{button_id}", Button).press() - - key = event.key - if key.isdecimal(): - press(f"number-{key}") - elif key == "c": - press("c") - press("ac") - else: - button_id = self.NAME_MAP.get(key) - if button_id is not None: - press(self.NAME_MAP.get(key, key)) - - def on_button_pressed(self, event: Button.Pressed) -> None: - """Called when a button is pressed.""" - - button_id = event.button.id - assert button_id is not None - - def do_math() -> None: - """Does the math: LEFT OPERATOR RIGHT""" - try: - if self.operator == "plus": - self.left += self.right - elif self.operator == "minus": - self.left -= self.right - elif self.operator == "divide": - self.left /= self.right - elif self.operator == "multiply": - self.left *= self.right - self.numbers = str(self.left) - self.value = "" - except Exception: - self.numbers = "Error" - - if button_id.startswith("number-"): - number = button_id.partition("-")[-1] - self.numbers = self.value = self.value.lstrip("0") + number - elif button_id == "plus-minus": - self.numbers = self.value = str(Decimal(self.value or "0") * -1) - elif button_id == "percent": - self.numbers = self.value = str(Decimal(self.value or "0") / Decimal(100)) - elif button_id == "point": - if "." not in self.value: - self.numbers = self.value = (self.value or "0") + "." - elif button_id == "ac": - self.value = "" - self.left = self.right = Decimal(0) - self.operator = "plus" - self.numbers = "0" - elif button_id == "c": - self.value = "" - self.numbers = "0" - elif button_id in ("plus", "minus", "divide", "multiply"): - self.right = Decimal(self.value or "0") - do_math() - self.operator = button_id - elif button_id == "equals": - if self.value: - self.right = Decimal(self.value) - do_math() - - -if __name__ == "__main__": - CalculatorApp().run() diff --git a/examples/download.py b/examples/download.py deleted file mode 100644 index 4397e49..0000000 --- a/examples/download.py +++ /dev/null @@ -1,74 +0,0 @@ -import io -from pathlib import Path - -from textual import on -from textual.app import App, ComposeResult -from textual.events import DeliveryComplete -from textual.widgets import Button, Input, Label - - -class ScreenshotApp(App[None]): - def compose(self) -> ComposeResult: - yield Button("screenshot: no filename or mime", id="button-1") - yield Button("screenshot: screenshot.svg / open in browser", id="button-2") - yield Button("screenshot: screenshot.svg / download", id="button-3") - yield Button( - "screenshot: screenshot.svg / open in browser / plaintext mime", - id="button-4", - ) - yield Label("Deliver custom file:") - yield Input(id="custom-path-input", placeholder="Path to file...") - - @on(Button.Pressed, selector="#button-1") - def on_button_pressed(self) -> None: - screenshot_string = self.export_screenshot() - string_io = io.StringIO(screenshot_string) - self.deliver_text(string_io) - - @on(Button.Pressed, selector="#button-2") - def on_button_pressed_2(self) -> None: - screenshot_string = self.export_screenshot() - string_io = io.StringIO(screenshot_string) - self.deliver_text( - string_io, save_filename="screenshot.svg", open_method="browser" - ) - - @on(Button.Pressed, selector="#button-3") - def on_button_pressed_3(self) -> None: - screenshot_string = self.export_screenshot() - string_io = io.StringIO(screenshot_string) - self.deliver_text( - string_io, save_filename="screenshot.svg", open_method="download" - ) - - @on(Button.Pressed, selector="#button-4") - def on_button_pressed_4(self) -> None: - screenshot_string = self.export_screenshot() - string_io = io.StringIO(screenshot_string) - self.deliver_text( - string_io, - save_filename="screenshot.svg", - open_method="browser", - mime_type="text/plain", - ) - - @on(DeliveryComplete) - def on_delivery_complete(self, event: DeliveryComplete) -> None: - self.notify(title="Download complete", message=event.key) - - @on(Input.Submitted) - def on_input_submitted(self, event: Input.Submitted) -> None: - path = Path(event.value) - if path.exists(): - self.deliver_binary(path) - else: - self.notify( - title="Invalid path", - message="The path does not exist.", - severity="error", - ) - - -app = ScreenshotApp() -if __name__ == "__main__": - app.run() diff --git a/examples/env.py b/examples/env.py deleted file mode 100644 index e148ea0..0000000 --- a/examples/env.py +++ /dev/null @@ -1,13 +0,0 @@ -import os - -from textual.app import App, ComposeResult -from textual.widgets import Pretty - - -class TerminalEnv(App): - def compose(self) -> ComposeResult: - yield Pretty(dict(os.environ)) - - -if __name__ == "__main__": - TerminalEnv().run() diff --git a/examples/ganglion.toml b/examples/ganglion.toml deleted file mode 100644 index f15619b..0000000 --- a/examples/ganglion.toml +++ /dev/null @@ -1,47 +0,0 @@ -[account] - -[app.Calculator] -path = "./" -command = "python calculator.py" - - -[app.Easing] -slug = "easing" -path = "./" -command = "textual easing" - - -[app.Keys] -slug = "keys" -path = "./" -command = "textual keys" - - -[app.Borders] -slug = "borders" -path = "./" -command = "textual borders" - - -[app.Demo] -name = "Demo" -slug = "demo" -path = "./" -command = "python -m textual" - -[terminal.Terminal] -name = "Terminal" -path = "./" -terminal = true - -[app.OpenLink] -name = "Open Link" -slug = "open-link" -path = "./" -command = "python open_link.py" - -[app.Download] -name = "Download" -slug = "download" -path = "./" -command = "python download.py" \ No newline at end of file diff --git a/examples/open_link.py b/examples/open_link.py deleted file mode 100644 index bc51f8d..0000000 --- a/examples/open_link.py +++ /dev/null @@ -1,24 +0,0 @@ -from textual import on -from textual.app import App, ComposeResult -from textual.widgets import Button - - -class OpenLink(App[None]): - """Demonstrates opening a URL in the same tab or a new tab.""" - - def compose(self) -> ComposeResult: - yield Button("Visit the terminal docs", id="open-link-same-tab") - yield Button("Visit the terminal docs in a new tab", id="open-link-new-tab") - - @on(Button.Pressed) - def open_link(self, event: Button.Pressed) -> None: - """Open the URL in the same tab or a new tab depending on which button was pressed.""" - self.open_url( - "https://example.com", - new_tab=event.button.id == "open-link-new-tab", - ) - - -app = OpenLink() -if __name__ == "__main__": - app.run()