usecli 0.1.43__tar.gz → 0.1.45__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.
Files changed (71) hide show
  1. {usecli-0.1.43 → usecli-0.1.45}/PKG-INFO +1 -1
  2. {usecli-0.1.43 → usecli-0.1.45}/pyproject.toml +1 -1
  3. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/init_command.py +8 -0
  4. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/config/colors.py +155 -13
  5. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/ui/title.py +3 -2
  6. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/shared/config/manager.py +8 -1
  7. {usecli-0.1.43 → usecli-0.1.45}/LICENSE +0 -0
  8. {usecli-0.1.43 → usecli-0.1.45}/README.md +0 -0
  9. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/__init__.py +0 -0
  10. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/__init__.py +0 -0
  11. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/README.md +0 -0
  12. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/__init__.py +0 -0
  13. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/custom/README.md +0 -0
  14. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/custom/__init__.py +0 -0
  15. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  16. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  17. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  18. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  19. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  20. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  21. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  22. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  23. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  24. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  25. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  26. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  27. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/config/__init__.py +0 -0
  28. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/__init__.py +0 -0
  29. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/base_command.py +0 -0
  30. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/error/__init__.py +0 -0
  31. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/error/handler.py +0 -0
  32. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/error/utils.py +0 -0
  33. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  34. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/exceptions/base.py +0 -0
  35. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/exceptions/config.py +0 -0
  36. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/exceptions/usage.py +0 -0
  37. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/exceptions/validation.py +0 -0
  38. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/skill_generator.py +0 -0
  39. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/ui/__init__.py +0 -0
  40. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/ui/list.py +0 -0
  41. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/ui/title.txt +0 -0
  42. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/validators/__init__.py +0 -0
  43. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/validators/network.py +0 -0
  44. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/validators/numeric.py +0 -0
  45. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/validators/path.py +0 -0
  46. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/core/validators/string.py +0 -0
  47. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/services/__init__.py +0 -0
  48. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/services/command_service.py +0 -0
  49. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/templates/command.py.j2 +0 -0
  50. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  51. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  52. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  53. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  54. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  55. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  56. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  57. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/default.toml +0 -0
  58. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/dracula.toml +0 -0
  59. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  60. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/nord.toml +0 -0
  61. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  62. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/utils/__init__.py +0 -0
  63. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  64. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  65. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/menu.py +0 -0
  66. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/params.py +0 -0
  67. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/shared/__init__.py +0 -0
  68. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/shared/config/__init__.py +0 -0
  69. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/shared/config/globals.py +0 -0
  70. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/ui.py +0 -0
  71. {usecli-0.1.43 → usecli-0.1.45}/src/usecli/usecli.config.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.43
3
+ Version: 0.1.45
4
4
  Summary: A powerful Python CLI framework for building beautiful, developer-friendly command-line tools.
5
5
  Author: Edward Boswell
6
6
  Author-email: Edward Boswell <thememium@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.43"
3
+ version = "0.1.45"
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,21 @@ 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
+ return any(part in ConfigManager._SKIP_DIRS for part in path.parts)
112
+
109
113
  def _resolve_config_path(self, value: str, project_root: Path) -> Path:
110
114
  path = Path(value).expanduser()
111
115
  if not path.is_absolute():
112
116
  path = project_root / path
113
117
  if path.exists() and path.is_dir():
114
118
  return (path / USECLI_CONFIG_TOML).resolve()
119
+ if not path.exists() and path.suffix == "":
120
+ return (path / USECLI_CONFIG_TOML).resolve()
115
121
  return path.resolve()
116
122
 
