npcsh 1.1.19__py3-none-any.whl → 1.1.21__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.
Files changed (173) hide show
  1. npcsh/_state.py +16 -78
  2. npcsh/diff_viewer.py +3 -3
  3. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  4. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +18 -7
  5. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +18 -7
  6. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +20 -9
  7. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +53 -15
  8. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  9. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +393 -317
  10. npcsh/npc_team/jinxs/lib/utils/models.jinx +343 -0
  11. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +8 -7
  12. npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
  13. npcsh/npc_team/jinxs/modes/arxiv.jinx +6 -6
  14. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  15. npcsh/npc_team/jinxs/modes/corca.jinx +4 -4
  16. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  17. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  18. npcsh/npc_team/jinxs/modes/kg.jinx +941 -0
  19. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  20. npcsh/npc_team/jinxs/modes/nql.jinx +460 -0
  21. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  22. npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
  23. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/reattach.jinx +4 -4
  25. npcsh/npc_team/jinxs/modes/spool.jinx +4 -4
  26. npcsh/npc_team/jinxs/modes/team.jinx +504 -0
  27. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  28. npcsh/npc_team/jinxs/modes/wander.jinx +455 -182
  29. npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  30. npcsh/npcsh.py +112 -47
  31. npcsh/routes.py +12 -3
  32. npcsh/salmon_simulation.py +0 -0
  33. npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
  34. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +6 -6
  35. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  36. npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
  37. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  38. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +4 -4
  39. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +18 -7
  40. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +18 -7
  41. npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
  42. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +4 -4
  43. npcsh-1.1.21.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  44. npcsh-1.1.21.data/data/npcsh/npc_team/kg.jinx +941 -0
  45. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +20 -9
  46. npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
  47. npcsh-1.1.21.data/data/npcsh/npc_team/models.jinx +343 -0
  48. npcsh-1.1.21.data/data/npcsh/npc_team/nql.jinx +460 -0
  49. npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
  50. npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
  51. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +1 -1
  52. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +4 -4
  53. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +8 -7
  54. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +4 -4
  55. npcsh-1.1.21.data/data/npcsh/npc_team/team.jinx +504 -0
  56. npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  57. npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
  58. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +53 -15
  59. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
  60. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
  61. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/RECORD +147 -148
  62. npcsh-1.1.21.dist-info/entry_points.txt +11 -0
  63. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -299
  64. npcsh/npc_team/jinxs/bin/memories.jinx +0 -316
  65. npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
  66. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  67. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  68. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  69. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  70. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  71. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  72. npcsh/npc_team/plonkjr.npc +0 -23
  73. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  74. npcsh-1.1.19.data/data/npcsh/npc_team/compress.jinx +0 -140
  75. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +0 -299
  76. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  77. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  78. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  79. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +0 -316
  80. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  81. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  82. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +0 -379
  83. npcsh-1.1.19.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  84. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  85. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  86. npcsh-1.1.19.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  87. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +0 -455
  88. npcsh-1.1.19.dist-info/entry_points.txt +0 -22
  89. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  90. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  91. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  92. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  93. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
  142. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  143. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
  144. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  145. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
  146. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  147. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
  148. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
  149. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
  150. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
  151. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  152. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
  153. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  154. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  155. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
  156. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
  157. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
  158. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  159. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  160. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
  161. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
  162. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  163. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  165. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
  166. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  167. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
  168. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  169. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
  170. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  171. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
  172. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
  173. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
@@ -1,331 +1,407 @@
1
1
  jinx_name: jinxs
2
- description: Interactive browser for available jinxs
3
- inputs:
4
- - path: ""
5
- - text: "false"
6
-
2
+ description: Interactive jinx browser - browse, search, and preview available jinxs
3
+ interactive: true
4
+ inputs: []
7
5
  steps:
8
- - name: list_jinxs
6
+ - name: jinxs_browser
9
7
  engine: python
10
8
  code: |
11
9
  import os
12
10
  import sys
13
11
  import tty
14
12
  import termios
