cmdbox-cli 1.0.0__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.
- cmdbox/__init__.py +0 -0
- cmdbox/cli/__init__.py +0 -0
- cmdbox/cli/app.py +125 -0
- cmdbox/cli/commands/__init__.py +0 -0
- cmdbox/cli/commands/alias_fallback.py +102 -0
- cmdbox/cli/commands/command_crud.py +429 -0
- cmdbox/cli/commands/command_run.py +255 -0
- cmdbox/cli/commands/history.py +109 -0
- cmdbox/cli/commands/init.py +54 -0
- cmdbox/cli/commands/settings.py +62 -0
- cmdbox/cli/commands/tag_crud.py +277 -0
- cmdbox/cli/commands/variable_crud.py +349 -0
- cmdbox/cli/common/__init__.py +0 -0
- cmdbox/cli/common/errors.py +58 -0
- cmdbox/cli/common/update_fields.py +88 -0
- cmdbox/cli/completions/__init__.py +0 -0
- cmdbox/cli/completions/commands.py +26 -0
- cmdbox/cli/completions/fields.py +31 -0
- cmdbox/cli/completions/tags.py +24 -0
- cmdbox/cli/completions/variables.py +26 -0
- cmdbox/cli/handlers/__init__.py +0 -0
- cmdbox/cli/handlers/command_handlers.py +357 -0
- cmdbox/cli/handlers/common_handlers.py +15 -0
- cmdbox/cli/handlers/history_handlers.py +94 -0
- cmdbox/cli/handlers/init_handler.py +127 -0
- cmdbox/cli/handlers/run_handler.py +178 -0
- cmdbox/cli/handlers/settings_handler.py +59 -0
- cmdbox/cli/handlers/tag_handlers.py +220 -0
- cmdbox/cli/handlers/variable_handlers.py +272 -0
- cmdbox/cli/prompts/__init__.py +0 -0
- cmdbox/cli/prompts/completers.py +161 -0
- cmdbox/cli/prompts/prompts.py +108 -0
- cmdbox/cli/prompts/validators.py +46 -0
- cmdbox/cli/ui/__init__.py +0 -0
- cmdbox/cli/ui/console.py +31 -0
- cmdbox/cli/ui/editor.py +141 -0
- cmdbox/cli/ui/presenters/__init__.py +0 -0
- cmdbox/cli/ui/presenters/app_presenter.py +8 -0
- cmdbox/cli/ui/presenters/command_presenter.py +168 -0
- cmdbox/cli/ui/presenters/history_presenter.py +83 -0
- cmdbox/cli/ui/presenters/init_instructions.py +52 -0
- cmdbox/cli/ui/presenters/init_presenter.py +57 -0
- cmdbox/cli/ui/presenters/result_presenter.py +144 -0
- cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
- cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
- cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
- cmdbox/cli/ui/primitives.py +410 -0
- cmdbox/cli/ui/theme.py +43 -0
- cmdbox/cli/ui/theme_builder.py +49 -0
- cmdbox/common/__init__.py +0 -0
- cmdbox/common/io.py +34 -0
- cmdbox/container.py +156 -0
- cmdbox/core/__init__.py +0 -0
- cmdbox/core/fields.py +48 -0
- cmdbox/core/paths.py +52 -0
- cmdbox/database.py +65 -0
- cmdbox/exceptions.py +10 -0
- cmdbox/init/__init__.py +0 -0
- cmdbox/init/detect.py +82 -0
- cmdbox/init/integrations/bash.sh +10 -0
- cmdbox/init/integrations/cmd.bat +14 -0
- cmdbox/init/integrations/fish.fish +11 -0
- cmdbox/init/integrations/powershell.ps1 +14 -0
- cmdbox/init/integrations/zsh.sh +10 -0
- cmdbox/init/io.py +68 -0
- cmdbox/init/specs.py +54 -0
- cmdbox/logging_setup/__init__.py +0 -0
- cmdbox/logging_setup/log_config.py +123 -0
- cmdbox/logging_setup/log_decorators.py +40 -0
- cmdbox/logging_setup/log_handlers.py +94 -0
- cmdbox/migrations/__init__.py +1 -0
- cmdbox/migrations/errors.py +10 -0
- cmdbox/migrations/runner.py +127 -0
- cmdbox/migrations/versions/__init__.py +0 -0
- cmdbox/models.py +165 -0
- cmdbox/repositories/__init__.py +0 -0
- cmdbox/repositories/base_repository.py +181 -0
- cmdbox/repositories/command_repository.py +391 -0
- cmdbox/repositories/errors.py +120 -0
- cmdbox/repositories/history_repository.py +155 -0
- cmdbox/repositories/results.py +37 -0
- cmdbox/repositories/tag_repository.py +91 -0
- cmdbox/repositories/validators.py +256 -0
- cmdbox/repositories/variable_repository.py +324 -0
- cmdbox/resolve/__init__.py +0 -0
- cmdbox/resolve/errors.py +65 -0
- cmdbox/resolve/lookup.py +137 -0
- cmdbox/resolve/resolver.py +402 -0
- cmdbox/resolve/type_defs.py +96 -0
- cmdbox/runtime/__init__.py +0 -0
- cmdbox/runtime/executor.py +454 -0
- cmdbox/runtime/results.py +25 -0
- cmdbox/runtime/shell.py +90 -0
- cmdbox/services/__init__.py +0 -0
- cmdbox/services/command_services.py +261 -0
- cmdbox/services/errors.py +37 -0
- cmdbox/services/field_selection.py +162 -0
- cmdbox/services/history_service.py +68 -0
- cmdbox/services/run_service.py +204 -0
- cmdbox/services/tag_services.py +134 -0
- cmdbox/services/variable_services.py +224 -0
- cmdbox/settings/__init__.py +0 -0
- cmdbox/settings/models.py +129 -0
- cmdbox/settings/settings_repository.py +36 -0
- cmdbox/settings/settings_service.py +144 -0
- cmdbox/version.py +1 -0
- cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
- cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
- cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
- cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from typing import Any, Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.align import Align
|
|
7
|
+
from rich.console import Group, RenderableType
|
|
8
|
+
from rich.padding import Padding
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.rule import Rule
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class UiStyles:
|
|
17
|
+
# General
|
|
18
|
+
title: str = "ui.title"
|
|
19
|
+
subtitle: str = "ui.subtitle"
|
|
20
|
+
muted: str = "ui.muted"
|
|
21
|
+
dim: str = "ui.dim"
|
|
22
|
+
|
|
23
|
+
# Status
|
|
24
|
+
success: str = "status.success"
|
|
25
|
+
info: str = "status.info"
|
|
26
|
+
warning: str = "status.warning"
|
|
27
|
+
error: str = "status.error"
|
|
28
|
+
|
|
29
|
+
# Panels / borders
|
|
30
|
+
panel_border: str = "ui.border"
|
|
31
|
+
panel_title: str = "ui.panel_title"
|
|
32
|
+
|
|
33
|
+
# Tables
|
|
34
|
+
table_header: str = "ui.table_header"
|
|
35
|
+
table_caption: str = "ui.caption"
|
|
36
|
+
kv_key: str = "ui.kv.key"
|
|
37
|
+
kv_value: str = "ui.kv.value"
|
|
38
|
+
|
|
39
|
+
# Code-ish output (semantic)
|
|
40
|
+
code: str = "code"
|
|
41
|
+
code_inline: str = "code.inline"
|
|
42
|
+
code_block: str = "code.block"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def to_text(value: Any, *, style: str | None = None) -> Text:
|
|
46
|
+
"""
|
|
47
|
+
Converts a given value into a formatted textual representation, optionally applying
|
|
48
|
+
a text style.
|
|
49
|
+
|
|
50
|
+
The function takes various types of input (e.g., None, datetime, date, or other types)
|
|
51
|
+
and returns a corresponding textual representation as a `Text` object. If a style
|
|
52
|
+
is specified, it will stylize the output text.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
value: The input value to convert. Can be of type None, datetime, date,
|
|
56
|
+
or any type that can be converted into a string representation.
|
|
57
|
+
style: An optional string specifying the style to apply to the output text.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Text: A formatted textual representation of the input value.
|
|
61
|
+
"""
|
|
62
|
+
if value is None:
|
|
63
|
+
t = Text("null")
|
|
64
|
+
elif isinstance(value, Text):
|
|
65
|
+
if style:
|
|
66
|
+
value.stylize(style)
|
|
67
|
+
return value
|
|
68
|
+
elif isinstance(value, datetime):
|
|
69
|
+
t = Text(value.isoformat(sep=" ", timespec="seconds"))
|
|
70
|
+
elif isinstance(value, date):
|
|
71
|
+
t = Text(value.isoformat())
|
|
72
|
+
else:
|
|
73
|
+
t = Text(str(value))
|
|
74
|
+
|
|
75
|
+
if style:
|
|
76
|
+
t.stylize(style)
|
|
77
|
+
return t
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def spacer(lines: int = 1) -> RenderableType:
|
|
81
|
+
return Text("\n" * max(0, lines))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def divider(title: str | None = None, *, style: str = "ui.dim") -> Rule:
|
|
85
|
+
if title:
|
|
86
|
+
return Rule(Text(title), style=style)
|
|
87
|
+
return Rule(style=style)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def col(name: str, *, style: str = "", **kwargs) -> tuple[str, dict[str, Any]]:
|
|
91
|
+
"""
|
|
92
|
+
A helper function that generates a table column.
|
|
93
|
+
|
|
94
|
+
This function facilitates the creation of a tuple that consists of a string name and a
|
|
95
|
+
dictionary. The dictionary contains a "style" key with a specified value and may also include
|
|
96
|
+
other key-value pairs provided as additional arguments.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
name (str): The name or identifier for the column.
|
|
100
|
+
style (str, optional): A string specifying the style attribute. Defaults to an empty string.
|
|
101
|
+
**kwargs: Additional key-value pairs to include in the dictionary.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
tuple[str, dict[str, Any]]: A tuple where the first element is the provided name, and the
|
|
105
|
+
second element is a dictionary containing the "style" attribute along with any additional
|
|
106
|
+
key-value pairs.
|
|
107
|
+
"""
|
|
108
|
+
data = {"style": style}
|
|
109
|
+
data.update(kwargs)
|
|
110
|
+
return name, data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def banner(
|
|
114
|
+
title: str,
|
|
115
|
+
subtitle: str | None = None,
|
|
116
|
+
*,
|
|
117
|
+
status: str | None = None,
|
|
118
|
+
styles: UiStyles = UiStyles(),
|
|
119
|
+
) -> RenderableType:
|
|
120
|
+
"""
|
|
121
|
+
A consistent header block.
|
|
122
|
+
|
|
123
|
+
status: one of "success" | "info" | "warning" | "error" or a Theme style name
|
|
124
|
+
"""
|
|
125
|
+
if status:
|
|
126
|
+
# Allow semantic names or full style names
|
|
127
|
+
status_style = getattr(styles, status, status)
|
|
128
|
+
border_style = status_style
|
|
129
|
+
title_style = status_style
|
|
130
|
+
else:
|
|
131
|
+
border_style = styles.panel_border
|
|
132
|
+
title_style = styles.title
|
|
133
|
+
|
|
134
|
+
title_text = Text(title, style=title_style)
|
|
135
|
+
|
|
136
|
+
parts: list[RenderableType] = [title_text]
|
|
137
|
+
|
|
138
|
+
if subtitle:
|
|
139
|
+
parts.append(Text(subtitle, style=styles.subtitle))
|
|
140
|
+
|
|
141
|
+
return Panel(
|
|
142
|
+
Group(*parts),
|
|
143
|
+
border_style=border_style,
|
|
144
|
+
padding=(0, 1),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def tag_pill(
|
|
149
|
+
name: str,
|
|
150
|
+
*,
|
|
151
|
+
style: str = "tag.pill",
|
|
152
|
+
padding: tuple[int, int] = (0, 1),
|
|
153
|
+
) -> RenderableType:
|
|
154
|
+
text = Text(name, style=style)
|
|
155
|
+
return Padding(text, padding)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def tag_block(
|
|
159
|
+
tags: Sequence[str],
|
|
160
|
+
*,
|
|
161
|
+
style: str = "tag.pill",
|
|
162
|
+
separator: str = " ",
|
|
163
|
+
) -> RenderableType:
|
|
164
|
+
text = Text()
|
|
165
|
+
|
|
166
|
+
for i, tag in enumerate(tags):
|
|
167
|
+
if i > 0:
|
|
168
|
+
text.append(separator)
|
|
169
|
+
text.append(f" {tag} ", style=style)
|
|
170
|
+
|
|
171
|
+
return text
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def status_line(
|
|
175
|
+
message: str,
|
|
176
|
+
*,
|
|
177
|
+
status: str = "info",
|
|
178
|
+
styles: UiStyles = UiStyles(),
|
|
179
|
+
) -> Text:
|
|
180
|
+
"""
|
|
181
|
+
One line status message with semantic styling.
|
|
182
|
+
"""
|
|
183
|
+
status_style = getattr(styles, status, status)
|
|
184
|
+
return Text(message, style=status_style)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def bullet_list(
|
|
188
|
+
items: Sequence[Any],
|
|
189
|
+
*,
|
|
190
|
+
bullet: str = "•",
|
|
191
|
+
item_style: str = "ui.kv.value",
|
|
192
|
+
bullet_style: str = "ui.muted",
|
|
193
|
+
styles: UiStyles = UiStyles(),
|
|
194
|
+
empty_message: str | None = None,
|
|
195
|
+
) -> RenderableType:
|
|
196
|
+
"""
|
|
197
|
+
Simple bullet list that is safe against markup.
|
|
198
|
+
"""
|
|
199
|
+
if not items:
|
|
200
|
+
if empty_message is None:
|
|
201
|
+
return Text("")
|
|
202
|
+
return Text(empty_message, style=styles.muted)
|
|
203
|
+
|
|
204
|
+
lines: list[RenderableType] = []
|
|
205
|
+
for item in items:
|
|
206
|
+
b = Text(f"{bullet} ", style=bullet_style if bullet_style else styles.muted)
|
|
207
|
+
v = to_text(item, style=item_style if item_style else styles.kv_value)
|
|
208
|
+
lines.append(Text.assemble(b, v))
|
|
209
|
+
|
|
210
|
+
return Group(*lines)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def kv_table(
|
|
214
|
+
rows: Sequence[tuple[str, Any]],
|
|
215
|
+
*,
|
|
216
|
+
key_style: str = "ui.kv.key",
|
|
217
|
+
value_style: str = "ui.kv.value",
|
|
218
|
+
show_header: bool = False,
|
|
219
|
+
styles: UiStyles = UiStyles(),
|
|
220
|
+
) -> Table:
|
|
221
|
+
"""
|
|
222
|
+
Two-column key/value table for detail views.
|
|
223
|
+
"""
|
|
224
|
+
table = Table(
|
|
225
|
+
show_header=show_header,
|
|
226
|
+
box=None,
|
|
227
|
+
pad_edge=False,
|
|
228
|
+
padding=(0, 1),
|
|
229
|
+
collapse_padding=True,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if show_header:
|
|
233
|
+
table.add_column("Field", style=styles.table_header)
|
|
234
|
+
table.add_column("Value", style=styles.table_header)
|
|
235
|
+
else:
|
|
236
|
+
table.add_column(
|
|
237
|
+
justify="right", style=key_style or styles.kv_key, no_wrap=True
|
|
238
|
+
)
|
|
239
|
+
table.add_column(style=value_style or styles.kv_value, overflow="fold")
|
|
240
|
+
|
|
241
|
+
for k, v in rows:
|
|
242
|
+
table.add_row(
|
|
243
|
+
to_text(k, style=key_style or styles.kv_key),
|
|
244
|
+
to_text(v, style=value_style or styles.kv_value),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return table
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def kv_panel(
|
|
251
|
+
title: str,
|
|
252
|
+
rows: Sequence[tuple[str, Any]],
|
|
253
|
+
*,
|
|
254
|
+
border_style: str = "ui.border",
|
|
255
|
+
styles: UiStyles = UiStyles(),
|
|
256
|
+
) -> Panel:
|
|
257
|
+
"""
|
|
258
|
+
Panel containing a key/value table.
|
|
259
|
+
"""
|
|
260
|
+
return Panel(
|
|
261
|
+
kv_table(rows, styles=styles),
|
|
262
|
+
title=Text(title, style=styles.panel_title),
|
|
263
|
+
border_style=border_style,
|
|
264
|
+
padding=(0, 1),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def table_panel(
|
|
269
|
+
title: str,
|
|
270
|
+
columns: Sequence[tuple[str, dict[str, Any]]],
|
|
271
|
+
rows: Sequence[Sequence[Any]],
|
|
272
|
+
*,
|
|
273
|
+
caption: str | None = None,
|
|
274
|
+
border_style: str = "ui.border",
|
|
275
|
+
styles: UiStyles = UiStyles(),
|
|
276
|
+
) -> Panel:
|
|
277
|
+
"""
|
|
278
|
+
Build a standard table inside a panel.
|
|
279
|
+
|
|
280
|
+
columns: [("Alias", {"style": "...", "no_wrap": True}), ...]
|
|
281
|
+
rows: list of row sequences. Each cell is converted with to_text.
|
|
282
|
+
"""
|
|
283
|
+
table = Table(
|
|
284
|
+
show_header=True,
|
|
285
|
+
header_style=styles.table_header,
|
|
286
|
+
pad_edge=False,
|
|
287
|
+
collapse_padding=True,
|
|
288
|
+
box=box.MINIMAL,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
for name, kwargs in columns:
|
|
292
|
+
table.add_column(name, **kwargs)
|
|
293
|
+
|
|
294
|
+
for row in rows:
|
|
295
|
+
table.add_row(*[to_text(cell) for cell in row])
|
|
296
|
+
|
|
297
|
+
if caption:
|
|
298
|
+
table.caption = caption
|
|
299
|
+
table.caption_style = styles.table_caption
|
|
300
|
+
|
|
301
|
+
return Panel(
|
|
302
|
+
table,
|
|
303
|
+
title=Text(title, style=styles.panel_title),
|
|
304
|
+
border_style=border_style,
|
|
305
|
+
padding=(0, 1),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def code_inline(
|
|
310
|
+
text: Any,
|
|
311
|
+
*,
|
|
312
|
+
style: str = "code.inline",
|
|
313
|
+
styles: UiStyles = UiStyles(),
|
|
314
|
+
) -> Text:
|
|
315
|
+
"""
|
|
316
|
+
Semantic inline code styling. Does not change terminal font, but gives a consistent look.
|
|
317
|
+
"""
|
|
318
|
+
# If you do not define code.inline, fall back to code, then to a generic style.
|
|
319
|
+
resolved = style
|
|
320
|
+
if style == "code.inline":
|
|
321
|
+
resolved = styles.code_inline or styles.code or "bold"
|
|
322
|
+
return to_text(text, style=resolved)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def code_block(
|
|
326
|
+
text: Any,
|
|
327
|
+
*,
|
|
328
|
+
title: str | None = None,
|
|
329
|
+
border_style: str = "ui.border",
|
|
330
|
+
style: str = "code.block",
|
|
331
|
+
styles: UiStyles = UiStyles(),
|
|
332
|
+
fit: bool = False,
|
|
333
|
+
) -> Panel:
|
|
334
|
+
"""
|
|
335
|
+
Panel for code-ish blocks (templates, resolved commands, previews).
|
|
336
|
+
"""
|
|
337
|
+
resolved = style
|
|
338
|
+
if style == "code.block":
|
|
339
|
+
resolved = styles.code_block or styles.code or "bold"
|
|
340
|
+
|
|
341
|
+
body = to_text(text, style=resolved)
|
|
342
|
+
|
|
343
|
+
panel_title = Text(title, style=styles.panel_title) if title else None
|
|
344
|
+
panel_cls = Panel.fit if fit else Panel
|
|
345
|
+
|
|
346
|
+
return panel_cls(
|
|
347
|
+
body,
|
|
348
|
+
title=panel_title,
|
|
349
|
+
border_style=border_style,
|
|
350
|
+
padding=(1, 1),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def centered(renderable: RenderableType) -> RenderableType:
|
|
355
|
+
return Align.center(renderable)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def stack(*renderables: RenderableType) -> RenderableType:
|
|
359
|
+
"""
|
|
360
|
+
Group renderables vertically.
|
|
361
|
+
"""
|
|
362
|
+
return Group(*renderables)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def section(
|
|
366
|
+
title: str,
|
|
367
|
+
body: RenderableType,
|
|
368
|
+
caption: str | None = None,
|
|
369
|
+
*,
|
|
370
|
+
border_style: str = "ui.border",
|
|
371
|
+
styles: UiStyles = UiStyles(),
|
|
372
|
+
) -> Panel:
|
|
373
|
+
"""
|
|
374
|
+
Generic titled panel wrapper.
|
|
375
|
+
"""
|
|
376
|
+
return Panel(
|
|
377
|
+
body,
|
|
378
|
+
title=Text(title, style=styles.panel_title),
|
|
379
|
+
subtitle=Text(caption, style="ui.caption") if caption else None,
|
|
380
|
+
border_style=border_style,
|
|
381
|
+
padding=(0, 1),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def summary_counts(
|
|
386
|
+
counts: Mapping[str, int],
|
|
387
|
+
*,
|
|
388
|
+
styles: UiStyles = UiStyles(),
|
|
389
|
+
) -> RenderableType:
|
|
390
|
+
"""
|
|
391
|
+
Small helper for displaying a few counts, like:
|
|
392
|
+
Commands: 12 Variables: 4 Tags: 9
|
|
393
|
+
"""
|
|
394
|
+
parts: list[Text] = []
|
|
395
|
+
first = True
|
|
396
|
+
for label, count in counts.items():
|
|
397
|
+
if not first:
|
|
398
|
+
parts.append(Text(" ", style=styles.muted))
|
|
399
|
+
first = False
|
|
400
|
+
parts.append(Text(f"{label}:", style=styles.muted))
|
|
401
|
+
parts.append(Text(" ", style=styles.muted))
|
|
402
|
+
parts.append(Text(str(count), style=styles.title))
|
|
403
|
+
|
|
404
|
+
return Text.assemble(*parts)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def pluralize(count: int, singular: str, plural: str | None = None) -> str:
|
|
408
|
+
if count == 1:
|
|
409
|
+
return f"1 {singular}"
|
|
410
|
+
return f"{count} {plural or (singular + 's')}"
|
cmdbox/cli/ui/theme.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from rich.theme import Theme
|
|
2
|
+
|
|
3
|
+
CMDBOX_THEME = Theme(
|
|
4
|
+
{
|
|
5
|
+
# Core text
|
|
6
|
+
"ui.title": "bold",
|
|
7
|
+
"ui.subtitle": "dim",
|
|
8
|
+
"ui.muted": "dim",
|
|
9
|
+
"ui.dim": "dim",
|
|
10
|
+
"ui.border": "dim",
|
|
11
|
+
"ui.panel_title": "bold",
|
|
12
|
+
# Tables
|
|
13
|
+
"ui.table_header": "bold",
|
|
14
|
+
"ui.caption": "dim",
|
|
15
|
+
# Key/value panels
|
|
16
|
+
"ui.kv.key": "dim",
|
|
17
|
+
"ui.kv.value": "",
|
|
18
|
+
# Status
|
|
19
|
+
"status.success": "bold green",
|
|
20
|
+
"status.info": "bold cyan",
|
|
21
|
+
"status.warning": "bold yellow",
|
|
22
|
+
"status.error": "bold red",
|
|
23
|
+
# Code semantics (not a font switch, just a consistent code look)
|
|
24
|
+
"code": "cyan",
|
|
25
|
+
"code.inline": "cyan",
|
|
26
|
+
"code.block": "cyan",
|
|
27
|
+
# Common entity accents (optional, keep minimal)
|
|
28
|
+
"entity.name": "bold",
|
|
29
|
+
"entity.id": "magenta",
|
|
30
|
+
"entity.count": "bold",
|
|
31
|
+
"entity.time": "dim",
|
|
32
|
+
# Tags
|
|
33
|
+
"tag.pill": "bold white on dark_green",
|
|
34
|
+
"tag.pill_muted": "white on grey23",
|
|
35
|
+
# Execution and previews
|
|
36
|
+
"run.command": "cyan",
|
|
37
|
+
"run.stdout": "",
|
|
38
|
+
"run.stderr": "bold red",
|
|
39
|
+
"run.trace.kind": "dim",
|
|
40
|
+
"run.trace.key": "magenta",
|
|
41
|
+
"run.trace.value": "cyan",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from rich.theme import Theme as RichTheme
|
|
2
|
+
|
|
3
|
+
from cmdbox.settings.models import Settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_theme(settings: Settings) -> RichTheme:
|
|
7
|
+
c = settings.ui.colors
|
|
8
|
+
|
|
9
|
+
style_map = {
|
|
10
|
+
"ui.title": c.title,
|
|
11
|
+
"ui.subtitle": c.subtitle,
|
|
12
|
+
"ui.muted": c.muted,
|
|
13
|
+
"ui.dim": c.muted,
|
|
14
|
+
"ui.border": c.border,
|
|
15
|
+
"ui.panel_title": c.panel_title,
|
|
16
|
+
# Tables
|
|
17
|
+
"ui.table_header": c.table_header,
|
|
18
|
+
"ui.caption": c.caption,
|
|
19
|
+
# Key/value panels
|
|
20
|
+
"ui.kv.key": c.kv_key,
|
|
21
|
+
"ui.kv.value": c.kv_value,
|
|
22
|
+
# Status
|
|
23
|
+
"status.success": c.success,
|
|
24
|
+
"status.info": c.info,
|
|
25
|
+
"status.warning": c.warning,
|
|
26
|
+
"status.error": c.error,
|
|
27
|
+
"status.debug": c.debug,
|
|
28
|
+
# Code semantics (not a font switch, just a consistent code look)
|
|
29
|
+
"code": c.code,
|
|
30
|
+
"code.inline": c.code_inline,
|
|
31
|
+
"code.block": c.code_block,
|
|
32
|
+
# Common entity accents (optional, keep minimal)
|
|
33
|
+
"entity.name": c.entity_name,
|
|
34
|
+
"entity.id": c.entity_id,
|
|
35
|
+
"entity.count": c.entity_count,
|
|
36
|
+
"entity.time": c.entity_time,
|
|
37
|
+
# Tags
|
|
38
|
+
"tag.pill": c.tag_pill,
|
|
39
|
+
"tag.pill_muted": c.tag_pill_muted,
|
|
40
|
+
# Execution and previews
|
|
41
|
+
"run.command": c.run_command,
|
|
42
|
+
"run.stdout": c.run_stdout,
|
|
43
|
+
"run.stderr": c.run_stderr,
|
|
44
|
+
"run.trace.kind": c.trace_kind,
|
|
45
|
+
"run.trace.key": c.trace_key,
|
|
46
|
+
"run.trace.value": c.trace_value,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return RichTheme(style_map)
|
|
File without changes
|
cmdbox/common/io.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from cmdbox.logging_setup.log_decorators import log_action
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@log_action(__name__, "atomic_write_text")
|
|
9
|
+
def atomic_write_text(dest: Path, text: str, encoding: str = "utf-8") -> None:
|
|
10
|
+
"""
|
|
11
|
+
Performs an atomic write operation to a text file. This ensures that the file content is written
|
|
12
|
+
to a temporary file first and then atomically replaces the original file to prevent partial writes
|
|
13
|
+
or data corruption.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
dest (Path): The destination path for the final output file.
|
|
17
|
+
text (str): The text content to write to the file.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
with tempfile.NamedTemporaryFile(
|
|
22
|
+
mode="w",
|
|
23
|
+
encoding=encoding,
|
|
24
|
+
dir=str(dest.parent),
|
|
25
|
+
delete=False,
|
|
26
|
+
prefix=dest.name + ".",
|
|
27
|
+
suffix=".tmp",
|
|
28
|
+
newline="\n",
|
|
29
|
+
) as tf:
|
|
30
|
+
tmp_path = Path(tf.name)
|
|
31
|
+
tf.write(text)
|
|
32
|
+
tf.flush()
|
|
33
|
+
os.fsync(tf.fileno())
|
|
34
|
+
tmp_path.replace(dest)
|
cmdbox/container.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
from cmdbox.cli.ui.console import ConsoleUI
|
|
4
|
+
from cmdbox.cli.ui.theme_builder import build_theme
|
|
5
|
+
from cmdbox.core.fields import (
|
|
6
|
+
COMMAND_DISPLAY_FIELDS,
|
|
7
|
+
COMMAND_SEARCH_FIELDS,
|
|
8
|
+
VARIABLE_DISPLAY_FIELDS,
|
|
9
|
+
VARIABLE_SEARCH_FIELDS,
|
|
10
|
+
TAG_DISPLAY_FIELDS,
|
|
11
|
+
TAG_SEARCH_FIELDS,
|
|
12
|
+
)
|
|
13
|
+
from cmdbox.repositories.history_repository import HistoryRepository
|
|
14
|
+
from cmdbox.services.field_selection import FieldSelectionResolver
|
|
15
|
+
from cmdbox.services.history_service import HistoryService
|
|
16
|
+
from cmdbox.services.variable_services import VariableServices
|
|
17
|
+
from cmdbox.settings.models import Settings
|
|
18
|
+
from cmdbox.settings.settings_repository import SettingsRepository
|
|
19
|
+
from cmdbox.settings.settings_service import SettingsService
|
|
20
|
+
from cmdbox.core.paths import get_app_data_dir
|
|
21
|
+
from cmdbox.database import get_db
|
|
22
|
+
from cmdbox.repositories.command_repository import CommandRepository
|
|
23
|
+
from cmdbox.repositories.tag_repository import TagRepository
|
|
24
|
+
from cmdbox.repositories.variable_repository import VariableRepository
|
|
25
|
+
from cmdbox.resolve.lookup import MemoizedLookup, RepoLookup
|
|
26
|
+
from cmdbox.resolve.resolver import Resolver
|
|
27
|
+
from cmdbox.runtime.executor import Executor
|
|
28
|
+
from cmdbox.services.command_services import CommandServices
|
|
29
|
+
from cmdbox.services.run_service import RunService
|
|
30
|
+
from cmdbox.services.tag_services import TagServices
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@lru_cache(maxsize=1)
|
|
34
|
+
def get_settings_service() -> SettingsService:
|
|
35
|
+
config_path = get_app_data_dir() / "config.toml"
|
|
36
|
+
repo = SettingsRepository(config_path)
|
|
37
|
+
settings = SettingsService(repo)
|
|
38
|
+
return settings
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_settings() -> Settings:
|
|
42
|
+
return get_settings_service().get()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@lru_cache(maxsize=1)
|
|
46
|
+
def get_console() -> ConsoleUI:
|
|
47
|
+
_settings = get_settings()
|
|
48
|
+
theme = build_theme(_settings)
|
|
49
|
+
return ConsoleUI(theme)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@lru_cache(maxsize=1)
|
|
53
|
+
def get_command_repo() -> CommandRepository:
|
|
54
|
+
return CommandRepository()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@lru_cache(maxsize=1)
|
|
58
|
+
def get_variable_repo() -> VariableRepository:
|
|
59
|
+
return VariableRepository()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@lru_cache(maxsize=1)
|
|
63
|
+
def get_tag_repo() -> TagRepository:
|
|
64
|
+
return TagRepository()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=1)
|
|
68
|
+
def get_history_repo() -> HistoryRepository:
|
|
69
|
+
get_db()
|
|
70
|
+
return HistoryRepository()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@lru_cache(maxsize=1)
|
|
74
|
+
def get_resolver(strict: bool = False) -> Resolver:
|
|
75
|
+
get_db()
|
|
76
|
+
command_repo = get_command_repo()
|
|
77
|
+
variable_repo = get_variable_repo()
|
|
78
|
+
repo_lookup = RepoLookup(command_repo, variable_repo)
|
|
79
|
+
lookup = MemoizedLookup(repo_lookup)
|
|
80
|
+
return Resolver(lookup, strict=strict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@lru_cache(maxsize=1)
|
|
84
|
+
def get_run_service() -> RunService:
|
|
85
|
+
get_db()
|
|
86
|
+
cmd_repo = get_command_repo()
|
|
87
|
+
resolver = get_resolver()
|
|
88
|
+
executor = Executor()
|
|
89
|
+
return RunService(
|
|
90
|
+
cmd_repo,
|
|
91
|
+
resolver,
|
|
92
|
+
executor,
|
|
93
|
+
history_repo=get_history_repo(),
|
|
94
|
+
get_settings=get_settings,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@lru_cache(maxsize=1)
|
|
99
|
+
def get_command_services() -> CommandServices:
|
|
100
|
+
get_db()
|
|
101
|
+
cmd_repo = get_command_repo()
|
|
102
|
+
tag_repo = get_tag_repo()
|
|
103
|
+
return CommandServices(command_repository=cmd_repo, tag_repository=tag_repo)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@lru_cache(maxsize=1)
|
|
107
|
+
def get_variable_services() -> VariableServices:
|
|
108
|
+
get_db()
|
|
109
|
+
var_repo = get_variable_repo()
|
|
110
|
+
tag_repo = get_tag_repo()
|
|
111
|
+
return VariableServices(variable_repository=var_repo, tag_repository=tag_repo)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@lru_cache(maxsize=1)
|
|
115
|
+
def get_tag_services() -> TagServices:
|
|
116
|
+
get_db()
|
|
117
|
+
tag_repo = get_tag_repo()
|
|
118
|
+
return TagServices(tag_repository=tag_repo)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@lru_cache(maxsize=1)
|
|
122
|
+
def get_history_service() -> HistoryService:
|
|
123
|
+
return HistoryService(
|
|
124
|
+
repo=get_history_repo(),
|
|
125
|
+
get_settings=get_settings,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@lru_cache(maxsize=1)
|
|
130
|
+
def get_command_display_field_resolver() -> FieldSelectionResolver:
|
|
131
|
+
return FieldSelectionResolver(allowed_fields=COMMAND_DISPLAY_FIELDS)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@lru_cache(maxsize=1)
|
|
135
|
+
def get_command_search_field_resolver() -> FieldSelectionResolver:
|
|
136
|
+
return FieldSelectionResolver(allowed_fields=COMMAND_SEARCH_FIELDS)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@lru_cache(maxsize=1)
|
|
140
|
+
def get_variable_display_field_resolver() -> FieldSelectionResolver:
|
|
141
|
+
return FieldSelectionResolver(allowed_fields=VARIABLE_DISPLAY_FIELDS)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@lru_cache(maxsize=1)
|
|
145
|
+
def get_variable_search_field_resolver() -> FieldSelectionResolver:
|
|
146
|
+
return FieldSelectionResolver(allowed_fields=VARIABLE_SEARCH_FIELDS)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@lru_cache(maxsize=1)
|
|
150
|
+
def get_tag_display_field_resolver() -> FieldSelectionResolver:
|
|
151
|
+
return FieldSelectionResolver(allowed_fields=TAG_DISPLAY_FIELDS)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@lru_cache(maxsize=1)
|
|
155
|
+
def get_tag_search_field_resolver() -> FieldSelectionResolver:
|
|
156
|
+
return FieldSelectionResolver(allowed_fields=TAG_SEARCH_FIELDS)
|
cmdbox/core/__init__.py
ADDED
|
File without changes
|