claudechic 0.2.2__py3-none-any.whl → 0.3.1__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.
- claudechic/__init__.py +3 -1
- claudechic/__main__.py +12 -1
- claudechic/agent.py +60 -19
- claudechic/agent_manager.py +8 -2
- claudechic/analytics.py +62 -0
- claudechic/app.py +267 -158
- claudechic/commands.py +120 -6
- claudechic/config.py +80 -0
- claudechic/features/worktree/commands.py +70 -1
- claudechic/help_data.py +200 -0
- claudechic/messages.py +0 -17
- claudechic/processes.py +120 -0
- claudechic/profiling.py +18 -1
- claudechic/protocols.py +1 -1
- claudechic/remote.py +249 -0
- claudechic/sessions.py +60 -50
- claudechic/styles.tcss +19 -18
- claudechic/widgets/__init__.py +112 -41
- claudechic/widgets/base/__init__.py +20 -0
- claudechic/widgets/base/clickable.py +23 -0
- claudechic/widgets/base/copyable.py +55 -0
- claudechic/{cursor.py → widgets/base/cursor.py} +9 -28
- claudechic/widgets/base/tool_protocol.py +30 -0
- claudechic/widgets/content/__init__.py +41 -0
- claudechic/widgets/{diff.py → content/diff.py} +11 -65
- claudechic/widgets/{chat.py → content/message.py} +25 -76
- claudechic/widgets/{tools.py → content/tools.py} +12 -24
- claudechic/widgets/input/__init__.py +9 -0
- claudechic/widgets/layout/__init__.py +51 -0
- claudechic/widgets/{chat_view.py → layout/chat_view.py} +92 -43
- claudechic/widgets/{footer.py → layout/footer.py} +17 -7
- claudechic/widgets/{indicators.py → layout/indicators.py} +55 -7
- claudechic/widgets/layout/processes.py +68 -0
- claudechic/widgets/{agents.py → layout/sidebar.py} +163 -82
- claudechic/widgets/modals/__init__.py +9 -0
- claudechic/widgets/modals/process_modal.py +121 -0
- claudechic/widgets/{profile_modal.py → modals/profile.py} +2 -1
- claudechic/widgets/primitives/__init__.py +13 -0
- claudechic/widgets/{button.py → primitives/button.py} +1 -1
- claudechic/widgets/{collapsible.py → primitives/collapsible.py} +5 -1
- claudechic/widgets/{scroll.py → primitives/scroll.py} +2 -0
- claudechic/widgets/primitives/spinner.py +57 -0
- claudechic/widgets/prompts.py +146 -17
- claudechic/widgets/reports/__init__.py +10 -0
- claudechic-0.3.1.dist-info/METADATA +88 -0
- claudechic-0.3.1.dist-info/RECORD +71 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/WHEEL +1 -1
- claudechic-0.3.1.dist-info/licenses/LICENSE +21 -0
- claudechic/features/worktree/prompts.py +0 -101
- claudechic/widgets/model_prompt.py +0 -56
- claudechic-0.2.2.dist-info/METADATA +0 -58
- claudechic-0.2.2.dist-info/RECORD +0 -54
- /claudechic/widgets/{todo.py → content/todo.py} +0 -0
- /claudechic/widgets/{autocomplete.py → input/autocomplete.py} +0 -0
- /claudechic/widgets/{history_search.py → input/history_search.py} +0 -0
- /claudechic/widgets/{context_report.py → reports/context.py} +0 -0
- /claudechic/widgets/{usage.py → reports/usage.py} +0 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/entry_points.txt +0 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/top_level.txt +0 -0
claudechic/widgets/__init__.py
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
"""Textual widgets for Claude Code UI.
|
|
1
|
+
"""Textual widgets for Claude Code UI.
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Re-exports all widgets from submodules for backward compatibility.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Base classes and mixins
|
|
7
|
+
from claudechic.widgets.base import (
|
|
8
|
+
ClickableMixin,
|
|
9
|
+
PointerMixin,
|
|
10
|
+
CopyButton,
|
|
11
|
+
CopyableMixin,
|
|
12
|
+
ToolWidget,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Primitives
|
|
16
|
+
from claudechic.widgets.primitives import (
|
|
17
|
+
Button,
|
|
18
|
+
QuietCollapsible,
|
|
19
|
+
AutoHideScroll,
|
|
20
|
+
Spinner,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Content widgets
|
|
24
|
+
from claudechic.widgets.content import (
|
|
7
25
|
ChatMessage,
|
|
8
26
|
ChatInput,
|
|
9
27
|
ThinkingIndicator,
|
|
@@ -11,53 +29,85 @@ from claudechic.widgets.chat import (
|
|
|
11
29
|
ErrorMessage,
|
|
12
30
|
SystemInfo,
|
|
13
31
|
ChatAttachment,
|
|
14
|
-
Spinner,
|
|
15
|
-
)
|
|
16
|
-
from claudechic.widgets.tools import (
|
|
17
32
|
ToolUseWidget,
|
|
18
33
|
TaskWidget,
|
|
19
34
|
AgentToolWidget,
|
|
20
35
|
AgentListWidget,
|
|
21
36
|
ShellOutputWidget,
|
|
22
37
|
EditPlanRequested,
|
|
38
|
+
DiffWidget,
|
|
39
|
+
TodoWidget,
|
|
40
|
+
TodoPanel,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Input widgets
|
|
44
|
+
from claudechic.widgets.input import TextAreaAutoComplete, HistorySearch
|
|
45
|
+
|
|
46
|
+
# Layout widgets
|
|
47
|
+
from claudechic.widgets.layout import (
|
|
48
|
+
ChatView,
|
|
49
|
+
AgentItem,
|
|
50
|
+
AgentSection,
|
|
51
|
+
WorktreeItem,
|
|
52
|
+
PlanItem,
|
|
53
|
+
PlanSection,
|
|
54
|
+
SidebarSection,
|
|
55
|
+
SidebarItem,
|
|
56
|
+
HamburgerButton,
|
|
57
|
+
SessionItem,
|
|
58
|
+
AutoEditLabel,
|
|
59
|
+
ModelLabel,
|
|
60
|
+
StatusFooter,
|
|
61
|
+
IndicatorWidget,
|
|
62
|
+
CPUBar,
|
|
63
|
+
ContextBar,
|
|
64
|
+
ProcessIndicator,
|
|
65
|
+
ProcessPanel,
|
|
66
|
+
ProcessItem,
|
|
23
67
|
)
|
|
24
|
-
|
|
25
|
-
|
|
68
|
+
|
|
69
|
+
# Base re-exports (ClickableLabel used by layout widgets)
|
|
70
|
+
from claudechic.widgets.base import ClickableLabel
|
|
71
|
+
|
|
72
|
+
# Data classes (re-exported for convenience)
|
|
73
|
+
from claudechic.processes import BackgroundProcess
|
|
74
|
+
|
|
75
|
+
# Report widgets
|
|
76
|
+
from claudechic.widgets.reports import UsageReport, ContextReport
|
|
77
|
+
|
|
78
|
+
# Modal screens
|
|
79
|
+
from claudechic.widgets.modals import ProfileModal, ProcessModal
|
|
80
|
+
|
|
81
|
+
# Prompts
|
|
26
82
|
from claudechic.widgets.prompts import (
|
|
27
83
|
BasePrompt,
|
|
28
84
|
SelectionPrompt,
|
|
29
85
|
QuestionPrompt,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
from claudechic.widgets.autocomplete import TextAreaAutoComplete
|
|
34
|
-
from claudechic.widgets.agents import (
|
|
35
|
-
AgentItem,
|
|
36
|
-
AgentSidebar,
|
|
37
|
-
WorktreeItem,
|
|
38
|
-
PlanButton,
|
|
39
|
-
HamburgerButton,
|
|
86
|
+
ModelPrompt,
|
|
87
|
+
WorktreePrompt,
|
|
88
|
+
UncommittedChangesPrompt,
|
|
40
89
|
)
|
|
41
|
-
from claudechic.widgets.scroll import AutoHideScroll
|
|
42
|
-
from claudechic.widgets.chat_view import ChatView
|
|
43
|
-
from claudechic.widgets.collapsible import QuietCollapsible
|
|
44
|
-
from claudechic.widgets.history_search import HistorySearch
|
|
45
|
-
from claudechic.widgets.usage import UsageReport
|
|
46
|
-
from claudechic.widgets.profile_modal import ProfileModal
|
|
47
90
|
|
|
48
91
|
__all__ = [
|
|
49
|
-
|
|
92
|
+
# Base
|
|
50
93
|
"ClickableMixin",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
94
|
+
"PointerMixin",
|
|
95
|
+
"CopyButton",
|
|
96
|
+
"CopyableMixin",
|
|
97
|
+
"ToolWidget",
|
|
98
|
+
# Primitives
|
|
99
|
+
"Button",
|
|
100
|
+
"QuietCollapsible",
|
|
101
|
+
"AutoHideScroll",
|
|
102
|
+
"Spinner",
|
|
103
|
+
# Content
|
|
53
104
|
"ChatMessage",
|
|
54
105
|
"ChatInput",
|
|
55
|
-
"ChatAttachment",
|
|
56
|
-
"Spinner",
|
|
57
106
|
"ThinkingIndicator",
|
|
58
107
|
"ImageAttachments",
|
|
59
108
|
"ErrorMessage",
|
|
60
109
|
"SystemInfo",
|
|
110
|
+
"ChatAttachment",
|
|
61
111
|
"ToolUseWidget",
|
|
62
112
|
"TaskWidget",
|
|
63
113
|
"AgentToolWidget",
|
|
@@ -67,21 +117,42 @@ __all__ = [
|
|
|
67
117
|
"DiffWidget",
|
|
68
118
|
"TodoWidget",
|
|
69
119
|
"TodoPanel",
|
|
70
|
-
|
|
71
|
-
"SelectionPrompt",
|
|
72
|
-
"QuestionPrompt",
|
|
73
|
-
"SessionItem",
|
|
120
|
+
# Input
|
|
74
121
|
"TextAreaAutoComplete",
|
|
122
|
+
"HistorySearch",
|
|
123
|
+
# Layout
|
|
124
|
+
"ChatView",
|
|
75
125
|
"AgentItem",
|
|
76
|
-
"
|
|
126
|
+
"AgentSection",
|
|
77
127
|
"WorktreeItem",
|
|
78
|
-
"
|
|
128
|
+
"SessionItem",
|
|
129
|
+
"PlanItem",
|
|
130
|
+
"PlanSection",
|
|
131
|
+
"SidebarSection",
|
|
132
|
+
"SidebarItem",
|
|
79
133
|
"HamburgerButton",
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
134
|
+
"ClickableLabel",
|
|
135
|
+
"AutoEditLabel",
|
|
136
|
+
"ModelLabel",
|
|
137
|
+
"StatusFooter",
|
|
138
|
+
"IndicatorWidget",
|
|
139
|
+
"CPUBar",
|
|
140
|
+
"ContextBar",
|
|
141
|
+
"ProcessIndicator",
|
|
142
|
+
"ProcessPanel",
|
|
143
|
+
"ProcessItem",
|
|
144
|
+
"BackgroundProcess",
|
|
145
|
+
# Reports
|
|
84
146
|
"UsageReport",
|
|
147
|
+
"ContextReport",
|
|
148
|
+
# Modals
|
|
85
149
|
"ProfileModal",
|
|
150
|
+
"ProcessModal",
|
|
151
|
+
# Prompts
|
|
152
|
+
"BasePrompt",
|
|
153
|
+
"SelectionPrompt",
|
|
154
|
+
"QuestionPrompt",
|
|
86
155
|
"ModelPrompt",
|
|
156
|
+
"WorktreePrompt",
|
|
157
|
+
"UncommittedChangesPrompt",
|
|
87
158
|
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Base classes and mixins for widgets."""
|
|
2
|
+
|
|
3
|
+
from claudechic.widgets.base.cursor import (
|
|
4
|
+
ClickableMixin,
|
|
5
|
+
PointerMixin,
|
|
6
|
+
set_pointer,
|
|
7
|
+
)
|
|
8
|
+
from claudechic.widgets.base.copyable import CopyButton, CopyableMixin
|
|
9
|
+
from claudechic.widgets.base.clickable import ClickableLabel
|
|
10
|
+
from claudechic.widgets.base.tool_protocol import ToolWidget
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ClickableMixin",
|
|
14
|
+
"PointerMixin",
|
|
15
|
+
"set_pointer",
|
|
16
|
+
"CopyButton",
|
|
17
|
+
"CopyableMixin",
|
|
18
|
+
"ClickableLabel",
|
|
19
|
+
"ToolWidget",
|
|
20
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Clickable label widget - static text with pointer cursor."""
|
|
2
|
+
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
|
|
5
|
+
from claudechic.widgets.base.cursor import ClickableMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClickableLabel(Static, ClickableMixin):
|
|
9
|
+
"""A static label that is clickable with pointer cursor.
|
|
10
|
+
|
|
11
|
+
Base class for labels that respond to clicks. Override on_click()
|
|
12
|
+
to handle clicks, or post custom messages.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
class MyLabel(ClickableLabel):
|
|
16
|
+
class Clicked(Message):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def on_click(self, event) -> None:
|
|
20
|
+
self.post_message(self.Clicked())
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
pass
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Copyable mixin for widgets with copy-to-clipboard functionality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from claudechic.widgets.primitives.button import Button
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from textual.app import App
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CopyButton(Button):
|
|
14
|
+
"""Copy button with hand cursor on hover."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CopyableMixin:
|
|
20
|
+
"""Mixin for widgets that support copying content to clipboard.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
class MyWidget(Static, CopyableMixin):
|
|
24
|
+
def compose(self):
|
|
25
|
+
yield CopyButton("⧉", classes="copy-btn")
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def get_copyable_content(self) -> str:
|
|
29
|
+
return "content to copy"
|
|
30
|
+
|
|
31
|
+
def on_button_pressed(self, event):
|
|
32
|
+
if self.handle_copy_button(event):
|
|
33
|
+
return
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def get_copyable_content(self) -> str:
|
|
37
|
+
"""Return content to copy. Override in subclass."""
|
|
38
|
+
raise NotImplementedError("Subclass must implement get_copyable_content()")
|
|
39
|
+
|
|
40
|
+
# Type hint for app - mixin expects to be used with Widget
|
|
41
|
+
app: App
|
|
42
|
+
|
|
43
|
+
def handle_copy_button(self, event: Button.Pressed) -> bool:
|
|
44
|
+
"""Handle copy button press. Returns True if handled."""
|
|
45
|
+
if "copy-btn" in event.button.classes:
|
|
46
|
+
event.stop()
|
|
47
|
+
try:
|
|
48
|
+
import pyperclip
|
|
49
|
+
|
|
50
|
+
pyperclip.copy(self.get_copyable_content())
|
|
51
|
+
self.app.notify("Copied to clipboard")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.app.notify(f"Copy failed: {e}", severity="error")
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
"""Mouse cursor
|
|
1
|
+
"""Mouse cursor mixins for Textual widgets.
|
|
2
2
|
|
|
3
|
-
Provides
|
|
4
|
-
- ClickableMixin: Hand cursor
|
|
3
|
+
Provides two mixins:
|
|
4
|
+
- ClickableMixin: Hand cursor on hover (for buttons)
|
|
5
5
|
- PointerMixin: Configurable cursor style (for text areas)
|
|
6
|
-
- HoverableMixin: Just .hovered class (for hover effects without cursor)
|
|
7
6
|
|
|
8
7
|
Uses OSC 22 escape sequences supported by modern terminals
|
|
9
8
|
(Ghostty, Kitty, WezTerm, foot). Unsupported terminals ignore the sequence.
|
|
9
|
+
|
|
10
|
+
Note: For hover visual effects, use CSS :hover pseudo-class instead of
|
|
11
|
+
adding/removing classes - much more efficient (no DOM style recalc).
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import os
|
|
@@ -72,28 +74,11 @@ class PointerMixin:
|
|
|
72
74
|
super().on_leave() # type: ignore[misc]
|
|
73
75
|
|
|
74
76
|
|
|
75
|
-
class HoverableMixin:
|
|
76
|
-
"""Mixin for widgets that need a .hovered class for CSS styling.
|
|
77
|
-
|
|
78
|
-
Adds/removes 'hovered' class on mouse enter/leave. Useful when
|
|
79
|
-
CSS :hover doesn't propagate properly through child widgets.
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
def on_enter(self) -> None:
|
|
83
|
-
if not self.has_class("hovered"): # type: ignore[attr-defined]
|
|
84
|
-
self.add_class("hovered") # type: ignore[attr-defined]
|
|
85
|
-
|
|
86
|
-
def on_leave(self) -> None:
|
|
87
|
-
if self.has_class("hovered"): # type: ignore[attr-defined]
|
|
88
|
-
self.remove_class("hovered") # type: ignore[attr-defined]
|
|
89
|
-
set_pointer("default")
|
|
90
|
-
|
|
91
|
-
|
|
92
77
|
class ClickableMixin:
|
|
93
|
-
"""Mixin for clickable widgets with hand cursor
|
|
78
|
+
"""Mixin for clickable widgets with hand cursor.
|
|
94
79
|
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
Shows pointer cursor on hover. Use CSS :hover for visual styling
|
|
81
|
+
instead of .hovered class (much more efficient).
|
|
97
82
|
|
|
98
83
|
Example:
|
|
99
84
|
class MyButton(Static, ClickableMixin):
|
|
@@ -106,10 +91,6 @@ class ClickableMixin:
|
|
|
106
91
|
|
|
107
92
|
def on_enter(self) -> None:
|
|
108
93
|
set_pointer("pointer")
|
|
109
|
-
if not self.has_class("hovered"): # type: ignore[attr-defined]
|
|
110
|
-
self.add_class("hovered") # type: ignore[attr-defined]
|
|
111
94
|
|
|
112
95
|
def on_leave(self) -> None:
|
|
113
96
|
set_pointer("default")
|
|
114
|
-
if self.has_class("hovered"): # type: ignore[attr-defined]
|
|
115
|
-
self.remove_class("hovered") # type: ignore[attr-defined]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Protocol for tool display widgets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from claude_agent_sdk import ToolResultBlock
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class ToolWidget(Protocol):
|
|
13
|
+
"""Protocol for widgets that display tool use/results.
|
|
14
|
+
|
|
15
|
+
Implemented by:
|
|
16
|
+
- ToolUseWidget: Standard tool display with collapsible details
|
|
17
|
+
- TaskWidget: Nested subagent display
|
|
18
|
+
- AgentToolWidget: MCP chic agent tools (spawn_agent, ask_agent, etc.)
|
|
19
|
+
|
|
20
|
+
This protocol enables ChatView to treat all tool widgets uniformly
|
|
21
|
+
for operations like collapse() and set_result().
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def collapse(self) -> None:
|
|
25
|
+
"""Collapse this widget to save visual space."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def set_result(self, result: "ToolResultBlock") -> None:
|
|
29
|
+
"""Update the widget with the tool's result."""
|
|
30
|
+
...
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Content display widgets - messages, tools, diffs."""
|
|
2
|
+
|
|
3
|
+
from claudechic.widgets.content.message import (
|
|
4
|
+
ChatMessage,
|
|
5
|
+
ChatInput,
|
|
6
|
+
ThinkingIndicator,
|
|
7
|
+
ImageAttachments,
|
|
8
|
+
ErrorMessage,
|
|
9
|
+
SystemInfo,
|
|
10
|
+
ChatAttachment,
|
|
11
|
+
)
|
|
12
|
+
from claudechic.widgets.content.tools import (
|
|
13
|
+
ToolUseWidget,
|
|
14
|
+
TaskWidget,
|
|
15
|
+
AgentToolWidget,
|
|
16
|
+
AgentListWidget,
|
|
17
|
+
ShellOutputWidget,
|
|
18
|
+
EditPlanRequested,
|
|
19
|
+
)
|
|
20
|
+
from claudechic.widgets.content.diff import DiffWidget
|
|
21
|
+
from claudechic.widgets.content.todo import TodoWidget, TodoPanel, TodoItem
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ChatMessage",
|
|
25
|
+
"ChatInput",
|
|
26
|
+
"ThinkingIndicator",
|
|
27
|
+
"ImageAttachments",
|
|
28
|
+
"ErrorMessage",
|
|
29
|
+
"SystemInfo",
|
|
30
|
+
"ChatAttachment",
|
|
31
|
+
"ToolUseWidget",
|
|
32
|
+
"TaskWidget",
|
|
33
|
+
"AgentToolWidget",
|
|
34
|
+
"AgentListWidget",
|
|
35
|
+
"ShellOutputWidget",
|
|
36
|
+
"EditPlanRequested",
|
|
37
|
+
"DiffWidget",
|
|
38
|
+
"TodoWidget",
|
|
39
|
+
"TodoPanel",
|
|
40
|
+
"TodoItem",
|
|
41
|
+
]
|
|
@@ -5,7 +5,6 @@ import re
|
|
|
5
5
|
from functools import lru_cache
|
|
6
6
|
|
|
7
7
|
from pygments.lexers import get_lexer_by_name
|
|
8
|
-
from pygments.token import Token
|
|
9
8
|
from pygments.util import ClassNotFound
|
|
10
9
|
from textual.content import Content, Span
|
|
11
10
|
from textual.containers import HorizontalScroll
|
|
@@ -15,74 +14,25 @@ from textual.widgets import Static
|
|
|
15
14
|
from claudechic.formatting import get_lang_from_path
|
|
16
15
|
|
|
17
16
|
|
|
17
|
+
# Colors - line backgrounds
|
|
18
|
+
REMOVED_BG = "#301010"
|
|
19
|
+
ADDED_BG = "#103010"
|
|
20
|
+
# Word-level change highlights
|
|
21
|
+
REMOVED_WORD_STYLE = "underline on #501818"
|
|
22
|
+
ADDED_WORD_STYLE = "underline on #185018"
|
|
23
|
+
|
|
24
|
+
|
|
18
25
|
@lru_cache(maxsize=64)
|
|
19
26
|
def _get_cached_lexer(language: str):
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
Caching lexers avoids the expensive lexer loading/guessing on every highlight call.
|
|
23
|
-
The profiler showed ~15% of CPU time was spent in _load_lexers and guess_lexer.
|
|
24
|
-
"""
|
|
27
|
+
"""Cache Pygments lexers to avoid repeated loading (~15% CPU savings)."""
|
|
25
28
|
try:
|
|
26
29
|
return get_lexer_by_name(language, stripnl=False, ensurenl=True, tabsize=8)
|
|
27
30
|
except ClassNotFound:
|
|
28
31
|
return None
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
# Colors - line backgrounds (subtle tint)
|
|
32
|
-
REMOVED_BG = "#200000"
|
|
33
|
-
ADDED_BG = "#002000"
|
|
34
|
-
# Word-level change highlights - subtle background + underline
|
|
35
|
-
REMOVED_WORD_STYLE = "underline on #330808"
|
|
36
|
-
ADDED_WORD_STYLE = "underline on #083308"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class DiffHighlightTheme(HighlightTheme):
|
|
40
|
-
"""Syntax highlighting theme for diffs, aligned with chic theme.
|
|
41
|
-
|
|
42
|
-
Uses orange as primary accent, saturated blues for structure,
|
|
43
|
-
and avoids red/green that clash with diff backgrounds.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
STYLES = {
|
|
47
|
-
Token.Comment: "#888888", # Brighter gray for visibility
|
|
48
|
-
Token.Error: "#ff6b6b", # Soft red for errors
|
|
49
|
-
Token.Generic.Strong: "bold",
|
|
50
|
-
Token.Generic.Emph: "italic",
|
|
51
|
-
Token.Generic.Error: "#ff6b6b",
|
|
52
|
-
Token.Generic.Heading: "#ff9922 underline", # Bright orange
|
|
53
|
-
Token.Generic.Subheading: "#ff9922",
|
|
54
|
-
Token.Keyword: "#ff9922", # Bright orange for keywords
|
|
55
|
-
Token.Keyword.Constant: "#66bbff bold", # Vivid blue
|
|
56
|
-
Token.Keyword.Namespace: "#ff9922", # Bright orange
|
|
57
|
-
Token.Keyword.Type: "#66bbff bold", # Vivid blue
|
|
58
|
-
Token.Literal.Number: "#ffcc66", # Bright gold
|
|
59
|
-
Token.Literal.String.Backtick: "#888888", # Gray
|
|
60
|
-
Token.Literal.String: "#77ccff", # Bright cyan-blue
|
|
61
|
-
Token.Literal.String.Doc: "#77ccff italic",
|
|
62
|
-
Token.Literal.String.Double: "#77ccff",
|
|
63
|
-
Token.Name: "#dddddd", # Bright base text
|
|
64
|
-
Token.Name.Attribute: "#ffcc66", # Bright gold
|
|
65
|
-
Token.Name.Builtin: "#66bbff", # Vivid blue
|
|
66
|
-
Token.Name.Builtin.Pseudo: "#66bbff italic",
|
|
67
|
-
Token.Name.Class: "#ff9922 bold", # Bright orange
|
|
68
|
-
Token.Name.Constant: "#66bbff", # Vivid blue
|
|
69
|
-
Token.Name.Decorator: "#ff9922 bold", # Bright orange
|
|
70
|
-
Token.Name.Function: "#ffcc66", # Bright gold
|
|
71
|
-
Token.Name.Function.Magic: "#ffcc66",
|
|
72
|
-
Token.Name.Tag: "#ff9922 bold", # Bright orange
|
|
73
|
-
Token.Name.Variable: "#88ddff", # Light cyan
|
|
74
|
-
Token.Operator: "#dddddd bold", # Bright base
|
|
75
|
-
Token.Operator.Word: "#ff9922 bold", # Bright orange
|
|
76
|
-
Token.Whitespace: "",
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
34
|
def _highlight_text(text: str, language: str) -> Content:
|
|
81
|
-
"""Syntax highlight text using cached lexer and
|
|
82
|
-
|
|
83
|
-
Performance-optimized version of textual.highlight.highlight() that
|
|
84
|
-
reuses cached lexer instances instead of creating new ones each time.
|
|
85
|
-
"""
|
|
35
|
+
"""Syntax highlight text using cached lexer and default HighlightTheme."""
|
|
86
36
|
if not language:
|
|
87
37
|
return Content(text)
|
|
88
38
|
|
|
@@ -90,19 +40,15 @@ def _highlight_text(text: str, language: str) -> Content:
|
|
|
90
40
|
if lexer is None:
|
|
91
41
|
return Content(text)
|
|
92
42
|
|
|
93
|
-
# Normalize line endings
|
|
94
43
|
text = "\n".join(text.splitlines())
|
|
95
|
-
|
|
96
|
-
# Build spans from Pygments tokens
|
|
97
44
|
token_start = 0
|
|
98
45
|
spans: list[Span] = []
|
|
99
46
|
|
|
100
47
|
for token_type, token in lexer.get_tokens(text):
|
|
101
48
|
token_end = token_start + len(token)
|
|
102
|
-
# Walk up parent chain to find matching style
|
|
103
49
|
current_type = token_type
|
|
104
50
|
while True:
|
|
105
|
-
if style :=
|
|
51
|
+
if style := HighlightTheme.STYLES.get(current_type):
|
|
106
52
|
spans.append(Span(token_start, token_end, style))
|
|
107
53
|
break
|
|
108
54
|
if (current_type := current_type.parent) is None:
|