15
- from pathlib import Path
13
+ import select
16
14
  import yaml
15
+ from pathlib import Path
16
+
17
+ if not sys.stdin.isatty():
18
+ context['output'] = "Jinxs browser requires an interactive terminal."
19
+
20
+ elif not state or not state.team:
21
+ context['output'] = "No team loaded."
17
22
 
18
- filter_path = context.get('path', '').strip()
19
- text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
20
-
21
- # Find jinxs directory from team or fallback
22
- jinxs_dir = None
23
- if hasattr(npc, 'team') and npc.team:
24
- if hasattr(npc.team, 'jinxs_dir') and npc.team.jinxs_dir:
25
- jinxs_dir = Path(npc.team.jinxs_dir)
26
- elif hasattr(npc.team, 'team_path') and npc.team.team_path:
27
- candidate = Path(npc.team.team_path) / "jinxs"
28
- if candidate.exists():
29
- jinxs_dir = candidate
30
-
31
- if not jinxs_dir:
32
- global_jinxs = Path.home() / ".npcsh" / "npc_team" / "jinxs"
33
- if global_jinxs.exists():
34
- jinxs_dir = global_jinxs
35
-
36
- if not jinxs_dir or not jinxs_dir.exists():
37
- context['output'] = "Error: Could not find jinxs directory"
38
23
  else:
39
- def get_jinx_info(jinx_path):
24
+ # ── data loading ─────────────────────────────────────
25
+ def load_jinxs():
26
+ """Load jinxs from team directory and global ~/.npcsh directory."""
27
+ items = []
28
+ seen_paths = set()
29
+
30
+ def scan_dir(base_dir, source_label):
31
+ if not base_dir.exists():
32
+ return
33
+ for sub in sorted(base_dir.iterdir()):
34
+ if sub.is_dir():
35
+ for jf in sorted(sub.glob('*.jinx')):
36
+ rp = str(jf.resolve())
37
+ if rp in seen_paths:
38
+ continue
39
+ seen_paths.add(rp)
40
+ try:
41
+ with open(jf) as f:
42
+ content = f.read()
43
+ header = content.split('steps:')[0] if 'steps:' in content else content
44
+ data = yaml.safe_load(header) or {}
45
+ except Exception:
46
+ data = {}
47
+ items.append({
48
+ 'name': data.get('jinx_name', jf.stem),
49
+ 'folder': sub.name,
50
+ 'description': data.get('description', ''),
51
+ 'inputs': data.get('inputs', []),
52
+ 'interactive': data.get('interactive', False),
53
+ 'path': str(jf),
54
+ 'source': source_label,
55
+ })
56
+
57
+ # Team jinxs
58
+ team_dir = Path(state.team.team_path) / 'jinxs'
59
+ scan_dir(team_dir, 'team')
60
+
61
+ # Global jinxs
62
+ global_dir = Path.home() / '.npcsh' / 'npc_team' / 'jinxs'
63
+ if global_dir.resolve() != team_dir.resolve():
64
+ scan_dir(global_dir, 'global')
65
+
66
+ return items
67
+
68
+ # ── TUI state ────────────────────────────────────────
69
+ class TUIState:
70
+ def __init__(self):
71
+ self.tab = 0
72
+ self.tabs = ['All', 'Bin', 'Modes', 'Interactive']
73
+ self.sel = 0
74
+ self.scroll = 0
75
+ self.all_jinxs = []
76
+ self.filtered = []
77
+ self.search_mode = False
78
+ self.search_buf = ""
79
+ self.search_query = ""
80
+ self.detail = False
81
+ self.detail_scroll = 0
82
+ self.status = ""
83
+
84
+ ui = TUIState()
85
+
86
+ def term_size():
40
87
  try:
