skit-cli 0.0.1__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.
skit/tui_health.py ADDED
@@ -0,0 +1,150 @@
1
+ """Health check (D): the checklist that can also act.
2
+
3
+ Every ⚠ row is selectable — Enter jumps the Library to that script; R rebuilds the
4
+ index in place. Checks only what existing capabilities can actually verify (uv, the
5
+ registry↔meta correspondence, missing targets, the one deliberate whole-library drift
6
+ sweep, mirror state). Nothing invented.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import override
12
+
13
+ from rich.markup import escape
14
+ from textual import on
15
+ from textual.app import ComposeResult
16
+ from textual.binding import Binding
17
+ from textual.containers import VerticalScroll
18
+ from textual.screen import Screen
19
+ from textual.widgets import OptionList, Static
20
+ from textual.widgets.option_list import Option
21
+
22
+ from . import config, flows, launcher, store, tui_footer
23
+ from .i18n import gettext, ngettext
24
+ from .paths import scripts_dir
25
+
26
+
27
+ class HealthScreen(Screen[str | None]):
28
+ """Dismisses with a slug to select in the Library, or None."""
29
+
30
+ BINDINGS = [
31
+ Binding("escape", "close", gettext("Back")),
32
+ Binding("R", "rebuild", gettext("Rebuild index")),
33
+ ]
34
+ DEFAULT_CSS = """
35
+ HealthScreen #hc-body {
36
+ padding: 0 1;
37
+ border: round $skit-box-green;
38
+ border-title-color: ansi_bright_white;
39
+ border-title-style: bold;
40
+ }
41
+ HealthScreen #hc-issues { height: auto; border: none; }
42
+ HealthScreen .ok { color: $success; }
43
+ HealthScreen .warn { color: $warning; }
44
+ HealthScreen .bad { color: $error; }
45
+ HealthScreen .hint { color: $text-muted; }
46
+ HealthScreen #hc-keys { dock: bottom; height: 1; color: $text-muted; padding: 0 1; }
47
+ """
48
+
49
+ def on_mount(self) -> None:
50
+ self.query_one("#hc-body").border_title = gettext("Health check")
51
+
52
+ @override
53
+ def compose(self) -> ComposeResult:
54
+ with VerticalScroll(id="hc-body"):
55
+ uv = launcher.find_uv()
56
+ if uv:
57
+ yield Static(f"✓ {gettext('uv: %(path)s') % {'path': escape(uv)}}", classes="ok")
58
+ else:
59
+ yield Static(
60
+ f"✗ {gettext('uv: not found. Install it from https://docs.astral.sh/uv/getting-started/installation/')}",
61
+ classes="bad",
62
+ )
63
+ entries = store.list_entries()
64
+ yield Static(
65
+ "✓ "
66
+ + ngettext(
67
+ "%(count)s script registered", "%(count)s scripts registered", len(entries)
68
+ )
69
+ % {"count": len(entries)},
70
+ classes="ok",
71
+ )
72
+ issues: list[Option] = [
73
+ Option(
74
+ f"⚠ {escape(e.meta.name)} — " + gettext("the launch target is gone from disk"),
75
+ id=e.slug,
76
+ )
77
+ for e in entries
78
+ if launcher.target_missing(e)
79
+ ]
80
+ for e in entries:
81
+ if e.meta.kind != "python" or not e.script_path.exists():
82
+ continue
83
+ if flows.plan_for_entry(e).drift_lines:
84
+ issues.append(
85
+ Option(
86
+ f"⚠ {escape(e.meta.name)} — "
87
+ + gettext(
88
+ "form definitions are out of sync (open Script settings → Resync)"
89
+ ),
90
+ id=e.slug,
91
+ )
92
+ )
93
+ if issues:
94
+ yield Static(gettext("Issues (Enter jumps to the script):"), classes="warn")
95
+ yield OptionList(*issues, id="hc-issues")
96
+ mirror = config.load_mirror()
97
+ if mirror.enabled:
98
+ yield Static(
99
+ "✓ " + gettext("Mirror: on (%(pypi)s)") % {"pypi": escape(mirror.pypi)},
100
+ classes="ok",
101
+ )
102
+ else:
103
+ yield Static("✓ " + gettext("Mirror: off"), classes="ok")
104
+ location = scripts_dir()
105
+ yield Static(
106
+ gettext("Library: %(path)s (%(count)s · %(size)s)")
107
+ % {
108
+ "path": escape(str(location)),
109
+ "count": len(entries),
110
+ "size": store.human_size(store.dir_size(location)),
111
+ },
112
+ classes="hint",
113
+ )
114
+ yield Static("", id="hc-rebuilt", classes="ok")
115
+ yield Static(
116
+ tui_footer.bar(
117
+ tui_footer.chip("screen.jump", "Enter", gettext("Jump to script")),
118
+ tui_footer.chip("screen.rebuild", "R", gettext("Rebuild index")),
119
+ tui_footer.chip("screen.close", "Esc", gettext("Back")),
120
+ ),
121
+ id="hc-keys",
122
+ markup=True,
123
+ )
124
+
125
+ @on(OptionList.OptionSelected, "#hc-issues")
126
+ def _jump(self, event: OptionList.OptionSelected) -> None:
127
+ self.dismiss(str(event.option.id))
128
+
129
+ def action_jump(self) -> None:
130
+ """Footer/Enter twin of clicking an issue line: dismiss to the highlighted
131
+ script. A no-op when the store is healthy (there is no issue list to jump into)."""
132
+ issues = self.query("#hc-issues")
133
+ if not issues:
134
+ return
135
+ option_list = issues.first(OptionList)
136
+ if option_list.highlighted is not None:
137
+ self.dismiss(str(option_list.get_option_at_index(option_list.highlighted).id))
138
+
139
+ def action_rebuild(self) -> None:
140
+ count, problems = store.doctor_rebuild()
141
+ report = self.query_one("#hc-rebuilt", Static)
142
+ lines = [
143
+ ngettext("Index rebuilt: %(count)s entry", "Index rebuilt: %(count)s entries", count)
144
+ % {"count": count}
145
+ ]
146
+ lines += [escape(p) for p in problems]
147
+ report.update("\n".join(lines))
148
+
149
+ def action_close(self) -> None:
150
+ self.dismiss(None)
skit/tui_prefs.py ADDED
@@ -0,0 +1,175 @@
1
+ """Preferences (,): skit's global settings, one screen, everything visible.
2
+
3
+ Every setting shows what is ACTUALLY in effect right now (the most common question a
4
+ settings screen gets is "what happens if I leave this empty"). Language is a dropdown
5
+ (the locale list will grow); the form style governs every interactive flow; the custom
6
+ mirror enforces https for the uv binary inline (downloaded-and-executed ⇒ MITM→RCE).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from typing import override
13
+
14
+ from rich.markup import escape
15
+ from textual import on
16
+ from textual.app import ComposeResult
17
+ from textual.binding import Binding
18
+ from textual.containers import VerticalScroll
19
+ from textual.screen import Screen
20
+ from textual.widgets import Input, RadioButton, RadioSet, Select, Static
21
+
22
+ from . import config, i18n, tui_footer
23
+ from .i18n import gettext
24
+
25
+ _MIRROR_CHOICES = [*config.PYPI_PRESETS, "custom", "off"]
26
+
27
+
28
+ class PreferencesScreen(Screen[bool]):
29
+ BINDINGS = [
30
+ Binding("escape", "close", gettext("Back")),
31
+ Binding("ctrl+a", "save", gettext("Save"), priority=True),
32
+ ]
33
+ DEFAULT_CSS = """
34
+ PreferencesScreen #pf-body {
35
+ padding: 0 1;
36
+ border: round $skit-box-indigo;
37
+ border-title-color: ansi_bright_white;
38
+ border-title-style: bold;
39
+ }
40
+ PreferencesScreen .section { color: $accent; margin: 1 0 0 0; }
41
+ PreferencesScreen .hint { color: $text-muted; }
42
+ PreferencesScreen .error { color: $error; }
43
+ PreferencesScreen RadioSet { layout: horizontal; height: auto; border: none; }
44
+ PreferencesScreen RadioSet > RadioButton { width: auto; margin: 0 3 0 0; }
45
+ PreferencesScreen #pf-keys { dock: bottom; height: 1; color: $text-muted; padding: 0 1; }
46
+ """
47
+
48
+ def on_mount(self) -> None:
49
+ self.query_one("#pf-body").border_title = gettext("Preferences")
50
+ self._toggle_custom()
51
+
52
+ @override
53
+ def compose(self) -> ComposeResult:
54
+ with VerticalScroll(id="pf-body"):
55
+ yield Static(gettext("Interface language"), classes="section")
56
+ current = config.load_config().get("language", "")
57
+ options = [(gettext("Automatic (follow the system)"), "auto")]
58
+ options += [(locale, locale) for locale in i18n.available_locales()]
59
+ yield Select(
60
+ options, value=current if current else "auto", allow_blank=False, id="pf-lang"
61
+ )
62
+ yield Static(
63
+ gettext("Currently in effect: %(locale)s") % {"locale": i18n.current_locale()},
64
+ classes="hint",
65
+ )
66
+
67
+ yield Static(gettext("Editor"), classes="section")
68
+ yield Input(
69
+ value=config.load_editor(),
70
+ placeholder=gettext("e.g. code --wait (empty = use $VISUAL / $EDITOR)"),
71
+ id="pf-editor",
72
+ )
73
+ fallback = os.environ.get("VISUAL") or os.environ.get("EDITOR") or ""
74
+ if fallback:
75
+ yield Static(
76
+ gettext("Empty means: %(cmd)s (from $VISUAL / $EDITOR)")
77
+ % {"cmd": escape(fallback)},
78
+ classes="hint",
79
+ )
80
+
81
+ yield Static(gettext("Interactive form"), classes="section")
82
+ with RadioSet(id="pf-form"):
83
+ yield RadioButton(
84
+ gettext("Mini form — opens in place, fully clickable"),
85
+ value=config.load_form() == "tui",
86
+ )
87
+ yield RadioButton(
88
+ gettext("Line-by-line prompts — plainest, best over slow terminals"),
89
+ value=config.load_form() == "plain",
90
+ )
91
+
92
+ yield Static(
93
+ gettext("Download mirror (mainland-China acceleration)"), classes="section"
94
+ )
95
+ mirror = config.load_mirror()
96
+ if not mirror.enabled:
97
+ selected = "off"
98
+ else:
99
+ selected = next(
100
+ (k for k, v in config.PYPI_PRESETS.items() if v == mirror.pypi), "custom"
101
+ )
102
+ with RadioSet(id="pf-mirror"):
103
+ for choice in _MIRROR_CHOICES:
104
+ yield RadioButton(choice, value=(choice == selected))
105
+ yield Static(
106
+ gettext("Affects where dependencies and Python itself are downloaded from."),
107
+ classes="hint",
108
+ )
109
+ yield Input(value=mirror.pypi, placeholder=gettext("PyPI index URL"), id="pf-pypi")
110
+ yield Input(
111
+ value=mirror.python_install,
112
+ placeholder=gettext("Python-install mirror URL"),
113
+ id="pf-pyinstall",
114
+ )
115
+ yield Input(
116
+ value=mirror.uv_binary, placeholder=gettext("uv binary mirror URL"), id="pf-uv"
117
+ )
118
+ yield Static("", id="pf-uv-error", classes="error")
119
+ yield Static(
120
+ tui_footer.bar(
121
+ tui_footer.chip("screen.save", "Ctrl+A", gettext("Save")),
122
+ tui_footer.chip("screen.close", "Esc", gettext("Back")),
123
+ ),
124
+ id="pf-keys",
125
+ markup=True,
126
+ )
127
+
128
+ @on(RadioSet.Changed, "#pf-mirror")
129
+ def _mirror_changed(self, event: RadioSet.Changed) -> None:
130
+ self._toggle_custom()
131
+
132
+ def _mirror_choice(self) -> str:
133
+ index = self.query_one("#pf-mirror", RadioSet).pressed_index
134
+ return _MIRROR_CHOICES[index] if 0 <= index < len(_MIRROR_CHOICES) else "off"
135
+
136
+ def _toggle_custom(self) -> None:
137
+ custom = self._mirror_choice() == "custom"
138
+ for wid in ("#pf-pypi", "#pf-pyinstall", "#pf-uv"):
139
+ self.query_one(wid, Input).display = custom
140
+ self.query_one("#pf-uv-error", Static).display = custom
141
+
142
+ def action_save(self) -> None:
143
+ lang_value = self.query_one("#pf-lang", Select).value
144
+ i18n.set_language("" if lang_value in ("auto", Select.BLANK) else str(lang_value))
145
+ config.save_editor(self.query_one("#pf-editor", Input).value)
146
+ form_index = self.query_one("#pf-form", RadioSet).pressed_index
147
+ config.save_form("plain" if form_index == 1 else "tui")
148
+ choice = self._mirror_choice()
149
+ if choice == "off":
150
+ config.disable()
151
+ elif choice == "custom":
152
+ uv_url = self.query_one("#pf-uv", Input).value.strip()
153
+ if uv_url and not uv_url.startswith("https://"):
154
+ self.query_one("#pf-uv-error", Static).update(
155
+ gettext(
156
+ "The uv binary is downloaded and executed, so its mirror URL must "
157
+ "use https:// (got: %(url)s)."
158
+ )
159
+ % {"url": escape(uv_url)}
160
+ )
161
+ return
162
+ config.save_mirror(
163
+ config.MirrorConfig(
164
+ enabled=True,
165
+ pypi=self.query_one("#pf-pypi", Input).value.strip(),
166
+ python_install=self.query_one("#pf-pyinstall", Input).value.strip(),
167
+ uv_binary=uv_url,
168
+ )
169
+ )
170
+ else:
171
+ config.save_mirror(config.preset(choice))
172
+ self.dismiss(True)
173
+
174
+ def action_close(self) -> None:
175
+ self.dismiss(False)