usecli 0.1.44__tar.gz → 0.1.46__tar.gz
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.
- {usecli-0.1.44 → usecli-0.1.46}/PKG-INFO +1 -1
- {usecli-0.1.44 → usecli-0.1.46}/pyproject.toml +1 -1
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/init_command.py +58 -7
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/config/colors.py +260 -14
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/ui/title.py +3 -2
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/shared/config/manager.py +8 -1
- {usecli-0.1.44 → usecli-0.1.46}/LICENSE +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/README.md +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/base_command.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/skill_generator.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/ui/list.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/ui/title.txt +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/services/command_service.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/templates/command.py.j2 +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/menu.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/params.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/shared/config/globals.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/ui.py +0 -0
- {usecli-0.1.44 → usecli-0.1.46}/src/usecli/usecli.config.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.46"
|
|
4
4
|
description = "A powerful Python CLI framework for building beautiful, developer-friendly command-line tools."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Edward Boswell", email = "thememium@gmail.com" }]
|
|
@@ -103,15 +103,31 @@ class InitCommand(BaseCommand):
|
|
|
103
103
|
if not should_overwrite:
|
|
104
104
|
return "skipped"
|
|
105
105
|
|
|
106
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
107
|
config_path.write_text(config_content.rstrip() + "\n")
|
|
107
108
|
return "updated" if existed else "created"
|
|
108
109
|
|
|
110
|
+
def _should_skip_config_path(self, path: Path) -> bool:
|
|
111
|
+
try:
|
|
112
|
+
resolved = path.resolve()
|
|
113
|
+
except OSError:
|
|
114
|
+
resolved = path
|
|
115
|
+
if any(part in ConfigManager._SKIP_DIRS for part in resolved.parts):
|
|
116
|
+
return True
|
|
117
|
+
try:
|
|
118
|
+
resolved.relative_to(Path(sys.prefix).resolve())
|
|
119
|
+
except ValueError:
|
|
120
|
+
return False
|
|
121
|
+
return True
|
|
122
|
+
|
|
109
123
|
def _resolve_config_path(self, value: str, project_root: Path) -> Path:
|
|
110
124
|
path = Path(value).expanduser()
|
|
111
125
|
if not path.is_absolute():
|
|
112
126
|
path = project_root / path
|
|
113
127
|
if path.exists() and path.is_dir():
|
|
114
128
|
return (path / USECLI_CONFIG_TOML).resolve()
|
|
129
|
+
if not path.exists() and path.suffix == "":
|
|
130
|
+
return (path / USECLI_CONFIG_TOML).resolve()
|
|
115
131
|
return path.resolve()
|
|
116
132
|
|
|
117
133
|
def _ensure_project_scripts(
|
|
@@ -180,6 +196,36 @@ class InitCommand(BaseCommand):
|
|
|
180
196
|
|
|
181
197
|
return created
|
|
182
198
|
|
|
199
|
+
def _find_project_root_for_init(self, start_dir: Path) -> Path:
|
|
200
|
+
current = start_dir.resolve()
|
|
201
|
+
git_root: Path | None = None
|
|
202
|
+
while True:
|
|
203
|
+
if (current / "pyproject.toml").exists():
|
|
204
|
+
return current
|
|
205
|
+
if (current / USECLI_CONFIG_TOML).exists():
|
|
206
|
+
return current
|
|
207
|
+
git_dir = current / ".git"
|
|
208
|
+
if git_dir.exists():
|
|
209
|
+
git_root = current
|
|
210
|
+
break
|
|
211
|
+
parent = current.parent
|
|
212
|
+
if parent == current:
|
|
213
|
+
break
|
|
214
|
+
current = parent
|
|
215
|
+
return (git_root or start_dir).resolve()
|
|
216
|
+
|
|
217
|
+
def _find_pyproject_path_for_init(self, start_dir: Path) -> Path | None:
|
|
218
|
+
current = start_dir.resolve()
|
|
219
|
+
while True:
|
|
220
|
+
pyproject_path = current / "pyproject.toml"
|
|
221
|
+
if pyproject_path.exists():
|
|
222
|
+
return pyproject_path
|
|
223
|
+
parent = current.parent
|
|
224
|
+
if parent == current:
|
|
225
|
+
break
|
|
226
|
+
current = parent
|
|
227
|
+
return None
|
|
228
|
+
|
|
183
229
|
def _derive_templates_dir(self, commands_dir: str) -> str:
|
|
184
230
|
commands_path = Path(commands_dir)
|
|
185
231
|
parent = commands_path.parent
|
|
@@ -503,12 +549,9 @@ include = ["{root_package}*"]
|
|
|
503
549
|
),
|
|
504
550
|
) -> None:
|
|
505
551
|
cwd = Path.cwd()
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
config_manager.pyproject_path
|
|
510
|
-
if config_manager.pyproject_path.exists()
|
|
511
|
-
else project_root / "pyproject.toml"
|
|
552
|
+
project_root = self._find_project_root_for_init(cwd)
|
|
553
|
+
pyproject_path = self._find_pyproject_path_for_init(cwd) or (
|
|
554
|
+
project_root / "pyproject.toml"
|
|
512
555
|
)
|
|
513
556
|
|
|
514
557
|
console.print()
|
|
@@ -706,12 +749,20 @@ include = ["{root_package}*"]
|
|
|
706
749
|
config_root = project_root
|
|
707
750
|
if commands_path.parent != project_root:
|
|
708
751
|
config_root = commands_path.parent
|
|
709
|
-
existing_config = ConfigManager(
|
|
752
|
+
existing_config = ConfigManager._find_usecli_config_in_tree(
|
|
753
|
+
project_root,
|
|
754
|
+
config_root,
|
|
755
|
+
skip_venv=True,
|
|
756
|
+
)
|
|
757
|
+
if existing_config is None or self._should_skip_config_path(existing_config):
|
|
758
|
+
existing_config = config_root / USECLI_CONFIG_TOML
|
|
710
759
|
default_config_path = (
|
|
711
760
|
existing_config
|
|
712
761
|
if existing_config.exists()
|
|
713
762
|
else config_root / USECLI_CONFIG_TOML
|
|
714
763
|
)
|
|
764
|
+
if self._should_skip_config_path(default_config_path):
|
|
765
|
+
default_config_path = config_root / USECLI_CONFIG_TOML
|
|
715
766
|
config_location = Prompt.ask(
|
|
716
767
|
f"[bold {COLOR.SECONDARY}]Config file location[/bold {COLOR.SECONDARY}]"
|
|
717
768
|
" (path or directory)",
|
|
@@ -15,8 +15,9 @@ import importlib.metadata
|
|
|
15
15
|
import importlib.util
|
|
16
16
|
import os
|
|
17
17
|
import sys
|
|
18
|
+
import time
|
|
18
19
|
from pathlib import Path
|
|
19
|
-
from typing import Any, Callable, Final, final
|
|
20
|
+
from typing import Any, Callable, Final, Protocol, cast, final
|
|
20
21
|
|
|
21
22
|
if sys.version_info >= (3, 11):
|
|
22
23
|
import tomllib
|
|
@@ -99,6 +100,110 @@ def _find_usecli_config_path(
|
|
|
99
100
|
return selection[0]
|
|
100
101
|
|
|
101
102
|
|
|
103
|
+
def _get_command_name() -> str | None:
|
|
104
|
+
"""Get the current command name from sys.argv."""
|
|
105
|
+
if not sys.argv:
|
|
106
|
+
return None
|
|
107
|
+
command = os.path.basename(sys.argv[0])
|
|
108
|
+
return command if command else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_console_script_aliases(command_name: str | None) -> set[str]:
|
|
112
|
+
"""Get all aliases for a console script from package metadata."""
|
|
113
|
+
if not command_name:
|
|
114
|
+
return set()
|
|
115
|
+
aliases: set[str] = {command_name}
|
|
116
|
+
try:
|
|
117
|
+
distributions = importlib.metadata.distributions()
|
|
118
|
+
except Exception:
|
|
119
|
+
return aliases
|
|
120
|
+
for dist in distributions:
|
|
121
|
+
try:
|
|
122
|
+
entry_points = dist.entry_points
|
|
123
|
+
except Exception:
|
|
124
|
+
continue
|
|
125
|
+
names = [
|
|
126
|
+
entry_point.name
|
|
127
|
+
for entry_point in entry_points
|
|
128
|
+
if entry_point.group == "console_scripts"
|
|
129
|
+
]
|
|
130
|
+
if command_name in names:
|
|
131
|
+
aliases.update(names)
|
|
132
|
+
break
|
|
133
|
+
return aliases
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _config_matches_command(path: Path, command_name: str | None) -> bool:
|
|
137
|
+
"""Check if a config file matches the given command name."""
|
|
138
|
+
if command_name is None:
|
|
139
|
+
return True
|
|
140
|
+
try:
|
|
141
|
+
data = _load_usecli_config_file(path)
|
|
142
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
143
|
+
return True
|
|
144
|
+
config_command = data.get("command_name")
|
|
145
|
+
if not isinstance(config_command, str):
|
|
146
|
+
return True
|
|
147
|
+
normalized = config_command.strip()
|
|
148
|
+
if not normalized:
|
|
149
|
+
return True
|
|
150
|
+
if normalized == command_name:
|
|
151
|
+
return True
|
|
152
|
+
aliases = _get_console_script_aliases(command_name)
|
|
153
|
+
return normalized in aliases
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _find_usecli_config_path_for_command(
|
|
157
|
+
root_dir: Path, start_dir: Path, *, skip_venv: bool
|
|
158
|
+
) -> Path | None:
|
|
159
|
+
"""Find usecli config that matches the current command."""
|
|
160
|
+
if not root_dir.exists() or not root_dir.is_dir():
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
|
|
164
|
+
if skip_venv:
|
|
165
|
+
candidates = [
|
|
166
|
+
path
|
|
167
|
+
for path in candidates
|
|
168
|
+
if not any(part in _SKIP_DIRS for part in path.parts)
|
|
169
|
+
]
|
|
170
|
+
if not candidates:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
command_name = _get_command_name()
|
|
174
|
+
|
|
175
|
+
# Filter candidates by command_name matching
|
|
176
|
+
if command_name:
|
|
177
|
+
candidates = [
|
|
178
|
+
path for path in candidates if _config_matches_command(path, command_name)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if not candidates:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
start_dir = start_dir.resolve()
|
|
185
|
+
preferred: list[Path] = []
|
|
186
|
+
for path in candidates:
|
|
187
|
+
try:
|
|
188
|
+
path.relative_to(start_dir)
|
|
189
|
+
preferred.append(path)
|
|
190
|
+
except ValueError:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
selection = preferred or candidates
|
|
194
|
+
|
|
195
|
+
def _depth_key(path: Path) -> tuple[int, str]:
|
|
196
|
+
try:
|
|
197
|
+
relative = path.relative_to(start_dir)
|
|
198
|
+
return (len(relative.parts), str(path))
|
|
199
|
+
except ValueError:
|
|
200
|
+
relative = path.relative_to(root_dir)
|
|
201
|
+
return (len(relative.parts), str(path))
|
|
202
|
+
|
|
203
|
+
selection.sort(key=_depth_key)
|
|
204
|
+
return selection[0]
|
|
205
|
+
|
|
206
|
+
|
|
102
207
|
def _find_usecli_config_in_package() -> Path | None:
|
|
103
208
|
spec = importlib.util.find_spec(_get_package_name())
|
|
104
209
|
if spec is None or not spec.submodule_search_locations:
|
|
@@ -233,13 +338,15 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
|
|
|
233
338
|
return git_root
|
|
234
339
|
|
|
235
340
|
|
|
236
|
-
def _load_usecli_config(
|
|
341
|
+
def _load_usecli_config(
|
|
342
|
+
project_root: Path | None,
|
|
343
|
+
) -> tuple[dict[str, Any], Path | None]:
|
|
237
344
|
if project_root is None:
|
|
238
|
-
return {}
|
|
345
|
+
return {}, None
|
|
239
346
|
|
|
240
347
|
config_path = project_root / USECLI_CONFIG_TOML
|
|
241
348
|
if not config_path.exists():
|
|
242
|
-
config_path =
|
|
349
|
+
config_path = _find_usecli_config_path_for_command(
|
|
243
350
|
project_root,
|
|
244
351
|
project_root,
|
|
245
352
|
skip_venv=True,
|
|
@@ -247,14 +354,14 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
|
|
|
247
354
|
if not config_path or not config_path.exists():
|
|
248
355
|
console_match = _find_usecli_config_for_console_script()
|
|
249
356
|
if console_match:
|
|
250
|
-
return _load_usecli_config_file(console_match)
|
|
357
|
+
return _load_usecli_config_file(console_match), console_match
|
|
251
358
|
package_match = _find_usecli_config_in_package()
|
|
252
359
|
if package_match:
|
|
253
360
|
config_path = package_match
|
|
254
361
|
if not config_path or not config_path.exists():
|
|
255
|
-
return {}
|
|
362
|
+
return {}, None
|
|
256
363
|
|
|
257
|
-
return _load_usecli_config_file(config_path)
|
|
364
|
+
return _load_usecli_config_file(config_path), config_path
|
|
258
365
|
|
|
259
366
|
|
|
260
367
|
def _load_usecli_config_file(config_path: Path) -> dict[str, Any]:
|
|
@@ -461,16 +568,16 @@ def _load_theme_file(theme_path: Path) -> dict[str, Any]:
|
|
|
461
568
|
return data
|
|
462
569
|
|
|
463
570
|
|
|
464
|
-
def _load_theme() -> tuple[dict[str, str], dict[str, str]]:
|
|
571
|
+
def _load_theme() -> tuple[dict[str, str], dict[str, str], str, Path | None]:
|
|
465
572
|
project_root = _find_project_root()
|
|
466
|
-
|
|
573
|
+
config_values, _ = _load_usecli_config(project_root)
|
|
467
574
|
|
|
468
575
|
theme_name = DEFAULT_THEME_NAME
|
|
469
|
-
config_theme =
|
|
576
|
+
config_theme = config_values.get("theme")
|
|
470
577
|
if isinstance(config_theme, str) and config_theme.strip():
|
|
471
578
|
theme_name = config_theme.strip()
|
|
472
579
|
|
|
473
|
-
theme_path = _resolve_theme_path(theme_name, project_root,
|
|
580
|
+
theme_path = _resolve_theme_path(theme_name, project_root, config_values)
|
|
474
581
|
theme_data = _load_theme_file(theme_path) if theme_path else {}
|
|
475
582
|
|
|
476
583
|
colors = _merge_theme_values(
|
|
@@ -480,14 +587,153 @@ def _load_theme() -> tuple[dict[str, str], dict[str, str]]:
|
|
|
480
587
|
)
|
|
481
588
|
ansi = _build_ansi_palette(colors)
|
|
482
589
|
|
|
483
|
-
return colors, ansi
|
|
590
|
+
return colors, ansi, theme_name, theme_path
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
_THEME_CACHE: dict[str, Any] = {
|
|
594
|
+
"context": None,
|
|
595
|
+
"cwd": None,
|
|
596
|
+
"config_path": None,
|
|
597
|
+
"config_sig": None,
|
|
598
|
+
"last_checked": 0.0,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _config_signature(path: Path) -> tuple[Path, int | None, int | None]:
|
|
603
|
+
try:
|
|
604
|
+
stat = path.stat()
|
|
605
|
+
except OSError:
|
|
606
|
+
return (path.resolve(), None, None)
|
|
607
|
+
return (path.resolve(), int(stat.st_mtime), int(stat.st_size))
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _compute_theme_context() -> tuple[Path | None, Path | None, str, Path | None]:
|
|
611
|
+
project_root = _find_project_root()
|
|
612
|
+
config_values, config_path = _load_usecli_config(project_root)
|
|
613
|
+
theme_name = DEFAULT_THEME_NAME
|
|
614
|
+
config_theme = config_values.get("theme")
|
|
615
|
+
if isinstance(config_theme, str) and config_theme.strip():
|
|
616
|
+
theme_name = config_theme.strip()
|
|
617
|
+
theme_path = _resolve_theme_path(theme_name, project_root, config_values)
|
|
618
|
+
return (
|
|
619
|
+
project_root.resolve() if project_root else None,
|
|
620
|
+
config_path.resolve() if config_path else None,
|
|
621
|
+
theme_name,
|
|
622
|
+
theme_path.resolve() if theme_path else None,
|
|
623
|
+
)
|
|
484
624
|
|
|
485
625
|
|
|
486
|
-
|
|
626
|
+
def _theme_context() -> tuple[Path | None, Path | None, str, Path | None]:
|
|
627
|
+
now = time.monotonic()
|
|
628
|
+
cached_context = _THEME_CACHE.get("context")
|
|
629
|
+
cached_cwd = _THEME_CACHE.get("cwd")
|
|
630
|
+
cached_sig = _THEME_CACHE.get("config_sig")
|
|
631
|
+
cached_path = _THEME_CACHE.get("config_path")
|
|
632
|
+
|
|
633
|
+
cwd = Path.cwd().resolve()
|
|
634
|
+
if cached_context is not None and cached_cwd == cwd:
|
|
635
|
+
if cached_path is None:
|
|
636
|
+
return cached_context
|
|
637
|
+
if now - float(_THEME_CACHE.get("last_checked", 0.0)) < 0.25:
|
|
638
|
+
return cached_context
|
|
639
|
+
_THEME_CACHE["last_checked"] = now
|
|
640
|
+
current_sig = _config_signature(cached_path)
|
|
641
|
+
if current_sig == cached_sig:
|
|
642
|
+
return cached_context
|
|
643
|
+
|
|
644
|
+
context = _compute_theme_context()
|
|
645
|
+
_THEME_CACHE["context"] = context
|
|
646
|
+
_THEME_CACHE["cwd"] = cwd
|
|
647
|
+
config_path = context[1]
|
|
648
|
+
_THEME_CACHE["config_path"] = config_path
|
|
649
|
+
_THEME_CACHE["config_sig"] = _config_signature(config_path) if config_path else None
|
|
650
|
+
_THEME_CACHE["last_checked"] = now
|
|
651
|
+
return context
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class _AnsiNamespace(Protocol):
|
|
655
|
+
PRIMARY: str
|
|
656
|
+
SECONDARY: str
|
|
657
|
+
ACCENT: str
|
|
658
|
+
FOREGROUND: str
|
|
659
|
+
FOREGROUND_MUTED: str
|
|
660
|
+
RESET: str
|
|
661
|
+
RED: str
|
|
662
|
+
GREEN: str
|
|
663
|
+
YELLOW: str
|
|
664
|
+
BLUE: str
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class _ColorNamespace(Protocol):
|
|
668
|
+
ANSI: type[_AnsiNamespace]
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _apply_theme(
|
|
672
|
+
color_class: _ColorNamespace, colors: dict[str, str], ansi: dict[str, str]
|
|
673
|
+
) -> None:
|
|
674
|
+
for key in (
|
|
675
|
+
"primary",
|
|
676
|
+
"secondary",
|
|
677
|
+
"accent",
|
|
678
|
+
"success",
|
|
679
|
+
"error",
|
|
680
|
+
"warning",
|
|
681
|
+
"info",
|
|
682
|
+
"foreground",
|
|
683
|
+
"foreground_muted",
|
|
684
|
+
"background",
|
|
685
|
+
"border",
|
|
686
|
+
"border_focus",
|
|
687
|
+
"command",
|
|
688
|
+
"option",
|
|
689
|
+
"link",
|
|
690
|
+
"prompt",
|
|
691
|
+
"panel_primary",
|
|
692
|
+
"panel_secondary",
|
|
693
|
+
"panel_accent",
|
|
694
|
+
):
|
|
695
|
+
setattr(color_class, key.upper(), colors[key])
|
|
696
|
+
|
|
697
|
+
ansi_class = color_class.ANSI
|
|
698
|
+
for key in (
|
|
699
|
+
"primary",
|
|
700
|
+
"secondary",
|
|
701
|
+
"accent",
|
|
702
|
+
"foreground",
|
|
703
|
+
"foreground_muted",
|
|
704
|
+
"reset",
|
|
705
|
+
"red",
|
|
706
|
+
"green",
|
|
707
|
+
"yellow",
|
|
708
|
+
"blue",
|
|
709
|
+
):
|
|
710
|
+
setattr(ansi_class, key.upper(), ansi[key])
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _ensure_theme_loaded(color_class: type[Any]) -> None:
|
|
714
|
+
global _THEME_CONTEXT
|
|
715
|
+
context = _theme_context()
|
|
716
|
+
if context == _THEME_CONTEXT:
|
|
717
|
+
return
|
|
718
|
+
colors, ansi, _, _ = _load_theme()
|
|
719
|
+
_THEME_CONTEXT = context
|
|
720
|
+
_THEME_COLORS.update(colors)
|
|
721
|
+
_THEME_ANSI.update(ansi)
|
|
722
|
+
_apply_theme(cast(_ColorNamespace, color_class), _THEME_COLORS, _THEME_ANSI)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
_THEME_COLORS, _THEME_ANSI, _THEME_NAME, _THEME_PATH = _load_theme()
|
|
726
|
+
_THEME_CONTEXT: tuple[Path | None, Path | None, str, Path | None] = _theme_context()
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class _ColorMeta(type):
|
|
730
|
+
def __getattribute__(cls, name: str) -> Any:
|
|
731
|
+
_ensure_theme_loaded(cls)
|
|
732
|
+
return super().__getattribute__(name)
|
|
487
733
|
|
|
488
734
|
|
|
489
735
|
@final
|
|
490
|
-
class COLOR:
|
|
736
|
+
class COLOR(metaclass=_ColorMeta):
|
|
491
737
|
"""Semantic color system for usecli CLI.
|
|
492
738
|
|
|
493
739
|
All colors are defined as hex color codes compatible with Rich console.
|
|
@@ -68,8 +68,9 @@ def get_project_name() -> str:
|
|
|
68
68
|
# First, try to get the title from the config
|
|
69
69
|
config = get_config()
|
|
70
70
|
title = config.get("title")
|
|
71
|
-
if title:
|
|
72
|
-
|
|
71
|
+
if config.has_key("title") and isinstance(title, str) and title.strip():
|
|
72
|
+
normalized = title.strip()
|
|
73
|
+
return "useCli" if normalized == "usecli" else normalized
|
|
73
74
|
|
|
74
75
|
# Fall back to command name from pyproject.toml scripts
|
|
75
76
|
command_name = _get_script_command_name(Path.cwd())
|
|
@@ -582,8 +582,15 @@ _config_manager: ConfigManager | None = None
|
|
|
582
582
|
def get_config() -> ConfigManager:
|
|
583
583
|
"""Get the global ConfigManager instance."""
|
|
584
584
|
global _config_manager
|
|
585
|
+
current_root = find_project_root(Path.cwd())
|
|
586
|
+
if current_root is None:
|
|
587
|
+
current_root = Path.cwd().resolve()
|
|
585
588
|
if _config_manager is None:
|
|
586
|
-
_config_manager = ConfigManager()
|
|
589
|
+
_config_manager = ConfigManager(start_dir=Path.cwd())
|
|
590
|
+
else:
|
|
591
|
+
cached_root = _config_manager.project_root.resolve()
|
|
592
|
+
if cached_root != current_root.resolve():
|
|
593
|
+
_config_manager = ConfigManager(start_dir=Path.cwd())
|
|
587
594
|
return _config_manager
|
|
588
595
|
|
|
589
596
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{usecli-0.1.44 → usecli-0.1.46}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|