41
- with open(jinx_path, 'r') as f:
42
- content = f.read()
43
- header = content.split('steps:')[0] if 'steps:' in content else content
44
- data = yaml.safe_load(header)
45
- name = data.get('jinx_name', jinx_path.stem)
46
- desc = data.get('description', 'No description')
47
- inputs = data.get('inputs', [])
48
- return name, desc, inputs
49
- except:
50
- return jinx_path.stem, 'No description', []
51
-
52
- def get_all_jinxs(base_path):
53
- jinxs = []
54
- folders = set()
55
- for root, dirs, files in os.walk(base_path):
56
- dirs[:] = [d for d in dirs if not d.startswith('.')]
57
- rel_path = Path(root).relative_to(base_path)
58
- folder = str(rel_path) if str(rel_path) != '.' else 'root'
59
- if folder != 'root':
60
- folders.add(folder.split('/')[0])
61
- for jf in files:
62
- if jf.endswith('.jinx'):
63
- jinx_path = Path(root) / jf
64
- name, desc, inputs = get_jinx_info(jinx_path)
65
- jinxs.append({
66
- 'name': name,
67
- 'description': desc,
68
- 'inputs': inputs,
69
- 'folder': folder,
70
- 'path': str(jinx_path)
71
- })
72
- return jinxs, sorted(folders)
73
-
74
- all_jinxs, folders = get_all_jinxs(jinxs_dir)
75
- folders = ['root'] + list(folders)
76
-
77
- if text_mode or not all_jinxs:
78
- # Text-only output
79
- output_lines = ["Available Jinxs", "=" * 40, ""]
80
- by_folder = {}
81
- for j in all_jinxs:
82
- f = j['folder']
83
- if f not in by_folder:
84
- by_folder[f] = []
85
- by_folder[f].append(j)
86
-
87
- for folder in sorted(by_folder.keys()):
88
- if folder == 'root':
89
- output_lines.append("Root:")
88
+ s = os.get_terminal_size()
89
+ return s.columns, s.lines
90
+ except Exception:
91
+ return 80, 24
92
+
93
+ def apply_filters():
94
+ """Apply tab filter and search query to the full jinx list."""
95
+ items = ui.all_jinxs
96
+
97
+ # Tab filter
98
+ if ui.tab == 1:
99
+ items = [j for j in items if j['folder'] == 'bin']
100
+ elif ui.tab == 2:
101
+ items = [j for j in items if j['folder'] == 'modes']
102
+ elif ui.tab == 3:
103
+ items = [j for j in items if j['interactive']]
104
+
105
+ # Search filter
106
+ if ui.search_query:
107
+ srch = ui.search_query.lower()
108
+ items = [j for j in items if srch in j['name'].lower() or srch in j['description'].lower()]
109
+
110
+ ui.filtered = items
111
+ # Clamp selection
112
+ if ui.sel >= len(ui.filtered):
113
+ ui.sel = max(0, len(ui.filtered) - 1)
114
+ if ui.scroll > ui.sel:
115
+ ui.scroll = ui.sel
116
+
117
+ # ── rendering ────────────────────────────────────────
118
+ def wline(row, text):
119
+ """Write full line at row, clear to EOL."""
120
+ return f"\033[{row};1H\033[K{text}"
121
+
122
+ def render():
123
+ W, H = term_size()
124
+ out = []
125
+
126
+ # Home cursor (no full-screen clear)
127
+ out.append("\033[H")
128
+
129
+ # ── header ──
130
+ hdr = " Jinxs "
131
+ pad = '=' * W
132
+ out.append(wline(1, f"\033[7;1m{pad}\033[0m"))
133
+ out.append(f"\033[1;{max(1, (W - len(hdr)) // 2)}H\033[7;1m{hdr}\033[0m")
134
+
135
+ # ── tabs ──
136
+ tb = ""
137
+ for i, t in enumerate(ui.tabs):
138
+ if i == ui.tab:
139
+ tb += f"\033[7;1m [{t}] \033[0m"
90
140
  else:
