Skip to content
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
23 changes: 18 additions & 5 deletions src/toad/frame_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

if TYPE_CHECKING:
from toad.screens.workspace import WorkspaceFrame
from toad.widgets.transcript_history import TranscriptPageView
from toad.widgets.transcript_history import LineHistory, TranscriptPageView


class FrameState(DeclaredFamily, affix="Frame"):
Expand Down Expand Up @@ -201,30 +201,43 @@ async def wait(self) -> bool:


class EffectPhase:
"""A running effect; its subclasses are the effect not yet started and finished."""

# Whether the effect is moving now.
in_motion: ClassVar[bool] = True

def paint(self, owner: AnimationEffects[Mix], effect: Effect[Mix], strip: Strip):
return owner.paint_effects(strip, owner.effect_frame(effect))

def keeps_arrivals[Item](self, arrivals: Iterable[Item]) -> Iterable[Item]:
return arrivals

def on_new_arrival(self, owner: AnimationEffects[Mix], effect: Effect[Mix], pages: deque[TranscriptPageView]):
pass
def on_new_arrival(self, owner: LineHistory, effect: Effect[Mix], pages: deque[TranscriptPageView]):
"""Arrivals during a running burst join its fade and reveal."""


class EffectUnstarted(EffectPhase):
in_motion = False

def paint(self, owner: AnimationEffects[Mix], effect: Effect[Mix], strip: Strip):
owner.run_effect(effect)
return super().paint(owner, effect, strip)

def on_new_arrival(self, owner: LineHistory, effect: Effect[Mix], pages: deque[TranscriptPageView]):
owner.arrivals_began()


class EffectFinished(EffectPhase):
in_motion = False

def keeps_arrivals[Item](self, arrivals: Iterable[Item]) -> Iterable[Item]:
return ()

def on_new_arrival(self, owner: AnimationEffects[Mix], effect: Effect[Mix], pages: deque[TranscriptPageView]):
def on_new_arrival(self, owner: LineHistory, effect: Effect[Mix], pages: deque[TranscriptPageView]):
for page in pages:
page.arrivals.clear()
owner.stop_effect(effect)
owner.arrivals_began()


EFFECT_UNSTARTED, EFFECT_RUNNING, EFFECT_FINISHED = EffectUnstarted(), EffectPhase(), EffectFinished()
Expand Down Expand Up @@ -264,7 +277,7 @@ def stop_effect(self, *effects: Effect) -> None:
if animation := self.effects.pop(effect, None):
animation.stop()

def effect_frame(self, effect: Effect[FrameT]) -> FrameT:
def effect_frame[Shown: Frame](self, effect: Effect[Shown]) -> Shown:
animation = self.effects.get(effect)
return effect.final if animation is None else animation.frame()

Expand Down
7 changes: 0 additions & 7 deletions src/toad/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ def of(cls, invocation: KeyInvocation, mode: type[KeyMode]) -> KeymapRow:
def binding_id(self) -> str:
return KeyInvocation.in_mode(self.declaration, self.mode().binding_mode)

@property
def defaults(self) -> tuple[str, ...]:
return KeymapCatalog.capture((self.mode,)).defaults(self)

def effective(self, keymap: Keymap) -> tuple[str, ...]:
return keymap.keys_for(self, self.defaults)


@dataclass(frozen=True)
class KeyRemap(KeymapRow):
Expand Down
76 changes: 76 additions & 0 deletions src/toad/large_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Text drawn three rows tall in box-drawing glyphs, for a name read at a glance.

Textual's own 3x3 font (`textual.renderables.digits`) owns the digits, `A`-`F`
and a few signs; this module adds the letters and signs it lacks, in the same
light style. Lower case is drawn as upper case. A character neither table
has is drawn at normal size on the middle row.
"""

from __future__ import annotations

from typing import ClassVar

from rich.cells import cell_len
from textual.renderables.digits import DIGITS, DIGITS3X3

Glyph = tuple[str, str, str]


def _textual_glyphs() -> dict[str, Glyph]:
"""Textual's glyphs, each padded to its three-cell width."""
return {
character: tuple(row.ljust(3) for row in DIGITS3X3[index * 3 : index * 3 + 3]) # type: ignore[misc]
for index, character in enumerate(DIGITS)
}


