npcsh 1.1.18__py3-none-any.whl → 1.1.20__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 (165) hide show
  1. npcsh/_state.py +19 -7
  2. npcsh/benchmark/npcsh_agent.py +47 -16
  3. npcsh/config.py +1 -0
  4. npcsh/diff_viewer.py +452 -0
  5. npcsh/npc_team/jinxs/bin/config_tui.jinx +300 -0
  6. npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
  7. npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
  8. npcsh/npc_team/jinxs/bin/memories.jinx +317 -0
  9. npcsh/npc_team/jinxs/bin/models.jinx +343 -0
  10. npcsh/npc_team/jinxs/bin/nql.jinx +380 -50
  11. npcsh/npc_team/jinxs/bin/setup.jinx +241 -0
  12. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  13. npcsh/npc_team/jinxs/bin/team.jinx +504 -0
  14. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  15. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  16. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  17. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  18. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  19. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  20. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  21. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  22. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  23. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  24. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  27. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  28. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  29. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  30. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  31. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
  32. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
  33. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
  34. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
  35. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
  36. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
  37. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
  38. npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
  39. npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
  40. npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
  41. npcsh/npc_team/jinxs/modes/guac.jinx +4 -6
  42. npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
  43. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  44. npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
  45. npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
  46. npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
  47. npcsh/routes.py +8 -2
  48. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  49. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.jinx +1 -1
  50. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/arxiv.jinx +1 -1
  51. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  52. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  53. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  54. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  55. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.jinx +1 -1
  56. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/db_search.jinx +1 -1
  57. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/file_search.jinx +1 -1
  58. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  59. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.jinx +4 -6
  60. npcsh-1.1.20.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  61. npcsh-1.1.20.data/data/npcsh/npc_team/kg.jinx +941 -0
  62. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kg_search.jinx +1 -1
  63. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  64. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_search.jinx +1 -1
  65. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +317 -0
  66. npcsh-1.1.20.data/data/npcsh/npc_team/models.jinx +343 -0
  67. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  68. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/notify.jinx +1 -1
  69. npcsh-1.1.20.data/data/npcsh/npc_team/nql.jinx +471 -0
  70. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  71. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paper_search.jinx +1 -1
  72. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.jinx +1 -1
  73. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/pti.jinx +1 -1
  74. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  75. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/reattach.jinx +1 -1
  76. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  77. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/semantic_scholar.jinx +1 -1
  78. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  79. npcsh-1.1.20.data/data/npcsh/npc_team/setup.jinx +241 -0
  80. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  81. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.jinx +1 -1
  82. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  83. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  84. npcsh-1.1.20.data/data/npcsh/npc_team/sync.jinx +223 -0
  85. npcsh-1.1.20.data/data/npcsh/npc_team/team.jinx +504 -0
  86. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wander.jinx +1 -1
  87. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/web_search.jinx +1 -1
  88. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  89. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  90. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/METADATA +21 -14
  91. npcsh-1.1.20.dist-info/RECORD +248 -0
  92. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/entry_points.txt +7 -0
  93. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
  94. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  95. npcsh-1.1.18.data/data/npcsh/npc_team/nql.jinx +0 -141
  96. npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
  97. npcsh-1.1.18.dist-info/RECORD +0 -235
  98. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  99. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.png +0 -0
  100. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  101. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  102. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  103. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/build.jinx +0 -0
  104. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/chat.jinx +0 -0
  105. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/click.jinx +0 -0
  106. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  107. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  108. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compile.jinx +0 -0
  109. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compress.jinx +0 -0
  110. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/convene.jinx +0 -0
  111. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.npc +0 -0
  112. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.png +0 -0
  113. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca_example.png +0 -0
  114. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  115. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  116. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic.npc +0 -0
  117. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic4.png +0 -0
  118. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.npc +0 -0
  119. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.png +0 -0
  120. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/help.jinx +0 -0
  121. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  122. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/init.jinx +0 -0
  123. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  124. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  125. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  126. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  127. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  129. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  130. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  131. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  132. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/ots.jinx +0 -0
  133. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paste.jinx +0 -0
  134. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.npc +0 -0
  135. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.png +0 -0
  136. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  137. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  138. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/python.jinx +0 -0
  139. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sample.jinx +0 -0
  141. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  142. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/search.jinx +0 -0
  143. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/serve.jinx +0 -0
  144. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/set.jinx +0 -0
  145. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sh.jinx +0 -0
  146. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/shh.jinx +0 -0
  147. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  148. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.png +0 -0
  149. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  150. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.png +0 -0
  151. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sql.jinx +0 -0
  152. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch.jinx +0 -0
  153. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switches.jinx +0 -0
  154. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  155. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  156. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  157. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/usage.jinx +0 -0
  158. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  159. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  160. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wait.jinx +0 -0
  161. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.jinx +0 -0
  162. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.png +0 -0
  163. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/WHEEL +0 -0
  164. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/licenses/LICENSE +0 -0
  165. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/top_level.txt +0 -0
@@ -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
+ q = ui.search_query.lower()
108
+ items = [j for j in items if q in j['name'].lower() or q 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[44;37;1m{pad}\033[0m"))
133
+ out.append(f"\033[1;{max(1, (W - len(hdr)) // 2)}H\033[44;37;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[44;37m{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([sys.stdin], [], [], 0.05)[0]:
300
+ c2 = sys.stdin.read(1)
301
+ if c2 == '[':
302
+ c3 = sys.stdin.read(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([sys.stdin], [], [], 0.05)[0]:
326
+ c2 = sys.stdin.read(1)
327
+ if c2 == '[':
328
+ sys.stdin.read(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 = sys.stdin.read(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."