91
- output_lines.append(f"{folder}/:")
92
- for j in by_folder[folder]:
93
- output_lines.append(f" /{j['name']} - {j['description'][:50]}")
94
- output_lines.append("")
95
-
96
- output_lines.append("Use /jinxs path=<folder> for details")
97
- output_lines.append("Use text=false for interactive TUI")
98
- context['output'] = "\n".join(output_lines)
99
- else:
100
- # Interactive TUI mode
101
- def get_terminal_size():
102
- try:
103
- size = os.get_terminal_size()
104
- return size.columns, size.lines
105
- except:
106
- return 80, 24
107
-
108
- width, height = get_terminal_size()
109
- selected_folder = 0
110
- selected_jinx = 0
111
- jinx_scroll = 0
112
- list_height = height - 5
113
- mode = 'list' # list or preview
114
- preview_scroll = 0
115
- filter_text = ''
116
-
117
- def get_jinxs_in_folder(folder):
118
- if folder == 'root':
119
- return [j for j in all_jinxs if j['folder'] == 'root']
120
- return [j for j in all_jinxs if j['folder'].startswith(folder)]
121
-
122
- def filter_jinxs(jinxs, text):
123
- if not text:
124
- return jinxs
125
- text = text.lower()
126
- return [j for j in jinxs if text in j['name'].lower() or text in j['description'].lower()]
127
-
128
- current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
129
-
130
- fd = sys.stdin.fileno()
131
- old_settings = termios.tcgetattr(fd)
132
-
133
- try:
134
- tty.setcbreak(fd)
135
- sys.stdout.write('\033[?25l')
136
- sys.stdout.write('\033[2J\033[H')
137
-
138
- while True:
139
- width, height = get_terminal_size()
140
- list_height = height - 5
141
-
142
- if mode == 'list':
143
- if selected_jinx < jinx_scroll:
144
- jinx_scroll = selected_jinx
145
- elif selected_jinx >= jinx_scroll + list_height:
146
- jinx_scroll = selected_jinx - list_height + 1
147
-
148
- sys.stdout.write('\033[H')
149
-
150
- # Header
151
- if mode == 'list':
152
- f = folders[selected_folder] if folders else 'none'
153
- flt = f" filter:'{filter_text}'" if filter_text else ""
154
- header = f" JINXS ({len(current_jinxs)}): {f}{flt} "
141
+ tb += f" {t} "
142
+ out.append(wline(2, f" {tb}"))
143
+
144
+ # ── count line ──
145
+ count = len(ui.filtered)
146
+ total = len(ui.all_jinxs)
147
+ if ui.search_query:
148
+ count_text = f" {count} matching (of {total}) | search: \"{ui.search_query}\""
149
+ elif ui.tab == 0:
150
+ count_text = f" {total} jinxs loaded"
151
+ else:
152
+ count_text = f" {count} jinxs ({ui.tabs[ui.tab].lower()})"
153
+ out.append(wline(3, f"\033[90m{'─' * W}\033[0m"))
154
+ out.append(wline(4, count_text))
155
+ out.append(wline(5, f"\033[90m{'─' * W}\033[0m"))
156
+
157
+ # ── body ──
158
+ body_start = 6
159
+ body_end = H - 3 # leave room for separator, status, footer
160
+ body_h = body_end - body_start + 1
161
+ if body_h < 1:
162
+ body_h = 1
163
+
164
+ if ui.detail:
165
+ render_detail(out, W, body_start, body_h)
166
+ else:
167
+ render_list(out, W, body_start, body_h)
168
+
169
+ # ── separator ──
170
+ out.append(wline(H - 2, f"\033[90m{'─' * W}\033[0m"))
171
+
172
+ # ── status / search ──
173
+ if ui.search_mode:
174
+ out.append(wline(H - 1, f" \033[33m/\033[0m\033[1m{ui.search_buf}\033[0m\033[90m_\033[0m"))
175
+ elif ui.status:
176
+ out.append(wline(H - 1, f" \033[33m{ui.status[:W-2]}\033[0m"))
177
+ else:
178
+ out.append(wline(H - 1, ""))
179
+
180
+ # ── footer ──
181
+ if ui.search_mode:
182
+ foot = " [Enter] Apply [Esc] Cancel"
183
+ elif ui.detail:
184
+ foot = " [j/k] Scroll [q/Esc] Back"
185
+ else:
186
+ foot = " [Tab] Filter [j/k] Nav [Enter] Detail [/] Search [q] Quit"
187
+ out.append(wline(H, f"\033[7m{foot[:W].ljust(W)}\033[0m"))
188
+
189
+ sys.stdout.write(''.join(out))
190
+ sys.stdout.flush()
191
+
192
+ def render_list(out, W, start, body_h):
193
+ vis = ui.filtered[ui.scroll:ui.scroll + body_h]
194
+ name_col = 22 # width for folder/name column
195
+ marker_col = 6 # width for [i] marker
196
+ for r in range(body_h):
197
+ row = start + r
198
+ idx = r + ui.scroll
199
+ if r >= len(vis):
200
+ out.append(wline(row, ""))
201
+ continue
202
+ j = vis[r]
203
+ label = f"{j['folder']}/{j['name']}"
204
+ if len(label) > name_col:
205
+ label = label[:name_col - 1] + "\u2026"
206
+ marker = " [i] " if j['interactive'] else " "
207
+ desc_max = W - name_col - marker_col - 6
208
+ desc = j['description']
209
+ if len(desc) > desc_max:
210
+ desc = desc[:desc_max - 3] + "..."
211
+ if idx == ui.sel:
212
+ line = f" > {label:<{name_col}} {marker} {desc}"
213
+ out.append(wline(row, f"\033[7m{line[:W].ljust(W)}\033[0m"))
214
+ else:
215
+ out.append(wline(row, f" {label:<{name_col}} \033[36m{marker}\033[0m\033[90m{desc}\033[0m"))
216
+ if not ui.filtered:
217
+ out.append(wline(start, " \033[90mNo jinxs match the current filter.\033[0m"))
218
+ for r in range(1, body_h):
219
+ out.append(wline(start + r, ""))
220
+
221
+ def render_detail(out, W, start, body_h):
222
+ if not ui.filtered:
223
+ for r in range(body_h):
224
+ out.append(wline(start + r, ""))
225
+ return
226
+
227
+ j = ui.filtered[ui.sel]
228
+ lines = []
229
+ lines.append(f"\033[1mName:\033[0m /{j['name']}")
230
+ lines.append(f"\033[1mFolder:\033[0m {j['folder']}/")
231
+ lines.append(f"\033[1mSource:\033[0m {j['source']}")
232
+ lines.append(f"\033[1mInteractive:\033[0m {'yes' if j['interactive'] else 'no'}")
233
+ lines.append("")
234
+ lines.append(f"\033[1mDescription:\033[0m")
235
+ # Wrap description
236
+ desc = j['description']
237
+ while desc:
238
+ lines.append(f" {desc[:W-4]}")
239
+ desc = desc[W-4:]
240
+ lines.append("")
241
+ lines.append(f"\033[1mInputs:\033[0m")
242
+ if j['inputs']:
243
+ for inp in j['inputs']:
244
+ if isinstance(inp, dict):
245
+ for k, v in inp.items():
246
+ default = f" = {v}" if v is not None and v != '' else ""
247
+ lines.append(f" - {k}{default}")
248
+ elif isinstance(inp, str):
249
+ lines.append(f" - {inp}")
155
250
  else:
