sqlsaber 0.28.0__tar.gz → 0.29.0__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.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

Files changed (128) hide show
  1. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/PKG-INFO +1 -1
  2. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/changelog.md +15 -4
  3. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/reference/commands.md +33 -0
  4. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/pyproject.toml +1 -1
  5. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/commands.py +2 -0
  6. sqlsaber-0.29.0/src/sqlsaber/cli/theme.py +146 -0
  7. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/theme/manager.py +87 -77
  8. sqlsaber-0.29.0/tests/test_theme/test_manager.py +38 -0
  9. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/uv.lock +1 -1
  10. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.github/workflows/claude-code-review.yml +0 -0
  11. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.github/workflows/claude.yml +0 -0
  12. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.github/workflows/deploy-docs.yml +0 -0
  13. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.github/workflows/publish.yml +0 -0
  14. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.github/workflows/test.yml +0 -0
  15. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.gitignore +0 -0
  16. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/.python-version +0 -0
  17. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/AGENT.md +0 -0
  18. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/CLAUDE.md +0 -0
  19. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/LICENSE +0 -0
  20. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/README.md +0 -0
  21. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/.gitignore +0 -0
  22. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/.vscode/extensions.json +0 -0
  23. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/.vscode/launch.json +0 -0
  24. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/CLAUDE.md +0 -0
  25. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/astro.config.mjs +0 -0
  26. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/package-lock.json +0 -0
  27. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/package.json +0 -0
  28. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/public/CNAME +0 -0
  29. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/public/favicon.svg +0 -0
  30. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/assets/sqlsaber-hero.svg +0 -0
  31. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/authentication.mdx +0 -0
  32. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/database-setup.mdx +0 -0
  33. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/getting-started.mdx +0 -0
  34. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/memory.mdx +0 -0
  35. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/models.mdx +0 -0
  36. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/queries.mdx +0 -0
  37. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/guides/threads.md +0 -0
  38. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/index.mdx +0 -0
  39. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content/docs/installation.mdx +0 -0
  40. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/content.config.ts +0 -0
  41. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/src/styles/global.css +0 -0
  42. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/docs/tsconfig.json +0 -0
  43. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/legislators.db +0 -0
  44. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/pytest.ini +0 -0
  45. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/sqlsaber.gif +0 -0
  46. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/sqlsaber.svg +0 -0
  47. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/__init__.py +0 -0
  48. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/__main__.py +0 -0
  49. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/agents/__init__.py +0 -0
  50. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/agents/base.py +0 -0
  51. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/agents/mcp.py +0 -0
  52. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/agents/pydantic_ai_agent.py +0 -0
  53. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/application/__init__.py +0 -0
  54. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/application/auth_setup.py +0 -0
  55. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/application/db_setup.py +0 -0
  56. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/application/model_selection.py +0 -0
  57. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/application/prompts.py +0 -0
  58. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/__init__.py +0 -0
  59. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/auth.py +0 -0
  60. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/completers.py +0 -0
  61. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/database.py +0 -0
  62. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/display.py +0 -0
  63. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/interactive.py +0 -0
  64. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/memory.py +0 -0
  65. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/models.py +0 -0
  66. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/onboarding.py +0 -0
  67. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/streaming.py +0 -0
  68. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/cli/threads.py +0 -0
  69. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/__init__.py +0 -0
  70. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/api_keys.py +0 -0
  71. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/auth.py +0 -0
  72. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/database.py +0 -0
  73. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/oauth_flow.py +0 -0
  74. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
  75. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/providers.py +0 -0
  76. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/config/settings.py +0 -0
  77. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/__init__.py +0 -0
  78. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/base.py +0 -0
  79. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/csv.py +0 -0
  80. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/duckdb.py +0 -0
  81. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/mysql.py +0 -0
  82. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/postgresql.py +0 -0
  83. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/resolver.py +0 -0
  84. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/schema.py +0 -0
  85. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/database/sqlite.py +0 -0
  86. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/mcp/__init__.py +0 -0
  87. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/mcp/mcp.py +0 -0
  88. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/memory/__init__.py +0 -0
  89. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/memory/manager.py +0 -0
  90. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/memory/storage.py +0 -0
  91. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/theme/__init__.py +0 -0
  92. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/threads/__init__.py +0 -0
  93. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/threads/storage.py +0 -0
  94. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/__init__.py +0 -0
  95. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/base.py +0 -0
  96. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/enums.py +0 -0
  97. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/instructions.py +0 -0
  98. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/registry.py +0 -0
  99. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/sql_guard.py +0 -0
  100. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/src/sqlsaber/tools/sql_tools.py +0 -0
  101. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/__init__.py +0 -0
  102. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/conftest.py +0 -0
  103. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_cli/__init__.py +0 -0
  104. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_cli/test_auth_reset.py +0 -0
  105. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_cli/test_commands.py +0 -0
  106. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_cli/test_threads.py +0 -0
  107. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_config/__init__.py +0 -0
  108. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_config/test_database.py +0 -0
  109. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_config/test_oauth.py +0 -0
  110. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_config/test_providers.py +0 -0
  111. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_config/test_settings.py +0 -0
  112. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/__init__.py +0 -0
  113. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_connection.py +0 -0
  114. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_csv_connection.py +0 -0
  115. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_csv_module.py +0 -0
  116. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_duckdb_module.py +0 -0
  117. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_schema.py +0 -0
  118. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_schema_display.py +0 -0
  119. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_sqlite_module.py +0 -0
  120. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database/test_timeout.py +0 -0
  121. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_database_resolver.py +0 -0
  122. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_threads_storage.py +0 -0
  123. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_tools/__init__.py +0 -0
  124. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_tools/test_base.py +0 -0
  125. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_tools/test_instructions.py +0 -0
  126. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_tools/test_registry.py +0 -0
  127. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_tools/test_sql_guard.py +0 -0
  128. {sqlsaber-0.28.0 → sqlsaber-0.29.0}/tests/test_tools/test_sql_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.28.0
