lazyopencode 0.1.0__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.
- lazyopencode/__init__.py +48 -0
- lazyopencode/__main__.py +6 -0
- lazyopencode/_version.py +34 -0
- lazyopencode/app.py +310 -0
- lazyopencode/bindings.py +27 -0
- lazyopencode/mixins/filtering.py +33 -0
- lazyopencode/mixins/help.py +74 -0
- lazyopencode/mixins/navigation.py +184 -0
- lazyopencode/models/__init__.py +17 -0
- lazyopencode/models/customization.py +120 -0
- lazyopencode/services/__init__.py +7 -0
- lazyopencode/services/discovery.py +350 -0
- lazyopencode/services/gitignore_filter.py +123 -0
- lazyopencode/services/parsers/__init__.py +152 -0
- lazyopencode/services/parsers/agent.py +93 -0
- lazyopencode/services/parsers/command.py +94 -0
- lazyopencode/services/parsers/mcp.py +67 -0
- lazyopencode/services/parsers/plugin.py +127 -0
- lazyopencode/services/parsers/rules.py +65 -0
- lazyopencode/services/parsers/skill.py +138 -0
- lazyopencode/services/parsers/tool.py +67 -0
- lazyopencode/styles/app.tcss +173 -0
- lazyopencode/themes.py +30 -0
- lazyopencode/widgets/__init__.py +17 -0
- lazyopencode/widgets/app_footer.py +71 -0
- lazyopencode/widgets/combined_panel.py +345 -0
- lazyopencode/widgets/detail_pane.py +338 -0
- lazyopencode/widgets/filter_input.py +88 -0
- lazyopencode/widgets/helpers/__init__.py +5 -0
- lazyopencode/widgets/helpers/rendering.py +17 -0
- lazyopencode/widgets/status_panel.py +70 -0
- lazyopencode/widgets/type_panel.py +501 -0
- lazyopencode-0.1.0.dist-info/METADATA +118 -0
- lazyopencode-0.1.0.dist-info/RECORD +37 -0
- lazyopencode-0.1.0.dist-info/WHEEL +4 -0
- lazyopencode-0.1.0.dist-info/entry_points.txt +2 -0
- lazyopencode-0.1.0.dist-info/licenses/LICENSE +21 -0
lazyopencode/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""LazyOpenCode - TUI for managing OpenCode customizations."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from lazyopencode._version import __version__
|
|
5
|
+
except ImportError:
|
|
6
|
+
__version__ = "0.0.0+dev"
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from lazyopencode.app import create_app
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
"""Run the LazyOpenCode application."""
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="A lazygit-style TUI for visualizing OpenCode customizations",
|
|
18
|
+
prog="lazyopencode",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-d",
|
|
27
|
+
"--directory",
|
|
28
|
+
type=Path,
|
|
29
|
+
default=None,
|
|
30
|
+
help="Project directory to scan for customizations (default: current directory)",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-u",
|
|
35
|
+
"--user-config",
|
|
36
|
+
type=Path,
|
|
37
|
+
default=None,
|
|
38
|
+
help="Override user config path (default: ~/.config/opencode)",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
# Handle directory argument - resolve to absolute path
|
|
44
|
+
project_root = args.directory.resolve() if args.directory else None
|
|
45
|
+
user_config = args.user_config.resolve() if args.user_config else None
|
|
46
|
+
|
|
47
|
+
app = create_app(project_root=project_root, global_config_path=user_config)
|
|
48
|
+
app.run()
|
lazyopencode/__main__.py
ADDED
lazyopencode/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
lazyopencode/app.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Main LazyOpenCode TUI Application."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from textual.app import App, ComposeResult
|
|
10
|
+
from textual.containers import Container
|
|
11
|
+
|
|
12
|
+
from lazyopencode import __version__
|
|
13
|
+
from lazyopencode.bindings import APP_BINDINGS
|
|
14
|
+
from lazyopencode.mixins.filtering import FilteringMixin
|
|
15
|
+
from lazyopencode.mixins.help import HelpMixin
|
|
16
|
+
from lazyopencode.mixins.navigation import NavigationMixin
|
|
17
|
+
from lazyopencode.models.customization import (
|
|
18
|
+
ConfigLevel,
|
|
19
|
+
Customization,
|
|
20
|
+
CustomizationType,
|
|
21
|
+
)
|
|
22
|
+
from lazyopencode.services.discovery import ConfigDiscoveryService
|
|
23
|
+
from lazyopencode.themes import CUSTOM_THEMES
|
|
24
|
+
from lazyopencode.widgets.app_footer import AppFooter
|
|
25
|
+
from lazyopencode.widgets.combined_panel import CombinedPanel
|
|
26
|
+
from lazyopencode.widgets.detail_pane import MainPane
|
|
27
|
+
from lazyopencode.widgets.filter_input import FilterInput
|
|
28
|
+
from lazyopencode.widgets.status_panel import StatusPanel
|
|
29
|
+
from lazyopencode.widgets.type_panel import TypePanel
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
|
|
33
|
+
"""A lazygit-style TUI for visualizing OpenCode customizations."""
|
|
34
|
+
|
|
35
|
+
CSS_PATH = "styles/app.tcss"
|
|
36
|
+
LAYERS = ["default", "overlay"]
|
|
37
|
+
BINDINGS = APP_BINDINGS
|
|
38
|
+
|
|
39
|
+
TITLE = f"LazyOpenCode v{__version__}"
|
|
40
|
+
SUB_TITLE = ""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
discovery_service: ConfigDiscoveryService | None = None,
|
|
45
|
+
project_root: Path | None = None,
|
|
46
|
+
global_config_path: Path | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize LazyOpenCode application."""
|
|
49
|
+
super().__init__()
|
|
50
|
+
self.theme = "gruvbox"
|
|
51
|
+
self._discovery_service = discovery_service or ConfigDiscoveryService(
|
|
52
|
+
project_root=project_root,
|
|
53
|
+
global_config_path=global_config_path,
|
|
54
|
+
)
|
|
55
|
+
self._customizations: list[Customization] = []
|
|
56
|
+
self._level_filter: ConfigLevel | None = None
|
|
57
|
+
self._search_query: str = ""
|
|
58
|
+
self._panels: list[TypePanel | CombinedPanel] = []
|
|
59
|
+
self._status_panel: StatusPanel | None = None
|
|
60
|
+
self._main_pane: MainPane | None = None
|
|
61
|
+
self._filter_input: FilterInput | None = None
|
|
62
|
+
self._app_footer: AppFooter | None = None
|
|
63
|
+
self._last_focused_panel: TypePanel | CombinedPanel | None = None
|
|
64
|
+
|
|
65
|
+
def compose(self) -> ComposeResult:
|
|
66
|
+
"""Compose the application layout."""
|
|
67
|
+
with Container(id="sidebar"):
|
|
68
|
+
self._status_panel = StatusPanel(id="status-panel")
|
|
69
|
+
yield self._status_panel
|
|
70
|
+
|
|
71
|
+
# [1]+[2] Combined Panel: Commands, Agents
|
|
72
|
+
cp1 = CombinedPanel(
|
|
73
|
+
tabs=[
|
|
74
|
+
(CustomizationType.COMMAND, 1, "Commands"),
|
|
75
|
+
(CustomizationType.AGENT, 2, "Agents"),
|
|
76
|
+
],
|
|
77
|
+
id="panel-combined-1",
|
|
78
|
+
)
|
|
79
|
+
self._panels.append(cp1)
|
|
80
|
+
yield cp1
|
|
81
|
+
|
|
82
|
+
# [3] Type Panel: Skills
|
|
83
|
+
tp_skills = TypePanel(CustomizationType.SKILL, id="panel-skill")
|
|
84
|
+
tp_skills.panel_number = 3
|
|
85
|
+
self._panels.append(tp_skills)
|
|
86
|
+
yield tp_skills
|
|
87
|
+
|
|
88
|
+
# [4] Type Panel: Agent Memory (Rules)
|
|
89
|
+
tp_rules = TypePanel(CustomizationType.RULES, id="panel-rules")
|
|
90
|
+
tp_rules.panel_number = 4
|
|
91
|
+
self._panels.append(tp_rules)
|
|
92
|
+
yield tp_rules
|
|
93
|
+
|
|
94
|
+
# [5]+[6]+[7] Combined Panel: MCPs, Tools, Plugins
|
|
95
|
+
cp2 = CombinedPanel(
|
|
96
|
+
tabs=[
|
|
97
|
+
(CustomizationType.MCP, 5, "MCPs"),
|
|
98
|
+
(CustomizationType.TOOL, 6, "Tools"),
|
|
99
|
+
(CustomizationType.PLUGIN, 7, "Plugins"),
|
|
100
|
+
],
|
|
101
|
+
id="panel-combined-2",
|
|
102
|
+
)
|
|
103
|
+
self._panels.append(cp2)
|
|
104
|
+
yield cp2
|
|
105
|
+
|
|
106
|
+
self._main_pane = MainPane(id="main-pane")
|
|
107
|
+
yield self._main_pane
|
|
108
|
+
|
|
109
|
+
self._filter_input = FilterInput(id="filter-input")
|
|
110
|
+
yield self._filter_input
|
|
111
|
+
|
|
112
|
+
self._app_footer = AppFooter(id="app-footer")
|
|
113
|
+
yield self._app_footer
|
|
114
|
+
|
|
115
|
+
def on_mount(self) -> None:
|
|
116
|
+
"""Handle mount event - load customizations."""
|
|
117
|
+
for theme in CUSTOM_THEMES:
|
|
118
|
+
self.register_theme(theme)
|
|
119
|
+
|
|
120
|
+
self._load_customizations()
|
|
121
|
+
self._update_status_panel()
|
|
122
|
+
project_name = self._discovery_service.project_root.name
|
|
123
|
+
self.title = f"{project_name} - LazyOpenCode"
|
|
124
|
+
# Focus first non-empty panel or first panel
|
|
125
|
+
if self._panels:
|
|
126
|
+
self._panels[0].focus()
|
|
127
|
+
|
|
128
|
+
def _update_status_panel(self) -> None:
|
|
129
|
+
"""Update status panel with current config path and filter level."""
|
|
130
|
+
filter_label = self._level_filter.label if self._level_filter else "All"
|
|
131
|
+
|
|
132
|
+
if self._status_panel:
|
|
133
|
+
project_name = self._discovery_service.project_root.name
|
|
134
|
+
self._status_panel.config_path = project_name
|
|
135
|
+
self._status_panel.filter_level = filter_label
|
|
136
|
+
|
|
137
|
+
if self._app_footer:
|
|
138
|
+
self._app_footer.filter_level = filter_label
|
|
139
|
+
# Also update search active status
|
|
140
|
+
self._app_footer.search_active = bool(self._search_query)
|
|
141
|
+
|
|
142
|
+
def _load_customizations(self) -> None:
|
|
143
|
+
"""Load customizations from discovery service."""
|
|
144
|
+
self._customizations = self._discovery_service.discover_all()
|
|
145
|
+
self._update_panels()
|
|
146
|
+
|
|
147
|
+
def _update_panels(self) -> None:
|
|
148
|
+
"""Update all panels with filtered customizations."""
|
|
149
|
+
customizations = self._get_filtered_customizations()
|
|
150
|
+
for panel in self._panels:
|
|
151
|
+
panel.set_customizations(customizations)
|
|
152
|
+
|
|
153
|
+
def _get_filtered_customizations(self) -> list[Customization]:
|
|
154
|
+
"""Get customizations filtered by current level and search query."""
|
|
155
|
+
result = self._customizations
|
|
156
|
+
if self._level_filter:
|
|
157
|
+
result = [c for c in result if c.level == self._level_filter]
|
|
158
|
+
if self._search_query:
|
|
159
|
+
query = self._search_query.lower()
|
|
160
|
+
result = [c for c in result if query in c.name.lower()]
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
# Panel selection message handlers
|
|
164
|
+
|
|
165
|
+
def on_type_panel_selection_changed(
|
|
166
|
+
self, message: TypePanel.SelectionChanged
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Handle selection change in a type panel."""
|
|
169
|
+
if self._main_pane:
|
|
170
|
+
self._main_pane.customization = message.customization
|
|
171
|
+
|
|
172
|
+
def on_type_panel_drill_down(self, message: TypePanel.DrillDown) -> None:
|
|
173
|
+
"""Handle drill down into a customization."""
|
|
174
|
+
if self._main_pane:
|
|
175
|
+
self._last_focused_panel = self._get_focused_panel()
|
|
176
|
+
self._main_pane.customization = message.customization
|
|
177
|
+
self._main_pane.focus()
|
|
178
|
+
|
|
179
|
+
def on_type_panel_skill_file_selected(
|
|
180
|
+
self, message: TypePanel.SkillFileSelected
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Handle skill file selection in the skills tree."""
|
|
183
|
+
if self._main_pane:
|
|
184
|
+
self._main_pane.selected_file = message.file_path
|
|
185
|
+
|
|
186
|
+
def on_combined_panel_selection_changed(
|
|
187
|
+
self, message: CombinedPanel.SelectionChanged
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Handle selection change in the combined panel."""
|
|
190
|
+
if self._main_pane:
|
|
191
|
+
self._main_pane.customization = message.customization
|
|
192
|
+
|
|
193
|
+
def on_combined_panel_drill_down(self, message: CombinedPanel.DrillDown) -> None:
|
|
194
|
+
"""Handle drill down from the combined panel."""
|
|
195
|
+
if self._main_pane:
|
|
196
|
+
self._last_focused_panel = self._get_focused_panel()
|
|
197
|
+
self._main_pane.customization = message.customization
|
|
198
|
+
self._main_pane.focus()
|
|
199
|
+
|
|
200
|
+
# Filter input message handlers
|
|
201
|
+
|
|
202
|
+
def on_filter_input_filter_changed(
|
|
203
|
+
self, message: FilterInput.FilterChanged
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Handle filter query changes (real-time filtering)."""
|
|
206
|
+
self._search_query = message.query
|
|
207
|
+
self._last_focused_panel = None
|
|
208
|
+
if self._main_pane:
|
|
209
|
+
self._main_pane.customization = None
|
|
210
|
+
self._update_status_panel() # Updates footer search active state
|
|
211
|
+
self._update_panels()
|
|
212
|
+
|
|
213
|
+
def on_filter_input_filter_cancelled(
|
|
214
|
+
self,
|
|
215
|
+
message: FilterInput.FilterCancelled, # noqa: ARG002
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Handle filter cancellation."""
|
|
218
|
+
self._search_query = ""
|
|
219
|
+
self._last_focused_panel = None
|
|
220
|
+
if self._main_pane:
|
|
221
|
+
self._main_pane.customization = None
|
|
222
|
+
self._update_status_panel()
|
|
223
|
+
self._update_panels()
|
|
224
|
+
# Restore focus
|
|
225
|
+
if self._panels:
|
|
226
|
+
self._panels[0].focus()
|
|
227
|
+
|
|
228
|
+
def on_filter_input_filter_applied(
|
|
229
|
+
self,
|
|
230
|
+
message: FilterInput.FilterApplied, # noqa: ARG002
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Handle filter application (Enter key)."""
|
|
233
|
+
if self._filter_input:
|
|
234
|
+
self._filter_input.hide()
|
|
235
|
+
# Restore focus
|
|
236
|
+
if self._panels:
|
|
237
|
+
self._panels[0].focus()
|
|
238
|
+
|
|
239
|
+
# Navigation actions (handled by NavigationMixin)
|
|
240
|
+
|
|
241
|
+
# Filter actions (handled by FilteringMixin)
|
|
242
|
+
|
|
243
|
+
def action_search(self) -> None:
|
|
244
|
+
"""Activate search."""
|
|
245
|
+
if self._filter_input:
|
|
246
|
+
if self._filter_input.is_visible:
|
|
247
|
+
self._filter_input.hide()
|
|
248
|
+
else:
|
|
249
|
+
self._filter_input.show()
|
|
250
|
+
|
|
251
|
+
# Other actions
|
|
252
|
+
|
|
253
|
+
def action_refresh(self) -> None:
|
|
254
|
+
"""Refresh customizations from disk."""
|
|
255
|
+
self._discovery_service.refresh()
|
|
256
|
+
self._load_customizations()
|
|
257
|
+
self.notify("Refreshed", severity="information")
|
|
258
|
+
|
|
259
|
+
# action_toggle_help handled by HelpMixin
|
|
260
|
+
|
|
261
|
+
def action_open_in_editor(self) -> None:
|
|
262
|
+
"""Open currently selected customization in editor."""
|
|
263
|
+
panel = self._get_focused_panel()
|
|
264
|
+
customization = None
|
|
265
|
+
|
|
266
|
+
if panel:
|
|
267
|
+
customization = panel.selected_customization
|
|
268
|
+
|
|
269
|
+
if not customization:
|
|
270
|
+
self.notify("No customization selected", severity="warning")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
file_path = customization.path
|
|
274
|
+
if customization.type == CustomizationType.SKILL:
|
|
275
|
+
file_path = customization.path.parent
|
|
276
|
+
|
|
277
|
+
if file_path and file_path.exists():
|
|
278
|
+
self._open_path_in_editor(file_path)
|
|
279
|
+
else:
|
|
280
|
+
self.notify("File does not exist", severity="error")
|
|
281
|
+
|
|
282
|
+
def action_open_user_config(self) -> None:
|
|
283
|
+
"""Open user configuration in editor."""
|
|
284
|
+
config_path = self._discovery_service.global_config_path / "opencode.json"
|
|
285
|
+
if not config_path.exists():
|
|
286
|
+
# Fallback to the directory if file doesn't exist
|
|
287
|
+
config_path = self._discovery_service.global_config_path
|
|
288
|
+
|
|
289
|
+
self._open_path_in_editor(config_path)
|
|
290
|
+
|
|
291
|
+
def _open_path_in_editor(self, path: Path) -> None:
|
|
292
|
+
"""Helper to open a path in the system editor."""
|
|
293
|
+
editor = os.environ.get("EDITOR", "vi")
|
|
294
|
+
try:
|
|
295
|
+
cmd = shlex.split(editor) + [str(path)]
|
|
296
|
+
subprocess.Popen(cmd, shell=(sys.platform == "win32"))
|
|
297
|
+
except Exception as e:
|
|
298
|
+
self.notify(f"Error opening editor: {e}", severity="error")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def create_app(
|
|
302
|
+
project_root: Path | None = None,
|
|
303
|
+
global_config_path: Path | None = None,
|
|
304
|
+
) -> LazyOpenCode:
|
|
305
|
+
"""Create application with all dependencies wired."""
|
|
306
|
+
discovery_service = ConfigDiscoveryService(
|
|
307
|
+
project_root=project_root,
|
|
308
|
+
global_config_path=global_config_path,
|
|
309
|
+
)
|
|
310
|
+
return LazyOpenCode(discovery_service=discovery_service)
|
lazyopencode/bindings.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""App keybindings configuration."""
|
|
2
|
+
|
|
3
|
+
from textual.binding import Binding, BindingType
|
|
4
|
+
|
|
5
|
+
APP_BINDINGS: list[BindingType] = [
|
|
6
|
+
Binding("q", "quit", "Quit"),
|
|
7
|
+
Binding("?", "toggle_help", "Help"),
|
|
8
|
+
Binding("r", "refresh", "Refresh"),
|
|
9
|
+
Binding("e", "open_in_editor", "Edit"),
|
|
10
|
+
Binding("ctrl+u", "open_user_config", "User Config"),
|
|
11
|
+
Binding("tab", "focus_next_panel", "Next Panel", show=False),
|
|
12
|
+
Binding("shift+tab", "focus_previous_panel", "Prev Panel", show=False),
|
|
13
|
+
Binding("a", "filter_all", "All"),
|
|
14
|
+
Binding("g", "filter_global", "Global"),
|
|
15
|
+
Binding("p", "filter_project", "Project"),
|
|
16
|
+
Binding("/", "search", "Search"),
|
|
17
|
+
Binding("[", "prev_view", "[", show=True),
|
|
18
|
+
Binding("]", "next_view", "]", show=True),
|
|
19
|
+
Binding("0", "focus_main_pane", "Panel 0", show=False),
|
|
20
|
+
Binding("1", "focus_panel_1", "Panel 1", show=False),
|
|
21
|
+
Binding("2", "focus_panel_2", "Panel 2", show=False),
|
|
22
|
+
Binding("3", "focus_panel_3", "Panel 3", show=False),
|
|
23
|
+
Binding("4", "focus_panel_4", "Panel 4", show=False),
|
|
24
|
+
Binding("5", "focus_panel_5", "Panel 5", show=False),
|
|
25
|
+
Binding("6", "focus_panel_6", "Panel 6", show=False),
|
|
26
|
+
Binding("7", "focus_panel_7", "Panel 7", show=False),
|
|
27
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Filtering mixin for handling filter actions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from lazyopencode.app import LazyOpenCode
|
|
7
|
+
|
|
8
|
+
from lazyopencode.models.customization import ConfigLevel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FilteringMixin:
|
|
12
|
+
"""Mixin for filtering actions."""
|
|
13
|
+
|
|
14
|
+
def action_filter_all(self) -> None:
|
|
15
|
+
"""Show all customizations."""
|
|
16
|
+
app = cast("LazyOpenCode", self)
|
|
17
|
+
app._level_filter = None
|
|
18
|
+
app._update_status_panel()
|
|
19
|
+
app._update_panels()
|
|
20
|
+
|
|
21
|
+
def action_filter_global(self) -> None:
|
|
22
|
+
"""Show only global customizations."""
|
|
23
|
+
app = cast("LazyOpenCode", self)
|
|
24
|
+
app._level_filter = ConfigLevel.GLOBAL
|
|
25
|
+
app._update_status_panel()
|
|
26
|
+
app._update_panels()
|
|
27
|
+
|
|
28
|
+
def action_filter_project(self) -> None:
|
|
29
|
+
"""Show only project customizations."""
|
|
30
|
+
app = cast("LazyOpenCode", self)
|
|
31
|
+
app._level_filter = ConfigLevel.PROJECT
|
|
32
|
+
app._update_status_panel()
|
|
33
|
+
app._update_panels()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Help mixin for LazyOpenCode application."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
from textual.app import App
|
|
6
|
+
from textual.widgets import Static
|
|
7
|
+
|
|
8
|
+
from lazyopencode import __version__
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HelpMixin:
|
|
15
|
+
"""Mixin providing help overlay functionality."""
|
|
16
|
+
|
|
17
|
+
_help_visible: bool = False
|
|
18
|
+
|
|
19
|
+
def action_toggle_help(self) -> None:
|
|
20
|
+
"""Toggle help overlay visibility."""
|
|
21
|
+
if self._help_visible:
|
|
22
|
+
self._hide_help()
|
|
23
|
+
else:
|
|
24
|
+
self._show_help()
|
|
25
|
+
|
|
26
|
+
def _show_help(self) -> None:
|
|
27
|
+
"""Show help overlay."""
|
|
28
|
+
app = cast("App", self)
|
|
29
|
+
help_content = f"""[bold]LazyOpenCode v{__version__}[/]
|
|
30
|
+
|
|
31
|
+
[bold]Navigation[/]
|
|
32
|
+
j/k or Up/Down Move up/down in list
|
|
33
|
+
d/u Page down/up (detail pane)
|
|
34
|
+
g/G Go to top/bottom
|
|
35
|
+
0 Focus main pane
|
|
36
|
+
1-3 Focus panel by number
|
|
37
|
+
4-6 Focus combined panel tab
|
|
38
|
+
Tab Switch between panels
|
|
39
|
+
Enter Drill down
|
|
40
|
+
Esc Go back
|
|
41
|
+
|
|
42
|
+
[bold]Filtering[/]
|
|
43
|
+
/ Search by name/description
|
|
44
|
+
a Show all levels
|
|
45
|
+
g Show global-level only
|
|
46
|
+
p Show project-level only
|
|
47
|
+
|
|
48
|
+
[bold]Views[/]
|
|
49
|
+
[ / ] Main: content/metadata
|
|
50
|
+
Combined: switch tabs
|
|
51
|
+
|
|
52
|
+
[bold]Actions[/]
|
|
53
|
+
e Open in $EDITOR
|
|
54
|
+
ctrl+u Open User Config
|
|
55
|
+
r Refresh from disk
|
|
56
|
+
? Toggle this help
|
|
57
|
+
q Quit
|
|
58
|
+
|
|
59
|
+
[dim]Press ? or Esc to close[/]"""
|
|
60
|
+
|
|
61
|
+
if not app.query("#help-overlay"):
|
|
62
|
+
help_widget = Static(help_content, id="help-overlay")
|
|
63
|
+
app.mount(help_widget)
|
|
64
|
+
self._help_visible = True
|
|
65
|
+
|
|
66
|
+
def _hide_help(self) -> None:
|
|
67
|
+
"""Hide help overlay."""
|
|
68
|
+
app = cast("App", self)
|
|
69
|
+
try:
|
|
70
|
+
help_widget = app.query_one("#help-overlay")
|
|
71
|
+
help_widget.remove()
|
|
72
|
+
self._help_visible = False
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|