npcsh 1.1.17__py3-none-any.whl → 1.1.18__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 (169) hide show
  1. npcsh/_state.py +114 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +46 -12
  5. npcsh/benchmark/runner.py +85 -43
  6. npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
  7. npcsh/build.py +2 -4
  8. npcsh/completion.py +2 -6
  9. npcsh/config.py +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  18. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  19. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  20. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  21. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  22. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  23. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  24. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  25. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  26. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  27. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  28. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  29. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  30. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  31. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  32. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  33. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  34. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  35. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  36. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  37. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  38. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  39. npcsh/npcsh.py +7 -4
  40. npcsh/plonk.py +0 -1
  41. npcsh/pti.py +0 -1
  42. npcsh/routes.py +1 -3
  43. npcsh/spool.py +0 -1
  44. npcsh/ui.py +0 -1
  45. npcsh/wander.py +0 -1
  46. npcsh/yap.py +0 -1
  47. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  48. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  49. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  50. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  51. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  52. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  53. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  55. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  56. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  57. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  58. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  59. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  60. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  61. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  62. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  63. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  64. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  65. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  66. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  67. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  68. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
  69. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
  70. npcsh-1.1.18.dist-info/RECORD +235 -0
  71. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  72. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
  73. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  74. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  75. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  76. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  77. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  78. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  79. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  80. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  81. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  82. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  83. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  84. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  85. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  86. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  87. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  88. npcsh-1.1.17.dist-info/RECORD +0 -219
  89. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  90. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  91. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  92. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  93. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  94. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
  96. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  98. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  100. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
  102. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
  103. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  104. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  106. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  107. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  108. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  109. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  110. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  112. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  113. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  114. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  115. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
  116. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  117. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
  118. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  119. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  120. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  121. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  122. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  123. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  124. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  125. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
  126. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  127. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  128. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
  129. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  130. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  131. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
  132. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  133. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  134. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  135. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  168. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  169. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,339 @@
