fast-resume 1.12.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ """Utility functions for the TUI."""
2
+
3
+ import math
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import humanize
11
+ from datetime import datetime
12
+ from rich.columns import Columns
13
+ from rich.console import RenderableType
14
+ from rich.text import Text
15
+ from textual_image.renderable import Image as ImageRenderable
16
+
17
+ from ..config import AGENTS
18
+
19
+ # Asset paths for agent icons
20
+ ASSETS_DIR = Path(__file__).parent.parent / "assets"
21
+
22
+ # Cache for agent icon renderables (textual_image has incomplete type stubs)
23
+ _icon_cache: dict[str, Any] = {}
24
+
25
+
26
+ def get_agent_icon(agent: str) -> RenderableType:
27
+ """Get the icon + name renderable for an agent."""
28
+ agent_config = AGENTS.get(agent, {"color": "white", "badge": agent})
29
+
30
+ if agent not in _icon_cache:
31
+ icon_path = ASSETS_DIR / f"{agent}.png"
32
+ if icon_path.exists():
33
+ try:
34
+ _icon_cache[agent] = ImageRenderable(icon_path, width=2, height=1)
35
+ except Exception:
36
+ _icon_cache[agent] = None
37
+ else:
38
+ _icon_cache[agent] = None
39
+
40
+ icon = _icon_cache[agent]
41
+ name = Text(agent_config["badge"])
42
+ name.stylize(agent_config["color"])
43
+
44
+ if icon is not None:
45
+ # Combine icon and name horizontally
46
+ return Columns([icon, name], padding=(0, 1), expand=False)
47
+
48
+ # Fallback to just colored name with a dot
49
+ badge = Text(f"● {agent_config['badge']}")
50
+ badge.stylize(agent_config["color"], 0, 1) # Color just the dot
51
+ return badge
52
+
53
+
54
+ def format_time_ago(dt: datetime) -> str:
55
+ """Format a datetime as a human-readable time ago string."""
56
+ return humanize.naturaltime(dt)
57
+
58
+
59
+ def format_directory(path: str) -> str:
60
+ """Format directory path, replacing home with ~."""
61
+ if not path:
62
+ return "n/a"
63
+ home = os.path.expanduser("~")
64
+ if path.startswith(home):
65
+ return "~" + path[len(home) :]
66
+ return path
67
+
68
+
69
+ def highlight_matches(
70
+ text: str, query: str, max_len: int | None = None, style: str = "bold reverse"
71
+ ) -> Text:
72
+ """Highlight matching portions of text based on query terms.
73
+
74
+ Returns a Rich Text object with matches highlighted.
75
+ """
76
+ if max_len and len(text) > max_len:
77
+ text = text[: max_len - 3] + "..."
78
+
79
+ if not query:
80
+ return Text(text)
81
+
82
+ result = Text(text)
83
+ query_lower = query.lower()
84
+ text_lower = text.lower()
85
+
86
+ # Split query into terms and highlight each
87
+ terms = query_lower.split()
88
+ for term in terms:
89
+ if not term:
90
+ continue
91
+ start = 0
92
+ while True:
93
+ idx = text_lower.find(term, start)
94
+ if idx == -1:
95
+ break
96
+ result.stylize(style, idx, idx + len(term))
97
+ start = idx + 1
98
+
99
+ return result
100
+
101
+
102
+ def get_age_color(age_hours: float) -> str:
103
+ """Return a hex color based on session age using exponential decay gradient.
104
+
105
+ Colors transition: Green (0h) → Yellow (24h) → Orange (~2.5d) → Dim gray (7d+)
106
+ """
107
+ # Anchor: 24 hours should hit the green→yellow transition (t=0.3)
108
+ decay_rate = -math.log(1 - 0.3) / 24 # ≈ 0.0149
109
+ t = 1 - math.exp(-decay_rate * age_hours) # 0 at 0h, approaches 1 asymptotically
110
+
111
+ # Interpolate through color stops: green → yellow → orange → gray
112
+ if t < 0.3:
113
+ # Muted green to yellow
114
+ s = t / 0.3
115
+ r = int(100 + s * 100) # 100 → 200
116
+ g = int(200 - s * 20) # 200 → 180
117
+ b = int(50 - s * 50) # 50 → 0
118
+ elif t < 0.6:
119
+ # Yellow to muted orange
120
+ s = (t - 0.3) / 0.3
121
+ r = 200
122
+ g = int(180 - s * 80) # 180 → 100
123
+ b = int(0 + s * 50) # 0 → 50
124
+ else:
125
+ # Muted orange to dim gray
126
+ s = (t - 0.6) / 0.4
127
+ r = int(200 - s * 100) # 200 → 100
128
+ g = 100
129
+ b = int(50 + s * 50) # 50 → 100
130
+
131
+ return f"#{r:02x}{g:02x}{b:02x}"
132
+
133
+
134
+ def copy_to_clipboard(text: str) -> bool:
135
+ """Copy text to system clipboard.
136
+
137
+ Returns True on success, False on failure.
138
+ """
139
+ try:
140
+ if sys.platform == "darwin":
141
+ subprocess.run(["pbcopy"], input=text.encode(), check=True)
142
+ elif sys.platform == "win32":
143
+ subprocess.run(["clip"], input=text.encode(), check=True)
144
+ else:
145
+ # Linux - try xclip or xsel
146
+ try:
147
+ subprocess.run(
148
+ ["xclip", "-selection", "clipboard"],
149
+ input=text.encode(),
150
+ check=True,
151
+ )
152
+ except FileNotFoundError:
153
+ subprocess.run(
154
+ ["xsel", "--clipboard", "--input"],
155
+ input=text.encode(),
156
+ check=True,
157
+ )
158
+ return True
159
+ except Exception:
160
+ return False