117
123
  def _ensure_project_scripts(
@@ -707,6 +713,8 @@ include = ["{root_package}*"]
707
713
  if commands_path.parent != project_root:
708
714
  config_root = commands_path.parent
709
715
  existing_config = ConfigManager(start_dir=config_root).usecli_config_path
716
+ if existing_config.exists() and self._should_skip_config_path(existing_config):
717
+ existing_config = config_root / USECLI_CONFIG_TOML
710
718
  default_config_path = (
711
719
  existing_config
712
720
  if existing_config.exists()
@@ -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
@@ -233,9 +234,11 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
233
234
  return git_root
234
235
 
235
236
 
236
- def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
237
+ def _load_usecli_config(
238
+ project_root: Path | None,
239
+ ) -> tuple[dict[str, Any], Path | None]:
237
240
  if project_root is None:
238
- return {}
241
+ return {}, None
239
242
 
240
243
  config_path = project_root / USECLI_CONFIG_TOML
241
244
  if not config_path.exists():
@@ -247,14 +250,14 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
247
250
  if not config_path or not config_path.exists():
248
251
  console_match = _find_usecli_config_for_console_script()
249
252
  if console_match:
250
- return _load_usecli_config_file(console_match)
253
+ return _load_usecli_config_file(console_match), console_match
251
254
  package_match = _find_usecli_config_in_package()
252
255
  if package_match:
253
256
  config_path = package_match
254
257
  if not config_path or not config_path.exists():
255
- return {}
258
+ return {}, None
256
259
 
257
- return _load_usecli_config_file(config_path)
260
+ return _load_usecli_config_file(config_path), config_path
258
261
 
259
262
 
260
263
  def _load_usecli_config_file(config_path: Path) -> dict[str, Any]:
@@ -461,16 +464,16 @@ def _load_theme_file(theme_path: Path) -> dict[str, Any]:
461
464
  return data
462
465
 
463
466
 
464
- def _load_theme() -> tuple[dict[str, str], dict[str, str]]:
467
+ def _load_theme() -> tuple[dict[str, str], dict[str, str], str, Path | None]:
465
468
  project_root = _find_project_root()
466
- config = _load_usecli_config(project_root)
469
+ config_values, _ = _load_usecli_config(project_root)
467
470
 
468
471
  theme_name = DEFAULT_THEME_NAME
469
- config_theme = config.get("theme")
472
+ config_theme = config_values.get("theme")
470
473
  if isinstance(config_theme, str) and config_theme.strip():
471
474
  theme_name = config_theme.strip()
472
475
 
473
- theme_path = _resolve_theme_path(theme_name, project_root, config)
476
+ theme_path = _resolve_theme_path(theme_name, project_root, config_values)
474
477
  theme_data = _load_theme_file(theme_path) if theme_path else {}
475
478
 
476
479
  colors = _merge_theme_values(
@@ -480,14 +483,153 @@ def _load_theme() -> tuple[dict[str, str], dict[str, str]]:
480
483
  )
481
484
  ansi = _build_ansi_palette(colors)
482
485
 
483
- return colors, ansi
486
+ return colors, ansi, theme_name, theme_path
487
+
488
+
489
+ _THEME_CACHE: dict[str, Any] = {
490
+ "context": None,
491
+ "cwd": None,
492
+ "config_path": None,
493
+ "config_sig": None,
494
+ "last_checked": 0.0,
495
+ }
496
+
497
+
498
+ def _config_signature(path: Path) -> tuple[Path, int | None, int | None]:
499
+ try:
500
+ stat = path.stat()
501
+ except OSError:
502
+ return (path.resolve(), None, None)
503
+ return (path.resolve(), int(stat.st_mtime), int(stat.st_size))
504
+
505
+
506
+ def _compute_theme_context() -> tuple[Path | None, Path | None, str, Path | None]:
507
+ project_root = _find_project_root()
508
+ config_values, config_path = _load_usecli_config(project_root)
509
+ theme_name = DEFAULT_THEME_NAME
510
+ config_theme = config_values.get("theme")
511
+ if isinstance(config_theme, str) and config_theme.strip():
512
+ theme_name = config_theme.strip()
513
+ theme_path = _resolve_theme_path(theme_name, project_root, config_values)
514
+ return (
515
+ project_root.resolve() if project_root else None,
516
+ config_path.resolve() if config_path else None,
517
+ theme_name,
518
+ theme_path.resolve() if theme_path else None,
519
+ )
484
520
 
485
521
 
486
- _THEME_COLORS, _THEME_ANSI = _load_theme()
522
+ def _theme_context() -> tuple[Path | None, Path | None, str, Path | None]:
523
+ now = time.monotonic()
524
+ cached_context = _THEME_CACHE.get("context")
525
+ cached_cwd = _THEME_CACHE.get("cwd")
526
+ cached_sig = _THEME_CACHE.get("config_sig")
527
+ cached_path = _THEME_CACHE.get("config_path")
528
+
529
+ cwd = Path.cwd().resolve()
530
+ if cached_context is not None and cached_cwd == cwd:
531
+ if cached_path is None:
532
+ return cached_context
533
+ if now - float(_THEME_CACHE.get("last_checked", 0.0)) < 0.25:
534
+ return cached_context
535
+ _THEME_CACHE["last_checked"] = now
536
+ current_sig = _config_signature(cached_path)
537
+ if current_sig == cached_sig:
538
+ return cached_context
539
+
540
+ context = _compute_theme_context()
541
+ _THEME_CACHE["context"] = context
542
+ _THEME_CACHE["cwd"] = cwd
543
+ config_path = context[1]
544
+ _THEME_CACHE["config_path"] = config_path
545
+ _THEME_CACHE["config_sig"] = _config_signature(config_path) if config_path else None
546
+ _THEME_CACHE["last_checked"] = now
547
+ return context
548
+
549
+
550
+ class _AnsiNamespace(Protocol):
551
+ PRIMARY: str
552
+ SECONDARY: str
553
+ ACCENT: str
554
+ FOREGROUND: str
555
+ FOREGROUND_MUTED: str
556
+ RESET: str
557
+ RED: str
558
+ GREEN: str
559
+ YELLOW: str
560
+ BLUE: str
561
+
562
+
563
+ class _ColorNamespace(Protocol):
564
+ ANSI: type[_AnsiNamespace]
565
+
566
+
567
+ def _apply_theme(
568
+ color_class: _ColorNamespace, colors: dict[str, str], ansi: dict[str, str]
569
+ ) -> None:
570
+ for key in (
571
+ "primary",
572
+ "secondary",
573
+ "accent",
574
+ "success",
575
+ "error",
576
+ "warning",
577
+ "info",
578
+ "foreground",
579
+ "foreground_muted",
580
+ "background",
581
+ "border",
582
+ "border_focus",
583
+ "command",
584
+ "option",
585
+ "link",
586
+ "prompt",
587
+ "panel_primary",
588
+ "panel_secondary",
589
+ "panel_accent",
590
+ ):
591
+ setattr(color_class, key.upper(), colors[key])
592
+
593
+ ansi_class = color_class.ANSI
594
+ for key in (
595
+ "primary",
596
+ "secondary",
597
+ "accent",
598
+ "foreground",
599
+ "foreground_muted",
600
+ "reset",
601
+ "red",
602
+ "green",
603
+ "yellow",
604
+ "blue",
605
+ ):
606
+ setattr(ansi_class, key.upper(), ansi[key])
607
+
608
+
609
+ def _ensure_theme_loaded(color_class: type[Any]) -> None:
610
+ global _THEME_CONTEXT
611
+ context = _theme_context()
612
+ if context == _THEME_CONTEXT:
613
+ return
614
+ colors, ansi, _, _ = _load_theme()
615
+ _THEME_CONTEXT = context
616
+ _THEME_COLORS.update(colors)
617
+ _THEME_ANSI.update(ansi)
618
+ _apply_theme(cast(_ColorNamespace, color_class), _THEME_COLORS, _THEME_ANSI)
619
+
620
+
621
+ _THEME_COLORS, _THEME_ANSI, _THEME_NAME, _THEME_PATH = _load_theme()
622
+ _THEME_CONTEXT: tuple[Path | None, Path | None, str, Path | None] = _theme_context()
623
+
624
+
625
+ class _ColorMeta(type):
626
+ def __getattribute__(cls, name: str) -> Any:
627
+ _ensure_theme_loaded(cls)
628
+ return super().__getattribute__(name)
487
629
 
488
630
 
489
631
  @final
490
- class COLOR:
632
+ class COLOR(metaclass=_ColorMeta):
491
633
  """Semantic color system for usecli CLI.
492
634
 
493
635
  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 and title != "usecli":
72
- return title
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