1
+ jinx_name: file_search
2
+ description: Find and browse files with interactive TUI
3
+ inputs:
4
+ - pattern: ""
5
+ - path: "."
6
+ - recursive: "true"
7
+ - text: "false"
8
+
9
+ steps:
10
+ - name: search_files
11
+ engine: python
12
+ code: |
13
+ import os
14
+ import sys
15
+ import tty
16
+ import termios
17
+ import glob as globmod
18
+ import subprocess
19
+ import fnmatch
20
+ from datetime import datetime
21
+
22
+ pattern = context.get('pattern', '').strip()
23
+ base_path = context.get('path', '.').strip() or '.'
24
+ recursive = context.get('recursive', 'true').lower() in ('true', '1', 'yes')
25
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
26
+
27
+ if not pattern:
28
+ lines = [
29
+ "Usage: /file_search <pattern>",
30
+ "",
31
+ "Options:",
32
+ " path - Base directory to search (default: current dir)",
33
+ " recursive - Search subdirectories (default: true)",
34
+ " text - Text-only output, no TUI (true/false)",
35
+ "",
36
+ "TUI Controls:",
37
+ " j/k or arrows - Navigate",
38
+ " 1/2/3 - Sort by name/size/date",
39
+ " p - Preview file contents",
40
+ " o - Open in $EDITOR",
41
+ " i - Open in incognide",
42
+ " c - Copy path to clipboard",
43
+ " q/ESC - Quit",
44
+ "",
45
+ "Examples:",
46
+ " /file_search *.py",
47
+ " /file_search *.js path=src",
48
+ " /file_search test_*.py recursive=false",
49
+ ]
50
+ context['output'] = "\n".join(lines)
51
+ else:
52
+ base_path = os.path.expanduser(base_path)
53
+ if not os.path.isabs(base_path):
54
+ base_path = os.path.abspath(base_path)
55
+
56
+ try:
57
+ # Find matching files
58
+ files = []
59
+
60
+ if recursive:
61
+ # Walk directory tree
62
+ for root, dirs, filenames in os.walk(base_path):
63
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
64
+ for f in filenames:
65
+ if fnmatch.fnmatch(f, pattern) or fnmatch.fnmatch(f.lower(), pattern.lower()):
66
+ fpath = os.path.join(root, f)
67
+ try:
68
+ stat = os.stat(fpath)
69
+ files.append({
70
+ 'name': f,
71
+ 'path': fpath,
72
+ 'size': stat.st_size,
73
+ 'mtime': stat.st_mtime
74
+ })
75
+ except:
76
+ pass
77
+ else:
78
+ # Just current directory
79
+ for f in os.listdir(base_path):
80
+ if fnmatch.fnmatch(f, pattern) or fnmatch.fnmatch(f.lower(), pattern.lower()):
81
+ fpath = os.path.join(base_path, f)
82
+ if os.path.isfile(fpath):
83
+ try:
84
+ stat = os.stat(fpath)
85
+ files.append({
86
+ 'name': f,
87
+ 'path': fpath,
88
+ 'size': stat.st_size,
89
+ 'mtime': stat.st_mtime
90
+ })
91
+ except:
92
+ pass
93
+
94
+ if not files:
95
+ context['output'] = f"No files matching '{pattern}' in {base_path}"
96
+ elif text_mode:
97
+ # Text-only output
98
+ lines = [f"Found {len(files)} files matching '{pattern}':", ""]
99
+ for f in sorted(files, key=lambda x: x['name']):
100
+ lines.append(f" {f['path']}")
101
+ context['output'] = "\n".join(lines)
102
+ else:
103
+ # Interactive TUI mode
104
+ def get_terminal_size():
105
+ try:
106
+ size = os.get_terminal_size()
107
+ return size.columns, size.lines
108
+ except:
109
+ return 80, 24
110
+
111
+ def format_size(size):
112
+ if size < 1024:
113
+ return f"{size}B"
114
+ elif size < 1024 * 1024:
115
+ return f"{size // 1024}K"
116
+ else:
117
+ return f"{size // (1024 * 1024)}M"
118
+
119
+ def format_date(mtime):
120
+ try:
121
+ dt = datetime.fromtimestamp(mtime)
122
+ now = datetime.now()
123
+ diff = now - dt
124
+ if diff.days == 0:
125
+ return dt.strftime('%H:%M')
126
+ elif diff.days < 7:
127
+ return dt.strftime('%a %H:%M')
128
+ else:
129
+ return dt.strftime('%b %d')
130
+ except:
131
+ return '?'
132
+
133
+ width, height = get_terminal_size()
134
+ selected = 0
135
+ scroll = 0
136
+ list_height = height - 5
137
+ mode = 'list'
138
+ preview_scroll = 0
139
+ preview_lines = []
140
+ sort_mode = 'name' # name, size, date
141
+
142
+ def sort_files(files, sort_mode):
143
+ if sort_mode == 'name':
144
+ return sorted(files, key=lambda x: x['name'].lower())
145
+ elif sort_mode == 'size':
146
+ return sorted(files, key=lambda x: x['size'], reverse=True)
147
+ elif sort_mode == 'date':
148
+ return sorted(files, key=lambda x: x['mtime'], reverse=True)
149
+ return files
150
+
151
+ display_files = sort_files(files, sort_mode)
152
+
153
+ fd = sys.stdin.fileno()
154
+ old_settings = termios.tcgetattr(fd)
155
+
156
+ try:
157
+ tty.setcbreak(fd)
158
+ sys.stdout.write('\033[?25l')
159
+ sys.stdout.write('\033[2J\033[H')
160
+
161
+ while True:
162
+ width, height = get_terminal_size()
163
+ list_height = height - 5
164
+
165
+ if mode == 'list':
166
+ if selected < scroll:
167
+ scroll = selected
168
+ elif selected >= scroll + list_height:
169
+ scroll = selected - list_height + 1
170
+
171
+ sys.stdout.write('\033[H')
172
+
173
+ # Header
174
+ if mode == 'list':
175
+ sort_ind = {'name': '1', 'size': '2', 'date': '3'}[sort_mode]
176
+ header = f" FILES ({len(display_files)}): '{pattern}' [sort:{sort_mode}({sort_ind})] "
177
+ else:
178
+ header = f" PREVIEW: {display_files[selected]['name']} "
179
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
180
+
181
+ # Column headers
182
+ if mode == 'list':
183
+ col_header = f' {"NAME":<30} {"SIZE":<8} {"MODIFIED":<12} {"PATH":<30}'
184
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
185
+ else:
186
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
187
+
188
+ if mode == 'list':
189
+ for i in range(list_height):
190
+ idx = scroll + i
191
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
192
+ if idx >= len(display_files):
193
+ continue
194
+
195
+ f = display_files[idx]
196
+ name = f['name'][:30]
197
+ size = format_size(f['size'])
198
+ mtime = format_date(f['mtime'])
199
+ # Show relative path if possible
200
+ path = f['path']
201
+ cwd = os.getcwd()
202
+ if path.startswith(cwd):
203
+ path = '.' + path[len(cwd):]
204
+ path = path[:35]
205
+
206
+ line = f" {name:<30} {size:<8} {mtime:<12} {path}"
207
+ line = line[:width-1]
208
+
209
+ if idx == selected:
210
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
211
+ else:
212
+ sys.stdout.write(f' {line}')
213
+
214
+ # Status bar
215
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
216
+ f = display_files[selected] if display_files else {}
217
+ full_path = f.get('path', '')
218
+ sys.stdout.write(f'\033[{height-1};1H\033[K {full_path}'.ljust(width)[:width])
219
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav 1/2/3:Sort p:Preview o:Edit i:Incog c:Copy q:Quit [{selected+1}/{len(display_files)}] \033[0m')
220
+
221
+ elif mode == 'preview':
222
+ for i in range(list_height):
223
+ idx = preview_scroll + i
224
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
225
+ if idx < len(preview_lines):
226
+ sys.stdout.write(preview_lines[idx][:width-1])
227
+
228
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
229
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
230
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back o:Edit q:Quit \033[0m')
231
+
232
+ sys.stdout.flush()
233
+
234
+ c = sys.stdin.read(1)
235
+
236
+ if c == '\x1b':
237
+ c2 = sys.stdin.read(1)
238
+ if c2 == '[':
239
+ c3 = sys.stdin.read(1)
240
+ if c3 == 'A':
241
+ if mode == 'list' and selected > 0:
242
+ selected -= 1
243
+ elif mode == 'preview' and preview_scroll > 0:
244
+ preview_scroll -= 1
245
+ elif c3 == 'B':
246
+ if mode == 'list' and selected < len(display_files) - 1:
247
+ selected += 1
248
+ elif mode == 'preview' and preview_scroll < 500:
249
+ preview_scroll += 1
250
+ else:
251
+ if mode == 'preview':
252
+ mode = 'list'
253
+ sys.stdout.write('\033[2J\033[H')
254
+ else:
255
+ context['output'] = "Cancelled."
256
+ break
257
+ continue
258
+
259
+ if c == 'q' or c == '\x03':
260
+ context['output'] = "Cancelled."
261
+ break
262
+ elif c == 'k':
263
+ if mode == 'list' and selected > 0:
264
+ selected -= 1
265
+ elif mode == 'preview' and preview_scroll > 0:
266
+ preview_scroll -= 1
267
+ elif c == 'j':
268
+ if mode == 'list' and selected < len(display_files) - 1:
269
+ selected += 1
270
+ elif mode == 'preview' and preview_scroll < 500:
271
+ preview_scroll += 1
272
+ elif c == '1':
273
+ sort_mode = 'name'
274
+ display_files = sort_files(files, sort_mode)
275
+ selected = 0
276
+ scroll = 0
277
+ elif c == '2':
278
+ sort_mode = 'size'
279
+ display_files = sort_files(files, sort_mode)
280
+ selected = 0
281
+ scroll = 0
282
+ elif c == '3':
283
+ sort_mode = 'date'
284
+ display_files = sort_files(files, sort_mode)
285
+ selected = 0
286
+ scroll = 0
287
+ elif c == 'p' and mode == 'list' and display_files:
288
+ # Preview file contents
289
+ f = display_files[selected]
290
+ try:
291
+ with open(f['path'], 'r', errors='replace') as fp:
292
+ content = fp.read(50000)
293
+ preview_lines = content.split('\n')[:500]
294
+ except:
295
+ preview_lines = ['Error reading file']
296
+ mode = 'preview'
297
+ preview_scroll = 0
298
+ sys.stdout.write('\033[2J\033[H')
299
+ elif c == 'b' and mode == 'preview':
300
+ mode = 'list'
301
+ sys.stdout.write('\033[2J\033[H')
302
+ elif c == 'o' and display_files:
303
+ f = display_files[selected]
304
+ editor = os.environ.get('EDITOR', 'vim')
305
+ # Restore terminal, run editor, then back to TUI
306
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
307
+ sys.stdout.write('\033[?25h\033[2J\033[H')
308
+ os.system(f'{editor} "{f["path"]}"')
309
+ tty.setcbreak(fd)
310
+ sys.stdout.write('\033[?25l\033[2J\033[H')
311
+ elif c == 'i' and display_files:
312
+ f = display_files[selected]
313
+ try:
314
+ subprocess.run(['npcsh', '-c', f'/navigate url=file://{f["path"]}'], check=False, capture_output=True)
315
+ except:
316
+ pass
317
+ elif c == 'c' and display_files:
318
+ f = display_files[selected]
319
+ try:
320
+ subprocess.run(['xclip', '-selection', 'clipboard'], input=f['path'].encode(), check=True)
321
+ except:
322
+ try:
323
+ subprocess.run(['xsel', '--clipboard', '--input'], input=f['path'].encode(), check=True)
324
+ except:
325
+ pass
326
+ elif c in ('\r', '\n') and display_files:
327
+ f = display_files[selected]
328
+ context['output'] = f"Selected: {f['name']}\nPath: {f['path']}"
329
+ break
330
+
331
+ finally:
332
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
333
+ sys.stdout.write('\033[?25h')
334
+ sys.stdout.write('\033[2J\033[H')
335
+ sys.stdout.flush()
336
+
337
+ except Exception as e:
338
+ import traceback
339
+ context['output'] = "File search error: " + str(e) + "\n" + traceback.format_exc()