class LargeText:
"""A line of text three rows tall, with a column between glyphs."""

HEIGHT: ClassVar[int] = 3
GLYPHS: ClassVar[dict[str, Glyph]] = {
**_textual_glyphs(),
"G": ("╭─╴", "│╶╮", "╰─╯"),
"H": ("╷ ╷", "├─┤", "╵ ╵"),
"I": ("╶┬╴", " │ ", "╶┴╴"),
"J": (" ╷", " │", "╰─╯"),
"K": ("╷╭╯", "├┤ ", "╵╰╮"),
"L": ("╷ ", "│ ", "╰─╴"),
"M": ("╭┬╮", "│││", "╵╵╵"),
"N": ("╭─╮", "│ │", "╵ ╵"),
"O": ("╭─╮", "│ │", "╰─╯"),
"P": ("┌─╮", "├─╯", "╵ "),
"Q": ("╭─╮", "│ │", "╰─╳"),
"R": ("┌─╮", "├┬╯", "╵╰╴"),
"S": ("╭─╴", "╰─╮", "╶─╯"),
"T": ("╶┬╴", " │ ", " ╵ "),
"U": ("╷ ╷", "│ │", "╰─╯"),
"V": ("╷ ╷", "╲ ╱", " ╹ "),
"W": ("╷ ╷", "│││", "╰┴╯"),
"X": ("╲ ╱", " ╳ ", "╱ ╲"),
"Y": ("╷ ╷", "╰┬╯", " ╵ "),
"Z": ("╶─╮", " ╱ ", "╰─╴"),
".": (" ", " ", "•"),
"·": (" ", "•", " "),
"_": (" ", " ", "───"),
"/": (" ╱", " ╱ ", "╱ "),
"#": ("╪╪", "╪╪", " "),
}

@classmethod
def glyph(cls, character: str) -> Glyph:
return cls.GLYPHS.get(
character.upper(),
(" " * cell_len(character), character, " " * cell_len(character)),
)

@classmethod
def rows(cls, text: str) -> tuple[str, ...]:
glyphs = [cls.glyph(character) for character in text]
return tuple(
" ".join(glyph[row] for glyph in glyphs) for row in range(cls.HEIGHT)
)

@classmethod
def width(cls, text: str) -> int:
return cell_len(cls.rows(text)[0])
6 changes: 3 additions & 3 deletions src/toad/navigation_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
if TYPE_CHECKING:
from agent_comms.display_references import CurrentDisplayReferences

from toad.widgets.comms_sidebar import CommsRow
from toad.widgets.comms_sidebar import CommsRow, ListedRow
from toad.widgets.sidebar_tree import SidebarGroup

@dataclass(frozen=True)
Expand Down Expand Up @@ -55,7 +55,7 @@ def matches(self, target: NavigationTarget) -> bool:
"""Different opening behaviors may display the same sidebar address."""
return self.name == target.name

def rows_in(self, group: SidebarGroup[CommsRow]) -> tuple[CommsRow, ...]:
def rows_in(self, group: SidebarGroup[ListedRow]) -> tuple[CommsRow, ...]:
return group.rows_for_target(self.name)

@abstractmethod
Expand All @@ -73,7 +73,7 @@ def selected(self, owner: NavigationOwner) -> None:
def matches(self, target: NavigationTarget) -> bool:
return False

def rows_in(self, group: SidebarGroup[CommsRow]) -> tuple[CommsRow, ...]:
def rows_in(self, group: SidebarGroup[ListedRow]) -> tuple[CommsRow, ...]:
return ()

async def open(self, context: NavigationContext) -> str:
Expand Down
5 changes: 2 additions & 3 deletions src/toad/screens/action_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,26 @@
from textual import widgets
from textual.widget import Widget

