utim-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.
- utim_cli/__init__.py +40 -0
- utim_cli/agent.py +359 -0
- utim_cli/auth.py +208 -0
- utim_cli/backup.py +101 -0
- utim_cli/billing.py +40 -0
- utim_cli/blender_agent.py +1018 -0
- utim_cli/bootstrap.py +324 -0
- utim_cli/client_utils.py +135 -0
- utim_cli/config.py +194 -0
- utim_cli/context_pruner.py +504 -0
- utim_cli/doctor.py +118 -0
- utim_cli/knowledge_graph.py +462 -0
- utim_cli/logger.py +121 -0
- utim_cli/mcp_clean_wrapper.py +55 -0
- utim_cli/mcp_client.py +198 -0
- utim_cli/mcp_registry.json +1102 -0
- utim_cli/orchestrator.py +3209 -0
- utim_cli/reflection.py +200 -0
- utim_cli/report.py +100 -0
- utim_cli/scrapy_search.py +229 -0
- utim_cli/share.py +320 -0
- utim_cli/share_tui.py +554 -0
- utim_cli/situational_scoring.py +269 -0
- utim_cli/state.py +15 -0
- utim_cli/tools.py +3381 -0
- utim_cli/utim.py +4051 -0
- utim_cli/vector_memory.py +629 -0
- utim_cli/workspace.py +33 -0
- utim_cli-1.0.0.dist-info/METADATA +134 -0
- utim_cli-1.0.0.dist-info/RECORD +34 -0
- utim_cli-1.0.0.dist-info/WHEEL +5 -0
- utim_cli-1.0.0.dist-info/entry_points.txt +2 -0
- utim_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- utim_cli-1.0.0.dist-info/top_level.txt +1 -0
utim_cli/share_tui.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from prompt_toolkit import Application
|
|
4
|
+
from prompt_toolkit.layout import Layout, HSplit, Window
|
|
5
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
6
|
+
from prompt_toolkit.widgets import TextArea, Frame
|
|
7
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
8
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
9
|
+
from prompt_toolkit.filters import Condition
|
|
10
|
+
from prompt_toolkit.application import run_in_terminal
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from utim_cli.share import ShareManager, EXCLUDE_OPTIONS, EXPIRY_OPTIONS, ShareRecord
|
|
17
|
+
|
|
18
|
+
# Initialize rich console with standard styling matching terminal-ui-design guide
|
|
19
|
+
theme_console = Console()
|
|
20
|
+
|
|
21
|
+
def copy_to_clipboard(text: str) -> bool:
|
|
22
|
+
"""Helper to copy text to clipboard using clip.exe on Windows."""
|
|
23
|
+
try:
|
|
24
|
+
process = subprocess.Popen("clip", stdin=subprocess.PIPE, shell=True)
|
|
25
|
+
process.communicate(input=text.encode('utf-8'))
|
|
26
|
+
return process.returncode == 0
|
|
27
|
+
except Exception:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def _run_checkbox_dialog(items, title="", legend=""):
|
|
31
|
+
"""
|
|
32
|
+
Checklist dialog using prompt_toolkit.
|
|
33
|
+
Allows toggling options with SPACE and accepting with ENTER.
|
|
34
|
+
"""
|
|
35
|
+
sel = [0]
|
|
36
|
+
selected_keys = set(item["key"] for item in items) # Pre-select all by default
|
|
37
|
+
N = len(items)
|
|
38
|
+
act = [None]
|
|
39
|
+
|
|
40
|
+
def content():
|
|
41
|
+
out = []
|
|
42
|
+
out.append(('bold #42bcf5', f'\n {title}\n'))
|
|
43
|
+
out.append(('class:dim', f' {legend}\n\n'))
|
|
44
|
+
|
|
45
|
+
for i, item in enumerate(items):
|
|
46
|
+
is_selected = item["key"] in selected_keys
|
|
47
|
+
highlighted = i == sel[0]
|
|
48
|
+
|
|
49
|
+
bg = 'bg:#1a3a2a bold #cdd6f4' if highlighted else ''
|
|
50
|
+
check = ' [x] ' if is_selected else ' [ ] '
|
|
51
|
+
check_style = 'bold #a6e3a1' if is_selected else 'dim #f38ba8'
|
|
52
|
+
if highlighted:
|
|
53
|
+
check_style = 'bold bg:#1a3a2a'
|
|
54
|
+
|
|
55
|
+
out.extend([
|
|
56
|
+
(bg or check_style, check),
|
|
57
|
+
(bg or 'bold #cdd6f4', f"{item['name'].ljust(20)}"),
|
|
58
|
+
(bg or 'dim', f"— {item['desc']}\n")
|
|
59
|
+
])
|
|
60
|
+
out.append(('', '\n'))
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
kb = KeyBindings()
|
|
64
|
+
|
|
65
|
+
@kb.add('up')
|
|
66
|
+
@kb.add('k')
|
|
67
|
+
def _up(e):
|
|
68
|
+
sel[0] = (sel[0] - 1) % N
|
|
69
|
+
e.app.invalidate()
|
|
70
|
+
|
|
71
|
+
@kb.add('down')
|
|
72
|
+
@kb.add('j')
|
|
73
|
+
def _dn(e):
|
|
74
|
+
sel[0] = (sel[0] + 1) % N
|
|
75
|
+
e.app.invalidate()
|
|
76
|
+
|
|
77
|
+
@kb.add('space')
|
|
78
|
+
def _toggle(e):
|
|
79
|
+
key = items[sel[0]]["key"]
|
|
80
|
+
if key in selected_keys:
|
|
81
|
+
selected_keys.remove(key)
|
|
82
|
+
else:
|
|
83
|
+
selected_keys.add(key)
|
|
84
|
+
e.app.invalidate()
|
|
85
|
+
|
|
86
|
+
@kb.add('enter')
|
|
87
|
+
def _enter(e):
|
|
88
|
+
act[0] = 'select'
|
|
89
|
+
e.app.exit()
|
|
90
|
+
|
|
91
|
+
@kb.add('escape')
|
|
92
|
+
@kb.add('q')
|
|
93
|
+
@kb.add('c-c')
|
|
94
|
+
def _quit(e):
|
|
95
|
+
e.app.exit()
|
|
96
|
+
|
|
97
|
+
dialog_app = Application(
|
|
98
|
+
layout=Layout(Window(
|
|
99
|
+
FormattedTextControl(content),
|
|
100
|
+
wrap_lines=False,
|
|
101
|
+
)),
|
|
102
|
+
key_bindings=kb,
|
|
103
|
+
full_screen=True,
|
|
104
|
+
style=PTStyle.from_dict({'dim': '#555577', '': 'bg:#0d0d16 fg:#cdd6f4'}),
|
|
105
|
+
mouse_support=False,
|
|
106
|
+
)
|
|
107
|
+
dialog_app.run()
|
|
108
|
+
|
|
109
|
+
if act[0] == 'select':
|
|
110
|
+
return list(selected_keys)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def _run_expiry_dialog(options, title="", legend=""):
|
|
114
|
+
"""Simple selection list for picking the link expiry duration."""
|
|
115
|
+
sel = [0]
|
|
116
|
+
N = len(options)
|
|
117
|
+
act = [None]
|
|
118
|
+
|
|
119
|
+
def content():
|
|
120
|
+
out = []
|
|
121
|
+
out.append(('bold #42bcf5', f'\n {title}\n'))
|
|
122
|
+
out.append(('class:dim', f' {legend}\n\n'))
|
|
123
|
+
|
|
124
|
+
for i, opt in enumerate(options):
|
|
125
|
+
highlighted = i == sel[0]
|
|
126
|
+
bg = 'bg:#1a3a2a bold #cdd6f4' if highlighted else ''
|
|
127
|
+
bullet = '➔ ' if highlighted else '• '
|
|
128
|
+
bullet_style = 'bold #42bcf5' if highlighted else 'dim'
|
|
129
|
+
|
|
130
|
+
out.extend([
|
|
131
|
+
(bg or bullet_style, f" {bullet}"),
|
|
132
|
+
(bg or 'bold #cdd6f4', f"{opt['label']}\n")
|
|
133
|
+
])
|
|
134
|
+
out.append(('', '\n'))
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
kb = KeyBindings()
|
|
138
|
+
|
|
139
|
+
@kb.add('up')
|
|
140
|
+
@kb.add('k')
|
|
141
|
+
def _up(e):
|
|
142
|
+
sel[0] = (sel[0] - 1) % N
|
|
143
|
+
e.app.invalidate()
|
|
144
|
+
|
|
145
|
+
@kb.add('down')
|
|
146
|
+
@kb.add('j')
|
|
147
|
+
def _dn(e):
|
|
148
|
+
sel[0] = (sel[0] + 1) % N
|
|
149
|
+
e.app.invalidate()
|
|
150
|
+
|
|
151
|
+
@kb.add('enter')
|
|
152
|
+
def _enter(e):
|
|
153
|
+
act[0] = 'select'
|
|
154
|
+
e.app.exit()
|
|
155
|
+
|
|
156
|
+
@kb.add('escape')
|
|
157
|
+
@kb.add('q')
|
|
158
|
+
@kb.add('c-c')
|
|
159
|
+
def _quit(e):
|
|
160
|
+
e.app.exit()
|
|
161
|
+
|
|
162
|
+
dialog_app = Application(
|
|
163
|
+
layout=Layout(Window(
|
|
164
|
+
FormattedTextControl(content),
|
|
165
|
+
wrap_lines=False,
|
|
166
|
+
)),
|
|
167
|
+
key_bindings=kb,
|
|
168
|
+
full_screen=True,
|
|
169
|
+
style=PTStyle.from_dict({'dim': '#555577', '': 'bg:#0d0d16 fg:#cdd6f4'}),
|
|
170
|
+
mouse_support=False,
|
|
171
|
+
)
|
|
172
|
+
dialog_app.run()
|
|
173
|
+
|
|
174
|
+
if act[0] == 'select':
|
|
175
|
+
return options[sel[0]]
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _run_shares_dashboard(manager: ShareManager):
|
|
179
|
+
"""
|
|
180
|
+
Searchable dashboard showing existing shares, remaining time, delete options,
|
|
181
|
+
and a top option for creating a new share.
|
|
182
|
+
"""
|
|
183
|
+
current_raw_rows = []
|
|
184
|
+
filtered_rows = []
|
|
185
|
+
N_filtered = [0]
|
|
186
|
+
sel = [0]
|
|
187
|
+
act = [None]
|
|
188
|
+
viewport_start = [0]
|
|
189
|
+
|
|
190
|
+
def update_filtered_rows(query):
|
|
191
|
+
nonlocal filtered_rows
|
|
192
|
+
q = query.strip().lower()
|
|
193
|
+
if not q:
|
|
194
|
+
filtered_rows = list(enumerate(current_raw_rows))
|
|
195
|
+
else:
|
|
196
|
+
filtered_rows = []
|
|
197
|
+
for idx, row in enumerate(current_raw_rows):
|
|
198
|
+
if row["type"] == "new":
|
|
199
|
+
filtered_rows.append((idx, row))
|
|
200
|
+
else:
|
|
201
|
+
rec = row["record"]
|
|
202
|
+
if (q in rec.name.lower() or
|
|
203
|
+
q in rec.id.lower() or
|
|
204
|
+
q in rec.link.lower() or
|
|
205
|
+
any(q in excl.lower() for excl in rec.excluded)):
|
|
206
|
+
filtered_rows.append((idx, row))
|
|
207
|
+
N_filtered[0] = len(filtered_rows)
|
|
208
|
+
if N_filtered[0] > 0:
|
|
209
|
+
sel[0] = min(sel[0], N_filtered[0] - 1)
|
|
210
|
+
else:
|
|
211
|
+
sel[0] = 0
|
|
212
|
+
viewport_start[0] = min(viewport_start[0], sel[0])
|
|
213
|
+
|
|
214
|
+
def update_rows():
|
|
215
|
+
records = manager.get_all()
|
|
216
|
+
raw_rows = [{"type": "new"}]
|
|
217
|
+
for r in records:
|
|
218
|
+
raw_rows.append({"type": "record", "record": r})
|
|
219
|
+
nonlocal current_raw_rows
|
|
220
|
+
current_raw_rows = raw_rows
|
|
221
|
+
update_filtered_rows(search_field.text)
|
|
222
|
+
|
|
223
|
+
def get_row_height(row):
|
|
224
|
+
return 3 if row["type"] == "new" else 4
|
|
225
|
+
|
|
226
|
+
def content():
|
|
227
|
+
import shutil
|
|
228
|
+
term_h = shutil.get_terminal_size().lines
|
|
229
|
+
|
|
230
|
+
# 10 lines budget for frame, search, legend, spacing
|
|
231
|
+
available_height = max(5, term_h - 10)
|
|
232
|
+
|
|
233
|
+
if N_filtered[0] == 0:
|
|
234
|
+
return [
|
|
235
|
+
('', '\n'),
|
|
236
|
+
('bold #f38ba8', ' ⚠️ No matching shared items found.\n'),
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
matching_heights = [get_row_height(row) for _, row in filtered_rows]
|
|
240
|
+
|
|
241
|
+
# Slide viewport so that sel[0] is visible
|
|
242
|
+
if sel[0] < viewport_start[0]:
|
|
243
|
+
viewport_start[0] = sel[0]
|
|
244
|
+
else:
|
|
245
|
+
current_height = 0
|
|
246
|
+
for idx in range(viewport_start[0], sel[0] + 1):
|
|
247
|
+
current_height += matching_heights[idx]
|
|
248
|
+
while current_height > available_height and viewport_start[0] < sel[0]:
|
|
249
|
+
current_height -= matching_heights[viewport_start[0]]
|
|
250
|
+
viewport_start[0] += 1
|
|
251
|
+
|
|
252
|
+
out = []
|
|
253
|
+
if viewport_start[0] > 0:
|
|
254
|
+
out.append(('dim yellow', f' ▲ ... ({viewport_start[0]} more above) ... ▲\n\n'))
|
|
255
|
+
available_height -= 2
|
|
256
|
+
|
|
257
|
+
rendered_height = 0
|
|
258
|
+
end_idx = viewport_start[0]
|
|
259
|
+
while end_idx < N_filtered[0] and rendered_height + matching_heights[end_idx] <= available_height:
|
|
260
|
+
rendered_height += matching_heights[end_idx]
|
|
261
|
+
end_idx += 1
|
|
262
|
+
|
|
263
|
+
if end_idx <= sel[0]:
|
|
264
|
+
end_idx = sel[0] + 1
|
|
265
|
+
|
|
266
|
+
for i in range(viewport_start[0], end_idx):
|
|
267
|
+
orig_idx, row = filtered_rows[i]
|
|
268
|
+
highlighted = i == sel[0]
|
|
269
|
+
bg = 'bg:#1a3a2a bold #cdd6f4' if highlighted else ''
|
|
270
|
+
|
|
271
|
+
if row["type"] == "new":
|
|
272
|
+
out.extend([
|
|
273
|
+
(bg or 'bold #a6e3a1', " [+] Share New Link\n"),
|
|
274
|
+
(bg or 'dim', " Compress workspace and generate a new share link\n\n")
|
|
275
|
+
])
|
|
276
|
+
else:
|
|
277
|
+
rec = row["record"]
|
|
278
|
+
status = rec.time_remaining()
|
|
279
|
+
status_style = 'bold #a6e3a1' if status != 'Expired' else 'bold #f38ba8'
|
|
280
|
+
if highlighted:
|
|
281
|
+
status_style = 'bold bg:#1a3a2a'
|
|
282
|
+
|
|
283
|
+
excl_part = f"Excluded: {', '.join(rec.excluded) if rec.excluded else 'none'}"
|
|
284
|
+
|
|
285
|
+
out.extend([
|
|
286
|
+
(bg or 'bold #cdd6f4', f" 📁 {rec.name} "),
|
|
287
|
+
(bg or 'dim', f"({rec.id})"),
|
|
288
|
+
(bg or status_style, f" [{status}]".rjust(15) + "\n"),
|
|
289
|
+
(bg or 'dim cyan', f" {rec.link}\n"),
|
|
290
|
+
(bg or 'dim', f" {excl_part}\n\n")
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
remaining = N_filtered[0] - end_idx
|
|
294
|
+
if remaining > 0:
|
|
295
|
+
out.append(('dim yellow', f' ▼ ... ({remaining} more below) ... ▼\n'))
|
|
296
|
+
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
kb = KeyBindings()
|
|
300
|
+
|
|
301
|
+
# Tab navigation to shift focus
|
|
302
|
+
@kb.add('tab')
|
|
303
|
+
def _tab(e):
|
|
304
|
+
if e.app.layout.has_focus(search_field):
|
|
305
|
+
e.app.layout.focus(list_control)
|
|
306
|
+
else:
|
|
307
|
+
e.app.layout.focus(search_field)
|
|
308
|
+
|
|
309
|
+
@kb.add('s-tab')
|
|
310
|
+
def _stab(e):
|
|
311
|
+
if e.app.layout.has_focus(search_field):
|
|
312
|
+
e.app.layout.focus(list_control)
|
|
313
|
+
else:
|
|
314
|
+
e.app.layout.focus(search_field)
|
|
315
|
+
|
|
316
|
+
# Search mode navigation overrides
|
|
317
|
+
@kb.add('up', filter=Condition(lambda: dialog_app.layout.has_focus(search_field)))
|
|
318
|
+
def _up_search(e):
|
|
319
|
+
if N_filtered[0] > 0:
|
|
320
|
+
sel[0] = (sel[0] - 1) % N_filtered[0]
|
|
321
|
+
e.app.invalidate()
|
|
322
|
+
|
|
323
|
+
@kb.add('down', filter=Condition(lambda: dialog_app.layout.has_focus(search_field)))
|
|
324
|
+
def _down_search(e):
|
|
325
|
+
if N_filtered[0] > 0:
|
|
326
|
+
sel[0] = (sel[0] + 1) % N_filtered[0]
|
|
327
|
+
e.app.invalidate()
|
|
328
|
+
|
|
329
|
+
# List mode navigation
|
|
330
|
+
@kb.add('up', filter=Condition(lambda: dialog_app.layout.has_focus(list_control)))
|
|
331
|
+
@kb.add('k', filter=Condition(lambda: dialog_app.layout.has_focus(list_control)))
|
|
332
|
+
def _up_list(e):
|
|
333
|
+
if N_filtered[0] > 0:
|
|
334
|
+
sel[0] = (sel[0] - 1) % N_filtered[0]
|
|
335
|
+
e.app.invalidate()
|
|
336
|
+
|
|
337
|
+
@kb.add('down', filter=Condition(lambda: dialog_app.layout.has_focus(list_control)))
|
|
338
|
+
@kb.add('j', filter=Condition(lambda: dialog_app.layout.has_focus(list_control)))
|
|
339
|
+
def _down_list(e):
|
|
340
|
+
if N_filtered[0] > 0:
|
|
341
|
+
sel[0] = (sel[0] + 1) % N_filtered[0]
|
|
342
|
+
e.app.invalidate()
|
|
343
|
+
|
|
344
|
+
@kb.add('d', filter=Condition(lambda: dialog_app.layout.has_focus(list_control)))
|
|
345
|
+
def _delete_list(e):
|
|
346
|
+
if N_filtered[0] > 0:
|
|
347
|
+
orig_idx, row = filtered_rows[sel[0]]
|
|
348
|
+
if row["type"] == "record":
|
|
349
|
+
rec = row["record"]
|
|
350
|
+
manager.delete(rec.id)
|
|
351
|
+
update_rows()
|
|
352
|
+
e.app.invalidate()
|
|
353
|
+
|
|
354
|
+
@kb.add('enter')
|
|
355
|
+
def _enter(e):
|
|
356
|
+
if N_filtered[0] > 0:
|
|
357
|
+
orig_idx, row = filtered_rows[sel[0]]
|
|
358
|
+
if row["type"] == "new":
|
|
359
|
+
act[0] = ("new", None)
|
|
360
|
+
else:
|
|
361
|
+
act[0] = ("details", row["record"])
|
|
362
|
+
e.app.exit()
|
|
363
|
+
|
|
364
|
+
@kb.add('escape')
|
|
365
|
+
@kb.add('c-c')
|
|
366
|
+
def _quit(e):
|
|
367
|
+
e.app.exit()
|
|
368
|
+
|
|
369
|
+
@kb.add('q', filter=Condition(lambda: dialog_app.layout.has_focus(list_control)))
|
|
370
|
+
def _quit_list(e):
|
|
371
|
+
e.app.exit()
|
|
372
|
+
|
|
373
|
+
title_window = Window(FormattedTextControl([('bold #42bcf5', f'\n 📤 Workspace Share Manager\n')]), height=2)
|
|
374
|
+
|
|
375
|
+
search_field = TextArea(
|
|
376
|
+
multiline=False,
|
|
377
|
+
prompt=" 🔍 Search Shares: ",
|
|
378
|
+
style="class:search-field",
|
|
379
|
+
)
|
|
380
|
+
search_field.buffer.on_text_changed += lambda buf: update_filtered_rows(buf.text)
|
|
381
|
+
search_frame = Frame(search_field, title="Filter Shared Items (Search Name, ID, Link, or Exclusions)")
|
|
382
|
+
|
|
383
|
+
list_control = FormattedTextControl(content, focusable=True)
|
|
384
|
+
list_window = Window(list_control, wrap_lines=False)
|
|
385
|
+
list_frame = Frame(list_window, title="Shared Archives")
|
|
386
|
+
|
|
387
|
+
def get_legend():
|
|
388
|
+
if dialog_app.layout.has_focus(search_field):
|
|
389
|
+
return [
|
|
390
|
+
('bold #a6e3a1', " MODE: SEARCH "),
|
|
391
|
+
('dim', "— Type to filter | "),
|
|
392
|
+
('bold #cba6f7', "TAB"),
|
|
393
|
+
('dim', " to switch to list | "),
|
|
394
|
+
('bold #f38ba8', "ESC"),
|
|
395
|
+
('dim', " to close")
|
|
396
|
+
]
|
|
397
|
+
else:
|
|
398
|
+
return [
|
|
399
|
+
('bold #f9e2af', " MODE: SELECT "),
|
|
400
|
+
('dim', "— "),
|
|
401
|
+
('bold #89b4fa', "UP/DOWN / J/K"),
|
|
402
|
+
('dim', " to navigate | "),
|
|
403
|
+
('bold #a6e3a1', "ENTER"),
|
|
404
|
+
('dim', " to select | "),
|
|
405
|
+
('bold #f38ba8', "d"),
|
|
406
|
+
('dim', " to delete active link | "),
|
|
407
|
+
('bold #cba6f7', "TAB"),
|
|
408
|
+
('dim', " to search | "),
|
|
409
|
+
('bold #f38ba8', "ESC/Q"),
|
|
410
|
+
('dim', " to close")
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
legend_control = FormattedTextControl(get_legend)
|
|
414
|
+
legend_window = Window(legend_control, height=2)
|
|
415
|
+
|
|
416
|
+
layout = Layout(HSplit([
|
|
417
|
+
title_window,
|
|
418
|
+
search_frame,
|
|
419
|
+
list_frame,
|
|
420
|
+
legend_window
|
|
421
|
+
]))
|
|
422
|
+
|
|
423
|
+
dialog_app = Application(
|
|
424
|
+
layout=layout,
|
|
425
|
+
key_bindings=kb,
|
|
426
|
+
full_screen=True,
|
|
427
|
+
style=PTStyle.from_dict({
|
|
428
|
+
'dim': '#555577',
|
|
429
|
+
'': 'bg:#0d0d16 fg:#cdd6f4',
|
|
430
|
+
'frame.border': '#89b4fa',
|
|
431
|
+
'search-field': 'bg:#1e1e2e fg:#cdd6f4'
|
|
432
|
+
}),
|
|
433
|
+
mouse_support=False,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
update_rows()
|
|
437
|
+
dialog_app.layout.focus(search_field)
|
|
438
|
+
dialog_app.run()
|
|
439
|
+
|
|
440
|
+
return act[0]
|
|
441
|
+
|
|
442
|
+
def show_share_details(record: ShareRecord):
|
|
443
|
+
"""Render details of a share record and copy the link to the clipboard."""
|
|
444
|
+
copied = copy_to_clipboard(record.link)
|
|
445
|
+
clipboard_msg = " [green](Copied to clipboard!)[/green]" if copied else " [red](Failed to copy to clipboard)[/red]"
|
|
446
|
+
|
|
447
|
+
theme_console.print()
|
|
448
|
+
panel = Panel(
|
|
449
|
+
Text.from_markup(
|
|
450
|
+
f"[bold white]📁 Share Package: {record.name}[/bold white]\n\n"
|
|
451
|
+
f"[dim]ID:[/dim] {record.id}\n"
|
|
452
|
+
f"[dim]Created At:[/dim] {record.created_at}\n"
|
|
453
|
+
f"[dim]Expires At:[/dim] {record.expires_at}\n"
|
|
454
|
+
f"[dim]Time Left:[/dim] {record.time_remaining()}\n"
|
|
455
|
+
f"[dim]Link:[/dim] [bold cyan]{record.link}[/bold cyan]{clipboard_msg}\n"
|
|
456
|
+
f"[dim]File Path:[/dim] {record.file_path}\n"
|
|
457
|
+
f"[dim]Excluded:[/dim] {', '.join(record.excluded) if record.excluded else 'none'}"
|
|
458
|
+
),
|
|
459
|
+
title="Share Details",
|
|
460
|
+
border_style="#42bcf5",
|
|
461
|
+
expand=False,
|
|
462
|
+
padding=(1, 2)
|
|
463
|
+
)
|
|
464
|
+
theme_console.print(panel)
|
|
465
|
+
theme_console.print("\n[dim]Press Enter to return to Dashboard...[/dim]")
|
|
466
|
+
input()
|
|
467
|
+
|
|
468
|
+
def do_create_share(manager: ShareManager, exclude_keys, expiry_hours, chat_messages):
|
|
469
|
+
"""Run zip compression, upload, copy to clipboard, and print status."""
|
|
470
|
+
theme_console.print()
|
|
471
|
+
panel = Panel(
|
|
472
|
+
Text.from_markup(
|
|
473
|
+
"[bold yellow]⚡ Building Share Package...[/bold yellow]\n"
|
|
474
|
+
"[dim]Compressing workspace files and formatting chat history...[/dim]"
|
|
475
|
+
),
|
|
476
|
+
border_style="yellow",
|
|
477
|
+
expand=False,
|
|
478
|
+
padding=(1, 2)
|
|
479
|
+
)
|
|
480
|
+
theme_console.print(panel)
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
with theme_console.status("[bold green]Compressing and uploading...", spinner="dots"):
|
|
484
|
+
rec = manager.create_share(exclude_keys, expiry_hours, chat_messages)
|
|
485
|
+
|
|
486
|
+
copied = copy_to_clipboard(rec.link)
|
|
487
|
+
clipboard_msg = " [green](Copied to clipboard!)[/green]" if copied else " [red](Failed to copy to clipboard)[/red]"
|
|
488
|
+
|
|
489
|
+
theme_console.print("\n[bold green]✓ Share link created successfully![/bold green]")
|
|
490
|
+
theme_console.print(f"Link: [bold cyan]{rec.link}[/bold cyan]{clipboard_msg}\n")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
theme_console.print(f"\n[bold red]✗ Failed to create share link: {e}[/bold red]\n")
|
|
493
|
+
|
|
494
|
+
theme_console.print("[dim]Press Enter to return...[/dim]")
|
|
495
|
+
input()
|
|
496
|
+
|
|
497
|
+
def run_share_flow(orchestrator):
|
|
498
|
+
"""Main orchestrator for the /share flow."""
|
|
499
|
+
manager = ShareManager()
|
|
500
|
+
|
|
501
|
+
# Extract chat messages from orchestrator
|
|
502
|
+
chat_messages = getattr(orchestrator, "messages", [])
|
|
503
|
+
|
|
504
|
+
while True:
|
|
505
|
+
# If there are no shares, go directly to the wizard
|
|
506
|
+
records = manager.get_all()
|
|
507
|
+
if not records:
|
|
508
|
+
# Step 1: select exclusions
|
|
509
|
+
exclude_keys = _run_checkbox_dialog(
|
|
510
|
+
EXCLUDE_OPTIONS,
|
|
511
|
+
title="📤 Step 1: Select items to exclude (space to toggle)",
|
|
512
|
+
legend="Select which files/folders to omit to save space and exclude secrets"
|
|
513
|
+
)
|
|
514
|
+
if exclude_keys is None:
|
|
515
|
+
return # user cancelled
|
|
516
|
+
|
|
517
|
+
# Step 2: select expiry
|
|
518
|
+
expiry_opt = _run_expiry_dialog(
|
|
519
|
+
EXPIRY_OPTIONS,
|
|
520
|
+
title="⏰ Step 2: Choose link expiry duration",
|
|
521
|
+
legend="Select how long the link should remain active before expiring"
|
|
522
|
+
)
|
|
523
|
+
if expiry_opt is None:
|
|
524
|
+
return # user cancelled
|
|
525
|
+
|
|
526
|
+
# Step 3: create share
|
|
527
|
+
run_in_terminal(lambda: do_create_share(manager, exclude_keys, expiry_opt["hours"], chat_messages))
|
|
528
|
+
|
|
529
|
+
# Since records are no longer empty, we will loop and the dashboard will show next
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
# Otherwise, show the dashboard
|
|
533
|
+
action_data = _run_shares_dashboard(manager)
|
|
534
|
+
if not action_data:
|
|
535
|
+
break # user exited
|
|
536
|
+
|
|
537
|
+
action, value = action_data
|
|
538
|
+
if action == "new":
|
|
539
|
+
# Run the wizard
|
|
540
|
+
exclude_keys = _run_checkbox_dialog(
|
|
541
|
+
EXCLUDE_OPTIONS,
|
|
542
|
+
title="📤 Step 1: Select items to exclude (space to toggle)",
|
|
543
|
+
legend="Select which files/folders to omit to save space and exclude secrets"
|
|
544
|
+
)
|
|
545
|
+
if exclude_keys is not None:
|
|
546
|
+
expiry_opt = _run_expiry_dialog(
|
|
547
|
+
EXPIRY_OPTIONS,
|
|
548
|
+
title="⏰ Step 2: Choose link expiry duration",
|
|
549
|
+
legend="Select how long the link should remain active before expiring"
|
|
550
|
+
)
|
|
551
|
+
if expiry_opt is not None:
|
|
552
|
+
run_in_terminal(lambda: do_create_share(manager, exclude_keys, expiry_opt["hours"], chat_messages))
|
|
553
|
+
elif action == "details":
|
|
554
|
+
run_in_terminal(lambda: show_share_details(value))
|