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,5 +1,5 @@
1
1
  jinx_name: guac
2
- description: Interactive Python data analysis TUI - live variable inspector, DataFrame viewer, code execution
2
+ description: Interactive Python TUI - live variable inspector, code execution, DataFrame viewer
3
3
  inputs:
4
4
  - model: null
5
5
  - provider: null
@@ -202,7 +202,7 @@ steps:
202
202
  # ===== HEADER =====
203
203
  mode_colors = {"code": "\033[32m", "natural": "\033[35m", "inspect": "\033[33m"}
204
204
  mode_str = f"{mode_colors[state.mode]}[{state.mode}]\033[0m"
205
- header = f" GUAC - Python Data Analysis {mode_str} "
205
+ header = f" GUAC - Interactive Python {mode_str} "
206
206
  status_color = "\033[33m" if "..." in state.status else "\033[32m"
207
207
  out.append(f"\033[1;1H\033[42;30;1m{header.ljust(width)}\033[0m")
208
208
  out.append(f"\033[1;{width-len(state.status)-3}H{status_color}[{state.status}]\033[0m")
@@ -275,7 +275,7 @@ steps:
275
275
  tabs = ""
276
276
  for i, name in enumerate(panel_names):
277
277
  if i == state.panel:
278
- tabs += f"\033[47;30m {name} \033[0m"
278
+ tabs += f"\033[7m {name} \033[0m"
279
279
  else:
280
280
  tabs += f"\033[90m {name} \033[0m"
281
281
  out.append(f"\033[3;{right_x}H{tabs}")
@@ -291,7 +291,7 @@ steps:
291
291
  info = var_info(name, value)
292
292
  display = f"{name[:10]:<10} {info[:rpanel_w-12]}"
293
293
  if idx == state.selected_var:
294
- out.append(f"\033[{4+i};{right_x}H\033[47;30m>{display[:rpanel_w]}\033[0m")
294
+ out.append(f"\033[{4+i};{right_x}H\033[7m>{display[:rpanel_w]}\033[0m")
295
295
  elif isinstance(value, pd.DataFrame):
296
296
  out.append(f"\033[{4+i};{right_x}H\033[34m {display[:rpanel_w]}\033[0m")
297
297
  elif isinstance(value, np.ndarray):
@@ -0,0 +1,407 @@
1
+ jinx_name: jinxs
2
+ description: Interactive jinx browser - browse, search, and preview available jinxs
3
+ interactive: true
4
+ inputs: []
5
+ steps:
6
+ - name: jinxs_browser
7
+ engine: python
8
+ code: |
9
+ import os
10
+ import sys
11
+ import tty
12
+ import termios
13
+ import select
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."
22
+
23
+ else:
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():
87
+ try:
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"
140
+ else:
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}")
250
+ else:
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."