156
- j = current_jinxs[selected_jinx] if current_jinxs else {}
157
- header = f" PREVIEW: /{j.get('name', '')} "
158
- sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
159
-
160
- # Folder bar
161
- folder_bar = ""
162
- for i, f in enumerate(folders[:8]):
163
- if i == selected_folder:
164
- folder_bar += f'\033[47;30m [{f[:8]}] \033[0m'
165
- else:
166
- folder_bar += f' {f[:8]} '
167
- if len(folders) > 8:
168
- folder_bar += f'...+{len(folders)-8}'
169
- sys.stdout.write(f'{folder_bar[:width]}\n')
170
-
171
- if mode == 'list':
172
- for i in range(list_height):
173
- idx = jinx_scroll + i
174
- sys.stdout.write(f'\033[{3+i};1H\033[K')
175
- if idx >= len(current_jinxs):
176
- continue
177
-
178
- j = current_jinxs[idx]
179
- name = j['name'][:20]
180
- desc = j['description'][:width-25]
181
-
182
- if idx == selected_jinx:
183
- sys.stdout.write(f'\033[47;30;1m>/{name:<20} {desc}\033[0m')
184
- else:
185
- sys.stdout.write(f' /{name:<20} {desc}')
186
-
187
- # Status bar
188
- sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
189
- j = current_jinxs[selected_jinx] if current_jinxs else {}
190
- path = j.get('path', '')[-50:] if j else ''
191
- sys.stdout.write(f'\033[{height-1};1H\033[K {path}'.ljust(width))
192
- sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m h/l:Folder j/k:Jinx p:Preview Enter:Run f:Filter q:Quit [{selected_jinx+1}/{len(current_jinxs)}] \033[0m')
193
-
194
- else: # preview mode
195
- j = current_jinxs[selected_jinx]
196
- preview_lines = [
197
- f"Name: /{j['name']}",
198
- "",
199
- f"Description:",
200
- f" {j['description']}",
201
- "",
202
- f"Path: {j['path']}",
203
- f"Folder: {j['folder']}",
204
- "",
205
- ]
206
-
207
- if j.get('inputs'):
208
- preview_lines.append("Inputs:")
209
- for inp in j['inputs']:
210
- if isinstance(inp, dict):
211
- for k, v in inp.items():
212
- default = f" (default: {v})" if v else ""
213
- preview_lines.append(f" - {k}{default}")
214
- else:
215
- preview_lines.append(f" - {inp}")
216
- preview_lines.append("")
217
-
218
- preview_lines.append("Usage:")
219
- usage = f" /{j['name']}"
220
- if j.get('inputs'):
221
- for inp in j['inputs'][:3]:
222
- if isinstance(inp, dict):
223
- for k, v in inp.items():
224
- usage += f" {k}=<value>"
225
- preview_lines.append(usage)
226
-
227
- for i in range(list_height):
228
- idx = preview_scroll + i
229
- sys.stdout.write(f'\033[{3+i};1H\033[K')
230
- if idx < len(preview_lines):
231
- sys.stdout.write(preview_lines[idx][:width-1])
232
-
233
- sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
234
- sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
235
- sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back Enter:Run q:Quit \033[0m')
236
-
237
- sys.stdout.flush()
238
-
239
- c = sys.stdin.read(1)
240
-
241
- if c == '\x1b':
242
- c2 = sys.stdin.read(1)
243
- if c2 == '[':
244
- c3 = sys.stdin.read(1)
245
- if c3 == 'A': # Up
246
- if mode == 'list' and selected_jinx > 0:
247
- selected_jinx -= 1
248
- elif mode == 'preview' and preview_scroll > 0:
249
- preview_scroll -= 1
250
- elif c3 == 'B': # Down
251
- if mode == 'list' and selected_jinx < len(current_jinxs) - 1:
252
- selected_jinx += 1
253
- elif mode == 'preview' and preview_scroll < 50:
254
- preview_scroll += 1
255
- elif c3 == 'C': # Right
256
- if mode == 'list' and selected_folder < len(folders) - 1:
257
- selected_folder += 1
258
- current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
259
- selected_jinx = 0
260
- jinx_scroll = 0
261
- elif c3 == 'D': # Left
262
- if mode == 'list' and selected_folder > 0:
263
- selected_folder -= 1
264
- current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
265
- selected_jinx = 0
266
- jinx_scroll = 0
267
- else:
268
- if mode == 'preview':
269
- mode = 'list'
270
- sys.stdout.write('\033[2J\033[H')
271
- else:
272
- context['output'] = "Cancelled."
273
- break
274
- continue
275
-
276
- if c == 'q' or c == '\x03':
277
- context['output'] = "Cancelled."
278
- break
279
- elif c == 'k':
280
- if mode == 'list' and selected_jinx > 0:
281
- selected_jinx -= 1
282
- elif mode == 'preview' and preview_scroll > 0:
283
- preview_scroll -= 1
284
- elif c == 'j':
285
- if mode == 'list' and selected_jinx < len(current_jinxs) - 1:
286
- selected_jinx += 1
287
- elif mode == 'preview' and preview_scroll < 50:
288
- preview_scroll += 1
289
- elif c == 'h' and mode == 'list':
290
- if selected_folder > 0:
291
- selected_folder -= 1
292
- current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
293
- selected_jinx = 0
294
- jinx_scroll = 0
295
- elif c == 'l' and mode == 'list':
296
- if selected_folder < len(folders) - 1:
297
- selected_folder += 1
298
- current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
299
- selected_jinx = 0
300
- jinx_scroll = 0
301
- elif c == 'f' and mode == 'list':
302
- # Toggle filter - cycle through common filters or clear
303
- if not filter_text:
304
- filter_text = 'search'
305
- elif filter_text == 'search':
306
- filter_text = 'core'
307
- elif filter_text == 'core':
308
- filter_text = 'browser'
309
- else:
310
- filter_text = ''
311
- current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
312
- selected_jinx = 0
313
- jinx_scroll = 0
314
- elif c == 'p' and mode == 'list' and current_jinxs:
315
- mode = 'preview'
316
- preview_scroll = 0
317
- sys.stdout.write('\033[2J\033[H')
318
- elif c == 'b' and mode == 'preview':
319
- mode = 'list'
320
- sys.stdout.write('\033[2J\033[H')
321
- elif c in ('\r', '\n') and current_jinxs:
322
- j = current_jinxs[selected_jinx]
323
- context['output'] = f"Run: /{j['name']}\n\nDescription: {j['description']}"
324
- context['selected_jinx'] = j
325
- break
326
-
327
- finally:
328
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
329
- sys.stdout.write('\033[?25h')
330
- sys.stdout.write('\033[2J\033[H')
331
- sys.stdout.flush()
251
+ lines.append(f" - {inp}")
252
+ else:
253
+ lines.append(" (none)")
254
+ lines.append("")
255
+ lines.append(f"\033[1mPath:\033[0m")
256
+ lines.append(f" {j['path']}")
257
+
258
+ vis = lines[ui.detail_scroll:ui.detail_scroll + body_h]
259
+ for r in range(body_h):
260
+ row = start + r
261
+ if r < len(vis):
262
+ out.append(wline(row, f" {vis[r]}"))
263
+ else:
264
+ out.append(wline(row, ""))
265
+
266
+ # ── input handling ─────────────────────────────────────
267
+ def handle(c):
268
+ if ui.search_mode:
269
+ return handle_search(c)
270
+ if c == '\x1b':
271
+ return handle_esc()
272
+ if c == 'q':
273
+ if ui.detail:
274
+ ui.detail = False
275
+ ui.detail_scroll = 0
276
+ ui.status = ""
277
+ else:
278
+ return False
279
+ elif c == '\t':
280
+ ui.tab = (ui.tab + 1) % len(ui.tabs)
281
+ ui.sel = 0
282
+ ui.scroll = 0
283
+ ui.detail = False
284
+ ui.status = ""
285
+ apply_filters()
286
+ elif c == 'k':
287
+ nav_up()
288
+ elif c == 'j':
289
+ nav_down()
290
+ elif c in ('\r', '\n'):
291
+ do_enter()
292
+ elif c == '/':
293
+ ui.search_mode = True
294
+ ui.search_buf = ui.search_query
295
+ ui.status = ""
296
+ return True
297
+
298
+ def handle_esc():
299
+ if select.select([fd], [], [], 0.05)[0]:
300
+ c2 = os.read(fd, 1).decode('latin-1')
301
+ if c2 == '[':
302
+ c3 = os.read(fd, 1).decode('latin-1')
303
+ if c3 == 'A':
304
+ nav_up()
305
+ elif c3 == 'B':
306
+ nav_down()
307
+ # consume any other escape sequence
308
+ else:
309
+ # bare Esc
310
+ if ui.detail:
311
+ ui.detail = False
312
+ ui.detail_scroll = 0
313
+ ui.status = ""
314
+ elif ui.search_query:
315
+ ui.search_query = ""
316
+ ui.sel = 0
317
+ ui.scroll = 0
318
+ apply_filters()
319
+ ui.status = "Search cleared"
320
+ return True
321
+
322
+ def handle_search(c):
323
+ if c == '\x1b':
324
+ # Check if arrow key or bare esc
325
+ if select.select([fd], [], [], 0.05)[0]:
326
+ c2 = os.read(fd, 1).decode('latin-1')
327
+ if c2 == '[':
328
+ os.read(fd, 1).decode('latin-1') # consume arrow char
329
+ else:
330
+ ui.search_mode = False
331
+ ui.search_buf = ""
332
+ ui.status = "Search cancelled"
333
+ elif c in ('\r', '\n'):
334
+ ui.search_mode = False
335
+ ui.search_query = ui.search_buf
336
+ ui.search_buf = ""
337
+ ui.sel = 0
338
+ ui.scroll = 0
339
+ apply_filters()
340
+ if ui.search_query:
341
+ ui.status = f"Filter: \"{ui.search_query}\" ({len(ui.filtered)} results)"
342
+ else:
343
+ ui.status = "Search cleared"
344
+ elif c in ('\x7f', '\x08'):
345
+ ui.search_buf = ui.search_buf[:-1]
346
+ elif c == '\x15': # Ctrl+U clear line
347
+ ui.search_buf = ""
348
+ elif 32 <= ord(c) <= 126:
349
+ ui.search_buf += c
350
+ return True
351
+
352
+ def nav_up():
353
+ if ui.detail:
354
+ ui.detail_scroll = max(0, ui.detail_scroll - 1)
355
+ else:
356
+ ui.sel = max(0, ui.sel - 1)
357
+ if ui.sel < ui.scroll:
358
+ ui.scroll = ui.sel
359
+ ui.status = ""
360
+
361
+ def nav_down():
362
+ _, H = term_size()
363
+ body_h = H - 8 # body_start=6, footer uses 3 lines
364
+ if body_h < 1:
365
+ body_h = 1
366
+ if ui.detail:
367
+ ui.detail_scroll += 1
368
+ else:
369
+ mx = max(0, len(ui.filtered) - 1)
370
+ ui.sel = min(mx, ui.sel + 1)
371
+ if ui.sel >= ui.scroll + body_h:
372
+ ui.scroll = ui.sel - body_h + 1
373
+ ui.status = ""
374
+
375
+ def do_enter():
376
+ if ui.detail:
377
+ # Already in detail, do nothing extra
378
+ pass
379
+ elif ui.filtered:
380
+ ui.detail = True
381
+ ui.detail_scroll = 0
382
+ ui.status = ""
383
+
384
+ # ── main loop ──────────────────────────────────────────
385
+ ui.all_jinxs = load_jinxs()
386
+ apply_filters()
387
+
388
+ fd = sys.stdin.fileno()
389
+ old_attrs = termios.tcgetattr(fd)
390
+
391
+ try:
392
+ tty.setcbreak(fd)
393
+ sys.stdout.write('\033[?25l') # hide cursor
394
+ sys.stdout.write('\033[2J\033[H') # initial full clear
395
+ sys.stdout.flush()
396
+ render()
397
+ while True:
398
+ c = os.read(fd, 1).decode('latin-1')
399
+ if not handle(c):
400
+ break
401
+ render()
402
+ finally:
403
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
404
+ sys.stdout.write('\033[?25h\033[2J\033[H')
405
+ sys.stdout.flush()
406
+
407
+ context['output'] = "Jinxs browser closed."