Skip to content
Open
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
14 changes: 14 additions & 0 deletions tame/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from tame.ui.keys.manager import KeybindManager
from tame.ui.themes.manager import ThemeManager
from tame.ui.widgets import (
CherryBlossomPetals,
CommandPalette,
ConfirmDialog,
DiffViewer,
Expand Down Expand Up @@ -343,13 +344,20 @@ def compose(self) -> ComposeResult:
yield SessionSearchBar()
yield StatusBar()
yield ToastOverlay()
yield CherryBlossomPetals()

def on_mount(self) -> None:
loop = asyncio.get_running_loop()
self._session_manager.attach_to_loop(loop)
self.call_later(self._restore_tmux_sessions_async)
self._start_resource_poll()
self._start_tmux_health_check()
# Activate cherry blossom petals if starting with that theme.
if self._theme_manager.current == "cherry_blossom":
try:
self.query_one(CherryBlossomPetals).display = True
except Exception:
pass
log.info("TAME started")

# ------------------------------------------------------------------
Expand Down Expand Up @@ -641,6 +649,12 @@ def action_toggle_theme(self) -> None:
sidebar.styles.color = sfg
except Exception:
pass
# Toggle cherry blossom petal animation.
try:
petals = self.query_one(CherryBlossomPetals)
petals.display = new_theme == "cherry_blossom"
except Exception:
pass
log.info("Switched theme to '%s'", new_theme)

def action_delete_session(self) -> None:
Expand Down
110 changes: 110 additions & 0 deletions tame/ui/themes/builtin/cherry_blossom.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* TAME Cherry Blossom Theme */

Screen {
background: #fff0f5;
color: #4a3040;
}

#header-bar {
dock: top;
height: 1;
background: #d4678e;
color: #ffffff;
padding: 0 1;
}

#sidebar {
width: 28;
background: #ffe4ec;
border-right: solid #f0b8cc;
}

#sidebar-search {
margin: 0 1;
height: 1;
}

#session-list {
background: #ffe4ec;
}

.session-item {
padding: 0 1;
height: 1;
}

.session-item:hover {
background: #ffd0dc;
}

.session-item.--highlight {
background: #ffb6c8;
}

.session-item.status-active {
color: #5a9e5a;
}

.session-item.status-idle {
color: #b0909e;
}

.session-item.status-waiting {
color: #d4a058;
}

.session-item.status-error {
color: #c44569;
}

.session-item.status-done {
color: #5a9e5a;
}

.session-item.status-paused {
color: #b0909e;
}

#new-session-btn {
margin: 0 1;
height: 1;
background: #f0c0d0;
color: #4a3040;
}

#main-panel {
background: #fff0f5;
}

#session-header {
height: 2;
background: #ffe4ec;
padding: 0 1;
color: #8a607a;
border-bottom: solid #f0b8cc;
}

#session-viewer {
background: #fff0f5;
color: #4a3040;
}

#input-area {
dock: bottom;
height: 3;
background: #ffe4ec;
border-top: solid #f0b8cc;
padding: 0 1;
}

#status-bar {
dock: bottom;
height: 1;
background: #d4678e;
color: #ffffff;
padding: 0 1;
}

#toast-overlay {
layer: overlay;
}
8 changes: 8 additions & 0 deletions tame/ui/themes/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"gruvbox",
"solarized_dark",
"solarized_light",
"cherry_blossom",
]

# Colors for widgets that actually exist in the app.
Expand Down Expand Up @@ -77,6 +78,13 @@
"viewer": ("#fdf6e3", "#657b83"),
"status": ("#268bd2", "#fdf6e3"),
},
"cherry_blossom": {
"screen": ("#fff0f5", "#4a3040"),
"header": ("#d4678e", "#ffffff"),
"sidebar": ("#ffe4ec", "#4a3040"),
"viewer": ("#fff0f5", "#4a3040"),
"status": ("#d4678e", "#ffffff"),
},
}


Expand Down
2 changes: 2 additions & 0 deletions tame/ui/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from .cherry_blossom_petals import CherryBlossomPetals
from .command_palette import CommandPalette
from .confirm_dialog import ConfirmDialog
from .diff_viewer import DiffViewer
Expand All @@ -18,6 +19,7 @@
from .toast_overlay import ToastOverlay

__all__ = [
"CherryBlossomPetals",
"CommandPalette",
"ConfirmDialog",
"DiffViewer",
Expand Down
103 changes: 103 additions & 0 deletions tame/ui/widgets/cherry_blossom_petals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import annotations

import random

from rich.segment import Segment
from rich.style import Style
from textual.strip import Strip
from textual.timer import Timer
from textual.widget import Widget


class CherryBlossomPetals(Widget):
"""Animated falling cherry blossom petals overlay.

Only visible when the cherry_blossom theme is active.
Renders sparse ASCII petals drifting downward across the screen.
"""

DEFAULT_CSS = """
CherryBlossomPetals {
layer: overlay;
width: 1fr;
height: 1fr;
display: none;
}
"""

PETAL_CHARS = ("*", ".", "~", "*", ".", "°")
PETAL_COLORS = ("#d4678e", "#e891a8", "#ffb6d9", "#ffc9dd", "#f0a0b8")

can_focus = False

def __init__(self) -> None:
super().__init__(id="cherry-blossom-petals")
# Each petal: [row_float, col_int, char, color_hex, speed_float]
self._petals: list[list] = []
self._animation_timer: Timer | None = None

def on_mount(self) -> None:
self._animation_timer = self.set_interval(0.15, self._tick)

def _tick(self) -> None:
if not self.display:
return

height = self.size.height
width = self.size.width
if height <= 0 or width <= 0:
return

# Move existing petals downward with slight horizontal drift.
surviving: list[list] = []
for petal in self._petals:
row, col, char, color, speed = petal
new_row = row + speed
new_col = col + random.choice([-1, 0, 0, 0, 1])
if new_row < height and 0 <= new_col < width:
surviving.append([new_row, new_col, char, color, speed])

# Spawn a new petal occasionally (keep density low).
if random.random() < 0.35:
col = random.randint(0, max(0, width - 1))
char = random.choice(self.PETAL_CHARS)
color = random.choice(self.PETAL_COLORS)
speed = random.choice([0.5, 1.0, 1.0, 1.5])
surviving.append([0.0, col, char, color, speed])

self._petals = surviving
self.refresh()

def render_line(self, y: int) -> Strip:
"""Render a single line of the petal overlay."""
width = self.size.width
if width <= 0:
return Strip.blank(width)

# Collect petals on this row.
row_petals: dict[int, tuple[str, str]] = {}
for petal in self._petals:
row, col, char, color, _speed = petal
if int(row) == y and 0 <= int(col) < width:
row_petals[int(col)] = (char, color)

if not row_petals:
return Strip.blank(width)

# Build segments: spaces for empty cells, styled chars for petals.
segments: list[Segment] = []
blank = Style()
i = 0
while i < width:
if i in row_petals:
char, color = row_petals[i]
segments.append(Segment(char, Style(color=color, bold=True)))
i += 1
else:
# Accumulate consecutive blank spaces.
start = i
while i < width and i not in row_petals:
i += 1
segments.append(Segment(" " * (i - start), blank))

return Strip(segments, width)