3
+ Version: 0.29.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -7,15 +7,26 @@ All notable changes to SQLsaber will be documented here.
7
7
 
8
8
  ### Unreleased
9
9
 
10
+ ### v0.29.0 - 2025-10-04
11
+
12
+ #### Added
13
+
14
+ - Theme management commands
15
+ - `saber theme set` - Interactively select from all available Pygments themes with searchable list
16
+ - `saber theme reset` - Reset to default theme (nord)
17
+ - Theme configuration persists across sessions
18
+ - Environment variable `SQLSABER_THEME` can override configured theme
19
+
20
+ #### Changed
21
+
22
+ - Theme manager now derives semantic colors directly from selected Pygments styles, enabling out-of-the-box support for any upstream theme while retaining user overrides and fallbacks.
23
+
10
24
  ### v0.28.0 - 2025-10-03
11
25
 
12
26
  #### Added
13
27
 
14
28
  - Unified theming system
15
- - 7 built-in themes with exact Pygments color matching
16
- - Dark themes: `nord` (default), `dracula`, `one-dark`, `material`, `lightbulb`
17
- - Light themes: `solarized-light`, `vs`
18
- - Easy theme switching via `SQLSABER_THEME` environment variable or config file
29
+ - Easy theme switching via `SQLSABER_THEME` environment variable or config file
19
30
 
20
31
  #### Changed
21
32
 
@@ -310,6 +310,39 @@ saber models reset
310
310
 
311
311
  ---
312
312
 
313
+ ### `saber theme`
314
+
315
+ Manage syntax highlighting theme settings.
316
+
317
+ #### `saber theme set`
318
+
319
+ Interactively select a syntax highlighting theme from all available Pygments themes.
320
+
321
+ **Usage:**
322
+
323
+ ```bash
324
+ saber theme set
325
+ ```
326
+
327
+ You can also set themes via environment variable:
328
+
329
+ ```bash
330
+ export SQLSABER_THEME=dracula
331
+ saber
332
+ ```
333
+
334
+ #### `saber theme reset`
335
+
336
+ Reset to the default theme (nord).
337
+
338
+ **Usage:**
339
+
340
+ ```bash
341
+ saber theme reset
342
+ ```
343
+
344
+ ---
345
+
313
346
  ### `saber threads`