from toad.app import ToadApp
from toad.widgets.command_pane import CommandPane
from toad.terminal_execution import Command, TerminalExecution
from toad.agent_schema import AgentDefinition
from toad.catalog_actions import CatalogCommandAction
from toad.core.source_events import CommandComplete
from toad.core_event_carrier import CoreEventReceiver
from metaclass_registry.mro_dispatch import handles
from toad.app_node import ToadNode

UV_INSTALL = "curl -LsSf https://astral.sh/uv/install.sh | sh"


class ActionModal(CoreEventReceiver, DismissibleScreen[int]):
class ActionModal(CoreEventReceiver, DismissibleScreen[int], ToadNode):
"""Executes an action command."""

CSS_PATH = "store.tcss"

command_pane = getters.query_one(CommandPane)
ok_button = getters.query_one("#ok", widgets.Button)

app = getters.app(ToadApp)

def __init__(
self,
Expand Down
4 changes: 2 additions & 2 deletions src/toad/screens/agent_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from toad.agent_schema import AgentDefinition
from toad.catalog_actions import CatalogCommandAction, LaunchAction
from toad.core.session_requests import LaunchAgent
from toad.app import ToadApp
from toad.preferences import SETTINGS
from toad.platform import host
from toad.app_node import ToadNode


class DescriptionContainer(containers.VerticalScroll):
Expand All @@ -29,13 +29,13 @@ def allow_focus(self) -> bool:

class AgentModal(
PriorityActions, HiddenActions, Launches, DismissibleScreen[LaunchAgent | None],
ToadNode,
scopes={PriorityActions: (widget_actions.LaunchAction,),
HiddenActions: (widget_actions.DismissAction,)},
):
AUTO_FOCUS = "Select#action-select"


app = getters.app(ToadApp)
action_select = getters.query_one("#action-select", widgets.Select)
launcher_checkbox = getters.query_one("#launcher-checkbox", widgets.Checkbox)

Expand Down
7 changes: 3 additions & 4 deletions src/toad/screens/comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Literal
from agent_comms.repo import Checkout

from textual import containers, getters, on
from textual import containers, on
from textual.app import ComposeResult
from textual.document._editing import EditingMode
from toad.session_actions import ReturnToViewAction, CloseViewAction
Expand All @@ -21,7 +21,6 @@

from toad import messages
from toad.conversation_kind import ConversationKind
from toad.app import ToadApp
from toad.widgets.comms_chat import CommsChatView
from toad.core.input_events import SelectHistoricalIdentity
from toad.core.input_events import SelectTarget
Expand All @@ -32,6 +31,7 @@
from toad.widgets.session_tabs import SessionsTabs
from toad.widgets.side_bar import SideBar, ThreadSidebar, TabHistoryControls
from toad.widgets.thread_comms import RelationshipSort, ThreadCommsSidebar
from toad.app_node import ToadNode


class ViewAction(WidgetAction, affix="Action"):
Expand Down Expand Up @@ -79,7 +79,7 @@ def apply(self, context: CommsScreen) -> None:
chat.prepare_prompt()


class CommsScreen(TogglesIrc, CyclesSessions, CheckoutRoot, SessionView, can_focus=False,
class CommsScreen(TogglesIrc, CyclesSessions, CheckoutRoot, SessionView, ToadNode, can_focus=False,
actions=(ViewAction, ReturnToViewAction, CloseViewAction)):
"""A channel or DM represented as a native concurrent Toad session."""

Expand Down Expand Up @@ -108,7 +108,6 @@ def __init__(
self._content_error: BaseException | None = None
self._content_loaded = False

app = getters.app(ToadApp)

@property
def coordination_root(self) -> str | None:
Expand Down
18 changes: 9 additions & 9 deletions src/toad/screens/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ def __init__(self, focus_mode: type[EditingMode], focused: frozenset[str]) -> No
self.focus_mode, self.focused_ids = focus_mode, focused
self.mode_choices = FamilyChoices(KeyMode)
self.modes = self.mode_choices.choices()
self.catalog = KeymapCatalog.capture(self.modes)
self.declarations = tuple(reference for reference, invocation
in KeymapCatalog.capture(self.modes).invocations.items()
in self.catalog.invocations.items()
if not invocation.key_system())
self.visible_declarations = self.declarations

Expand All @@ -68,18 +69,17 @@ def populate(self, term: str) -> None:
table = self.query_one(DataTable)
table.clear()
focused_only = self.query_one("#keymap-focused", Checkbox).value
catalog = KeymapCatalog.capture(self.modes)
self.visible_declarations = tuple(declaration for declaration in self.declarations
if (not focused_only or KeyInvocation.in_mode(declaration, self.focus_mode) in self.focused_ids)
and term.casefold() in (catalog.require(declaration).key_title() + " "
+ catalog.require(declaration).key_summary()).casefold())
and term.casefold() in (self.catalog.require(declaration).key_title() + " "
+ self.catalog.require(declaration).key_summary()).casefold())
keymap = self.app.settings.ui.keymap
conflicts = {row.binding_id for mode in self.modes for conflict in keymap.conflicts(mode)
for row in (conflict.first, conflict.second)}
for declaration in self.visible_declarations:
title = Text(catalog.require(declaration).key_title(),
title = Text(self.catalog.require(declaration).key_title(),
style="" if KeyInvocation.in_mode(declaration, self.focus_mode) in self.focused_ids else "dim")
table.add_row(title, *(Text(", ".join(catalog.effective(row, keymap)) or "—",
table.add_row(title, *(Text(", ".join(self.catalog.effective(row, keymap)) or "—",
style="bold red" if row.binding_id in conflicts else "")
for mode in self.modes for row in (KeymapRow(declaration, mode),)))

Expand All @@ -96,16 +96,16 @@ def selected_rows(self) -> tuple[KeymapRow, ...]:
row, column = self.query_one(DataTable).cursor_coordinate
if column and row < len(self.visible_declarations):
selected = KeymapRow(self.visible_declarations[row], self.modes[column - 1])
return (selected,) if selected.defaults else ()
return (selected,) if self.catalog.defaults(selected) else ()
return ()

@on(DataTable.CellSelected)
def picked(self, message: DataTable.CellSelected) -> None:
for row in self.selected_rows():
self.query_one("#keymap-keys", Input).value = ", ".join(
row.effective(self.app.settings.ui.keymap))
self.catalog.effective(row, self.app.settings.ui.keymap))
self.query_one("#keymap-summary", Static).update(
KeymapCatalog.capture((row.mode,)).require(row.declaration).key_summary())
self.catalog.require(row.declaration).key_summary())
conflicts = tuple(conflict for conflict in self.app.settings.ui.keymap.conflicts(row.mode)
if row in (conflict.first, conflict.second))
self.query_one("#keymap-status", Static).update("; ".join(map(str, conflicts)))
Expand Down
5 changes: 2 additions & 3 deletions src/toad/screens/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
)

from toad.acp.agent_session import SessionDisposition
from toad.app import ToadApp
from toad.core import events as core_events
from toad.core import input_events, session_requests
from toad.core.events import SessionChangedEvent
Expand Down Expand Up @@ -60,6 +59,7 @@
from toad.widgets.session_thread_sidebar import SessionThreadSidebar
from toad.widgets.side_bar import SideBar
from toad.widgets.splash_text import ThreadLoading
from toad.app_node import ToadNode


class SessionCommands(Provider):
Expand Down Expand Up @@ -210,7 +210,7 @@ async def attached(self, screen: "MainScreen") -> None:
await screen.prepare_presentation()


class MainScreen(NativeMethodKeys, HiddenActions, TogglesIrc, CyclesSessions, ViewSessionOrigin, SessionView,
class MainScreen(NativeMethodKeys, HiddenActions, TogglesIrc, CyclesSessions, ViewSessionOrigin, SessionView, ToadNode,
scopes={HiddenActions: (SessionNavigationAction,)}, can_focus=False):
footer_compact = True

Expand All @@ -234,7 +234,6 @@ def side_bar(self):

project_path = var[Checkout](Required())

app = getters.app(ToadApp)

def __init__(
self,
Expand Down
Loading
Loading