npcsh 1.1.21__py3-none-any.whl → 1.1.23__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.
- npcsh/_state.py +282 -125
- npcsh/benchmark/npcsh_agent.py +77 -232
- npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
- npcsh/config.py +5 -2
- npcsh/mcp_server.py +9 -1
- npcsh/npc_team/alicanto.npc +8 -6
- npcsh/npc_team/corca.npc +5 -12
- npcsh/npc_team/frederic.npc +6 -9
- npcsh/npc_team/guac.npc +4 -4
- npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
- npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
- npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
- npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
- npcsh/npc_team/jinxs/modes/build.jinx +378 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
- npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
- npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
- npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
- npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
- npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
- npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
- npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
- npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
- npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
- npcsh/npc_team/kadiefa.npc +6 -6
- npcsh/npc_team/npcsh.ctx +16 -0
- npcsh/npc_team/plonk.npc +5 -9
- npcsh/npc_team/sibiji.npc +15 -7
- npcsh/npcsh.py +1 -0
- npcsh/routes.py +0 -4
- npcsh/yap.py +22 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
- npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
- npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
- npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
- npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
- npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
- npcsh-1.1.23.dist-info/RECORD +216 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
- npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
- npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
- npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
- npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
- npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
- npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
- npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
- npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
- npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
- npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
- npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
- npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
- npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
- npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
- npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
- npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
- npcsh-1.1.21.dist-info/RECORD +0 -243
- /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
- /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
- /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
|
@@ -24,6 +24,25 @@ steps:
|
|
|
24
24
|
|
|
25
25
|
from npcpy.llm_funcs import get_llm_response
|
|
26
26
|
|
|
27
|
+
# Helper to log jinx executions to DB
|
|
28
|
+
def _log_jinx(trigger_id, npc_name, inputs, output, status="success", error_msg=None):
|
|
29
|
+
try:
|
|
30
|
+
if state and hasattr(state, 'command_history') and state.command_history is not None and hasattr(state.command_history, 'save_jinx_execution'):
|
|
31
|
+
_conv_id = getattr(state, 'conversation_id', None) or ''
|
|
32
|
+
state.command_history.save_jinx_execution(
|
|
33
|
+
triggering_message_id=f"{_conv_id}-{trigger_id}",
|
|
34
|
+
conversation_id=_conv_id,
|
|
35
|
+
npc_name=npc_name,
|
|
36
|
+
jinx_name="plonk",
|
|
37
|
+
jinx_inputs=inputs,
|
|
38
|
+
jinx_output=str(output) if output else "",
|
|
39
|
+
status=status,
|
|
40
|
+
team_name=state.team.name if state and hasattr(state, 'team') and state.team else None,
|
|
41
|
+
error_message=error_msg,
|
|
42
|
+
)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
27
46
|
try:
|
|
28
47
|
from npcpy.data.image import capture_screenshot
|
|
29
48
|
from npcpy.work.desktop import perform_action
|
|
@@ -123,7 +142,7 @@ steps:
|
|
|
123
142
|
render_screen()
|
|
124
143
|
|
|
125
144
|
try:
|
|
126
|
-
ss = capture_screenshot()
|
|
145
|
+
ss = capture_screenshot(full=True)
|
|
127
146
|
if not ss or 'file_path' not in ss:
|
|
128
147
|
task['actions'].append({"action": "fail", "reason": "Screenshot failed"})
|
|
129
148
|
task['status'] = 'failed'
|
|
@@ -136,23 +155,43 @@ steps:
|
|
|
136
155
|
history_context = ""
|
|
137
156
|
if task['actions']:
|
|
138
157
|
history_context = "\nPrevious actions:\n"
|
|
139
|
-
for i, act in enumerate(task['actions'][-
|
|
158
|
+
for i, act in enumerate(task['actions'][-8:], 1):
|
|
140
159
|
history_context += " " + str(i) + ". " + act.get('action', '?')
|
|
141
160
|
if act.get('x'):
|
|
142
161
|
history_context += " at (" + str(act.get('x', '?')) + ", " + str(act.get('y', '?')) + ")"
|
|
162
|
+
if act.get('text'):
|
|
163
|
+
history_context += ' "' + act.get('text', '')[:50] + '"'
|
|
164
|
+
if act.get('command'):
|
|
165
|
+
history_context += ' [' + act.get('command', '')[:30] + ']'
|
|
143
166
|
history_context += " - " + act.get('reason', '') + "\n"
|
|
167
|
+
# Detect repetition: if last 3 actions are the same type, warn
|
|
168
|
+
recent = task['actions'][-3:]
|
|
169
|
+
if len(recent) == 3 and all(a.get('action') == recent[0].get('action') for a in recent):
|
|
170
|
+
history_context += "\n*** WARNING: You have repeated '" + recent[0].get('action', '?') + "' 3 times. "
|
|
171
|
+
history_context += "This is NOT working. You MUST try a completely different approach. ***\n"
|
|
144
172
|
|
|
145
173
|
prompt = "You are a GUI automation assistant. Analyze this screenshot and determine the next action.\n\n"
|
|
174
|
+
prompt += "CRITICAL RULES:\n"
|
|
175
|
+
prompt += "1. VERIFY: Check the screenshot BEFORE acting. Does it show the result of your last action? If not, wait.\n"
|
|
176
|
+
prompt += "2. NO REPEATS: If an action failed or had no effect, try a DIFFERENT approach. Never repeat the same action.\n"
|
|
177
|
+
prompt += "3. FULL TEXT: Always type the COMPLETE text in a single 'type' action. Never truncate or split text.\n"
|
|
178
|
+
prompt += "4. FOCUS FIRST: Before typing, make sure the target field is focused (click it or use keyboard shortcut).\n\n"
|
|
179
|
+
prompt += "KEYBOARD SHORTCUTS (use 'key' action with these):\n"
|
|
180
|
+
prompt += "- ctrl+l or F6: Focus browser address/search bar\n"
|
|
181
|
+
prompt += "- ctrl+a: Select all text in current field\n"
|
|
182
|
+
prompt += "- ctrl+t: New browser tab\n"
|
|
183
|
+
prompt += "- tab/shift+tab: Move between form fields\n"
|
|
184
|
+
prompt += "- escape: Close popups, cancel autocomplete\n\n"
|
|
146
185
|
prompt += "TASK: " + task['text'] + "\n"
|
|
147
186
|
prompt += history_context + "\n"
|
|
148
187
|
prompt += "Available actions:\n"
|
|
149
|
-
prompt += "- click: Click at x,y
|
|
150
|
-
prompt += "- type: Type text (
|
|
151
|
-
prompt += "- key: Press key
|
|
152
|
-
prompt += "- launch: Launch application
|
|
153
|
-
prompt += "- wait: Wait
|
|
154
|
-
prompt += "- done: Task completed
|
|
155
|
-
prompt += "- fail: Task
|
|
188
|
+
prompt += "- click: Click at x,y (0-100 % of screen). Fields: x, y\n"
|
|
189
|
+
prompt += "- type: Type text into focused field. Fields: text (MUST be complete text)\n"
|
|
190
|
+
prompt += "- key: Press key(s). Fields: text (e.g. 'enter', 'ctrl+l', 'tab', 'escape')\n"
|
|
191
|
+
prompt += "- launch: Launch application. Fields: command (e.g. " + app_examples + ")\n"
|
|
192
|
+
prompt += "- wait: Wait seconds. Fields: duration\n"
|
|
193
|
+
prompt += "- done: Task completed\n"
|
|
194
|
+
prompt += "- fail: Task impossible\n\n"
|
|
156
195
|
prompt += "Respond with JSON, e.g.: " + json_schema_example
|
|
157
196
|
|
|
158
197
|
ui.status = "Thinking... (iter " + str(ui.iteration) + "/" + str(ui.max_iter) + ")"
|
|
@@ -173,21 +212,38 @@ steps:
|
|
|
173
212
|
action_response = json.loads(action_response)
|
|
174
213
|
except:
|
|
175
214
|
task['actions'].append({"action": "error", "reason": "Invalid JSON from model"})
|
|
215
|
+
_log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}",
|
|
216
|
+
npc.name if npc and hasattr(npc, 'name') else "plonk",
|
|
217
|
+
{"task": task['text'], "iteration": ui.iteration, "type": "analysis"},
|
|
218
|
+
str(resp.get('response', '')), status="error", error_msg="Invalid JSON from model")
|
|
176
219
|
ui.status = "Bad response, retrying..."
|
|
177
220
|
return
|
|
178
221
|
|
|
179
222
|
action = action_response.get('action', 'fail')
|
|
180
223
|
reason = action_response.get('reason', '')
|
|
181
224
|
|
|
225
|
+
_npc_name = npc.name if npc and hasattr(npc, 'name') else "plonk"
|
|
226
|
+
_log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}",
|
|
227
|
+
_npc_name,
|
|
228
|
+
{"task": task['text'], "iteration": ui.iteration, "screenshot": screenshot_path,
|
|
229
|
+
"type": "analysis"},
|
|
230
|
+
json.dumps(action_response))
|
|
231
|
+
|
|
182
232
|
if action == 'done':
|
|
183
233
|
task['status'] = 'done'
|
|
184
234
|
task['actions'].append({"action": "done", "reason": reason})
|
|
235
|
+
_log_jinx(f"plonk-task-{ui.current_task}-done", _npc_name,
|
|
236
|
+
{"task": task['text'], "type": "task_complete", "total_actions": len(task['actions'])},
|
|
237
|
+
reason)
|
|
185
238
|
advance_to_next_task()
|
|
186
239
|
return
|
|
187
240
|
|
|
188
241
|
if action == 'fail':
|
|
189
242
|
task['status'] = 'failed'
|
|
190
243
|
task['actions'].append({"action": "fail", "reason": reason})
|
|
244
|
+
_log_jinx(f"plonk-task-{ui.current_task}-fail", _npc_name,
|
|
245
|
+
{"task": task['text'], "type": "task_failed", "total_actions": len(task['actions'])},
|
|
246
|
+
reason, status="error", error_msg=reason)
|
|
191
247
|
advance_to_next_task()
|
|
192
248
|
return
|
|
193
249
|
|
|
@@ -196,20 +252,25 @@ steps:
|
|
|
196
252
|
|
|
197
253
|
if action == 'click':
|
|
198
254
|
x, y = action_response.get('x', 50), action_response.get('y', 50)
|
|
199
|
-
perform_action(
|
|
255
|
+
perform_action({"type": "click", "x": x, "y": y})
|
|
200
256
|
act_record['x'] = x
|
|
201
257
|
act_record['y'] = y
|
|
202
258
|
elif action == 'type':
|
|
203
259
|
txt = action_response.get('text', '')
|
|
204
|
-
perform_action(
|
|
260
|
+
perform_action({"type": "type", "text": txt})
|
|
205
261
|
act_record['text'] = txt
|
|
206
262
|
elif action == 'key':
|
|
207
263
|
key = action_response.get('text', 'enter')
|
|
208
|
-
|
|
264
|
+
# Detect combo keys like ctrl+l, ctrl+a, shift+tab
|
|
265
|
+
if '+' in key:
|
|
266
|
+
parts = [k.strip() for k in key.split('+')]
|
|
267
|
+
perform_action({"type": "hotkey", "keys": parts})
|
|
268
|
+
else:
|
|
269
|
+
perform_action({"type": "key", "keys": key})
|
|
209
270
|
act_record['key'] = key
|
|
210
271
|
elif action == 'launch':
|
|
211
272
|
cmd = action_response.get('command', '')
|
|
212
|
-
perform_action(
|
|
273
|
+
perform_action({"type": "shell", "command": cmd})
|
|
213
274
|
act_record['command'] = cmd
|
|
214
275
|
time.sleep(2)
|
|
215
276
|
elif action == 'wait':
|
|
@@ -219,7 +280,13 @@ steps:
|
|
|
219
280
|
|
|
220
281
|
task['actions'].append(act_record)
|
|
221
282
|
ui.status = action + " - " + reason[:40]
|
|
222
|
-
|
|
283
|
+
# Wait for UI to settle after state-changing actions
|
|
284
|
+
if action in ('key', 'click'):
|
|
285
|
+
time.sleep(2.0)
|
|
286
|
+
elif action == 'type':
|
|
287
|
+
time.sleep(0.5)
|
|
288
|
+
else:
|
|
289
|
+
time.sleep(0.3)
|
|
223
290
|
|
|
224
291
|
if ui.mode == 'step':
|
|
225
292
|
ui.mode = 'paused'
|
|
@@ -227,6 +294,10 @@ steps:
|
|
|
227
294
|
|
|
228
295
|
except Exception as e:
|
|
229
296
|
task['actions'].append({"action": "error", "reason": str(e)})
|
|
297
|
+
_log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}-error",
|
|
298
|
+
npc.name if npc and hasattr(npc, 'name') else "plonk",
|
|
299
|
+
{"task": task['text'] if task else "unknown", "iteration": ui.iteration, "type": "error"},
|
|
300
|
+
str(e), status="error", error_msg=str(e))
|
|
230
301
|
ui.status = "Error: " + str(e)[:40]
|
|
231
302
|
|
|
232
303
|
def advance_to_next_task():
|
|
@@ -363,7 +434,7 @@ steps:
|
|
|
363
434
|
if a.get('x') is not None:
|
|
364
435
|
coords = "(" + str(a.get('x', '')) + "," + str(a.get('y', '')) + ") "
|
|
365
436
|
elif a.get('text'):
|
|
366
|
-
coords = '"' + str(a['text'])[:
|
|
437
|
+
coords = '"' + str(a['text'])[:40] + '" '
|
|
367
438
|
elif a.get('key'):
|
|
368
439
|
coords = '[' + str(a['key']) + '] '
|
|
369
440
|
elif a.get('command'):
|
|
@@ -11,15 +11,11 @@ colors:
|
|
|
11
11
|
top: "34,139,34"
|
|
12
12
|
bottom: "139,69,19"
|
|
13
13
|
primary_directive: |
|
|
14
|
-
You are plonk, the browser and
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Use get_elements to discover selectors on the page. Use xpath:// prefix for XPath selectors.
|
|
21
|
-
|
|
22
|
-
Desktop tools: screenshot, click, type_text, key_press, launch_app, wait
|
|
14
|
+
You are plonk, the automation specialist for browser and desktop.
|
|
15
|
+
Browser: open_browser, browser_action, browser_screenshot, close_browser.
|
|
16
|
+
browser_action: click, type, type_and_enter, select, wait, scroll, get_text, get_page, get_elements, press_key.
|
|
17
|
+
Desktop: screenshot, click, type_text, key_press, launch_app, wait.
|
|
18
|
+
Use get_elements to find selectors. Use xpath:// for XPath. Screenshot after each step to verify.
|
|
23
19
|
jinxs:
|
|
24
20
|
- lib/browser/*
|
|
25
21
|
- lib/computer_use/*
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
jinx_name: "roll"
|
|
2
|
+
description: "Video creation studio TUI - generate and manage videos with parameter controls"
|
|
3
|
+
interactive: true
|
|
4
|
+
inputs:
|
|
5
|
+
- prompt: null
|
|
6
|
+
- model: null
|
|
7
|
+
- provider: null
|
|
8
|
+
- output_path: null
|
|
9
|
+
- num_frames: null
|
|
10
|
+
- width: null
|
|
11
|
+
- height: null
|
|
12
|
+
steps:
|
|
13
|
+
- name: "roll_tui"
|
|
14
|
+
engine: "python"
|
|
15
|
+
code: |
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import tty
|
|
19
|
+
import termios
|
|
20
|
+
import select
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
from npcpy.llm_funcs import gen_video
|
|
24
|
+
|
|
25
|
+
npc = context.get('npc')
|
|
26
|
+
if isinstance(npc, str):
|
|
27
|
+
npc = None
|
|
28
|
+
|
|
29
|
+
# ========== State ==========
|
|
30
|
+
class RollState:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.prompt = ""
|
|
33
|
+
self.model = ""
|
|
34
|
+
self.provider = ""
|
|
35
|
+
self.width_val = 256
|
|
36
|
+
self.height_val = 256
|
|
37
|
+
self.num_frames = 125
|
|
38
|
+
self.output_path = ""
|
|
39
|
+
# UI state
|
|
40
|
+
self.params = ['prompt', 'model', 'provider', 'width', 'height', 'num_frames', 'output']
|
|
41
|
+
self.sel = 0
|
|
42
|
+
self.scroll = 0
|
|
43
|
+
self.mode = 'params' # params, editing, gallery, generating
|
|
44
|
+
self.edit_buf = ""
|
|
45
|
+
self.edit_cursor = 0
|
|
46
|
+
self.status = "Ready"
|
|
47
|
+
self.gallery = [] # [{"path": str, "prompt": str, "timestamp": str}]
|
|
48
|
+
self.gallery_sel = 0
|
|
49
|
+
self.gallery_scroll = 0
|
|
50
|
+
|
|
51
|
+
ui = RollState()
|
|
52
|
+
|
|
53
|
+
# Load from context
|
|
54
|
+
ui.prompt = str(context.get('prompt') or '')
|
|
55
|
+
ui.model = str(context.get('model') or os.getenv('NPCSH_VIDEO_GEN_MODEL', ''))
|
|
56
|
+
ui.provider = str(context.get('provider') or os.getenv('NPCSH_VIDEO_GEN_PROVIDER', ''))
|
|
57
|
+
if not ui.model and npc and hasattr(npc, 'model') and npc.model:
|
|
58
|
+
ui.model = npc.model
|
|
59
|
+
if not ui.provider and npc and hasattr(npc, 'provider') and npc.provider:
|
|
60
|
+
ui.provider = npc.provider
|
|
61
|
+
if not ui.model:
|
|
62
|
+
ui.model = "stable-video-diffusion"
|
|
63
|
+
if not ui.provider:
|
|
64
|
+
ui.provider = "diffusers"
|
|
65
|
+
try:
|
|
66
|
+
ui.width_val = int(context.get('width') or 256)
|
|
67
|
+
except:
|
|
68
|
+
pass
|
|
69
|
+
try:
|
|
70
|
+
ui.height_val = int(context.get('height') or 256)
|
|
71
|
+
except:
|
|
72
|
+
pass
|
|
73
|
+
try:
|
|
74
|
+
ui.num_frames = int(context.get('num_frames') or 125)
|
|
75
|
+
except:
|
|
76
|
+
pass
|
|
77
|
+
ui.output_path = str(context.get('output_path') or '')
|
|
78
|
+
|
|
79
|
+
# Load existing gallery
|
|
80
|
+
vid_dir = os.path.expanduser("~/.npcsh/videos/")
|
|
81
|
+
if os.path.isdir(vid_dir):
|
|
82
|
+
for f in sorted(os.listdir(vid_dir), reverse=True)[:50]:
|
|
83
|
+
if f.lower().endswith(('.mp4', '.webm', '.avi', '.mov', '.mkv')):
|
|
84
|
+
ui.gallery.append({"path": os.path.join(vid_dir, f), "prompt": "", "timestamp": f})
|
|
85
|
+
|
|
86
|
+
def get_size():
|
|
87
|
+
try:
|
|
88
|
+
s = os.get_terminal_size()
|
|
89
|
+
return s.columns, s.lines
|
|
90
|
+
except:
|
|
91
|
+
return 80, 24
|
|
92
|
+
|
|
93
|
+
def get_param_value(idx):
|
|
94
|
+
p = ui.params[idx]
|
|
95
|
+
if p == 'prompt': return ui.prompt
|
|
96
|
+
if p == 'model': return ui.model or '(default)'
|
|
97
|
+
if p == 'provider': return ui.provider or '(default)'
|
|
98
|
+
if p == 'width': return str(ui.width_val)
|
|
99
|
+
if p == 'height': return str(ui.height_val)
|
|
100
|
+
if p == 'num_frames': return str(ui.num_frames)
|
|
101
|
+
if p == 'output': return ui.output_path or '(auto)'
|
|
102
|
+
return ''
|
|
103
|
+
|
|
104
|
+
def set_param_value(idx, val):
|
|
105
|
+
p = ui.params[idx]
|
|
106
|
+
if p == 'prompt': ui.prompt = val
|
|
107
|
+
elif p == 'model': ui.model = val
|
|
108
|
+
elif p == 'provider': ui.provider = val
|
|
109
|
+
elif p == 'width':
|
|
110
|
+
try: ui.width_val = int(val)
|
|
111
|
+
except: pass
|
|
112
|
+
elif p == 'height':
|
|
113
|
+
try: ui.height_val = int(val)
|
|
114
|
+
except: pass
|
|
115
|
+
elif p == 'num_frames':
|
|
116
|
+
try: ui.num_frames = max(1, int(val))
|
|
117
|
+
except: pass
|
|
118
|
+
elif p == 'output': ui.output_path = val
|
|
119
|
+
|
|
120
|
+
def generate_videos():
|
|
121
|
+
ui.mode = 'generating'
|
|
122
|
+
ui.status = "Generating..."
|
|
123
|
+
render_screen()
|
|
124
|
+
|
|
125
|
+
if not ui.prompt:
|
|
126
|
+
ui.status = "Error: No prompt provided"
|
|
127
|
+
ui.mode = 'params'
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if ui.output_path and ui.output_path.strip():
|
|
132
|
+
out_file = os.path.expanduser(ui.output_path)
|
|
133
|
+
else:
|
|
134
|
+
os.makedirs(vid_dir, exist_ok=True)
|
|
135
|
+
out_file = os.path.join(vid_dir, "roll_" + datetime.now().strftime('%Y%m%d_%H%M%S') + ".mp4")
|
|
136
|
+
|
|
137
|
+
result = gen_video(
|
|
138
|
+
prompt=ui.prompt,
|
|
139
|
+
model=ui.model or None,
|
|
140
|
+
provider=ui.provider or None,
|
|
141
|
+
npc=npc,
|
|
142
|
+
num_frames=ui.num_frames,
|
|
143
|
+
width=ui.width_val,
|
|
144
|
+
height=ui.height_val,
|
|
145
|
+
output_path=out_file,
|
|
146
|
+
**context.get('api_kwargs', {})
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if isinstance(result, dict):
|
|
150
|
+
msg = result.get('output', 'Video generated.')
|
|
151
|
+
else:
|
|
152
|
+
msg = str(result)
|
|
153
|
+
|
|
154
|
+
ui.gallery.insert(0, {"path": out_file, "prompt": ui.prompt, "timestamp": os.path.basename(out_file)})
|
|
155
|
+
ui.status = "Generated: " + os.path.basename(out_file)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
ui.status = "Error: " + str(e)[:60]
|
|
158
|
+
|
|
159
|
+
ui.mode = 'params'
|
|
160
|
+
|
|
161
|
+
# ========== Rendering ==========
|
|
162
|
+
def render_screen():
|
|
163
|
+
width, height = get_size()
|
|
164
|
+
out = []
|
|
165
|
+
out.append("\033[H")
|
|
166
|
+
|
|
167
|
+
header = " ROLL - Video Creation Studio "
|
|
168
|
+
out.append("\033[1;1H\033[7;1m" + header.ljust(width) + "\033[0m")
|
|
169
|
+
|
|
170
|
+
if ui.mode in ('params', 'editing', 'generating'):
|
|
171
|
+
# Parameter panel
|
|
172
|
+
out.append("\033[3;1H\033[36;1m Parameters \033[90m" + ("-" * (width - 13)) + "\033[0m")
|
|
173
|
+
|
|
174
|
+
for i, p in enumerate(ui.params):
|
|
175
|
+
row = 4 + i
|
|
176
|
+
out.append("\033[" + str(row) + ";1H\033[K")
|
|
177
|
+
label = p.capitalize() + ":"
|
|
178
|
+
val = get_param_value(i)
|
|
179
|
+
|
|
180
|
+
if ui.mode == 'editing' and i == ui.sel:
|
|
181
|
+
line = " " + label.ljust(14) + "\033[7m " + ui.edit_buf + " \033[0m"
|
|
182
|
+
else:
|
|
183
|
+
line = " " + label.ljust(14) + val[:width - 18]
|
|
184
|
+
|
|
185
|
+
if i == ui.sel and ui.mode != 'editing':
|
|
186
|
+
out.append("\033[7m>" + line + "\033[0m")
|
|
187
|
+
else:
|
|
188
|
+
out.append(" " + line)
|
|
189
|
+
|
|
190
|
+
# Gallery preview
|
|
191
|
+
gallery_row = 4 + len(ui.params) + 1
|
|
192
|
+
out.append("\033[" + str(gallery_row) + ";1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
|
|
193
|
+
|
|
194
|
+
gallery_h = height - gallery_row - 4
|
|
195
|
+
for i in range(max(0, gallery_h)):
|
|
196
|
+
idx = ui.gallery_scroll + i
|
|
197
|
+
row = gallery_row + 1 + i
|
|
198
|
+
out.append("\033[" + str(row) + ";1H\033[K")
|
|
199
|
+
if idx >= len(ui.gallery):
|
|
200
|
+
continue
|
|
201
|
+
g = ui.gallery[idx]
|
|
202
|
+
fname = os.path.basename(g['path'])[:width - 6]
|
|
203
|
+
out.append(" " + fname)
|
|
204
|
+
|
|
205
|
+
elif ui.mode == 'gallery':
|
|
206
|
+
out.append("\033[3;1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
|
|
207
|
+
|
|
208
|
+
gallery_h = height - 6
|
|
209
|
+
for i in range(gallery_h):
|
|
210
|
+
idx = ui.gallery_scroll + i
|
|
211
|
+
row = 4 + i
|
|
212
|
+
out.append("\033[" + str(row) + ";1H\033[K")
|
|
213
|
+
if idx >= len(ui.gallery):
|
|
214
|
+
continue
|
|
215
|
+
g = ui.gallery[idx]
|
|
216
|
+
fname = os.path.basename(g['path'])
|
|
217
|
+
if idx == ui.gallery_sel:
|
|
218
|
+
out.append("\033[7m> " + fname[:width-4] + "\033[0m")
|
|
219
|
+
else:
|
|
220
|
+
out.append(" " + fname[:width-4])
|
|
221
|
+
|
|
222
|
+
# Status + footer
|
|
223
|
+
out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
|
|
224
|
+
out.append("\033[" + str(height-1) + ";1H\033[K " + ui.status[:width-2])
|
|
225
|
+
|
|
226
|
+
if ui.mode == 'editing':
|
|
227
|
+
footer = " Type value, Enter:Confirm Esc:Cancel "
|
|
228
|
+
elif ui.mode == 'gallery':
|
|
229
|
+
footer = " j/k:Nav o:Open b:Back q:Quit "
|
|
230
|
+
elif ui.mode == 'generating':
|
|
231
|
+
footer = " Generating... "
|
|
232
|
+
else:
|
|
233
|
+
footer = " j/k:Nav e:Edit Enter:Generate g:Gallery q:Quit "
|
|
234
|
+
out.append("\033[" + str(height) + ";1H\033[K\033[7m" + footer.ljust(width) + "\033[0m")
|
|
235
|
+
|
|
236
|
+
sys.stdout.write(''.join(out))
|
|
237
|
+
sys.stdout.flush()
|
|
238
|
+
|
|
239
|
+
# ========== Input Handling ==========
|
|
240
|
+
def handle_input(c, fd):
|
|
241
|
+
if ui.mode == 'editing':
|
|
242
|
+
return handle_edit(c, fd)
|
|
243
|
+
if ui.mode == 'gallery':
|
|
244
|
+
return handle_gallery(c, fd)
|
|
245
|
+
|
|
246
|
+
if c == '\x1b':
|
|
247
|
+
if select.select([fd], [], [], 0.05)[0]:
|
|
248
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
249
|
+
if c2 == '[':
|
|
250
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
251
|
+
if c3 == 'A':
|
|
252
|
+
ui.sel = max(0, ui.sel - 1)
|
|
253
|
+
elif c3 == 'B':
|
|
254
|
+
ui.sel = min(len(ui.params) - 1, ui.sel + 1)
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
if c == 'q':
|
|
258
|
+
return False
|
|
259
|
+
elif c == 'j':
|
|
260
|
+
ui.sel = min(len(ui.params) - 1, ui.sel + 1)
|
|
261
|
+
elif c == 'k':
|
|
262
|
+
ui.sel = max(0, ui.sel - 1)
|
|
263
|
+
elif c == 'e' or c in ('\r', '\n'):
|
|
264
|
+
if c in ('\r', '\n') and ui.sel == 0 and ui.prompt:
|
|
265
|
+
# Enter on prompt with existing prompt = generate
|
|
266
|
+
generate_videos()
|
|
267
|
+
else:
|
|
268
|
+
ui.mode = 'editing'
|
|
269
|
+
ui.edit_buf = get_param_value(ui.sel)
|
|
270
|
+
if ui.edit_buf in ('(default)', '(auto)'):
|
|
271
|
+
ui.edit_buf = ""
|
|
272
|
+
ui.edit_cursor = len(ui.edit_buf)
|
|
273
|
+
elif c == 'g':
|
|
274
|
+
ui.mode = 'gallery'
|
|
275
|
+
ui.gallery_sel = 0
|
|
276
|
+
ui.gallery_scroll = 0
|
|
277
|
+
elif c == 'G':
|
|
278
|
+
generate_videos()
|
|
279
|
+
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
def handle_edit(c, fd):
|
|
283
|
+
if c == '\x1b':
|
|
284
|
+
if select.select([fd], [], [], 0.05)[0]:
|
|
285
|
+
os.read(fd, 2)
|
|
286
|
+
ui.mode = 'params'
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
if c in ('\r', '\n'):
|
|
290
|
+
set_param_value(ui.sel, ui.edit_buf)
|
|
291
|
+
ui.mode = 'params'
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
if c == '\x7f' or c == '\x08':
|
|
295
|
+
if ui.edit_cursor > 0:
|
|
296
|
+
ui.edit_buf = ui.edit_buf[:ui.edit_cursor-1] + ui.edit_buf[ui.edit_cursor:]
|
|
297
|
+
ui.edit_cursor -= 1
|
|
298
|
+
elif c >= ' ' and c <= '~':
|
|
299
|
+
ui.edit_buf = ui.edit_buf[:ui.edit_cursor] + c + ui.edit_buf[ui.edit_cursor:]
|
|
300
|
+
ui.edit_cursor += 1
|
|
301
|
+
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
def handle_gallery(c, fd):
|
|
305
|
+
if c == '\x1b':
|
|
306
|
+
if select.select([fd], [], [], 0.05)[0]:
|
|
307
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
308
|
+
if c2 == '[':
|
|
309
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
310
|
+
if c3 == 'A':
|
|
311
|
+
ui.gallery_sel = max(0, ui.gallery_sel - 1)
|
|
312
|
+
elif c3 == 'B':
|
|
313
|
+
ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
|
|
314
|
+
else:
|
|
315
|
+
ui.mode = 'params'
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
if c == 'q':
|
|
319
|
+
return False
|
|
320
|
+
elif c == 'b':
|
|
321
|
+
ui.mode = 'params'
|
|
322
|
+
elif c == 'j':
|
|
323
|
+
ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
|
|
324
|
+
elif c == 'k':
|
|
325
|
+
ui.gallery_sel = max(0, ui.gallery_sel - 1)
|
|
326
|
+
elif c == 'o' and ui.gallery:
|
|
327
|
+
import subprocess
|
|
328
|
+
path = ui.gallery[ui.gallery_sel]['path']
|
|
329
|
+
try:
|
|
330
|
+
subprocess.Popen(['xdg-open', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
331
|
+
ui.status = "Opened: " + os.path.basename(path)
|
|
332
|
+
except:
|
|
333
|
+
ui.status = "Could not open video"
|
|
334
|
+
|
|
335
|
+
# Keep gallery_scroll in range
|
|
336
|
+
_, height = get_size()
|
|
337
|
+
gallery_h = height - 6
|
|
338
|
+
if ui.gallery_sel < ui.gallery_scroll:
|
|
339
|
+
ui.gallery_scroll = ui.gallery_sel
|
|
340
|
+
elif ui.gallery_sel >= ui.gallery_scroll + gallery_h:
|
|
341
|
+
ui.gallery_scroll = ui.gallery_sel - gallery_h + 1
|
|
342
|
+
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
# ========== One-shot mode ==========
|
|
346
|
+
if ui.prompt and context.get('prompt'):
|
|
347
|
+
# If prompt provided via CLI args, generate immediately
|
|
348
|
+
generate_videos()
|
|
349
|
+
context['output'] = ui.status
|
|
350
|
+
context['messages'] = context.get('messages', [])
|
|
351
|
+
|
|
352
|
+
# ========== Main TUI Loop ==========
|
|
353
|
+
elif not sys.stdin.isatty():
|
|
354
|
+
context['output'] = "Roll requires an interactive terminal."
|
|
355
|
+
else:
|
|
356
|
+
fd = sys.stdin.fileno()
|
|
357
|
+
old_settings = termios.tcgetattr(fd)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
tty.setcbreak(fd)
|
|
361
|
+
sys.stdout.write('\033[?25l')
|
|
362
|
+
sys.stdout.write('\033[2J')
|
|
363
|
+
render_screen()
|
|
364
|
+
|
|
365
|
+
running = True
|
|
366
|
+
while running:
|
|
367
|
+
c = os.read(fd, 1).decode('latin-1')
|
|
368
|
+
running = handle_input(c, fd)
|
|
369
|
+
render_screen()
|
|
370
|
+
|
|
371
|
+
finally:
|
|
372
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
373
|
+
sys.stdout.write('\033[?25h')
|
|
374
|
+
sys.stdout.write('\033[2J\033[H')
|
|
375
|
+
sys.stdout.flush()
|
|
376
|
+
|
|
377
|
+
context['output'] = ui.status
|
|
378
|
+
context['messages'] = context.get('messages', [])
|