314
347
 
315
348
  Manage conversation threads.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlsaber"
3
- version = "0.28.0"
3
+ version = "0.29.0"
4
4
  description = "SQLsaber - Open-source agentic SQL assistant"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -11,6 +11,7 @@ from sqlsaber.cli.database import create_db_app
11
11
  from sqlsaber.cli.memory import create_memory_app
12
12
  from sqlsaber.cli.models import create_models_app
13
13
  from sqlsaber.cli.onboarding import needs_onboarding, run_onboarding
14
+ from sqlsaber.cli.theme import create_theme_app
14
15
  from sqlsaber.cli.threads import create_threads_app
15
16
 
16
17
  # Lazy imports - only import what's needed for CLI parsing
@@ -35,6 +36,7 @@ app.command(create_auth_app(), name="auth")
35
36
  app.command(create_db_app(), name="db")
36
37
  app.command(create_memory_app(), name="memory")
37
38
  app.command(create_models_app(), name="models")
39
+ app.command(create_theme_app(), name="theme")
38
40
  app.command(create_threads_app(), name="threads")
39
41
 
40
42
  console = create_console()
@@ -0,0 +1,146 @@
1
+ """Theme management CLI commands."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import cyclopts
10
+ import questionary
11
+ from platformdirs import user_config_dir
12
+ from pygments.styles import get_all_styles
13
+
14
+ from sqlsaber.theme.manager import DEFAULT_THEME_NAME, create_console
15
+
16
+ console = create_console()
17
+
18
+ # Create the theme management CLI app
19
+ theme_app = cyclopts.App(
20
+ name="theme",
21
+ help="Manage theme settings",
22
+ )
23
+
24
+
25
+ class ThemeManager:
26
+ """Manages theme configuration persistence."""
27
+
28
+ def __init__(self):
29
+ self.config_dir = Path(user_config_dir("sqlsaber"))
30
+ self.config_file = self.config_dir / "theme.json"
31
+
32
+ def _ensure_config_dir(self) -> None:
33
+ """Ensure config directory exists."""
34
+ self.config_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def _load_config(self) -> dict:
37
+ """Load theme configuration from file."""
38
+ if not self.config_file.exists():
39
+ return {}
40
+
41
+ try:
42
+ with open(self.config_file, "r") as f:
43
+ return json.load(f)
44
+ except Exception:
45
+ return {}
46
+
47
+ def _save_config(self, config: dict) -> None:
48
+ """Save theme configuration to file."""
49
+ self._ensure_config_dir()
50
+
51
+ with open(self.config_file, "w") as f:
52
+ json.dump(config, f, indent=2)
53
+
54
+ def get_current_theme(self) -> str:
55
+ """Get the currently configured theme."""
56
+ config = self._load_config()
57
+ env_theme = os.getenv("SQLSABER_THEME")
58
+ if env_theme:
59
+ return env_theme
60
+ return config.get("theme", {}).get("pygments_style") or DEFAULT_THEME_NAME
61
+
62
+ def set_theme(self, theme_name: str) -> bool:
63
+ """Set the current theme."""
64
+ try:
65
+ config = self._load_config()
66
+ if "theme" not in config:
67
+ config["theme"] = {}
68
+ config["theme"]["name"] = theme_name
69
+ config["theme"]["pygments_style"] = theme_name
70
+ self._save_config(config)
71
+ return True
72
+ except Exception as e:
73
+ console.print(f"[error]Error setting theme: {e}[/error]")
74
+ return False
75
+
76
+ def reset_theme(self) -> bool:
77
+ """Reset to default theme."""
78
+ try:
79
+ if self.config_file.exists():
80
+ self.config_file.unlink()
81
+ return True
82
+ except Exception as e:
83
+ console.print(f"[error]Error resetting theme: {e}[/error]")
84
+ return False
85
+
86
+ def get_available_themes(self) -> list[str]:
87
+ """Get list of available Pygments themes."""
88
+ return sorted(get_all_styles())
89
+
90
+
91
+ theme_manager = ThemeManager()
92
+
93
+
94
+ @theme_app.command
95
+ def set():
96
+ """Set the theme to use for syntax highlighting."""
97
+
98
+ async def interactive_set():
99
+ themes = theme_manager.get_available_themes()
100
+ current_theme = theme_manager.get_current_theme()
101
+
102
+ # Create choices with current theme highlighted
103
+ choices = [
104
+ questionary.Choice(
105
+ title=f"{theme} (current)" if theme == current_theme else theme,
106
+ value=theme,
107
+ )
108
+ for theme in themes
109
+ ]
110
+
111
+ selected_theme = await questionary.select(
112
+ "Select a theme:",
113
+ choices=choices,
114
+ default=current_theme,
115
+ use_search_filter=True,
116
+ use_jk_keys=False,
117
+ ).ask_async()
118
+
119
+ if selected_theme:
120
+ if theme_manager.set_theme(selected_theme):
121
+ console.print(f"[success]✓ Theme set to: {selected_theme}[/success]")
122
+ else:
123
+ console.print("[error]✗ Failed to set theme[/error]")
124
+ sys.exit(1)
125
+ else:
126
+ console.print("[warning]Operation cancelled[/warning]")
127
+
128
+ asyncio.run(interactive_set())
129
+
130
+
131
+ @theme_app.command
132
+ def reset():
133
+ """Reset to the default theme."""
134
+
135
+ if theme_manager.reset_theme():
136
+ console.print(
137
+ f"[success]✓ Theme reset to default: {DEFAULT_THEME_NAME}[/success]"
138
+ )
139
+ else:
140
+ console.print("[error]✗ Failed to reset theme[/error]")
141
+ sys.exit(1)
142
+
143
+
144
+ def create_theme_app() -> cyclopts.App:
145
+ """Return the theme management CLI app."""
146
+ return theme_app
@@ -1,7 +1,7 @@
1
1
  """Theme management for unified theming across Rich and prompt_toolkit."""
2
2
 
3
+ import json
3
4
  import os
4
- import tomllib
5
5
  from dataclasses import dataclass
6
6
  from functools import lru_cache
7
7
  from typing import Dict
@@ -10,6 +10,8 @@ from platformdirs import user_config_dir
10
10
  from prompt_toolkit.styles import Style as PTStyle
11
11
  from prompt_toolkit.styles.pygments import style_from_pygments_cls
12
12
  from pygments.styles import get_style_by_name
13
+ from pygments.token import Token
14
+ from pygments.util import ClassNotFound
13
15
  from rich.console import Console
14
16
  from rich.theme import Theme
15
17
 
@@ -43,89 +45,97 @@ DEFAULT_ROLE_PALETTE = {
43
45
  "title": "bold $success",
44
46
  }
45
47
 
46
- # Theme presets using exact Pygments colors
47
- THEME_PRESETS = {
48
- # Nord - exact colors from pygments nord theme
49
- "nord": {
50
- "primary": "#81a1c1", # Keyword (frost)
51
- "accent": "#b48ead", # Number (aurora purple)
52
- "success": "#a3be8c", # String (aurora green)
53
- "warning": "#ebcb8b", # String.Escape (aurora yellow)
54
- "error": "#bf616a", # Error/Generic.Error (aurora red)
55
- "info": "#88c0d0", # Name.Function (frost cyan)
56
- "muted": "dim",
57
- },
58
- # Dracula - exact colors from pygments dracula theme
59
- "dracula": {
60
- "primary": "#bd93f9", # purple
61
- "accent": "#ff79c6", # pink
62
- "success": "#50fa7b", # green
63
- "warning": "#f1fa8c", # yellow
64
- "error": "#ff5555", # red
65
- "info": "#8be9fd", # cyan
66
- "muted": "dim",
67
- },
68
- # Solarized Light - exact colors from pygments solarized-light theme
69
- "solarized-light": {
70
- "primary": "#268bd2", # blue
71
- "accent": "#d33682", # magenta
72
- "success": "#859900", # green
73
- "warning": "#b58900", # yellow
74
- "error": "#dc322f", # red
75
- "info": "#2aa198", # cyan
76
- "muted": "dim",
77
- },
78
- # VS (Visual Studio Light) - exact colors from pygments vs theme
79
- "vs": {
80
- "primary": "#0000ff", # Keyword (blue)
81
- "accent": "#2b91af", # Keyword.Type/Name.Class
82
- "success": "#008000", # Comment (green)
83
- "warning": "#b58900", # (using solarized yellow as fallback)
84
- "error": "#dc322f", # (using solarized red as fallback)
85
- "info": "#2aa198", # (using solarized cyan as fallback)
86
- "muted": "dim",
87
- },
88
- # Material (approximation based on material design colors)
89
- "material": {
90
- "primary": "#89ddff", # cyan
91
- "accent": "#f07178", # pink/red
92
- "success": "#c3e88d", # green
93
- "warning": "#ffcb6b", # yellow
94
- "error": "#ff5370", # red
95
- "info": "#82aaff", # blue
96
- "muted": "dim",
97
- },
98
- # One Dark - exact colors from pygments one-dark theme
99
- "one-dark": {
100
- "primary": "#c678dd", # Keyword (purple)
101
- "accent": "#e06c75", # Name (red)
102
- "success": "#98c379", # String (green)
103
- "warning": "#e5c07b", # Keyword.Type (yellow)
104
- "error": "#e06c75", # Name (red, used for errors)
105
- "info": "#61afef", # Name.Function (blue)
106
- "muted": "dim",
107
- },
108
- # Lightbulb - exact colors from pygments lightbulb theme (minimal dark)
109
- "lightbulb": {
110
- "primary": "#73d0ff", # Keyword.Type/Name.Class (blue_1)
111
- "accent": "#dfbfff", # Number (magenta_1)
112
- "success": "#d5ff80", # String (green_1)
113
- "warning": "#ffd173", # Name.Function (yellow_1)
114
- "error": "#f88f7f", # Error (red_1)
115
- "info": "#95e6cb", # Name.Entity (cyan_1)
116
- "muted": "dim",
117
- },
48
+ ROLE_TOKEN_PREFERENCES: dict[str, tuple] = {
49
+ "primary": (
50
+ Token.Keyword,
51
+ Token.Keyword.Namespace,
52
+ Token.Name.Tag,
53
+ ),
54
+ "accent": (
55
+ Token.Name.Tag,
56
+ Token.Keyword.Type,
57
+ Token.Literal.Number,
58
+ Token.Operator.Word,
59
+ ),
60
+ "success": (
61
+ Token.Literal.String,
62
+ Token.Generic.Inserted,
63
+ Token.Name.Attribute,
64
+ ),
65
+ "warning": (
66
+ Token.Literal.String.Escape,
67
+ Token.Name.Constant,
68
+ Token.Generic.Emph,
69
+ ),
70
+ "error": (
71
+ Token.Error,
72
+ Token.Generic.Error,
73
+ Token.Generic.Deleted,
74
+ Token.Name.Exception,
75
+ ),
76
+ "info": (
77
+ Token.Name.Function,
78
+ Token.Name.Builtin,
79
+ Token.Keyword.Type,
80
+ ),
81
+ "muted": (
82
+ Token.Comment,
83
+ Token.Generic.Subheading,
84
+ Token.Text,
85
+ ),
118
86
  }
119
87
 
120
88
 
89
+ def _normalize_hex(color: str | None) -> str | None:
90
+ if not color:
91
+ return None
92
+ color = color.strip()
93
+ if not color:
94
+ return None
95
+ if color.startswith("#"):
96
+ color = color[1:]
97
+ if len(color) == 3:
98
+ color = "".join(ch * 2 for ch in color)
99
+ if len(color) != 6:
100
+ return None
101
+ return f"#{color.lower()}"
102
+
103
+
104
+ def _build_role_palette_from_style(style_name: str) -> dict[str, str]:
105
+ try:
106
+ style_cls = get_style_by_name(style_name)
107
+ except ClassNotFound:
108
+ return {}
109
+
110
+ palette: dict[str, str] = {}
111
+ try:
112
+ base_color = _normalize_hex(style_cls.style_for_token(Token.Text).get("color"))
113
+ except KeyError:
114
+ base_color = None
115
+ for role, tokens in ROLE_TOKEN_PREFERENCES.items():
116
+ for token in tokens:
117
+ try:
118
+ style_def = style_cls.style_for_token(token)
119
+ except KeyError:
120
+ continue
121
+ color = _normalize_hex(style_def.get("color"))
122
+ if not color or color == base_color:
123
+ continue
124
+ if role == "accent" and color == palette.get("primary"):
125
+ continue
126
+ palette[role] = color
127
+ break
128
+ return palette
129
+
130
+
121
131
  def _load_user_theme_config() -> dict:
122
132
  """Load theme configuration from user config directory."""
123
133
  cfg_dir = user_config_dir("sqlsaber")
124
- path = os.path.join(cfg_dir, "theme.toml")
134
+ path = os.path.join(cfg_dir, "theme.json")
125
135
  if not os.path.exists(path):
126
136
  return {}
127
- with open(path, "rb") as f:
128
- return tomllib.load(f)
137
+ with open(path, "r") as f:
138
+ return json.load(f)
129
139
 
130
140
 
131
141
  def _resolve_refs(palette: dict[str, str]) -> dict[str, str]:
@@ -204,7 +214,7 @@ def get_theme_manager() -> ThemeManager:
204
214
  pygments_style = user_cfg.get("theme", {}).get("pygments_style") or name
205
215
 
206
216
  roles = dict(DEFAULT_ROLE_PALETTE)
207
- roles.update(THEME_PRESETS.get(name, {}))
217
+ roles.update(_build_role_palette_from_style(pygments_style))
208
218
  roles.update(user_cfg.get("roles", {}))
209
219
 
210
220
  cfg = ThemeConfig(name=name, pygments_style=pygments_style, roles=roles)
@@ -0,0 +1,38 @@
1
+ import pytest
2
+
3
+ from sqlsaber.theme import manager
4
+
5
+
6
+ @pytest.fixture(autouse=True)
7
+ def clear_theme_cache():
8
+ manager.get_theme_manager.cache_clear()
9
+ yield
10
+ manager.get_theme_manager.cache_clear()
11
+
12
+
13
+ def test_dynamic_palette_uses_pygments_colors(monkeypatch):
14
+ monkeypatch.setenv("SQLSABER_THEME", "zenburn")
15
+
16
+ tm = manager.get_theme_manager()
17
+
18
+ assert tm.pygments_style_name == "zenburn"
19
+ assert tm.style("primary") == "#efdcbc"
20
+ assert tm.style("accent") == "#e89393"
21
+ assert tm.style("success") == "#cc9393"
22
+ assert tm.style("error") == "#e37170"
23
+ assert tm.style("info") == "#efef8f"
24
+ assert tm.style("muted") == "#7f9f7f"
25
+
26
+ monkeypatch.delenv("SQLSABER_THEME", raising=False)
27
+
28
+
29
+ def test_unknown_theme_falls_back_to_defaults(monkeypatch):
30
+ monkeypatch.setenv("SQLSABER_THEME", "does-not-exist")
31
+
32
+ tm = manager.get_theme_manager()
33
+
34
+ assert tm.pygments_style_name == "does-not-exist"
35
+ assert tm.style("primary") == manager.DEFAULT_ROLE_PALETTE["primary"]
36
+ assert tm.style("accent") == manager.DEFAULT_ROLE_PALETTE["accent"]
37
+
38
+ monkeypatch.delenv("SQLSABER_THEME", raising=False)
@@ -1888,7 +1888,7 @@ wheels = [
1888
1888
 
1889
1889
  [[package]]
1890
1890
  name = "sqlsaber"
1891
- version = "0.28.0"
1891
+ version = "0.29.0"
1892
1892
  source = { editable = "." }
1893
1893
  dependencies = [
1894
1894
  { name = "aiomysql" },
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