npcsh 1.1.16__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 (217) hide show
  1. npcsh/_state.py +138 -100
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +28 -0
  4. npcsh/benchmark/npcsh_agent.py +296 -0
  5. npcsh/benchmark/runner.py +611 -0
  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 +146 -0
  18. npcsh/npc_team/jinxs/bin/nql.jinx +7 -7
  19. npcsh/npc_team/jinxs/bin/roll.jinx +20 -23
  20. npcsh/npc_team/jinxs/bin/sample.jinx +6 -7
  21. npcsh/npc_team/jinxs/bin/sync.jinx +6 -6
  22. npcsh/npc_team/jinxs/bin/vixynt.jinx +8 -8
  23. npcsh/npc_team/jinxs/incognide/add_tab.jinx +11 -0
  24. npcsh/npc_team/jinxs/incognide/close_pane.jinx +9 -0
  25. npcsh/npc_team/jinxs/incognide/close_tab.jinx +10 -0
  26. npcsh/npc_team/jinxs/incognide/confirm.jinx +10 -0
  27. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +9 -0
  28. npcsh/npc_team/jinxs/{npc_studio/npc-studio.jinx → incognide/incognide.jinx} +2 -2
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +8 -0
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +10 -0
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +10 -0
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +13 -0
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +9 -0
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +10 -0
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +10 -0
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +12 -0
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +10 -0
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +10 -0
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +11 -0
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +9 -0
  41. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +4 -4
  42. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +1 -1
  43. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +2 -2
  44. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +2 -2
  45. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +1 -1
  46. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +1 -1
  47. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +1 -1
  48. npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +2 -2
  49. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +1 -1
  50. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +1 -1
  51. npcsh/npc_team/jinxs/lib/core/chat.jinx +4 -4
  52. npcsh/npc_team/jinxs/lib/core/cmd.jinx +4 -4
  53. npcsh/npc_team/jinxs/lib/core/compress.jinx +8 -8
  54. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +3 -0
  55. npcsh/npc_team/jinxs/lib/core/ots.jinx +7 -7
  56. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +348 -0
  57. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +339 -0
  58. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +418 -0
  59. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  60. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +388 -0
  61. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +283 -0
  62. npcsh/npc_team/jinxs/lib/core/search.jinx +52 -129
  63. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  64. npcsh/npc_team/jinxs/lib/core/sleep.jinx +29 -18
  65. npcsh/npc_team/jinxs/lib/core/sql.jinx +15 -11
  66. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +7 -7
  67. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +8 -9
  68. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +389 -78
  69. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +373 -56
  70. npcsh/npc_team/jinxs/lib/utils/build.jinx +5 -5
  71. npcsh/npc_team/jinxs/lib/utils/compile.jinx +2 -2
  72. npcsh/npc_team/jinxs/lib/utils/help.jinx +1 -1
  73. npcsh/npc_team/jinxs/lib/utils/init.jinx +5 -5
  74. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +300 -145
  75. npcsh/npc_team/jinxs/lib/utils/serve.jinx +2 -2
  76. npcsh/npc_team/jinxs/lib/utils/set.jinx +2 -2
  77. npcsh/npc_team/jinxs/lib/utils/switch.jinx +3 -3
  78. npcsh/npc_team/jinxs/lib/utils/switches.jinx +1 -1
  79. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +2 -2
  80. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  81. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  82. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  83. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  84. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  85. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  86. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  87. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  88. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  89. {npcsh-1.1.16.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/yap.jinx +8 -2
  90. npcsh/npc_team/sibiji.npc +1 -1
  91. npcsh/npcsh.py +87 -46
  92. npcsh/plonk.py +0 -1
  93. npcsh/pti.py +0 -1
  94. npcsh/routes.py +1 -3
  95. npcsh/spool.py +0 -1
  96. npcsh/ui.py +0 -1
  97. npcsh/wander.py +0 -1
  98. npcsh/yap.py +0 -1
  99. npcsh-1.1.18.data/data/npcsh/npc_team/add_tab.jinx +11 -0
  100. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  101. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  102. npcsh-1.1.18.data/data/npcsh/npc_team/benchmark.jinx +146 -0
  103. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +4 -4
  104. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +1 -1
  105. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +5 -5
  106. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +4 -4
  107. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +2 -2
  108. npcsh-1.1.18.data/data/npcsh/npc_team/close_pane.jinx +9 -0
  109. npcsh-1.1.18.data/data/npcsh/npc_team/close_tab.jinx +10 -0
  110. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +4 -4
  111. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +2 -2
  112. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +8 -8
  113. npcsh-1.1.18.data/data/npcsh/npc_team/confirm.jinx +10 -0
  114. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +7 -7
  115. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  116. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  117. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +8 -9
  118. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +3 -0
  119. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  120. npcsh-1.1.18.data/data/npcsh/npc_team/focus_pane.jinx +9 -0
  121. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  122. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +1 -1
  123. npcsh-1.1.16.data/data/npcsh/npc_team/npc-studio.jinx → npcsh-1.1.18.data/data/npcsh/npc_team/incognide.jinx +2 -2
  124. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +5 -5
  125. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  126. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +1 -1
  127. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  128. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +1 -1
  129. npcsh-1.1.18.data/data/npcsh/npc_team/list_panes.jinx +8 -0
  130. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  131. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  132. npcsh-1.1.18.data/data/npcsh/npc_team/navigate.jinx +10 -0
  133. npcsh-1.1.18.data/data/npcsh/npc_team/notify.jinx +10 -0
  134. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +7 -7
  135. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +2 -2
  136. npcsh-1.1.18.data/data/npcsh/npc_team/open_pane.jinx +13 -0
  137. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +7 -7
  138. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  139. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  140. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  141. npcsh-1.1.18.data/data/npcsh/npc_team/read_pane.jinx +9 -0
  142. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  143. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +20 -23
  144. npcsh-1.1.18.data/data/npcsh/npc_team/run_terminal.jinx +10 -0
  145. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +6 -7
  146. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +1 -1
  147. npcsh-1.1.18.data/data/npcsh/npc_team/search.jinx +54 -0
  148. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  149. npcsh-1.1.18.data/data/npcsh/npc_team/send_message.jinx +10 -0
  150. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +2 -2
  151. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +2 -2
  152. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +1 -1
  153. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +1 -1
  154. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +29 -18
  155. npcsh-1.1.18.data/data/npcsh/npc_team/split_pane.jinx +12 -0
  156. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  157. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  158. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +3 -3
  159. npcsh-1.1.18.data/data/npcsh/npc_team/switch_npc.jinx +10 -0
  160. npcsh-1.1.18.data/data/npcsh/npc_team/switch_tab.jinx +10 -0
  161. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +1 -1
  162. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +6 -6
  163. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +2 -2
  164. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +2 -2
  165. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +1 -1
  166. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +8 -8
  167. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +1 -1
  168. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  169. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  170. npcsh-1.1.18.data/data/npcsh/npc_team/write_file.jinx +11 -0
  171. {npcsh/npc_team/jinxs/bin → npcsh-1.1.18.data/data/npcsh/npc_team}/yap.jinx +8 -2
  172. npcsh-1.1.18.data/data/npcsh/npc_team/zen_mode.jinx +9 -0
  173. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/METADATA +99 -7
  174. npcsh-1.1.18.dist-info/RECORD +235 -0
  175. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  176. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +2 -3
  177. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  178. npcsh/npc_team/jinxs/bin/wander.jinx +0 -152
  179. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  180. npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  181. npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  182. npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  183. npcsh-1.1.16.data/data/npcsh/npc_team/search.jinx +0 -131
  184. npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  185. npcsh-1.1.16.data/data/npcsh/npc_team/spool.jinx +0 -161
  186. npcsh-1.1.16.data/data/npcsh/npc_team/sql.jinx +0 -16
  187. npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +0 -152
  188. npcsh-1.1.16.dist-info/RECORD +0 -170
  189. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  190. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  191. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  192. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  193. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  194. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  195. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  196. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  197. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  198. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  199. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  200. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  201. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  202. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  203. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  204. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  205. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  206. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  207. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  208. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  209. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  210. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  211. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  212. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  213. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  214. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  215. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  216. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  217. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,720 @@
1
+ jinx_name: arxiv
2
+ description: Interactive arXiv paper browser
3
+ inputs:
4
+ - query: ""
5
+ - author: ""
6
+ - category: ""
7
+ - title: ""
8
+ - abstract: ""
9
+ - limit: 10
10
+ - text: "false"
11
+
12
+ steps:
13
+ - name: search_and_browse
14
+ engine: python
15
+ code: |
16
+ import os
17
+ import sys
18
+ import tty
19
+ import termios
20
+ import subprocess
21
+ import urllib.request
22
+ import urllib.parse
23
+ import xml.etree.ElementTree as ET
24
+ import textwrap
25
+
26
+ def get_terminal_size():
27
+ try:
28
+ size = os.get_terminal_size()
29
+ return size.columns, size.lines
30
+ except:
31
+ return 80, 24
32
+
33
+ def image_to_ascii(image_path, width=40):
34
+ """Convert image to ASCII art"""
35
+ try:
36
+ from PIL import Image
37
+ img = Image.open(image_path)
38
+
39
+ # Skip tiny images (likely icons/logos)
40
+ if img.width < 50 or img.height < 50:
41
+ return None
42
+
43
+ # Convert to grayscale
44
+ img = img.convert('L')
45
+ # Resize - smaller for inline viewing
46
+ aspect = img.height / img.width
47
+ new_height = int(width * aspect * 0.4)
48
+ new_height = min(new_height, 20) # Cap height
49
+ img = img.resize((width, new_height))
50
+ # ASCII chars from dark to light
51
+ chars = ' .,:;+*?%#@'
52
+ pixels = list(img.getdata())
53
+ ascii_img = []
54
+ for i in range(0, len(pixels), width):
55
+ row = pixels[i:i+width]
56
+ ascii_row = ''.join(chars[min(p // 25, len(chars)-1)] for p in row)
57
+ ascii_img.append(' ' + ascii_row) # Indent
58
+ return ascii_img
59
+ except Exception as e:
60
+ return [' [Image error: ' + str(e)[:30] + ']']
61
+
62
+ def render_pdf_terminal(pdf_path, width=80):
63
+ """Render PDF as text + ASCII figures"""
64
+ import tempfile
65
+ lines = []
66
+
67
+ # Extract text - no layout flag for cleaner output
68
+ try:
69
+ result = subprocess.run(['pdftotext', '-nopgbrk', pdf_path, '-'],
70
+ capture_output=True, text=True, timeout=30)
71
+ if result.returncode == 0:
72
+ text_lines = result.stdout.split('\n')
73
+ # Clean up lines - remove excessive whitespace
74
+ for line in text_lines:
75
+ cleaned = line.strip()
76
+ if cleaned:
77
+ lines.append(cleaned[:width])
78
+ elif lines and lines[-1] != '':
79
+ lines.append('') # Keep single blank lines
80
+ except Exception as e:
81
+ lines.append('[Text extraction failed: ' + str(e) + ']')
82
+
83
+ # Try to extract and render images
84
+ try:
85
+ with tempfile.TemporaryDirectory() as tmpdir:
86
+ subprocess.run(['pdfimages', '-png', pdf_path, tmpdir + '/img'],
87
+ capture_output=True, timeout=60)
88
+ import glob
89
+ images = sorted(glob.glob(tmpdir + '/img*.png'))
90
+ # Filter to significant images only
91
+ sig_images = []
92
+ for img_path in images:
93
+ try:
94
+ from PIL import Image
95
+ img = Image.open(img_path)
96
+ if img.width >= 100 and img.height >= 100:
97
+ sig_images.append(img_path)
98
+ except:
99
+ pass
100
+
101
+ if sig_images:
102
+ lines.append('')
103
+ lines.append('─' * 50)
104
+ lines.append('FIGURES')
105
+ lines.append('─' * 50)
106
+ for i, img_path in enumerate(sig_images[:6]): # Max 6 figures
107
+ lines.append('')
108
+ lines.append('Fig ' + str(i+1) + ':')
109
+ ascii_lines = image_to_ascii(img_path, min(50, width-8))
110
+ if ascii_lines:
111
+ lines.extend(ascii_lines)
112
+ except Exception as e:
113
+ pass # Silently skip figure errors
114
+
115
+ return lines
116
+
117
+ def latex_to_unicode(text):
118
+ import re
119
+ # Greek letters
120
+ greek = {
121
+ r'\alpha': 'α', r'\beta': 'β', r'\gamma': 'γ', r'\delta': 'δ',
122
+ r'\epsilon': 'ε', r'\zeta': 'ζ', r'\eta': 'η', r'\theta': 'θ',
123
+ r'\iota': 'ι', r'\kappa': 'κ', r'\lambda': 'λ', r'\mu': 'μ',
124
+ r'\nu': 'ν', r'\xi': 'ξ', r'\pi': 'π', r'\rho': 'ρ',
125
+ r'\sigma': 'σ', r'\tau': 'τ', r'\upsilon': 'υ', r'\phi': 'φ',
126
+ r'\chi': 'χ', r'\psi': 'ψ', r'\omega': 'ω',
127
+ r'\Gamma': 'Γ', r'\Delta': 'Δ', r'\Theta': 'Θ', r'\Lambda': 'Λ',
128
+ r'\Xi': 'Ξ', r'\Pi': 'Π', r'\Sigma': 'Σ', r'\Phi': 'Φ',
129
+ r'\Psi': 'Ψ', r'\Omega': 'Ω',
130
+ }
131
+ # Math symbols
132
+ symbols = {
133
+ r'\times': '×', r'\div': '÷', r'\pm': '±', r'\mp': '∓',
134
+ r'\cdot': '·', r'\ast': '∗', r'\star': '★',
135
+ r'\leq': '≤', r'\geq': '≥', r'\neq': '≠', r'\approx': '≈',
136
+ r'\lesssim': '≲', r'\gtrsim': '≳', r'\ll': '≪', r'\gg': '≫',
137
+ r'\equiv': '≡', r'\sim': '∼', r'\simeq': '≃', r'\propto': '∝',
138
+ r'\infty': '∞', r'\partial': '∂', r'\nabla': '∇',
139
+ r'\sum': 'Σ', r'\prod': 'Π', r'\int': '∫',
140
+ r'\sqrt': '√', r'\circ': '°', r'\deg': '°',
141
+ r'\rightarrow': '→', r'\leftarrow': '←', r'\leftrightarrow': '↔',
142
+ r'\Rightarrow': '⇒', r'\Leftarrow': '⇐', r'\Leftrightarrow': '⇔',
143
+ r'\forall': '∀', r'\exists': '∃', r'\in': '∈', r'\notin': '∉',
144
+ r'\subset': '⊂', r'\supset': '⊃', r'\cup': '∪', r'\cap': '∩',
145
+ r'\emptyset': '∅', r'\ldots': '…', r'\cdots': '⋯',
146
+ r'\prime': '′', r'\hbar': 'ℏ', r'\ell': 'ℓ',
147
+ r'\odot': '⊙', r'\oplus': '⊕', r'\otimes': '⊗',
148
+ r'\%': '%', r'\&': '&', r'\#': '#',
149
+ r'\log': 'log', r'\ln': 'ln', r'\exp': 'exp',
150
+ r'\sin': 'sin', r'\cos': 'cos', r'\tan': 'tan',
151
+ r'\arcsin': 'arcsin', r'\arccos': 'arccos', r'\arctan': 'arctan',
152
+ r'\sinh': 'sinh', r'\cosh': 'cosh', r'\tanh': 'tanh',
153
+ r'\max': 'max', r'\min': 'min', r'\lim': 'lim',
154
+ r'\det': 'det', r'\ker': 'ker', r'\dim': 'dim',
155
+ r'\mathrm': '', r'\mathbf': '', r'\mathit': '', r'\textrm': '',
156
+ r'\textit': '', r'\textbf': '', r'\emph': '',
157
+ r'\AA': 'Å', r'\angstrom': 'Å',
158
+ r'\degree': '°', r'\celsius': '℃',
159
+ r'\le': '≤', r'\ge': '≥', r'\ne': '≠',
160
+ r'\to': '→', r'\gets': '←',
161
+ }
162
+ # Superscripts
163
+ superscripts = {'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
164
+ '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹',
165
+ '+': '⁺', '-': '⁻', 'n': 'ⁿ', 'i': 'ⁱ', '*': '✱'}
166
+ # Subscripts
167
+ subscripts = {'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄',
168
+ '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉',
169
+ '+': '₊', '-': '₋', 'a': 'ₐ', 'e': 'ₑ', 'i': 'ᵢ',
170
+ 'o': 'ₒ', 'r': 'ᵣ', 'u': 'ᵤ', 'v': 'ᵥ', 'x': 'ₓ',
171
+ '*': '∗', 'X': 'ₓ'}
172
+
173
+ # Apply Greek letters and symbols
174
+ for latex, uni in greek.items():
175
+ text = text.replace(latex, uni)
176
+ for latex, uni in symbols.items():
177
+ text = text.replace(latex, uni)
178
+
179
+ # Handle superscripts: ^{...} or ^x
180
+ def replace_super(m):
181
+ content = m.group(1) if m.group(1) else m.group(2)
182
+ return ''.join(superscripts.get(c, c) for c in content)
183
+ text = re.sub(r'\^{([^}]+)}|\^(\w)', replace_super, text)
184
+
185
+ # Handle subscripts: _{...} or _x
186
+ def replace_sub(m):
187
+ content = m.group(1) if m.group(1) else m.group(2)
188
+ return ''.join(subscripts.get(c, c) for c in content)
189
+ text = re.sub(r'_{([^}]+)}|_(\w)', replace_sub, text)
190
+
191
+ # Clean up $ delimiters and braces
192
+ text = text.replace('$', '')
193
+ text = re.sub(r'\\[a-zA-Z]+{([^}]*)}', r'\1', text) # \cmd{text} -> text
194
+ text = text.replace('{', '').replace('}', '')
195
+ text = text.replace('\\,', ' ') # thin space
196
+ text = text.replace('\\ ', ' ') # explicit space
197
+ text = text.replace('\\!', '') # negative space
198
+
199
+ # Common astronomy: M_* -> M∗, L_X -> Lₓ
200
+ text = text.replace('M*', 'M∗').replace('L*', 'L∗')
201
+
202
+ return text
203
+
204
+ query = context.get('query', '').strip()
205
+ author = context.get('author', '').strip()
206
+ category = context.get('category', '').strip()
207
+ title_filter = context.get('title', '').strip()
208
+ abstract_filter = context.get('abstract', '').strip()
209
+ limit = int(context.get('limit', 10) or 10)
210
+ text_only = str(context.get('text', 'false')).lower() in ('true', '1', 'yes')
211
+
212
+ # Build search query from inputs
213
+ search_parts = []
214
+
215
+ # Category expansion for parent categories
216
+ cat_expansions = {
217
+ 'astro-ph': ['astro-ph', 'astro-ph.GA', 'astro-ph.CO', 'astro-ph.EP', 'astro-ph.HE', 'astro-ph.IM', 'astro-ph.SR'],
218
+ 'cond-mat': ['cond-mat', 'cond-mat.dis-nn', 'cond-mat.mes-hall', 'cond-mat.mtrl-sci', 'cond-mat.other', 'cond-mat.quant-gas', 'cond-mat.soft', 'cond-mat.stat-mech', 'cond-mat.str-el', 'cond-mat.supr-con'],
219
+ 'hep': ['hep-ex', 'hep-lat', 'hep-ph', 'hep-th'],
220
+ 'physics': ['physics.acc-ph', 'physics.ao-ph', 'physics.atm-clus', 'physics.atom-ph', 'physics.bio-ph', 'physics.chem-ph', 'physics.class-ph', 'physics.comp-ph', 'physics.data-an', 'physics.flu-dyn', 'physics.gen-ph', 'physics.geo-ph', 'physics.hist-ph', 'physics.ins-det', 'physics.med-ph', 'physics.optics', 'physics.plasm-ph', 'physics.pop-ph', 'physics.soc-ph', 'physics.space-ph'],
221
+ }
222
+
223
+ if author:
224
+ for a in author.split(','):
225
+ search_parts.append("au:" + a.strip().replace(' ', '_'))
226
+ if category:
227
+ if category in cat_expansions:
228
+ cat_or = "+OR+".join(["cat:" + c for c in cat_expansions[category]])
229
+ search_parts.append("(" + cat_or + ")")
230
+ else:
231
+ search_parts.append("cat:" + category)
232
+ if title_filter:
233
+ search_parts.append("ti:" + title_filter)
234
+ if abstract_filter:
235
+ search_parts.append("abs:" + abstract_filter)
236
+ if query:
237
+ search_parts.append("all:" + query)
238
+
239
+ if not search_parts:
240
+ help_text = [
241
+ "Usage: /arxiv <query> [author=...] [category=...] [title=...] [limit=N]",
242
+ "",
243
+ "Inputs:",
244
+ " query - General search terms",
245
+ " author - Author name(s), comma-separated",
246
+ " category - arXiv category (astro-ph, astro-ph.GA, cs.AI, etc.)",
247
+ " title - Title keywords",
248
+ " abstract - Abstract keywords",
249
+ " limit - Max results (default 10)",
250
+ " text - Plain text output (true/false)",
251
+ "",
252
+ "Examples:",
253
+ " /arxiv author=Agostino",
254
+ " /arxiv author=Agostino category=astro-ph",
255
+ " /arxiv 'active galactic nuclei' author=Urry",
256
+ " /arxiv category=astro-ph.GA title='AGN feedback'",
257
+ " /arxiv author='Einstein, Bohr' limit=20"
258
+ ]
259
+ context['output'] = "\n".join(help_text)
260
+ else:
261
+ # Build search query
262
+ if len(search_parts) > 1:
263
+ search_query = "+AND+".join(search_parts)
264
+ else:
265
+ search_query = search_parts[0]
266
+
267
+ # Build URL manually to avoid encoding + signs in search query
268
+ url = "http://export.arxiv.org/api/query?search_query=" + search_query + "&start=0&max_results=" + str(limit) + "&sortBy=relevance&sortOrder=descending"
269
+
270
+ try:
271
+ with urllib.request.urlopen(url, timeout=30) as response:
272
+ data = response.read().decode('utf-8')
273
+
274
+ root = ET.fromstring(data)
275
+ ns = {'atom': 'http://www.w3.org/2005/Atom'}
276
+ entries = root.findall('atom:entry', ns)
277
+
278
+ if not entries:
279
+ context['output'] = "No papers found for: " + query
280
+ elif text_only:
281
+ results = []
282
+ for i, entry in enumerate(entries, 1):
283
+ title = entry.find('atom:title', ns).text.strip().replace('\n', ' ')
284
+ summary = entry.find('atom:summary', ns).text.strip()[:300] + '...'
285
+ published = entry.find('atom:published', ns).text[:10]
286
+ authors = [a.find('atom:name', ns).text for a in entry.findall('atom:author', ns)]
287
+ author_str = ', '.join(authors[:3])
288
+ if len(authors) > 3:
289
+ author_str += ' et al.'
290
+ link = entry.find('atom:id', ns).text
291
+
292
+ results.append(str(i) + ". " + title)
293
+ results.append(" Authors: " + author_str)
294
+ results.append(" Published: " + published)
295
+ results.append(" Abstract: " + summary)
296
+ results.append(" URL: " + link)
297
+ results.append("")
298
+
299
+ context['output'] = "Found " + str(len(entries)) + " papers on arXiv:\n\n" + "\n".join(results)
300
+ else:
301
+ papers = []
302
+ for entry in entries:
303
+ title = entry.find('atom:title', ns).text.strip().replace('\n', ' ')
304
+ abstract = entry.find('atom:summary', ns).text.strip()
305
+ published = entry.find('atom:published', ns).text[:10]
306
+ updated = entry.find('atom:updated', ns).text[:10] if entry.find('atom:updated', ns) is not None else published
307
+ authors = [a.find('atom:name', ns).text for a in entry.findall('atom:author', ns)]
308
+ arxiv_id = entry.find('atom:id', ns).text
309
+ aid = arxiv_id.split('/')[-1]
310
+ pdf_link = arxiv_id.replace('/abs/', '/pdf/') + '.pdf'
311
+
312
+ # Get primary category
313
+ primary_cat = entry.find('{http://arxiv.org/schemas/atom}primary_category')
314
+ cat = primary_cat.get('term') if primary_cat is not None else ''
315
+
316
+ # Get all categories
317
+ all_cats = [c.get('term') for c in entry.findall('atom:category', ns)]
318
+
319
+ # Get comment (often has page count)
320
+ comment_el = entry.find('{http://arxiv.org/schemas/atom}comment')
321
+ comment = comment_el.text if comment_el is not None else ''
322
+
323
+ # Get DOI if available
324
+ doi_el = entry.find('{http://arxiv.org/schemas/atom}doi')
325
+ doi = doi_el.text if doi_el is not None else ''
326
+
327
+ # Get journal ref if available
328
+ journal_el = entry.find('{http://arxiv.org/schemas/atom}journal_ref')
329
+ journal = journal_el.text if journal_el is not None else ''
330
+
331
+ papers.append({
332
+ 'title': title,
333
+ 'authors': authors,
334
+ 'first_author': authors[0].split()[-1] if authors else '',
335
+ 'num_authors': len(authors),
336
+ 'abstract': abstract,
337
+ 'published': published,
338
+ 'updated': updated,
339
+ 'url': arxiv_id,
340
+ 'aid': aid,
341
+ 'pdf': pdf_link,
342
+ 'category': cat,
343
+ 'categories': all_cats,
344
+ 'comment': comment,
345
+ 'doi': doi,
346
+ 'journal': journal
347
+ })
348
+
349
+ # Sorting and filtering
350
+ sort_key = 'relevance'
351
+ all_papers = papers[:]
352
+ filter_year = None
353
+ filter_author = None
354
+
355
+ width, height = get_terminal_size()
356
+ selected = 0
357
+ scroll = 0
358
+ list_height = height - 6
359
+ mode = 'list'
360
+ detail_scroll = 0
361
+ pdf_scroll = 0
362
+ pdf_lines = []
363
+ input_mode = None
364
+ input_buffer = ''
365
+
366
+ fd = sys.stdin.fileno()
367
+ old_settings = termios.tcgetattr(fd)
368
+
369
+ def apply_filters(papers, year, author):
370
+ result = papers
371
+ if year:
372
+ result = [p for p in result if p['published'].startswith(year)]
373
+ if author:
374
+ author_lower = author.lower()
375
+ result = [p for p in result if any(author_lower in a.lower() for a in p['authors'])]
376
+ return result
377
+
378
+ def sort_papers(papers, key, reverse=True):
379
+ if key == 'date':
380
+ return sorted(papers, key=lambda p: p['published'], reverse=reverse)
381
+ elif key == 'author':
382
+ return sorted(papers, key=lambda p: p['first_author'].lower(), reverse=not reverse)
383
+ elif key == 'title':
384
+ return sorted(papers, key=lambda p: p['title'].lower(), reverse=not reverse)
385
+ return papers
386
+
387
+ try:
388
+ tty.setcbreak(fd)
389
+ sys.stdout.write('\033[?25l')
390
+ sys.stdout.write('\033[2J\033[H')
391
+
392
+ while True:
393
+ width, height = get_terminal_size()
394
+ list_height = height - 6
395
+
396
+ if mode == 'list':
397
+ if selected < scroll:
398
+ scroll = selected
399
+ elif selected >= scroll + list_height:
400
+ scroll = selected - list_height + 1
401
+
402
+ sys.stdout.write('\033[H')
403
+
404
+ # Header
405
+ if input_mode:
406
+ header = " FILTER BY " + input_mode.upper() + ": " + input_buffer + "_ "
407
+ elif mode == 'list':
408
+ filter_info = []
409
+ if filter_year:
410
+ filter_info.append("year:" + filter_year)
411
+ if filter_author:
412
+ filter_info.append("author:" + filter_author)
413
+ filter_str = ' '.join(filter_info)
414
+ if filter_str:
415
+ header = " ARXIV: '" + query + "' (" + str(len(papers)) + "/" + str(len(all_papers)) + ") [" + sort_key + "] " + filter_str + " "
416
+ else:
417
+ header = " ARXIV: '" + query + "' (" + str(len(papers)) + " papers) [sort:" + sort_key + "] "
418
+ else:
419
+ header = " " + papers[selected]['title'][:width-4] + " "
420
+ sys.stdout.write('\033[45;37;1m' + header.ljust(width) + '\033[0m\n')
421
+
422
+ if mode == 'list':
423
+ # Column headers
424
+ date_w = 11
425
+ cat_w = 12
426
+ auth_w = 30
427
+ title_w = width - date_w - cat_w - auth_w - 6
428
+ col_header = " " + "DATE".ljust(date_w) + " " + "CAT".ljust(cat_w) + " " + "AUTHORS".ljust(auth_w) + " " + "TITLE".ljust(title_w)
429
+ sys.stdout.write('\033[90m' + col_header[:width] + '\033[0m\n')
430
+ sys.stdout.write('\033[90m' + ("─" * width) + '\033[0m\n')
431
+
432
+ for i in range(list_height):
433
+ idx = scroll + i
434
+ sys.stdout.write('\033[' + str(4+i) + ';1H\033[K')
435
+ if idx >= len(papers):
436
+ continue
437
+
438
+ p = papers[idx]
439
+ date = p['published']
440
+ cat = p['category'][:cat_w-1]
441
+ # Show multiple authors
442
+ author_names = [a.split()[-1] for a in p['authors'][:3]]
443
+ if p['num_authors'] > 3:
444
+ auth = ', '.join(author_names) + ' +' + str(p['num_authors']-3)
445
+ else:
446
+ auth = ', '.join(author_names)
447
+ auth = auth[:auth_w-1]
448
+ title = p['title'][:title_w-1]
449
+
450
+ line = " " + date.ljust(date_w) + " " + cat.ljust(cat_w) + " " + auth.ljust(auth_w) + " " + title
451
+ line = line[:width-1]
452
+
453
+ if idx == selected:
454
+ sys.stdout.write('\033[47;30;1m>' + line.ljust(width-1) + '\033[0m')
455
+ else:
456
+ sys.stdout.write(' ' + line)
457
+
458
+ sys.stdout.write('\033[' + str(height-1) + ';1H\033[K\033[90m' + ("─" * width) + '\033[0m')
459
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[45;37m j/k:Nav Enter:View o/i/p:Open d:Download 1-3:Sort y/a:Filter c:Clear q:Quit [' + str(selected+1) + '/' + str(len(papers)) + '] \033[0m')
460
+
461
+ elif mode == 'detail':
462
+ sys.stdout.write('\033[90m' + ("─" * width) + '\033[0m\n')
463
+ p = papers[selected]
464
+ lines = []
465
+ lines.append('\033[1mTitle:\033[0m ' + latex_to_unicode(p['title']))
466
+ lines.append('')
467
+ lines.append('\033[1mAuthors (' + str(p['num_authors']) + '):\033[0m ' + ', '.join(p['authors']))
468
+ lines.append('\033[1mPublished:\033[0m ' + p['published'] + ' \033[1mUpdated:\033[0m ' + p['updated'])
469
+ lines.append('\033[1mCategory:\033[0m ' + p['category'] + ' \033[1mAll:\033[0m ' + ', '.join(p['categories'][:5]))
470
+ lines.append('\033[1mArXiv ID:\033[0m ' + p['aid'])
471
+ lines.append('\033[1mURL:\033[0m ' + p['url'])
472
+ lines.append('\033[1mPDF:\033[0m ' + p['pdf'])
473
+ if p['doi']:
474
+ lines.append('\033[1mDOI:\033[0m ' + p['doi'])
475
+ if p['journal']:
476
+ lines.append('\033[1mJournal:\033[0m ' + p['journal'])
477
+ if p['comment']:
478
+ lines.append('\033[1mComment:\033[0m ' + p['comment'][:80])
479
+ lines.append('')
480
+ lines.append('\033[1mAbstract:\033[0m')
481
+ abstract_rendered = latex_to_unicode(p['abstract'])
482
+ wrapped = textwrap.wrap(abstract_rendered, width=width-4)
483
+ lines.extend(wrapped)
484
+
485
+ for i in range(list_height + 1):
486
+ idx = detail_scroll + i
487
+ sys.stdout.write('\033[' + str(3+i) + ';1H\033[K')
488
+ if idx < len(lines):
489
+ sys.stdout.write(' ' + lines[idx][:width-4])
490
+
491
+ sys.stdout.write('\033[' + str(height-1) + ';1H\033[K\033[90m' + ("─" * width) + '\033[0m')
492
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[45;37m j/k:Scroll b:Back o:Browser i:Incognide p:PDF d:Download v:TermView q:Quit \033[0m')
493
+
494
+ elif mode == 'pdfview':
495
+ # PDF terminal view mode
496
+ p = papers[selected]
497
+ header = " PDF VIEW: " + p['aid'] + " "
498
+ sys.stdout.write('\033[46;37;1m' + header.ljust(width) + '\033[0m\n')
499
+ sys.stdout.write('\033[90m' + ("─" * width) + '\033[0m\n')
500
+
501
+ view_height = height - 4
502
+ for i in range(view_height):
503
+ idx = pdf_scroll + i
504
+ sys.stdout.write('\033[' + str(3+i) + ';1H\033[K')
505
+ if idx < len(pdf_lines):
506
+ line = pdf_lines[idx]
507
+ # Apply LaTeX rendering to text
508
+ line = latex_to_unicode(line)
509
+ sys.stdout.write(' ' + line[:width-4])
510
+
511
+ sys.stdout.write('\033[' + str(height-1) + ';1H\033[K\033[90m' + ("─" * width) + '\033[0m')
512
+ pct = int((pdf_scroll / max(1, len(pdf_lines) - view_height)) * 100) if len(pdf_lines) > view_height else 100
513
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[46;37m j/k/PgDn/PgUp:Scroll b:Back d:Download q:Quit [' + str(pct) + '%] \033[0m')
514
+
515
+ sys.stdout.flush()
516
+
517
+ c = sys.stdin.read(1)
518
+
519
+ # Handle input mode (for filters)
520
+ if input_mode:
521
+ if c in ('\r', '\n'):
522
+ if input_mode == 'year' and input_buffer:
523
+ filter_year = input_buffer
524
+ elif input_mode == 'author' and input_buffer:
525
+ filter_author = input_buffer
526
+ papers = apply_filters(all_papers, filter_year, filter_author)
527
+ selected = 0
528
+ scroll = 0
529
+ input_mode = None
530
+ input_buffer = ''
531
+ sys.stdout.write('\033[2J\033[H')
532
+ elif c == '\x1b':
533
+ input_mode = None
534
+ input_buffer = ''
535
+ sys.stdout.write('\033[2J\033[H')
536
+ elif c == '\x7f' or c == '\b':
537
+ input_buffer = input_buffer[:-1]
538
+ elif c.isprintable():
539
+ input_buffer += c
540
+ continue
541
+
542
+ if c == '\x1b':
543
+ c2 = sys.stdin.read(1)
544
+ if c2 == '[':
545
+ c3 = sys.stdin.read(1)
546
+ if c3 == 'A': # Up
547
+ if mode == 'list' and selected > 0:
548
+ selected -= 1
549
+ elif mode == 'detail' and detail_scroll > 0:
550
+ detail_scroll -= 1
551
+ elif mode == 'pdfview' and pdf_scroll > 0:
552
+ pdf_scroll -= 1
553
+ elif c3 == 'B': # Down
554
+ if mode == 'list' and selected < len(papers) - 1:
555
+ selected += 1
556
+ elif mode == 'detail':
557
+ detail_scroll += 1
558
+ elif mode == 'pdfview':
559
+ pdf_scroll += 1
560
+ elif c3 == '5': # Page Up
561
+ sys.stdin.read(1) # consume ~
562
+ if mode == 'pdfview':
563
+ pdf_scroll = max(0, pdf_scroll - (height - 6))
564
+ elif c3 == '6': # Page Down
565
+ sys.stdin.read(1) # consume ~
566
+ if mode == 'pdfview':
567
+ pdf_scroll += height - 6
568
+ else:
569
+ if mode == 'pdfview':
570
+ mode = 'detail'
571
+ sys.stdout.write('\033[2J\033[H')
572
+ elif mode == 'detail':
573
+ mode = 'list'
574
+ sys.stdout.write('\033[2J\033[H')
575
+ else:
576
+ context['output'] = "Cancelled."
577
+ break
578
+ continue
579
+
580
+ if c == 'q' or c == '\x03':
581
+ context['output'] = "Searched: " + query + " (" + str(len(papers)) + " results)"
582
+ break
583
+ elif c == 'k':
584
+ if mode == 'list' and selected > 0:
585
+ selected -= 1
586
+ elif mode == 'detail' and detail_scroll > 0:
587
+ detail_scroll -= 1
588
+ elif mode == 'pdfview' and pdf_scroll > 0:
589
+ pdf_scroll -= 1
590
+ elif c == 'j':
591
+ if mode == 'list' and selected < len(papers) - 1:
592
+ selected += 1
593
+ elif mode == 'detail':
594
+ detail_scroll += 1
595
+ elif mode == 'pdfview':
596
+ pdf_scroll += 1
597
+ elif c in ('\r', '\n') and mode == 'list':
598
+ mode = 'detail'
599
+ detail_scroll = 0
600
+ sys.stdout.write('\033[2J\033[H')
601
+ elif c == 'b':
602
+ if mode == 'pdfview':
603
+ mode = 'detail'
604
+ sys.stdout.write('\033[2J\033[H')
605
+ elif mode == 'detail':
606
+ mode = 'list'
607
+ sys.stdout.write('\033[2J\033[H')
608
+ elif c == 'o':
609
+ p = papers[selected]
610
+ try:
611
+ subprocess.Popen(['xdg-open', p['url']], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
612
+ except:
613
+ pass
614
+ elif c == 'i':
615
+ p = papers[selected]
616
+ try:
617
+ subprocess.Popen(['incognide', p['url']], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
618
+ except:
619
+ try:
620
+ subprocess.Popen(['incognide', '--url', p['url']], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
621
+ except:
622
+ pass
623
+ elif c == 'p':
624
+ p = papers[selected]
625
+ try:
626
+ subprocess.Popen(['xdg-open', p['pdf']], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
627
+ except:
628
+ pass
629
+ elif c == 'd':
630
+ # Download PDF to current directory
631
+ p = papers[selected]
632
+ pdf_url = p['pdf']
633
+ # Create filename from arxiv ID
634
+ arxiv_id = p['aid'].replace('/', '_').replace('.', '_')
635
+ filename = arxiv_id + '.pdf'
636
+ filepath = os.path.join(os.getcwd(), filename)
637
+ try:
638
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[43;30m Downloading ' + filename + '... \033[0m')
639
+ sys.stdout.flush()
640
+ urllib.request.urlretrieve(pdf_url, filepath)
641
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[42;30m Downloaded: ' + filepath + ' \033[0m')
642
+ sys.stdout.flush()
643
+ import time
644
+ time.sleep(1)
645
+ except Exception as e:
646
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[41;37m Download failed: ' + str(e)[:50] + ' \033[0m')
647
+ sys.stdout.flush()
648
+ import time
649
+ time.sleep(2)
650
+ elif c == 'v':
651
+ # View PDF in terminal (text + ASCII figures)
652
+ p = papers[selected]
653
+ pdf_url = p['pdf']
654
+ import tempfile
655
+ try:
656
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[43;30m Fetching PDF for terminal view... \033[0m')
657
+ sys.stdout.flush()
658
+ with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
659
+ tmp_path = tmp.name
660
+ urllib.request.urlretrieve(pdf_url, tmp_path)
661
+ pdf_lines = render_pdf_terminal(tmp_path, width-4)
662
+ os.unlink(tmp_path)
663
+ # Switch to PDF view mode
664
+ mode = 'pdfview'
665
+ pdf_scroll = 0
666
+ sys.stdout.write('\033[2J\033[H')
667
+ except Exception as e:
668
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[41;37m PDF view failed: ' + str(e)[:50] + ' \033[0m')
669
+ sys.stdout.flush()
670
+ import time
671
+ time.sleep(2)
672
+ # Filter keys
673
+ elif c == 'y' and mode == 'list':
674
+ input_mode = 'year'
675
+ input_buffer = ''
676
+ elif c == 'a' and mode == 'list':
677
+ input_mode = 'author'
678
+ input_buffer = ''
679
+ elif c == 'c' and mode == 'list':
680
+ filter_year = None
681
+ filter_author = None
682
+ papers = all_papers[:]
683
+ selected = 0
684
+ scroll = 0
685
+ sys.stdout.write('\033[2J\033[H')
686
+ # Sorting keys
687
+ elif c == '1':
688
+ sort_key = 'date'
689
+ papers = sort_papers(papers, 'date', True)
690
+ selected = 0
691
+ scroll = 0
692
+ sys.stdout.write('\033[2J\033[H')
693
+ elif c == '2':
694
+ sort_key = 'author'
695
+ papers = sort_papers(papers, 'author', False)
696
+ selected = 0
697
+ scroll = 0
698
+ sys.stdout.write('\033[2J\033[H')
699
+ elif c == '3':
700
+ sort_key = 'title'
701
+ papers = sort_papers(papers, 'title', False)
702
+ selected = 0
703
+ scroll = 0
704
+ sys.stdout.write('\033[2J\033[H')
705
+ elif c == '0':
706
+ sort_key = 'relevance'
707
+ papers = apply_filters(all_papers, filter_year, filter_author)
708
+ selected = 0
709
+ scroll = 0
710
+ sys.stdout.write('\033[2J\033[H')
711
+
712
+ finally:
713
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
714
+ sys.stdout.write('\033[?25h')
715
+ sys.stdout.write('\033[2J\033[H')
716
+ sys.stdout.flush()
717
+
718
+ except Exception as e:
719
+ import traceback
720
+ context['output'] = "arXiv error: " + str(e) + "\n" + traceback.format_exc()