nexus-tui 0.1.4__py3-none-any.whl → 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.
- nexus/__init__.py +1 -0
- nexus/app.py +106 -0
- nexus/config.py +197 -0
- nexus/container.py +45 -0
- nexus/logger.py +48 -0
- nexus/models.py +43 -0
- nexus/screens/__init__.py +0 -0
- nexus/screens/create_project.py +115 -0
- nexus/screens/error.py +57 -0
- nexus/screens/help.py +62 -0
- nexus/screens/project_picker.py +250 -0
- nexus/screens/theme_picker.py +101 -0
- nexus/screens/tool_selector.py +482 -0
- nexus/services/__init__.py +0 -0
- nexus/services/executor.py +46 -0
- nexus/services/scanner.py +46 -0
- nexus/state.py +84 -0
- nexus/style.tcss +489 -0
- nexus/widgets/__init__.py +0 -0
- nexus/widgets/tool_list_item.py +194 -0
- {nexus_tui-0.1.4.dist-info → nexus_tui-0.1.5.dist-info}/METADATA +1 -1
- nexus_tui-0.1.5.dist-info/RECORD +26 -0
- nexus_tui-0.1.4.dist-info/RECORD +0 -6
- {nexus_tui-0.1.4.dist-info → nexus_tui-0.1.5.dist-info}/WHEEL +0 -0
- {nexus_tui-0.1.4.dist-info → nexus_tui-0.1.5.dist-info}/entry_points.txt +0 -0
- {nexus_tui-0.1.4.dist-info → nexus_tui-0.1.5.dist-info}/licenses/LICENSE +0 -0
nexus/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Nexus TUI Application package."""
|
nexus/app.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Main application entry point for Nexus.
|
|
2
|
+
|
|
3
|
+
Configures the Textual application class, global bindings, and initial screen loading.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
from textual.app import App
|
|
8
|
+
from textual.notifications import SeverityLevel
|
|
9
|
+
from nexus.container import get_container
|
|
10
|
+
from nexus.screens.tool_selector import ToolSelector
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NexusApp(App[None]):
|
|
14
|
+
"""The main Nexus application class.
|
|
15
|
+
|
|
16
|
+
Manages the application lifecycle, global bindings, and screen navigation.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
CSS_PATH = "style.tcss"
|
|
20
|
+
BINDINGS = [
|
|
21
|
+
("ctrl+q", "quit", "Quit"),
|
|
22
|
+
("ctrl+c", "quit", "Quit"),
|
|
23
|
+
("ctrl+b", "back", "Back"),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def on_mount(self) -> None:
|
|
27
|
+
"""Called when the application is mounted.
|
|
28
|
+
|
|
29
|
+
Push the ToolSelector screen to the stack on startup.
|
|
30
|
+
"""
|
|
31
|
+
# Initialize services
|
|
32
|
+
self.container = get_container()
|
|
33
|
+
|
|
34
|
+
# Apply keybindings from config
|
|
35
|
+
self._apply_bindings()
|
|
36
|
+
|
|
37
|
+
self.push_screen(ToolSelector())
|
|
38
|
+
|
|
39
|
+
def _apply_bindings(self) -> None:
|
|
40
|
+
"""Applies configurable keybindings."""
|
|
41
|
+
from nexus.config import get_keybindings
|
|
42
|
+
bindings = get_keybindings()
|
|
43
|
+
|
|
44
|
+
if "quit" in bindings:
|
|
45
|
+
self.bind(keys=bindings["quit"], action="quit", description="Quit")
|
|
46
|
+
if "force_quit" in bindings:
|
|
47
|
+
self.bind(keys=bindings["force_quit"], action="quit", description="Quit")
|
|
48
|
+
if "back" in bindings:
|
|
49
|
+
self.bind(keys=bindings["back"], action="back", description="Back")
|
|
50
|
+
|
|
51
|
+
async def action_back(self) -> None:
|
|
52
|
+
"""Navigates back to the previous screen.
|
|
53
|
+
|
|
54
|
+
Removes the current screen from the stack if there is more than one
|
|
55
|
+
screen present.
|
|
56
|
+
"""
|
|
57
|
+
if len(self.screen_stack) > 1:
|
|
58
|
+
self.pop_screen()
|
|
59
|
+
|
|
60
|
+
def notify(
|
|
61
|
+
self,
|
|
62
|
+
message: str,
|
|
63
|
+
*,
|
|
64
|
+
title: str = "",
|
|
65
|
+
severity: SeverityLevel = "information",
|
|
66
|
+
timeout: float | None = 1.0,
|
|
67
|
+
**kwargs: Any,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Override notify to use a shorter default timeout.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
message: The message to display.
|
|
73
|
+
title: The title of the notification.
|
|
74
|
+
severity: The severity of the notification (e.g., 'information', 'error').
|
|
75
|
+
timeout: Duration in seconds to show the notification.
|
|
76
|
+
**kwargs: Additional keyword arguments passed to the parent notify method.
|
|
77
|
+
"""
|
|
78
|
+
super().notify(
|
|
79
|
+
message, title=title, severity=severity, timeout=timeout, **kwargs
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def show_error(self, title: str, message: str, details: str = "") -> None:
|
|
83
|
+
"""Displays a modal error screen.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
title: The title of the error.
|
|
87
|
+
message: The user-friendly error message.
|
|
88
|
+
details: Optional technical details.
|
|
89
|
+
"""
|
|
90
|
+
from nexus.screens.error import ErrorScreen
|
|
91
|
+
|
|
92
|
+
self.push_screen(ErrorScreen(title, message, details))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> None:
|
|
96
|
+
"""Entry point for the application."""
|
|
97
|
+
from nexus.logger import configure_logging
|
|
98
|
+
|
|
99
|
+
configure_logging()
|
|
100
|
+
app = NexusApp()
|
|
101
|
+
app.run()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
106
|
+
|
nexus/config.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Configuration management for the Nexus application.
|
|
2
|
+
|
|
3
|
+
Handles loading tool definitions from TOML, determining terminal preferences,
|
|
4
|
+
and defining visual assets like colors and icons.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import platformdirs
|
|
14
|
+
from nexus.models import Tool
|
|
15
|
+
|
|
16
|
+
# Configuration Paths in priority order (lowest to highest)
|
|
17
|
+
CWD_NEXUS_CONFIG = Path.cwd() / "nexus" / "tools.local.toml"
|
|
18
|
+
CWD_CONFIG = Path.cwd() / "tools.local.toml"
|
|
19
|
+
USER_CONFIG_PATH = Path(platformdirs.user_config_dir("nexus", roaming=True)) / "tools.toml"
|
|
20
|
+
LOCAL_CONFIG_PATH = Path(__file__).parent / "tools.local.toml"
|
|
21
|
+
DEFAULT_CONFIG_PATH = Path(__file__).parent / "tools.toml"
|
|
22
|
+
|
|
23
|
+
CONFIG_PATHS = [
|
|
24
|
+
DEFAULT_CONFIG_PATH,
|
|
25
|
+
LOCAL_CONFIG_PATH,
|
|
26
|
+
USER_CONFIG_PATH,
|
|
27
|
+
CWD_NEXUS_CONFIG,
|
|
28
|
+
CWD_CONFIG,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Global error tracking for configuration loading
|
|
33
|
+
CONFIG_ERRORS: list[str] = []
|
|
34
|
+
_CONFIG_CACHE: dict[str, Any] | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_config_data() -> dict[str, Any]:
|
|
38
|
+
"""Loads and merges configuration data from all sources (Lazy).
|
|
39
|
+
|
|
40
|
+
Iterates through configuration paths in priority order and merges meaningful
|
|
41
|
+
data (tools, project root) into a single dictionary.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A dictionary containing the merged configuration data.
|
|
45
|
+
"""
|
|
46
|
+
global _CONFIG_CACHE
|
|
47
|
+
if _CONFIG_CACHE is not None:
|
|
48
|
+
return _CONFIG_CACHE
|
|
49
|
+
|
|
50
|
+
merged_data: dict[str, Any] = {"tool": [], "project_root": None}
|
|
51
|
+
|
|
52
|
+
def merge_from_file(path: Path) -> None:
|
|
53
|
+
if path.exists():
|
|
54
|
+
try:
|
|
55
|
+
with open(path, "rb") as f:
|
|
56
|
+
data = tomllib.load(f)
|
|
57
|
+
|
|
58
|
+
# Replacement strategy: If a config file defines tools,
|
|
59
|
+
# it replaces the set of tools from lower-priority configs.
|
|
60
|
+
if "tool" in data and data["tool"]:
|
|
61
|
+
merged_data["tool"] = data["tool"]
|
|
62
|
+
|
|
63
|
+
if "project_root" in data:
|
|
64
|
+
merged_data["project_root"] = data["project_root"]
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
CONFIG_ERRORS.append(f"Error in {path.name}: {e}")
|
|
68
|
+
|
|
69
|
+
for path in CONFIG_PATHS:
|
|
70
|
+
merge_from_file(path)
|
|
71
|
+
|
|
72
|
+
_CONFIG_CACHE = merged_data
|
|
73
|
+
return merged_data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_project_root() -> Path:
|
|
77
|
+
"""Returns the project root directory.
|
|
78
|
+
|
|
79
|
+
Priority:
|
|
80
|
+
1. NEXUS_PROJECT_ROOT environment variable
|
|
81
|
+
2. 'project_root' in configuration files
|
|
82
|
+
3. Default: ~/Projects
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The defined project root path.
|
|
86
|
+
"""
|
|
87
|
+
# 1. Environment Variable
|
|
88
|
+
env_root = os.environ.get("NEXUS_PROJECT_ROOT")
|
|
89
|
+
if env_root:
|
|
90
|
+
return Path(env_root).expanduser()
|
|
91
|
+
|
|
92
|
+
# 2. Configuration File
|
|
93
|
+
config = _load_config_data()
|
|
94
|
+
if config_root := config.get("project_root"):
|
|
95
|
+
path_str = str(config_root)
|
|
96
|
+
if path_str.startswith("~"):
|
|
97
|
+
return Path(path_str).expanduser()
|
|
98
|
+
return Path(path_str)
|
|
99
|
+
|
|
100
|
+
# 3. Default
|
|
101
|
+
return Path.home() / "Projects"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_tools() -> list[Tool]:
|
|
105
|
+
"""Returns the list of configured tools.
|
|
106
|
+
|
|
107
|
+
Parses the configuration data into Tool models, skipping any invalid entries.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A list of Tool objects.
|
|
111
|
+
"""
|
|
112
|
+
tools = []
|
|
113
|
+
config = _load_config_data()
|
|
114
|
+
for t in config.get("tool", []):
|
|
115
|
+
try:
|
|
116
|
+
tools.append(Tool(**t))
|
|
117
|
+
except Exception as e:
|
|
118
|
+
CONFIG_ERRORS.append(f"Invalid tool definition: {e}")
|
|
119
|
+
continue
|
|
120
|
+
return tools
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_keybindings() -> dict[str, str]:
|
|
124
|
+
"""Returns the keybinding configuration.
|
|
125
|
+
|
|
126
|
+
Merges default bindings with user overrides from configuration files.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A dictionary mapping action names to key sequences.
|
|
130
|
+
"""
|
|
131
|
+
defaults = {
|
|
132
|
+
"quit": "q",
|
|
133
|
+
"force_quit": "ctrl+c",
|
|
134
|
+
"back": "escape",
|
|
135
|
+
"theme": "ctrl+t",
|
|
136
|
+
"help": "?",
|
|
137
|
+
"fuzzy_search": "ctrl+f",
|
|
138
|
+
"toggle_favorite": "f",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
config = _load_config_data()
|
|
142
|
+
user_bindings = config.get("keybindings", {})
|
|
143
|
+
|
|
144
|
+
# Merge defaults with user bindings
|
|
145
|
+
return {**defaults, **user_bindings}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
CATEGORY_COLORS = {
|
|
149
|
+
"DEV": "blue",
|
|
150
|
+
"AI": "purple",
|
|
151
|
+
"MEDIA": "green",
|
|
152
|
+
"UTIL": "orange",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
USE_NERD_FONTS = True
|
|
156
|
+
|
|
157
|
+
CATEGORY_ICONS = {
|
|
158
|
+
"DEV": "", # fh-fa-code_fork
|
|
159
|
+
"AI": "", # fh-fa-microchip
|
|
160
|
+
"MEDIA": "", # fh-fa-video_camera
|
|
161
|
+
"UTIL": "", # fh-fa-wrench
|
|
162
|
+
"ALL": "", # fh-fa-list
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_preferred_terminal() -> str | None:
|
|
167
|
+
"""Determines the available terminal emulator based on a priority list.
|
|
168
|
+
|
|
169
|
+
Checks `pyproject.toml` for [tool.nexus.priority_terminals] and falls back
|
|
170
|
+
to a default list if configuration is missing.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The command string for the first found terminal, or None if no supported
|
|
174
|
+
terminal is found.
|
|
175
|
+
"""
|
|
176
|
+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
|
177
|
+
|
|
178
|
+
terminals = ["kitty", "ghostty", "gnome-terminal", "xterm"]
|
|
179
|
+
|
|
180
|
+
if pyproject_path.exists():
|
|
181
|
+
try:
|
|
182
|
+
with open(pyproject_path, "rb") as f:
|
|
183
|
+
data = tomllib.load(f)
|
|
184
|
+
config_terminals = (
|
|
185
|
+
data.get("tool", {}).get("nexus", {}).get("priority_terminals")
|
|
186
|
+
)
|
|
187
|
+
if config_terminals:
|
|
188
|
+
terminals = config_terminals
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
for term in terminals:
|
|
193
|
+
path = shutil.which(term)
|
|
194
|
+
if path:
|
|
195
|
+
return path
|
|
196
|
+
|
|
197
|
+
return None
|
nexus/container.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Dependency Injection Container for Nexus.
|
|
2
|
+
|
|
3
|
+
Manages the lifecycle and resolution of application services.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
from nexus.state import get_state_manager, StateManager
|
|
8
|
+
|
|
9
|
+
from nexus.services import executor, scanner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Container:
|
|
13
|
+
"""Simple service container."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
"""Initialize the container."""
|
|
17
|
+
# In a more complex app, we might use a proper DI library.
|
|
18
|
+
# For now, simplistic service location is sufficient.
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def executor(self) -> Any:
|
|
23
|
+
"""Returns the executor service module."""
|
|
24
|
+
# Currently the executor is a module with functions.
|
|
25
|
+
# Future refactor could make it a class.
|
|
26
|
+
return executor
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def scanner(self) -> Any:
|
|
30
|
+
"""Returns the scanner service module."""
|
|
31
|
+
return scanner
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def state_manager(self) -> StateManager:
|
|
35
|
+
"""Returns the state manager service."""
|
|
36
|
+
return get_state_manager()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Global instance
|
|
40
|
+
_container = Container()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_container() -> Container:
|
|
44
|
+
"""Returns the global service container."""
|
|
45
|
+
return _container
|
nexus/logger.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Logging configuration for Nexus.
|
|
2
|
+
|
|
3
|
+
Configures structlog to write asynchronous logs to a rotating file in the user's
|
|
4
|
+
cache directory, ensuring it does not interfere with the TUI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
# Define log path
|
|
15
|
+
LOG_DIR = Path.home() / ".cache" / "nexus"
|
|
16
|
+
LOG_FILE = LOG_DIR / "nexus.log"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def configure_logging() -> None:
|
|
20
|
+
"""Configures structural logging."""
|
|
21
|
+
if not LOG_DIR.exists():
|
|
22
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
# Configure standard logging to file
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
filename=str(LOG_FILE),
|
|
27
|
+
level=logging.INFO,
|
|
28
|
+
format="%(message)s",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
structlog.configure(
|
|
32
|
+
processors=[
|
|
33
|
+
structlog.contextvars.merge_contextvars,
|
|
34
|
+
structlog.processors.add_log_level,
|
|
35
|
+
structlog.processors.StackInfoRenderer(),
|
|
36
|
+
structlog.processors.format_exc_info,
|
|
37
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
38
|
+
structlog.processors.JSONRenderer(),
|
|
39
|
+
],
|
|
40
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
41
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
42
|
+
cache_logger_on_first_use=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_logger(name: str = "nexus") -> Any:
|
|
47
|
+
"""Returns a structured logger instance."""
|
|
48
|
+
return structlog.get_logger(name)
|
nexus/models.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Data models for the Nexus application.
|
|
2
|
+
|
|
3
|
+
Defines the Pydantic models used for validation and typehinting of
|
|
4
|
+
tools and projects within the application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Tool(BaseModel):
|
|
14
|
+
"""Represents a command-line tool available in Nexus.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
label: The display name of the tool.
|
|
18
|
+
category: The category the tool belongs to (DEV, AI, MEDIA, UTIL).
|
|
19
|
+
description: A brief description of the tool's purpose.
|
|
20
|
+
command: The shell command to execute.
|
|
21
|
+
requires_project: True if the tool needs a project directory to run.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
label: str
|
|
25
|
+
category: Literal["DEV", "AI", "MEDIA", "UTIL"]
|
|
26
|
+
description: str
|
|
27
|
+
command: str
|
|
28
|
+
requires_project: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Project(BaseModel):
|
|
32
|
+
"""Represents a local project directory.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: The name of the project folder.
|
|
36
|
+
path: The absolute path to the project directory.
|
|
37
|
+
is_git: True if the directory is a git repository.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
path: Path
|
|
42
|
+
is_git: bool
|
|
43
|
+
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Screen for creating a new project.
|
|
2
|
+
|
|
3
|
+
Collects user input for a new project directory name and creates the directory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from nexus.config import get_project_root
|
|
10
|
+
from textual.app import ComposeResult
|
|
11
|
+
from textual.binding import Binding
|
|
12
|
+
from textual.containers import Container, Horizontal
|
|
13
|
+
from textual.screen import ModalScreen
|
|
14
|
+
from textual.widgets import Button, Input, Label
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CreateProject(ModalScreen[None]):
|
|
18
|
+
"""A modal screen for creating a new project.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
on_created_callback: Callback function called with the new project name upon success.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
CSS_PATH = "../style.tcss"
|
|
25
|
+
|
|
26
|
+
def __init__(self, on_created: Callable[[str], None], **kwargs: Any):
|
|
27
|
+
"""Initializes the CreateProject screen.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
on_created: Callback function (str) -> None.
|
|
31
|
+
**kwargs: Additional arguments passed to ModalScreen.
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(**kwargs)
|
|
34
|
+
self.on_created_callback = on_created
|
|
35
|
+
|
|
36
|
+
BINDINGS = [
|
|
37
|
+
Binding("escape", "cancel", "Cancel"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
"""Composes the screen layout.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A ComposeResult containing the widget tree.
|
|
45
|
+
"""
|
|
46
|
+
with Container(id="create-project-dialog"):
|
|
47
|
+
yield Label("Create New Project", id="create-project-title")
|
|
48
|
+
yield Input(placeholder="Project Name", id="project-name-input")
|
|
49
|
+
yield Label("", id="create-error", classes="error-label hidden")
|
|
50
|
+
|
|
51
|
+
with Horizontal(id="create-project-buttons"):
|
|
52
|
+
yield Button("Cancel", variant="default", id="btn-cancel")
|
|
53
|
+
yield Button("Create", variant="primary", id="btn-create")
|
|
54
|
+
|
|
55
|
+
def on_mount(self) -> None:
|
|
56
|
+
"""Called when the screen is mounted."""
|
|
57
|
+
self.query_one("#project-name-input").focus()
|
|
58
|
+
|
|
59
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
60
|
+
"""Handles button press events.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
event: The button pressed event.
|
|
64
|
+
"""
|
|
65
|
+
if event.button.id == "btn-cancel":
|
|
66
|
+
self.action_cancel()
|
|
67
|
+
elif event.button.id == "btn-create":
|
|
68
|
+
self.create_project()
|
|
69
|
+
|
|
70
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
71
|
+
"""Handles enter key in input field.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
event: The input submitted event.
|
|
75
|
+
"""
|
|
76
|
+
self.create_project()
|
|
77
|
+
|
|
78
|
+
def create_project(self) -> None:
|
|
79
|
+
"""Validates input and attempts to create the project directory."""
|
|
80
|
+
name_input = self.query_one("#project-name-input", Input)
|
|
81
|
+
name = name_input.value.strip()
|
|
82
|
+
if not name:
|
|
83
|
+
self.show_error("Project name cannot be empty.")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
project_path = get_project_root() / name
|
|
87
|
+
|
|
88
|
+
if project_path.exists():
|
|
89
|
+
self.show_error("Project already exists.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
os.makedirs(project_path)
|
|
94
|
+
self.dismiss()
|
|
95
|
+
self.on_created_callback(name)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.show_error(f"Error: {e}")
|
|
98
|
+
|
|
99
|
+
def show_error(self, message: str) -> None:
|
|
100
|
+
"""Displays an error message to the user.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
message: The error description.
|
|
104
|
+
"""
|
|
105
|
+
lbl = self.query_one("#create-error", Label)
|
|
106
|
+
lbl.update(message)
|
|
107
|
+
lbl.remove_class("hidden")
|
|
108
|
+
|
|
109
|
+
def action_cancel(self) -> None:
|
|
110
|
+
"""Cancels the action and closes the modal."""
|
|
111
|
+
self.dismiss()
|
|
112
|
+
|
|
113
|
+
# Summary:
|
|
114
|
+
# Formatted docstrings to strict Google Style.
|
|
115
|
+
# Added module docstring.
|
nexus/screens/error.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Generic Error Screen for Nexus.
|
|
2
|
+
|
|
3
|
+
Displays an error message and functionality to dismiss or copy details.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
10
|
+
from textual.screen import ModalScreen
|
|
11
|
+
from textual.widgets import Button, Label, Static
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorScreen(ModalScreen[None]):
|
|
15
|
+
"""A modal screen that displays an error message."""
|
|
16
|
+
|
|
17
|
+
CSS_PATH = "../style.tcss"
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
title: str,
|
|
22
|
+
message: str,
|
|
23
|
+
details: str = "",
|
|
24
|
+
**kwargs: Any,
|
|
25
|
+
):
|
|
26
|
+
"""Initializes the ErrorScreen.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
title: The title of the error.
|
|
30
|
+
message: The main error message.
|
|
31
|
+
details: Optional technical details.
|
|
32
|
+
**kwargs: Additional arguments.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(**kwargs)
|
|
35
|
+
self.error_title = title
|
|
36
|
+
self.error_message = message
|
|
37
|
+
self.error_details = details
|
|
38
|
+
|
|
39
|
+
def compose(self) -> ComposeResult:
|
|
40
|
+
"""Composes the screen layout."""
|
|
41
|
+
with Container(id="error-dialog"):
|
|
42
|
+
with Horizontal(id="error-header"):
|
|
43
|
+
yield Label("Error", classes="error-icon")
|
|
44
|
+
yield Label(self.error_title, id="error-title")
|
|
45
|
+
|
|
46
|
+
with Vertical(id="error-body"):
|
|
47
|
+
yield Label(self.error_message, id="error-message")
|
|
48
|
+
if self.error_details:
|
|
49
|
+
yield Static(self.error_details, id="error-details")
|
|
50
|
+
|
|
51
|
+
with Horizontal(id="error-footer"):
|
|
52
|
+
yield Button("Close", variant="error", id="btn-error-close")
|
|
53
|
+
|
|
54
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
55
|
+
"""Handles button presses."""
|
|
56
|
+
if event.button.id == "btn-error-close":
|
|
57
|
+
self.dismiss()
|
nexus/screens/help.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Help screen for the Nexus application.
|
|
2
|
+
|
|
3
|
+
Displays information about key bindings and usage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Horizontal, Vertical
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Label, Markdown
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HelpScreen(ModalScreen[None]):
|
|
13
|
+
"""A modal screen that displays help and key bindings."""
|
|
14
|
+
|
|
15
|
+
CSS_PATH = "../style.tcss"
|
|
16
|
+
|
|
17
|
+
BINDINGS = [
|
|
18
|
+
("escape", "dismiss", "Close"),
|
|
19
|
+
("q", "dismiss", "Close"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def compose(self) -> ComposeResult:
|
|
23
|
+
"""Composes the screen layout.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A ComposeResult containing the widget tree.
|
|
27
|
+
"""
|
|
28
|
+
with Vertical(id="help-dialog"):
|
|
29
|
+
with Horizontal(id="help-title-container"):
|
|
30
|
+
yield Label("Nexus Help", id="help-title")
|
|
31
|
+
|
|
32
|
+
with Vertical(id="help-content"):
|
|
33
|
+
yield Markdown(
|
|
34
|
+
"""
|
|
35
|
+
**Navigation**
|
|
36
|
+
- `↑ / ↓` : Navigate lists
|
|
37
|
+
- `← / →` : Switch between Categories and Tool List
|
|
38
|
+
- `Enter` : Select Category or Launch Tool
|
|
39
|
+
|
|
40
|
+
**Search**
|
|
41
|
+
- Type any character to start searching tools.
|
|
42
|
+
- `Esc` : Clear search or Go Back.
|
|
43
|
+
|
|
44
|
+
**System**
|
|
45
|
+
- `Ctrl+t` : Open Theme Picker
|
|
46
|
+
- `Ctrl+c` : Quit Application
|
|
47
|
+
- `?` or `F1`: Show this Help Screen
|
|
48
|
+
"""
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with Horizontal(id="help-footer"):
|
|
52
|
+
yield Button("Close", variant="primary", id="btn-close")
|
|
53
|
+
|
|
54
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
55
|
+
"""Handles button press events.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
event: The button pressed event.
|
|
59
|
+
"""
|
|
60
|
+
if event.button.id == "btn-close":
|
|
61
|
+
self.dismiss()
|
|
62
|
+
|