ferp 0.7.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.
- ferp/__init__.py +3 -0
- ferp/__main__.py +4 -0
- ferp/__version__.py +1 -0
- ferp/app.py +9 -0
- ferp/cli.py +160 -0
- ferp/core/__init__.py +0 -0
- ferp/core/app.py +1312 -0
- ferp/core/bundle_installer.py +245 -0
- ferp/core/command_provider.py +77 -0
- ferp/core/dependency_manager.py +59 -0
- ferp/core/fs_controller.py +70 -0
- ferp/core/fs_watcher.py +144 -0
- ferp/core/messages.py +49 -0
- ferp/core/path_actions.py +124 -0
- ferp/core/paths.py +3 -0
- ferp/core/protocols.py +8 -0
- ferp/core/script_controller.py +515 -0
- ferp/core/script_protocol.py +35 -0
- ferp/core/script_runner.py +421 -0
- ferp/core/settings.py +16 -0
- ferp/core/settings_store.py +69 -0
- ferp/core/state.py +156 -0
- ferp/core/task_store.py +164 -0
- ferp/core/transcript_logger.py +95 -0
- ferp/domain/__init__.py +0 -0
- ferp/domain/scripts.py +29 -0
- ferp/fscp/host/__init__.py +11 -0
- ferp/fscp/host/host.py +439 -0
- ferp/fscp/host/managed_process.py +113 -0
- ferp/fscp/host/process_registry.py +124 -0
- ferp/fscp/protocol/__init__.py +13 -0
- ferp/fscp/protocol/errors.py +2 -0
- ferp/fscp/protocol/messages.py +55 -0
- ferp/fscp/protocol/schemas/__init__.py +0 -0
- ferp/fscp/protocol/schemas/fscp/1.0/cancel.json +16 -0
- ferp/fscp/protocol/schemas/fscp/1.0/definitions.json +29 -0
- ferp/fscp/protocol/schemas/fscp/1.0/discriminator.json +14 -0
- ferp/fscp/protocol/schemas/fscp/1.0/envelope.json +13 -0
- ferp/fscp/protocol/schemas/fscp/1.0/exit.json +20 -0
- ferp/fscp/protocol/schemas/fscp/1.0/init.json +36 -0
- ferp/fscp/protocol/schemas/fscp/1.0/input_response.json +21 -0
- ferp/fscp/protocol/schemas/fscp/1.0/log.json +21 -0
- ferp/fscp/protocol/schemas/fscp/1.0/message.json +23 -0
- ferp/fscp/protocol/schemas/fscp/1.0/progress.json +23 -0
- ferp/fscp/protocol/schemas/fscp/1.0/request_input.json +47 -0
- ferp/fscp/protocol/schemas/fscp/1.0/result.json +16 -0
- ferp/fscp/protocol/schemas/fscp/__init__.py +0 -0
- ferp/fscp/protocol/state.py +16 -0
- ferp/fscp/protocol/validator.py +123 -0
- ferp/fscp/scripts/__init__.py +0 -0
- ferp/fscp/scripts/runtime/__init__.py +4 -0
- ferp/fscp/scripts/runtime/__main__.py +40 -0
- ferp/fscp/scripts/runtime/errors.py +14 -0
- ferp/fscp/scripts/runtime/io.py +64 -0
- ferp/fscp/scripts/runtime/script.py +149 -0
- ferp/fscp/scripts/runtime/state.py +17 -0
- ferp/fscp/scripts/runtime/worker.py +13 -0
- ferp/fscp/scripts/sdk.py +548 -0
- ferp/fscp/transcript/__init__.py +3 -0
- ferp/fscp/transcript/events.py +14 -0
- ferp/resources/__init__.py +0 -0
- ferp/services/__init__.py +3 -0
- ferp/services/file_listing.py +120 -0
- ferp/services/monday_sync.py +155 -0
- ferp/services/releases.py +214 -0
- ferp/services/scripts.py +90 -0
- ferp/services/update_check.py +130 -0
- ferp/styles/index.tcss +638 -0
- ferp/themes/themes.py +238 -0
- ferp/widgets/__init__.py +17 -0
- ferp/widgets/dialogs.py +167 -0
- ferp/widgets/file_tree.py +991 -0
- ferp/widgets/forms.py +146 -0
- ferp/widgets/output_panel.py +244 -0
- ferp/widgets/panels.py +13 -0
- ferp/widgets/process_list.py +158 -0
- ferp/widgets/readme_modal.py +59 -0
- ferp/widgets/scripts.py +192 -0
- ferp/widgets/task_capture.py +74 -0
- ferp/widgets/task_list.py +493 -0
- ferp/widgets/top_bar.py +110 -0
- ferp-0.7.1.dist-info/METADATA +128 -0
- ferp-0.7.1.dist-info/RECORD +87 -0
- ferp-0.7.1.dist-info/WHEEL +5 -0
- ferp-0.7.1.dist-info/entry_points.txt +2 -0
- ferp-0.7.1.dist-info/licenses/LICENSE +21 -0
- ferp-0.7.1.dist-info/top_level.txt +1 -0
ferp/widgets/scripts.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.containers import Horizontal
|
|
7
|
+
from textual.widgets import Label, ListItem, ListView
|
|
8
|
+
|
|
9
|
+
from ferp.core.messages import RunScriptRequest, ShowReadmeRequest
|
|
10
|
+
from ferp.domain.scripts import Script, ScriptConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def script_from_config(cfg: ScriptConfig) -> Script:
|
|
14
|
+
return Script(
|
|
15
|
+
id=cfg["id"],
|
|
16
|
+
name=cfg["name"],
|
|
17
|
+
version=cfg["version"],
|
|
18
|
+
script=cfg["script"],
|
|
19
|
+
target=cfg["target"],
|
|
20
|
+
file_extensions=cfg.get("file_extensions"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_scripts_config(path: Path) -> list[Script]:
|
|
25
|
+
data = json.loads(path.read_text())
|
|
26
|
+
|
|
27
|
+
if "scripts" not in data or not isinstance(data["scripts"], list):
|
|
28
|
+
raise ValueError("Invalid config: missing 'scripts' list")
|
|
29
|
+
|
|
30
|
+
scripts: list[Script] = []
|
|
31
|
+
|
|
32
|
+
for raw in data["scripts"]:
|
|
33
|
+
scripts.append(script_from_config(raw)) # type: ignore[arg-type]
|
|
34
|
+
|
|
35
|
+
return sorted(scripts, key=lambda script: script.name.lower())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_scripts_configs(paths: Sequence[Path]) -> list[Script]:
|
|
39
|
+
scripts_by_id: dict[str, Script] = {}
|
|
40
|
+
errors: list[str] = []
|
|
41
|
+
|
|
42
|
+
for path in paths:
|
|
43
|
+
try:
|
|
44
|
+
loaded = load_scripts_config(path)
|
|
45
|
+
except ValueError as exc:
|
|
46
|
+
errors.append(f"{path}: {exc}")
|
|
47
|
+
continue
|
|
48
|
+
for script in loaded:
|
|
49
|
+
scripts_by_id.setdefault(script.id, script)
|
|
50
|
+
|
|
51
|
+
if not scripts_by_id and errors:
|
|
52
|
+
raise ValueError("; ".join(errors))
|
|
53
|
+
|
|
54
|
+
return sorted(scripts_by_id.values(), key=lambda script: script.name.lower())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ScriptItem(ListItem):
|
|
58
|
+
def __init__(self, script: Script) -> None:
|
|
59
|
+
category, name = (
|
|
60
|
+
script.name.split(":", 1)
|
|
61
|
+
if ":" in script.name
|
|
62
|
+
else ("General", script.name)
|
|
63
|
+
)
|
|
64
|
+
super().__init__(
|
|
65
|
+
Horizontal(
|
|
66
|
+
Label(f"{category}:", classes="script_category"),
|
|
67
|
+
Label(name, classes="script_name"),
|
|
68
|
+
Label(f"v{script.version}", classes="script_version"),
|
|
69
|
+
id="script_item",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
self.script = script
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ScriptManager(ListView):
|
|
76
|
+
FAST_CURSOR_STEP = 5
|
|
77
|
+
BINDINGS = [
|
|
78
|
+
Binding("g", "cursor_top", "To top", show=False),
|
|
79
|
+
Binding("G", "cursor_bottom", "To bottom", key_display="G", show=False),
|
|
80
|
+
Binding("k", "cursor_up", "Move cursor up", show=False),
|
|
81
|
+
Binding("K", "cursor_up_fast", "Cursor up (fast)", key_display="K", show=False),
|
|
82
|
+
Binding("j", "cursor_down", "Move cursor down", show=False),
|
|
83
|
+
Binding(
|
|
84
|
+
"J", "cursor_down_fast", "Cursor down (fast)", key_display="J", show=False
|
|
85
|
+
),
|
|
86
|
+
Binding("R", "run_script", "Run selected script", show=True),
|
|
87
|
+
Binding("enter", "show_readme", "Show readme", show=True),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self, config_paths: Sequence[Path] | Path, *, scripts_root: Path, id: str
|
|
92
|
+
) -> None:
|
|
93
|
+
if isinstance(config_paths, Path):
|
|
94
|
+
self.config_paths = [config_paths]
|
|
95
|
+
else:
|
|
96
|
+
self.config_paths = list(config_paths)
|
|
97
|
+
self.scripts_root = scripts_root
|
|
98
|
+
super().__init__(id=id)
|
|
99
|
+
|
|
100
|
+
def _get_selected_script(self) -> Script | None:
|
|
101
|
+
item = self.highlighted_child
|
|
102
|
+
if isinstance(item, ScriptItem):
|
|
103
|
+
return item.script
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def on_mount(self) -> None:
|
|
107
|
+
self.border_title = "Scripts"
|
|
108
|
+
self.load_scripts()
|
|
109
|
+
|
|
110
|
+
def load_scripts(self) -> None:
|
|
111
|
+
self.clear()
|
|
112
|
+
|
|
113
|
+
if not any(path.exists() for path in self.config_paths):
|
|
114
|
+
self.append(ListItem(Label("No config.json found")))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
scripts = load_scripts_configs(self.config_paths)
|
|
119
|
+
except ValueError as exc:
|
|
120
|
+
self.append(ListItem(Label(f"Invalid config: {exc}")))
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if not scripts:
|
|
124
|
+
self.append(ListItem(Label("No scripts configured")))
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
for script in scripts:
|
|
128
|
+
self.append(ScriptItem(script))
|
|
129
|
+
|
|
130
|
+
self.call_after_refresh(self._focus_first_script)
|
|
131
|
+
|
|
132
|
+
def _focus_first_script(self) -> None:
|
|
133
|
+
if not self.children:
|
|
134
|
+
return
|
|
135
|
+
self.index = 0
|
|
136
|
+
self.scroll_to(y=0)
|
|
137
|
+
|
|
138
|
+
def action_run_script(self) -> None:
|
|
139
|
+
script = self._get_selected_script()
|
|
140
|
+
if not script:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
self.post_message(RunScriptRequest(script))
|
|
144
|
+
|
|
145
|
+
def action_show_readme(self) -> None:
|
|
146
|
+
script = self._get_selected_script()
|
|
147
|
+
if not script:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
readme_path = self.resolve_readme(script)
|
|
151
|
+
self.post_message(ShowReadmeRequest(script, readme_path))
|
|
152
|
+
|
|
153
|
+
def resolve_readme(self, script: Script) -> Path | None:
|
|
154
|
+
script_path = Path(script.script)
|
|
155
|
+
if script_path.is_absolute():
|
|
156
|
+
full_script_path = script_path
|
|
157
|
+
elif script_path.parts and script_path.parts[0] == "scripts":
|
|
158
|
+
full_script_path = (self.scripts_root.parent / script_path).resolve()
|
|
159
|
+
else:
|
|
160
|
+
full_script_path = (self.scripts_root / script_path).resolve()
|
|
161
|
+
script_path = full_script_path
|
|
162
|
+
candidate = script_path.parent / "readme.md"
|
|
163
|
+
return candidate if candidate.exists() else None
|
|
164
|
+
|
|
165
|
+
def action_cursor_down_fast(self) -> None:
|
|
166
|
+
for _ in range(self.FAST_CURSOR_STEP):
|
|
167
|
+
super().action_cursor_down()
|
|
168
|
+
|
|
169
|
+
def action_cursor_up_fast(self) -> None:
|
|
170
|
+
for _ in range(self.FAST_CURSOR_STEP):
|
|
171
|
+
super().action_cursor_up()
|
|
172
|
+
|
|
173
|
+
def action_cursor_top(self) -> None:
|
|
174
|
+
if len(self.children) > 1:
|
|
175
|
+
self.index = 0
|
|
176
|
+
self.scroll_to(y=0)
|
|
177
|
+
|
|
178
|
+
def action_cursor_bottom(self) -> None:
|
|
179
|
+
if len(self.children) > 1:
|
|
180
|
+
self.index = len(self.children) - 1
|
|
181
|
+
|
|
182
|
+
def _visible_item_count(self) -> int:
|
|
183
|
+
if not self.children:
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
first = self.children[0]
|
|
187
|
+
row_height = first.size.height
|
|
188
|
+
|
|
189
|
+
if row_height <= 0:
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
return (self.size.height // row_height) - 1
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.containers import Container, Vertical
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.timer import Timer
|
|
9
|
+
from textual.widgets import Footer, Input, Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CaptureInput(Input):
|
|
13
|
+
"""Input widget that triggers a callback on submission."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, submit_callback: Callable[[], None]) -> None:
|
|
16
|
+
super().__init__(id="task_capture_input", placeholder="New task…")
|
|
17
|
+
self._submit_callback = submit_callback
|
|
18
|
+
|
|
19
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
20
|
+
event.stop()
|
|
21
|
+
self._submit_callback()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaskCaptureModal(ModalScreen[None]):
|
|
25
|
+
"""Popup used for rapid task entry."""
|
|
26
|
+
|
|
27
|
+
BINDINGS = [
|
|
28
|
+
Binding("escape", "close", "Close modal", show=True),
|
|
29
|
+
Binding("enter", "submit", "Submit new task", show=True),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def __init__(self, on_submit: Callable[[str], None]) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
self._on_submit = on_submit
|
|
35
|
+
self._area: CaptureInput | None = None
|
|
36
|
+
self._status: Static | None = None
|
|
37
|
+
self._clear_timer: Timer | None = None
|
|
38
|
+
|
|
39
|
+
def compose(self):
|
|
40
|
+
self._area = CaptureInput(self.action_submit)
|
|
41
|
+
self._status = Static("", classes="task_capture_status")
|
|
42
|
+
yield Container(
|
|
43
|
+
Vertical(self._area, self._status, Footer()),
|
|
44
|
+
id="task_capture_modal",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def on_mount(self) -> None:
|
|
48
|
+
container = self.query_one("#task_capture_modal", Container)
|
|
49
|
+
container.border_title = "Add a New Task"
|
|
50
|
+
if self._area:
|
|
51
|
+
self._area.focus()
|
|
52
|
+
|
|
53
|
+
def action_submit(self) -> None:
|
|
54
|
+
area = self._area or self.query_one(Input)
|
|
55
|
+
text = area.value.strip()
|
|
56
|
+
if not text:
|
|
57
|
+
return
|
|
58
|
+
self._on_submit(text)
|
|
59
|
+
area.value = ""
|
|
60
|
+
if self._status:
|
|
61
|
+
self._status.update("[green]Task saved[/]")
|
|
62
|
+
if self._clear_timer:
|
|
63
|
+
self._clear_timer.stop()
|
|
64
|
+
self._clear_timer = self.set_timer(1.5, self._clear_status)
|
|
65
|
+
|
|
66
|
+
def _clear_status(self) -> None:
|
|
67
|
+
if self._status:
|
|
68
|
+
self._status.update("")
|
|
69
|
+
if self._clear_timer:
|
|
70
|
+
self._clear_timer.stop()
|
|
71
|
+
self._clear_timer = None
|
|
72
|
+
|
|
73
|
+
def action_close(self) -> None:
|
|
74
|
+
self.dismiss(None)
|