superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
"""File browser modal widget with fuzzy search and preview."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from textual import on, work
|
|
11
|
+
from textual.app import ComposeResult
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
from textual.containers import Container, Horizontal, Vertical, VerticalScroll
|
|
14
|
+
from textual.message import Message
|
|
15
|
+
from textual.reactive import reactive
|
|
16
|
+
from textual.widget import Widget
|
|
17
|
+
from textual.widgets import Button, Input, Static
|
|
18
|
+
|
|
19
|
+
from superqode.utils.fuzzy import FuzzySearch, PathFuzzySearch
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FileItem:
|
|
24
|
+
"""A file or directory item."""
|
|
25
|
+
|
|
26
|
+
path: Path
|
|
27
|
+
name: str
|
|
28
|
+
is_dir: bool
|
|
29
|
+
size: int = 0
|
|
30
|
+
extension: str = ""
|
|
31
|
+
relative_path: str = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FileListItem(Static):
|
|
35
|
+
"""A single file item in the browser list."""
|
|
36
|
+
|
|
37
|
+
DEFAULT_CSS = """
|
|
38
|
+
FileListItem {
|
|
39
|
+
height: 1;
|
|
40
|
+
padding: 0 1;
|
|
41
|
+
layout: horizontal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
FileListItem:hover {
|
|
45
|
+
background: $primary-darken-2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
FileListItem.selected {
|
|
49
|
+
background: $primary;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
FileListItem.directory {
|
|
53
|
+
color: $secondary;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
FileListItem .file-icon {
|
|
57
|
+
width: 3;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
FileListItem .file-name {
|
|
61
|
+
width: 1fr;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
FileListItem.selected .file-name {
|
|
65
|
+
color: $text;
|
|
66
|
+
text-style: bold;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
FileListItem .file-size {
|
|
70
|
+
width: 10;
|
|
71
|
+
color: $text-muted;
|
|
72
|
+
text-align: right;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
FileListItem .file-path {
|
|
76
|
+
width: 30;
|
|
77
|
+
color: $text-muted;
|
|
78
|
+
text-style: dim;
|
|
79
|
+
}
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
class Selected(Message):
|
|
83
|
+
"""Message when item is selected."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, item: FileItem) -> None:
|
|
86
|
+
self.item = item
|
|
87
|
+
super().__init__()
|
|
88
|
+
|
|
89
|
+
selected: reactive[bool] = reactive(False)
|
|
90
|
+
|
|
91
|
+
def __init__(self, item: FileItem, show_path: bool = True, **kwargs) -> None:
|
|
92
|
+
super().__init__(**kwargs)
|
|
93
|
+
self.item = item
|
|
94
|
+
self.show_path = show_path
|
|
95
|
+
|
|
96
|
+
def compose(self) -> ComposeResult:
|
|
97
|
+
# Icon
|
|
98
|
+
if self.item.is_dir:
|
|
99
|
+
icon = "📁"
|
|
100
|
+
else:
|
|
101
|
+
# File type icons
|
|
102
|
+
ext_icons = {
|
|
103
|
+
".py": "🐍",
|
|
104
|
+
".js": "📜",
|
|
105
|
+
".ts": "📘",
|
|
106
|
+
".json": "📋",
|
|
107
|
+
".yaml": "⚙️",
|
|
108
|
+
".yml": "⚙️",
|
|
109
|
+
".md": "📝",
|
|
110
|
+
".txt": "📄",
|
|
111
|
+
".html": "🌐",
|
|
112
|
+
".css": "🎨",
|
|
113
|
+
".sh": "🔧",
|
|
114
|
+
".toml": "⚙️",
|
|
115
|
+
}
|
|
116
|
+
icon = ext_icons.get(self.item.extension, "📄")
|
|
117
|
+
|
|
118
|
+
yield Static(icon, classes="file-icon")
|
|
119
|
+
yield Static(self.item.name, classes="file-name")
|
|
120
|
+
|
|
121
|
+
if self.show_path and self.item.relative_path:
|
|
122
|
+
# Show parent directory
|
|
123
|
+
parent = str(Path(self.item.relative_path).parent)
|
|
124
|
+
if parent != ".":
|
|
125
|
+
yield Static(parent, classes="file-path")
|
|
126
|
+
|
|
127
|
+
# Size for files
|
|
128
|
+
if not self.item.is_dir and self.item.size > 0:
|
|
129
|
+
size_str = self._format_size(self.item.size)
|
|
130
|
+
yield Static(size_str, classes="file-size")
|
|
131
|
+
|
|
132
|
+
def _format_size(self, size: int) -> str:
|
|
133
|
+
"""Format file size for display."""
|
|
134
|
+
if size < 1024:
|
|
135
|
+
return f"{size} B"
|
|
136
|
+
elif size < 1024 * 1024:
|
|
137
|
+
return f"{size / 1024:.1f} KB"
|
|
138
|
+
else:
|
|
139
|
+
return f"{size / (1024 * 1024):.1f} MB"
|
|
140
|
+
|
|
141
|
+
def on_mount(self) -> None:
|
|
142
|
+
self.set_class(self.item.is_dir, "directory")
|
|
143
|
+
|
|
144
|
+
def watch_selected(self, selected: bool) -> None:
|
|
145
|
+
self.set_class(selected, "selected")
|
|
146
|
+
|
|
147
|
+
def on_click(self) -> None:
|
|
148
|
+
self.post_message(self.Selected(self.item))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class FilePreview(Static):
|
|
152
|
+
"""File content preview panel."""
|
|
153
|
+
|
|
154
|
+
DEFAULT_CSS = """
|
|
155
|
+
FilePreview {
|
|
156
|
+
width: 100%;
|
|
157
|
+
height: 100%;
|
|
158
|
+
background: $surface-darken-1;
|
|
159
|
+
border: round $primary-darken-2;
|
|
160
|
+
padding: 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
FilePreview #preview-header {
|
|
164
|
+
height: 1;
|
|
165
|
+
color: $primary;
|
|
166
|
+
text-style: bold;
|
|
167
|
+
border-bottom: solid $primary-darken-2;
|
|
168
|
+
margin-bottom: 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
FilePreview #preview-content {
|
|
172
|
+
height: 1fr;
|
|
173
|
+
color: $text;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
FilePreview .preview-line {
|
|
177
|
+
height: 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
FilePreview .line-number {
|
|
181
|
+
width: 4;
|
|
182
|
+
color: $text-muted;
|
|
183
|
+
text-align: right;
|
|
184
|
+
padding-right: 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
FilePreview .line-content {
|
|
188
|
+
width: 1fr;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
FilePreview .preview-error {
|
|
192
|
+
color: $error;
|
|
193
|
+
text-style: italic;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
FilePreview .preview-binary {
|
|
197
|
+
color: $warning;
|
|
198
|
+
text-style: italic;
|
|
199
|
+
}
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, **kwargs) -> None:
|
|
203
|
+
super().__init__(**kwargs)
|
|
204
|
+
self.current_file: Path | None = None
|
|
205
|
+
|
|
206
|
+
def compose(self) -> ComposeResult:
|
|
207
|
+
yield Static("No file selected", id="preview-header")
|
|
208
|
+
yield VerticalScroll(id="preview-content")
|
|
209
|
+
|
|
210
|
+
def show_file(self, path: Path, max_lines: int = 50) -> None:
|
|
211
|
+
"""Show preview of a file."""
|
|
212
|
+
self.current_file = path
|
|
213
|
+
|
|
214
|
+
header = self.query_one("#preview-header", Static)
|
|
215
|
+
content = self.query_one("#preview-content", VerticalScroll)
|
|
216
|
+
content.remove_children()
|
|
217
|
+
|
|
218
|
+
if not path.exists():
|
|
219
|
+
header.update(f"File not found: {path.name}")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
if path.is_dir():
|
|
223
|
+
header.update(f"📁 {path.name}/")
|
|
224
|
+
# Show directory contents
|
|
225
|
+
try:
|
|
226
|
+
items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
227
|
+
for item in items[:20]:
|
|
228
|
+
icon = "📁" if item.is_dir() else "📄"
|
|
229
|
+
content.mount(Static(f"{icon} {item.name}"))
|
|
230
|
+
if len(list(path.iterdir())) > 20:
|
|
231
|
+
content.mount(Static(f"... and more", classes="preview-line"))
|
|
232
|
+
except PermissionError:
|
|
233
|
+
content.mount(Static("Permission denied", classes="preview-error"))
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
header.update(f"📄 {path.name}")
|
|
237
|
+
|
|
238
|
+
# Check if file is binary
|
|
239
|
+
try:
|
|
240
|
+
with open(path, "rb") as f:
|
|
241
|
+
chunk = f.read(1024)
|
|
242
|
+
if b"\x00" in chunk:
|
|
243
|
+
content.mount(
|
|
244
|
+
Static("Binary file - preview not available", classes="preview-binary")
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
except Exception as e:
|
|
248
|
+
content.mount(Static(f"Error reading file: {e}", classes="preview-error"))
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Read and display text content
|
|
252
|
+
try:
|
|
253
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
254
|
+
lines = f.readlines()
|
|
255
|
+
|
|
256
|
+
for i, line in enumerate(lines[:max_lines], 1):
|
|
257
|
+
line = line.rstrip("\n\r")
|
|
258
|
+
if len(line) > 80:
|
|
259
|
+
line = line[:77] + "..."
|
|
260
|
+
# Escape Rich markup
|
|
261
|
+
line = line.replace("[", r"\[")
|
|
262
|
+
with Horizontal(classes="preview-line"):
|
|
263
|
+
content.mount(Static(f"{i:3}", classes="line-number"))
|
|
264
|
+
content.mount(Static(line, classes="line-content"))
|
|
265
|
+
|
|
266
|
+
if len(lines) > max_lines:
|
|
267
|
+
content.mount(
|
|
268
|
+
Static(f"... {len(lines) - max_lines} more lines", classes="preview-line")
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
content.mount(Static(f"Error: {e}", classes="preview-error"))
|
|
273
|
+
|
|
274
|
+
def clear(self) -> None:
|
|
275
|
+
"""Clear the preview."""
|
|
276
|
+
self.current_file = None
|
|
277
|
+
header = self.query_one("#preview-header", Static)
|
|
278
|
+
header.update("No file selected")
|
|
279
|
+
content = self.query_one("#preview-content", VerticalScroll)
|
|
280
|
+
content.remove_children()
|
|
281
|
+
|
|
282
|
+
class FileBrowser(Widget):
|
|
283
|
+
"""
|
|
284
|
+
Interactive file browser modal with fuzzy search.
|
|
285
|
+
|
|
286
|
+
Features:
|
|
287
|
+
- Fuzzy file search
|
|
288
|
+
- Directory navigation
|
|
289
|
+
- File preview
|
|
290
|
+
- Keyboard navigation
|
|
291
|
+
- Bookmarks and recent files
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
is_visible: reactive[bool] = reactive(False)
|
|
295
|
+
|
|
296
|
+
DEFAULT_CSS = """
|
|
297
|
+
FileBrowser {
|
|
298
|
+
layer: overlay;
|
|
299
|
+
align: center middle;
|
|
300
|
+
width: 90%;
|
|
301
|
+
height: 85%;
|
|
302
|
+
max-width: 120;
|
|
303
|
+
max-height: 40;
|
|
304
|
+
background: $surface;
|
|
305
|
+
border: tall $primary;
|
|
306
|
+
display: none;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
FileBrowser.visible {
|
|
310
|
+
display: block;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
FileBrowser #browser-header {
|
|
314
|
+
dock: top;
|
|
315
|
+
height: 3;
|
|
316
|
+
background: $primary-darken-2;
|
|
317
|
+
padding: 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
FileBrowser #browser-title {
|
|
321
|
+
text-style: bold;
|
|
322
|
+
color: $secondary;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
FileBrowser #browser-path {
|
|
326
|
+
color: $text-muted;
|
|
327
|
+
text-style: dim;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
FileBrowser #search-container {
|
|
331
|
+
dock: top;
|
|
332
|
+
height: 3;
|
|
333
|
+
padding: 1;
|
|
334
|
+
background: $surface-darken-1;
|
|
335
|
+
layout: horizontal;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
FileBrowser #search-input {
|
|
339
|
+
width: 1fr;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
FileBrowser #browser-main {
|
|
343
|
+
height: 1fr;
|
|
344
|
+
layout: horizontal;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
FileBrowser #file-list-container {
|
|
348
|
+
width: 1fr;
|
|
349
|
+
height: 100%;
|
|
350
|
+
border-right: solid $primary-darken-2;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
FileBrowser #file-list {
|
|
354
|
+
height: 100%;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
FileBrowser #preview-container {
|
|
358
|
+
width: 45%;
|
|
359
|
+
height: 100%;
|
|
360
|
+
padding: 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
FileBrowser #browser-footer {
|
|
364
|
+
dock: bottom;
|
|
365
|
+
height: 1;
|
|
366
|
+
background: $surface-darken-1;
|
|
367
|
+
color: $text-muted;
|
|
368
|
+
padding: 0 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
FileBrowser .empty-message {
|
|
372
|
+
padding: 2;
|
|
373
|
+
color: $text-muted;
|
|
374
|
+
text-style: italic;
|
|
375
|
+
text-align: center;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
FileBrowser #quick-actions {
|
|
379
|
+
dock: top;
|
|
380
|
+
height: 2;
|
|
381
|
+
padding: 0 1;
|
|
382
|
+
layout: horizontal;
|
|
383
|
+
background: $surface-darken-1;
|
|
384
|
+
border-bottom: solid $primary-darken-2;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
FileBrowser #quick-actions Button {
|
|
388
|
+
margin-right: 1;
|
|
389
|
+
min-width: 8;
|
|
390
|
+
}
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
BINDINGS = [
|
|
394
|
+
Binding("escape", "close", "Close"),
|
|
395
|
+
Binding("enter", "select", "Select"),
|
|
396
|
+
Binding("up", "move_up", "Up"),
|
|
397
|
+
Binding("down", "move_down", "Down"),
|
|
398
|
+
Binding("ctrl+u", "go_up", "Parent Dir"),
|
|
399
|
+
Binding("ctrl+h", "go_home", "Home"),
|
|
400
|
+
Binding("ctrl+b", "toggle_bookmarks", "Bookmarks"),
|
|
401
|
+
Binding("ctrl+r", "toggle_recent", "Recent"),
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
class FileSelected(Message):
|
|
405
|
+
"""Message when a file is selected."""
|
|
406
|
+
|
|
407
|
+
def __init__(self, path: Path) -> None:
|
|
408
|
+
self.path = path
|
|
409
|
+
super().__init__()
|
|
410
|
+
|
|
411
|
+
class Closed(Message):
|
|
412
|
+
"""Message when browser is closed."""
|
|
413
|
+
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
# State
|
|
417
|
+
visible: reactive[bool] = reactive(False)
|
|
418
|
+
selected_index: reactive[int] = reactive(0)
|
|
419
|
+
|
|
420
|
+
def __init__(
|
|
421
|
+
self,
|
|
422
|
+
root_path: Path | None = None,
|
|
423
|
+
on_select: Callable[[Path], None] | None = None,
|
|
424
|
+
**kwargs,
|
|
425
|
+
) -> None:
|
|
426
|
+
super().__init__(**kwargs)
|
|
427
|
+
self.root_path = root_path or Path.cwd()
|
|
428
|
+
self.current_path = self.root_path
|
|
429
|
+
self._on_select = on_select
|
|
430
|
+
self._items: list[FileItem] = []
|
|
431
|
+
self._filtered_items: list[FileItem] = []
|
|
432
|
+
self._search_query = ""
|
|
433
|
+
self._show_bookmarks = False
|
|
434
|
+
self._show_recent = False
|
|
435
|
+
self.fuzzy = PathFuzzySearch()
|
|
436
|
+
self._render_counter = 0 # Unique ID counter to prevent duplicates
|
|
437
|
+
|
|
438
|
+
def compose(self) -> ComposeResult:
|
|
439
|
+
# Header
|
|
440
|
+
with Vertical(id="browser-header"):
|
|
441
|
+
yield Static("📁 File Browser", id="browser-title")
|
|
442
|
+
yield Static(str(self.current_path), id="browser-path")
|
|
443
|
+
|
|
444
|
+
# Quick actions
|
|
445
|
+
with Horizontal(id="quick-actions"):
|
|
446
|
+
yield Button("⬆ Parent", id="btn-parent")
|
|
447
|
+
yield Button("🏠 Home", id="btn-home")
|
|
448
|
+
yield Button("🔖 Bookmarks", id="btn-bookmarks")
|
|
449
|
+
yield Button("📋 Recent", id="btn-recent")
|
|
450
|
+
|
|
451
|
+
# Search
|
|
452
|
+
with Horizontal(id="search-container"):
|
|
453
|
+
yield Input(placeholder="Search files... (fuzzy matching)", id="search-input")
|
|
454
|
+
|
|
455
|
+
# Main content
|
|
456
|
+
with Horizontal(id="browser-main"):
|
|
457
|
+
with Container(id="file-list-container"):
|
|
458
|
+
yield VerticalScroll(id="file-list")
|
|
459
|
+
with Container(id="preview-container"):
|
|
460
|
+
yield FilePreview(id="file-preview")
|
|
461
|
+
|
|
462
|
+
# Footer
|
|
463
|
+
yield Static(
|
|
464
|
+
"↑↓ Navigate │ Enter Select │ Ctrl+U Parent │ Esc Close",
|
|
465
|
+
id="browser-footer",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def show(self, path: Path | None = None) -> None:
|
|
469
|
+
"""Show the file browser."""
|
|
470
|
+
if path:
|
|
471
|
+
self.current_path = path
|
|
472
|
+
self.is_visible = True
|
|
473
|
+
self.add_class("visible")
|
|
474
|
+
self._load_directory()
|
|
475
|
+
|
|
476
|
+
# Focus search
|
|
477
|
+
self.query_one("#search-input", Input).focus()
|
|
478
|
+
|
|
479
|
+
def hide(self) -> None:
|
|
480
|
+
"""Hide file browser."""
|
|
481
|
+
self.is_visible = False
|
|
482
|
+
self.remove_class("visible")
|
|
483
|
+
self.post_message(self.Closed())
|
|
484
|
+
|
|
485
|
+
def _load_directory(self) -> None:
|
|
486
|
+
"""Load the current directory contents."""
|
|
487
|
+
self._items = []
|
|
488
|
+
self._show_bookmarks = False
|
|
489
|
+
self._show_recent = False
|
|
490
|
+
|
|
491
|
+
# Update path display
|
|
492
|
+
path_display = self.query_one("#browser-path", Static)
|
|
493
|
+
path_display.update(str(self.current_path))
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
# Get directory contents
|
|
497
|
+
entries = sorted(
|
|
498
|
+
self.current_path.iterdir(),
|
|
499
|
+
key=lambda x: (not x.is_dir(), x.name.lower()),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Filter out hidden and ignored files
|
|
503
|
+
from superqode.file_explorer import PathFilter
|
|
504
|
+
|
|
505
|
+
path_filter = PathFilter.from_git_root(self.root_path)
|
|
506
|
+
|
|
507
|
+
for entry in entries:
|
|
508
|
+
# Skip hidden files (starting with .)
|
|
509
|
+
if entry.name.startswith("."):
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
# Skip ignored files
|
|
513
|
+
try:
|
|
514
|
+
rel_path = entry.relative_to(self.root_path)
|
|
515
|
+
if path_filter.match(rel_path):
|
|
516
|
+
continue
|
|
517
|
+
except ValueError:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
size = entry.stat().st_size if entry.is_file() else 0
|
|
522
|
+
except OSError:
|
|
523
|
+
size = 0
|
|
524
|
+
|
|
525
|
+
self._items.append(
|
|
526
|
+
FileItem(
|
|
527
|
+
path=entry,
|
|
528
|
+
name=entry.name,
|
|
529
|
+
is_dir=entry.is_dir(),
|
|
530
|
+
size=size,
|
|
531
|
+
extension=entry.suffix.lower() if entry.is_file() else "",
|
|
532
|
+
relative_path=str(entry.relative_to(self.root_path)),
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
except PermissionError:
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
self._apply_filter()
|
|
540
|
+
|
|
541
|
+
def _load_bookmarks(self) -> None:
|
|
542
|
+
"""Load bookmarked files."""
|
|
543
|
+
from superqode.file_explorer import Bookmarks
|
|
544
|
+
|
|
545
|
+
self._items = []
|
|
546
|
+
self._show_bookmarks = True
|
|
547
|
+
self._show_recent = False
|
|
548
|
+
|
|
549
|
+
path_display = self.query_one("#browser-path", Static)
|
|
550
|
+
path_display.update("🔖 Bookmarks")
|
|
551
|
+
|
|
552
|
+
bookmarks = Bookmarks()
|
|
553
|
+
for name, path in bookmarks.get_bookmarks().items():
|
|
554
|
+
if path.exists():
|
|
555
|
+
try:
|
|
556
|
+
size = path.stat().st_size if path.is_file() else 0
|
|
557
|
+
except OSError:
|
|
558
|
+
size = 0
|
|
559
|
+
|
|
560
|
+
self._items.append(
|
|
561
|
+
FileItem(
|
|
562
|
+
path=path,
|
|
563
|
+
name=f"{name} → {path.name}",
|
|
564
|
+
is_dir=path.is_dir(),
|
|
565
|
+
size=size,
|
|
566
|
+
extension=path.suffix.lower() if path.is_file() else "",
|
|
567
|
+
relative_path=str(path),
|
|
568
|
+
)
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
self._apply_filter()
|
|
572
|
+
|
|
573
|
+
def _load_recent(self) -> None:
|
|
574
|
+
"""Load recent files."""
|
|
575
|
+
from superqode.file_explorer import RecentFiles
|
|
576
|
+
|
|
577
|
+
self._items = []
|
|
578
|
+
self._show_bookmarks = False
|
|
579
|
+
self._show_recent = True
|
|
580
|
+
|
|
581
|
+
path_display = self.query_one("#browser-path", Static)
|
|
582
|
+
path_display.update("📋 Recent Files")
|
|
583
|
+
|
|
584
|
+
recent = RecentFiles()
|
|
585
|
+
for path in recent.get_recent_files(limit=20):
|
|
586
|
+
if path.exists():
|
|
587
|
+
try:
|
|
588
|
+
size = path.stat().st_size if path.is_file() else 0
|
|
589
|
+
except OSError:
|
|
590
|
+
size = 0
|
|
591
|
+
|
|
592
|
+
self._items.append(
|
|
593
|
+
FileItem(
|
|
594
|
+
path=path,
|
|
595
|
+
name=path.name,
|
|
596
|
+
is_dir=path.is_dir(),
|
|
597
|
+
size=size,
|
|
598
|
+
extension=path.suffix.lower() if path.is_file() else "",
|
|
599
|
+
relative_path=str(path),
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
self._apply_filter()
|
|
604
|
+
|
|
605
|
+
def _apply_filter(self) -> None:
|
|
606
|
+
"""Apply search filter to items."""
|
|
607
|
+
if self._search_query:
|
|
608
|
+
# Fuzzy search
|
|
609
|
+
items = [(item.name, item) for item in self._items]
|
|
610
|
+
results = self.fuzzy.search_with_data(self._search_query, items, max_results=50)
|
|
611
|
+
self._filtered_items = [item for _, item in results]
|
|
612
|
+
else:
|
|
613
|
+
self._filtered_items = self._items
|
|
614
|
+
|
|
615
|
+
self.selected_index = 0
|
|
616
|
+
self._render_items()
|
|
617
|
+
|
|
618
|
+
def _render_items(self) -> None:
|
|
619
|
+
"""Render the file list."""
|
|
620
|
+
self._render_counter += 1
|
|
621
|
+
render_id = self._render_counter
|
|
622
|
+
|
|
623
|
+
file_list = self.query_one("#file-list", VerticalScroll)
|
|
624
|
+
file_list.remove_children()
|
|
625
|
+
|
|
626
|
+
if not self._filtered_items:
|
|
627
|
+
file_list.mount(
|
|
628
|
+
Static(
|
|
629
|
+
"No files found.\nTry a different search.",
|
|
630
|
+
classes="empty-message",
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
show_path = self._show_bookmarks or self._show_recent or bool(self._search_query)
|
|
636
|
+
|
|
637
|
+
for i, item in enumerate(self._filtered_items):
|
|
638
|
+
# Use render counter in ID to ensure uniqueness across renders
|
|
639
|
+
list_item = FileListItem(item, show_path=show_path, id=f"file-{render_id}-{i}")
|
|
640
|
+
list_item.selected = i == self.selected_index
|
|
641
|
+
file_list.mount(list_item)
|
|
642
|
+
|
|
643
|
+
# Update preview
|
|
644
|
+
self._update_preview()
|
|
645
|
+
|
|
646
|
+
def _update_selection(self) -> None:
|
|
647
|
+
"""Update visual selection state."""
|
|
648
|
+
for i, item in enumerate(self.query("#file-list FileListItem")):
|
|
649
|
+
if isinstance(item, FileListItem):
|
|
650
|
+
item.selected = i == self.selected_index
|
|
651
|
+
|
|
652
|
+
self._update_preview()
|
|
653
|
+
|
|
654
|
+
def _update_preview(self) -> None:
|
|
655
|
+
"""Update the file preview."""
|
|
656
|
+
preview = self.query_one("#file-preview", FilePreview)
|
|
657
|
+
|
|
658
|
+
if self._filtered_items and 0 <= self.selected_index < len(self._filtered_items):
|
|
659
|
+
item = self._filtered_items[self.selected_index]
|
|
660
|
+
preview.show_file(item.path)
|
|
661
|
+
else:
|
|
662
|
+
preview.clear()
|
|
663
|
+
|
|
664
|
+
# === Actions ===
|
|
665
|
+
|
|
666
|
+
def action_close(self) -> None:
|
|
667
|
+
"""Close the browser."""
|
|
668
|
+
self.hide()
|
|
669
|
+
|
|
670
|
+
def action_select(self) -> None:
|
|
671
|
+
"""Select the current item."""
|
|
672
|
+
if not self._filtered_items:
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
if 0 <= self.selected_index < len(self._filtered_items):
|
|
676
|
+
item = self._filtered_items[self.selected_index]
|
|
677
|
+
|
|
678
|
+
if item.is_dir:
|
|
679
|
+
# Navigate into directory
|
|
680
|
+
self.current_path = item.path
|
|
681
|
+
self._search_query = ""
|
|
682
|
+
self.query_one("#search-input", Input).value = ""
|
|
683
|
+
self._load_directory()
|
|
684
|
+
else:
|
|
685
|
+
# Select file
|
|
686
|
+
self.post_message(self.FileSelected(item.path))
|
|
687
|
+
if self._on_select:
|
|
688
|
+
self._on_select(item.path)
|
|
689
|
+
self.hide()
|
|
690
|
+
|
|
691
|
+
def action_move_up(self) -> None:
|
|
692
|
+
"""Move selection up."""
|
|
693
|
+
if self._filtered_items and self.selected_index > 0:
|
|
694
|
+
self.selected_index -= 1
|
|
695
|
+
self._update_selection()
|
|
696
|
+
|
|
697
|
+
def action_move_down(self) -> None:
|
|
698
|
+
"""Move selection down."""
|
|
699
|
+
if self._filtered_items and self.selected_index < len(self._filtered_items) - 1:
|
|
700
|
+
self.selected_index += 1
|
|
701
|
+
self._update_selection()
|
|
702
|
+
|
|
703
|
+
def action_go_up(self) -> None:
|
|
704
|
+
"""Go to parent directory."""
|
|
705
|
+
if self.current_path != self.root_path:
|
|
706
|
+
self.current_path = self.current_path.parent
|
|
707
|
+
self._search_query = ""
|
|
708
|
+
self.query_one("#search-input", Input).value = ""
|
|
709
|
+
self._load_directory()
|
|
710
|
+
|
|
711
|
+
def action_go_home(self) -> None:
|
|
712
|
+
"""Go to root directory."""
|
|
713
|
+
self.current_path = self.root_path
|
|
714
|
+
self._search_query = ""
|
|
715
|
+
self.query_one("#search-input", Input).value = ""
|
|
716
|
+
self._load_directory()
|
|
717
|
+
|
|
718
|
+
def action_toggle_bookmarks(self) -> None:
|
|
719
|
+
"""Toggle bookmarks view."""
|
|
720
|
+
if self._show_bookmarks:
|
|
721
|
+
self._load_directory()
|
|
722
|
+
else:
|
|
723
|
+
self._load_bookmarks()
|
|
724
|
+
|
|
725
|
+
def action_toggle_recent(self) -> None:
|
|
726
|
+
"""Toggle recent files view."""
|
|
727
|
+
if self._show_recent:
|
|
728
|
+
self._load_directory()
|
|
729
|
+
else:
|
|
730
|
+
self._load_recent()
|
|
731
|
+
|
|
732
|
+
# === Event handlers ===
|
|
733
|
+
|
|
734
|
+
@on(Input.Changed, "#search-input")
|
|
735
|
+
def on_search_changed(self, event: Input.Changed) -> None:
|
|
736
|
+
"""Handle search input changes."""
|
|
737
|
+
self._search_query = event.value
|
|
738
|
+
|
|
739
|
+
if self._show_bookmarks or self._show_recent:
|
|
740
|
+
# Just filter current list
|
|
741
|
+
self._apply_filter()
|
|
742
|
+
elif event.value:
|
|
743
|
+
# Do project-wide fuzzy search
|
|
744
|
+
self._search_project(event.value)
|
|
745
|
+
else:
|
|
746
|
+
# Show current directory
|
|
747
|
+
self._load_directory()
|
|
748
|
+
|
|
749
|
+
@work(exclusive=True)
|
|
750
|
+
async def _search_project(self, query: str) -> None:
|
|
751
|
+
"""Search files across the project."""
|
|
752
|
+
import asyncio
|
|
753
|
+
|
|
754
|
+
def do_search():
|
|
755
|
+
from superqode.file_explorer import fuzzy_find_files
|
|
756
|
+
|
|
757
|
+
results = fuzzy_find_files(query, max_results=50)
|
|
758
|
+
items = []
|
|
759
|
+
for path, rel_path, score in results:
|
|
760
|
+
try:
|
|
761
|
+
size = path.stat().st_size if path.is_file() else 0
|
|
762
|
+
except OSError:
|
|
763
|
+
size = 0
|
|
764
|
+
|
|
765
|
+
items.append(
|
|
766
|
+
FileItem(
|
|
767
|
+
path=path,
|
|
768
|
+
name=path.name,
|
|
769
|
+
is_dir=path.is_dir(),
|
|
770
|
+
size=size,
|
|
771
|
+
extension=path.suffix.lower() if path.is_file() else "",
|
|
772
|
+
relative_path=rel_path,
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
return items
|
|
776
|
+
|
|
777
|
+
self._items = await asyncio.to_thread(do_search)
|
|
778
|
+
self._filtered_items = self._items
|
|
779
|
+
self.selected_index = 0
|
|
780
|
+
self._render_items()
|
|
781
|
+
|
|
782
|
+
@on(Button.Pressed, "#btn-parent")
|
|
783
|
+
def on_parent_pressed(self, event: Button.Pressed) -> None:
|
|
784
|
+
self.action_go_up()
|
|
785
|
+
|
|
786
|
+
@on(Button.Pressed, "#btn-home")
|
|
787
|
+
def on_home_pressed(self, event: Button.Pressed) -> None:
|
|
788
|
+
self.action_go_home()
|
|
789
|
+
|
|
790
|
+
@on(Button.Pressed, "#btn-bookmarks")
|
|
791
|
+
def on_bookmarks_pressed(self, event: Button.Pressed) -> None:
|
|
792
|
+
self.action_toggle_bookmarks()
|
|
793
|
+
|
|
794
|
+
@on(Button.Pressed, "#btn-recent")
|
|
795
|
+
def on_recent_pressed(self, event: Button.Pressed) -> None:
|
|
796
|
+
self.action_toggle_recent()
|
|
797
|
+
|
|
798
|
+
@on(FileListItem.Selected)
|
|
799
|
+
def on_item_selected(self, event: FileListItem.Selected) -> None:
|
|
800
|
+
"""Handle item click."""
|
|
801
|
+
# Find index
|
|
802
|
+
for i, item in enumerate(self._filtered_items):
|
|
803
|
+
if item.path == event.item.path:
|
|
804
|
+
self.selected_index = i
|
|
805
|
+
self._update_selection()
|
|
806
|
+
break
|
|
807
|
+
|
|
808
|
+
# Double-click behavior (select on click)
|
|
809
|
+
self.action_select()
|