kward 0.71.0 → 0.73.0

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -0
  3. data/CHANGELOG.md +93 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +4 -0
  6. data/doc/agent-tools.md +15 -6
  7. data/doc/authentication.md +22 -1
  8. data/doc/code-search.md +42 -2
  9. data/doc/configuration.md +106 -3
  10. data/doc/context-budgeting.md +136 -0
  11. data/doc/context-tools.md +16 -3
  12. data/doc/editor.md +415 -0
  13. data/doc/extensibility.md +16 -7
  14. data/doc/files.md +100 -0
  15. data/doc/getting-started.md +25 -18
  16. data/doc/git.md +123 -0
  17. data/doc/memory.md +24 -4
  18. data/doc/personas.md +34 -5
  19. data/doc/plugins.md +72 -1
  20. data/doc/releasing.md +37 -9
  21. data/doc/rpc.md +75 -5
  22. data/doc/session-management.md +35 -1
  23. data/doc/shell.md +332 -0
  24. data/doc/tabs.md +122 -0
  25. data/doc/troubleshooting.md +77 -1
  26. data/doc/usage.md +79 -7
  27. data/doc/web-search.md +12 -4
  28. data/doc/workspace-tools.md +51 -12
  29. data/examples/plugins/space_invaders.rb +377 -0
  30. data/lib/kward/agent.rb +1 -1
  31. data/lib/kward/ansi.rb +62 -23
  32. data/lib/kward/cli/commands.rb +33 -2
  33. data/lib/kward/cli/git.rb +150 -0
  34. data/lib/kward/cli/interactive_turn.rb +73 -9
  35. data/lib/kward/cli/plugins.rb +54 -4
  36. data/lib/kward/cli/prompt_interface.rb +32 -1
  37. data/lib/kward/cli/rendering.rb +4 -1
  38. data/lib/kward/cli/runtime_helpers.rb +268 -4
  39. data/lib/kward/cli/sessions.rb +2 -2
  40. data/lib/kward/cli/settings.rb +217 -9
  41. data/lib/kward/cli/slash_commands.rb +628 -2
  42. data/lib/kward/cli/tabs.rb +725 -0
  43. data/lib/kward/cli/tool_summaries.rb +6 -0
  44. data/lib/kward/cli.rb +150 -26
  45. data/lib/kward/clipboard.rb +2 -3
  46. data/lib/kward/compactor.rb +7 -19
  47. data/lib/kward/config_files.rb +145 -1
  48. data/lib/kward/context_budget_meter.rb +44 -0
  49. data/lib/kward/conversation.rb +12 -4
  50. data/lib/kward/editor_mode.rb +25 -0
  51. data/lib/kward/ekwsh.rb +559 -0
  52. data/lib/kward/image_attachments.rb +3 -1
  53. data/lib/kward/interactive_pty_runner.rb +151 -0
  54. data/lib/kward/local_command_runner.rb +155 -0
  55. data/lib/kward/local_pty_command_runner.rb +171 -0
  56. data/lib/kward/model/context_usage.rb +2 -2
  57. data/lib/kward/model/payloads.rb +2 -5
  58. data/lib/kward/plugin_registry.rb +61 -0
  59. data/lib/kward/project_files.rb +52 -0
  60. data/lib/kward/prompt_history.rb +84 -0
  61. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  62. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  63. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  64. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  65. data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
  66. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  67. data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
  68. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  69. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  70. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  71. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  72. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  73. data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
  74. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
  75. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  76. data/lib/kward/prompt_interface/editor/renderer.rb +244 -0
  77. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  78. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  79. data/lib/kward/prompt_interface/editor/state.rb +1271 -0
  80. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  81. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -0
  82. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  83. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  84. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  85. data/lib/kward/prompt_interface/git_prompt.rb +288 -0
  86. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  87. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  88. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  89. data/lib/kward/prompt_interface/key_handler.rb +451 -57
  90. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  91. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  92. data/lib/kward/prompt_interface/question_prompt.rb +99 -56
  93. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  94. data/lib/kward/prompt_interface/screen.rb +19 -3
  95. data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
  96. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  97. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  98. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  99. data/lib/kward/prompt_interface.rb +366 -222
  100. data/lib/kward/prompts/commands.rb +9 -0
  101. data/lib/kward/prompts.rb +2 -0
  102. data/lib/kward/rpc/memory_methods.rb +83 -0
  103. data/lib/kward/rpc/server.rb +169 -83
  104. data/lib/kward/rpc/session_manager.rb +45 -121
  105. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  106. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  107. data/lib/kward/rpc/tool_metadata.rb +11 -0
  108. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  109. data/lib/kward/scratchpad_runner.rb +56 -0
  110. data/lib/kward/session_diff.rb +20 -3
  111. data/lib/kward/session_naming.rb +11 -0
  112. data/lib/kward/session_store.rb +44 -0
  113. data/lib/kward/session_tree_nodes.rb +136 -0
  114. data/lib/kward/session_tree_renderer.rb +9 -131
  115. data/lib/kward/tab_store.rb +47 -0
  116. data/lib/kward/terminal_keys.rb +84 -0
  117. data/lib/kward/terminal_sequences.rb +42 -0
  118. data/lib/kward/text_boundary.rb +25 -0
  119. data/lib/kward/tools/context_budget_stats.rb +54 -0
  120. data/lib/kward/tools/context_for_task.rb +204 -0
  121. data/lib/kward/tools/read_file.rb +8 -4
  122. data/lib/kward/tools/registry.rb +62 -16
  123. data/lib/kward/tools/tool_call.rb +10 -0
  124. data/lib/kward/version.rb +1 -1
  125. data/lib/kward/workers/git_guard.rb +93 -0
  126. data/lib/kward/workers/job.rb +99 -0
  127. data/lib/kward/workers/live_view.rb +49 -0
  128. data/lib/kward/workers/manager.rb +288 -0
  129. data/lib/kward/workers/queue_runner.rb +166 -0
  130. data/lib/kward/workers/queue_store.rb +112 -0
  131. data/lib/kward/workers/store.rb +72 -0
  132. data/lib/kward/workers/tool_policy.rb +23 -0
  133. data/lib/kward/workers/worker.rb +82 -0
  134. data/lib/kward/workers/write_lock.rb +38 -0
  135. data/lib/kward/workers.rb +10 -0
  136. data/lib/kward/workspace.rb +125 -87
  137. data/templates/default/fulldoc/html/css/kward.css +140 -36
  138. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  139. data/templates/default/fulldoc/html/setup.rb +1 -0
  140. data/templates/default/kward_navigation.rb +12 -1
  141. data/templates/default/layout/html/layout.erb +23 -34
  142. data/templates/default/layout/html/setup.rb +6 -0
  143. metadata +67 -1
@@ -0,0 +1,259 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Interactive terminal UI used by the CLI frontend.
4
+ class PromptInterface
5
+ # Emacs-style keymap for the built-in composer file editor.
6
+ module EmacsEditorMode
7
+ private
8
+
9
+ def handle_emacs_key(key)
10
+ return if handle_editor_bracketed_paste_key(key)
11
+
12
+ if @editor_state.emacs_pending == "C-x"
13
+ return handle_emacs_ctrl_x_key(key)
14
+ end
15
+
16
+ csi_result = handle_emacs_csi_u_key(key)
17
+ return csi_result unless csi_result == false
18
+
19
+ shift_result = handle_editor_shift_navigation_key(key)
20
+ return shift_result unless shift_result == false
21
+
22
+ editor_tab_result = handle_editor_tab_key(key)
23
+ return editor_tab_result unless editor_tab_result == false
24
+
25
+ tab_result = handle_tab_key_binding(key)
26
+ return tab_result unless tab_result == false
27
+
28
+ return true if handle_bundled_key(key) { |token| handle_emacs_key(token) }
29
+
30
+ case key
31
+ when "\n", "\r"
32
+ return editor_search_confirm if editor_search_active?
33
+ clear_editor_selection_before_edit
34
+ editor_insert_newline
35
+ when "\t"
36
+ editor_insert_tab unless editor_search_active?
37
+ when "\b", "\x7F"
38
+ clear_editor_selection_before_edit unless editor_search_active?
39
+ editor_search_active? ? editor_search_delete_character : editor_delete_before_cursor
40
+ when TerminalKeys::CTRL_SPACE
41
+ @editor_state.begin_selection unless editor_search_active?
42
+ when TerminalKeys::CTRL_A
43
+ @editor_state.move_line_start unless editor_search_active?
44
+ when TerminalKeys::CTRL_B
45
+ @editor_state.move_left unless editor_search_active?
46
+ when TerminalKeys::CTRL_D
47
+ @editor_state.delete_at_cursor unless editor_search_active?
48
+ when TerminalKeys::CTRL_E
49
+ @editor_state.move_line_end unless editor_search_active?
50
+ when TerminalKeys::CTRL_F
51
+ @editor_state.move_right unless editor_search_active?
52
+ when "\x07"
53
+ emacs_cancel
54
+ when TerminalKeys::CTRL_K
55
+ if editor_selection_active?
56
+ emacs_kill_selection
57
+ else
58
+ @editor_state.kill_line_after_cursor unless editor_search_active?
59
+ end
60
+ when TerminalKeys::CTRL_N
61
+ editor_move_down unless editor_search_active?
62
+ when TerminalKeys::CTRL_P
63
+ editor_move_up unless editor_search_active?
64
+ when TerminalKeys::CTRL_R
65
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:backward)
66
+ when TerminalKeys::CTRL_S
67
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:forward)
68
+ when TerminalKeys::CTRL_U
69
+ @editor_state.kill_line_before_cursor unless editor_search_active?
70
+ when TerminalKeys::CTRL_V
71
+ @editor_state.page_down(editor_page_rows) unless editor_search_active?
72
+ when TerminalKeys::CTRL_W
73
+ editor_selection_active? ? emacs_kill_selection : @editor_state.delete_word_before_cursor unless editor_search_active?
74
+ when TerminalKeys::CTRL_X
75
+ @editor_state.emacs_pending = "C-x"
76
+ @editor_state.status = "C-x"
77
+ when TerminalKeys::CTRL_Y
78
+ @editor_state.yank_from_kill_ring unless editor_search_active?
79
+ when "\e"
80
+ return editor_search_cancel if editor_search_active?
81
+ @editor_state.clear_selection
82
+ when "\eb", "\eB"
83
+ @editor_state.move_to_previous_word unless editor_search_active?
84
+ when "\ed", "\eD"
85
+ @editor_state.delete_word_after_cursor unless editor_search_active?
86
+ when "\ef", "\eF"
87
+ @editor_state.move_to_next_word unless editor_search_active?
88
+ when "\ev", "\eV"
89
+ @editor_state.page_up(editor_page_rows) unless editor_search_active?
90
+ when "\ew", "\eW"
91
+ emacs_copy_selection unless editor_search_active?
92
+ when "\ey", "\eY"
93
+ @editor_state.yank_pop unless editor_search_active?
94
+ when "\e", "\e\x7F"
95
+ @editor_state.delete_word_before_cursor unless editor_search_active?
96
+ else
97
+ ansi_result = handle_editor_modified_ansi_key(key)
98
+ return ansi_result unless ansi_result == false
99
+
100
+ key_name = key_name_for(key)
101
+ named_result = handle_editor_named_key(key_name) if key_name
102
+ return named_result unless named_result == false || named_result.nil?
103
+
104
+ if editor_search_active?
105
+ editor_search_append(key) if printable_key?(key)
106
+ elsif printable_key?(key)
107
+ editor_insert_printable(key)
108
+ end
109
+ end
110
+ end
111
+
112
+ def handle_emacs_csi_u_key(key)
113
+ sequence = parse_csi_u_key(key)
114
+ return false unless sequence
115
+
116
+ code = sequence[:code]
117
+ modifier = sequence[:modifier]
118
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
119
+ sequence = sequence.merge(remaining: "")
120
+ normalized_code = ctrl_code(code)
121
+
122
+ if ctrl_modifier?(modifier)
123
+ case normalized_code
124
+ when 13
125
+ return false if editor_search_active?
126
+
127
+ clear_editor_selection_before_edit
128
+ editor_insert_endwise_modifier_newline
129
+ when 32
130
+ @editor_state.begin_selection unless editor_search_active?
131
+ when 97
132
+ @editor_state.move_line_start unless editor_search_active?
133
+ when 98
134
+ @editor_state.move_left unless editor_search_active?
135
+ when 99, 103
136
+ emacs_cancel
137
+ when 100
138
+ @editor_state.delete_at_cursor unless editor_search_active?
139
+ when 101
140
+ @editor_state.move_line_end unless editor_search_active?
141
+ when 102
142
+ @editor_state.move_right unless editor_search_active?
143
+ when 107
144
+ @editor_state.kill_line_after_cursor unless editor_search_active?
145
+ when 110
146
+ editor_move_down unless editor_search_active?
147
+ when 112
148
+ editor_move_up unless editor_search_active?
149
+ when 114
150
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:backward)
151
+ when 115
152
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:forward)
153
+ when 118
154
+ @editor_state.page_down(editor_page_rows) unless editor_search_active?
155
+ when 119
156
+ editor_selection_active? ? emacs_kill_selection : @editor_state.delete_word_before_cursor unless editor_search_active?
157
+ when 120
158
+ @editor_state.emacs_pending = "C-x"
159
+ @editor_state.status = "C-x"
160
+ when 121
161
+ @editor_state.yank_from_kill_ring unless editor_search_active?
162
+ else
163
+ return false
164
+ end
165
+ elsif alt_modifier?(modifier)
166
+ case normalized_code
167
+ when 98
168
+ @editor_state.move_to_previous_word unless editor_search_active?
169
+ when 100
170
+ @editor_state.delete_word_after_cursor unless editor_search_active?
171
+ when 102
172
+ @editor_state.move_to_next_word unless editor_search_active?
173
+ when 118
174
+ @editor_state.page_up(editor_page_rows) unless editor_search_active?
175
+ when 119
176
+ emacs_copy_selection unless editor_search_active?
177
+ when 121
178
+ @editor_state.yank_pop unless editor_search_active?
179
+ else
180
+ return false
181
+ end
182
+ elsif code == 9 && !ctrl_modifier?(modifier) && !alt_modifier?(modifier) && !super_modifier?(modifier)
183
+ return false if editor_search_active?
184
+
185
+ shift_modifier?(modifier) ? editor_outdent_tab : editor_insert_tab
186
+ else
187
+ handle_parsed_editor_csi_u_key(sequence)
188
+ end
189
+ end
190
+
191
+ def handle_emacs_ctrl_x_key(key)
192
+ @editor_state.emacs_pending = nil
193
+ key = emacs_ctrl_x_csi_u_key(key)
194
+ case key
195
+ when TerminalKeys::CTRL_S
196
+ save_editor
197
+ when TerminalKeys::CTRL_C
198
+ quit_editor("Unsaved changes. Press C-x C-c again to discard.")
199
+ else
200
+ @editor_state.status = "Unknown C-x command"
201
+ true
202
+ end
203
+ end
204
+
205
+ def emacs_ctrl_x_csi_u_key(key)
206
+ sequence = parse_csi_u_key(key)
207
+ return key unless sequence && ctrl_modifier?(sequence[:modifier])
208
+
209
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
210
+ case ctrl_code(sequence[:code])
211
+ when 99
212
+ TerminalKeys::CTRL_C
213
+ when 115
214
+ TerminalKeys::CTRL_S
215
+ else
216
+ key
217
+ end
218
+ end
219
+
220
+ def emacs_kill_selection
221
+ return false unless editor_selection_active?
222
+
223
+ range = @editor_state.selection_range
224
+ @editor_state.cut_range(range[0], range[1])
225
+ @editor_state.status = "Killed region"
226
+ true
227
+ end
228
+
229
+ def emacs_copy_selection
230
+ if editor_selection_active?
231
+ range = @editor_state.selection_range
232
+ elsif @editor_state.selection_anchor
233
+ range = [@editor_state.selection_anchor, @editor_state.cursor + 1].minmax
234
+ else
235
+ return false
236
+ end
237
+
238
+ @editor_state.copy_for_kill_ring(range[0], range[1])
239
+ @output_io.print(TerminalSequences.osc52(@editor_state.kill_buffer))
240
+ @output_io.flush if @output_io.respond_to?(:flush)
241
+ @editor_state.clear_selection
242
+ @editor_state.status = "Copied region"
243
+ true
244
+ end
245
+
246
+ def emacs_cancel
247
+ if editor_search_active?
248
+ editor_search_cancel
249
+ else
250
+ @editor_state.emacs_pending = nil
251
+ @editor_state.clear_selection
252
+ @editor_state.status = "Cancelled"
253
+ end
254
+ true
255
+ end
256
+
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,354 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Interactive terminal UI used by the CLI frontend.
4
+ class PromptInterface
5
+ # Modern keymap for the built-in composer file editor.
6
+ module ModernEditorMode
7
+ private
8
+
9
+ def handle_modern_key(key)
10
+ return if handle_modern_bracketed_paste_key(key)
11
+
12
+ csi_result = handle_modern_csi_u_key(key)
13
+ return csi_result unless csi_result == false
14
+
15
+ multi_cursor_result = handle_modern_multi_cursor_key(key)
16
+ return multi_cursor_result unless multi_cursor_result == false
17
+
18
+ indentation_navigation_result = handle_modern_indentation_navigation_key(key)
19
+ return indentation_navigation_result unless indentation_navigation_result == false
20
+
21
+ modified_navigation_result = handle_modern_modified_navigation_key(key)
22
+ return modified_navigation_result unless modified_navigation_result == false
23
+
24
+ shift_result = handle_editor_shift_navigation_key(key)
25
+ return shift_result unless shift_result == false
26
+
27
+ binding_result = handle_modern_key_binding(key)
28
+ return binding_result unless binding_result == false
29
+
30
+ editor_tab_result = handle_editor_tab_key(key) { |direction| modern_record_undo { direction == :forward ? editor_insert_tab : editor_outdent_tab } }
31
+ return editor_tab_result unless editor_tab_result == false
32
+
33
+ tab_result = handle_tab_key_binding(key)
34
+ return tab_result unless tab_result == false
35
+
36
+ return true if handle_bundled_key(key) { |token| handle_modern_key(token) }
37
+
38
+ case key
39
+ when "\n", "\r"
40
+ return editor_search_confirm if editor_search_active?
41
+ modern_record_undo { modern_insert_text("\n") }
42
+ when "\t"
43
+ modern_record_undo { editor_insert_tab unless editor_search_active? }
44
+ when "\b", "\x7F"
45
+ editor_search_active? ? editor_search_delete_character : modern_record_undo { modern_delete_before_cursor }
46
+ when TerminalKeys::CTRL_C
47
+ return editor_search_cancel if editor_search_active?
48
+ when "\e"
49
+ return editor_search_cancel if editor_search_active?
50
+ return @editor_state.collapse_to_primary_selection if @editor_state.multi_cursor?
51
+ return @editor_state.clear_selection if @editor_state.selection_active?
52
+ when "/"
53
+ clear_editor_selection_before_edit unless editor_search_active?
54
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:forward)
55
+ when "?"
56
+ clear_editor_selection_before_edit unless editor_search_active?
57
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:backward)
58
+ when TerminalKeys::CTRL_Q
59
+ quit_editor
60
+ when TerminalKeys::CTRL_S
61
+ save_editor
62
+ when TerminalKeys::CTRL_R
63
+ modern_record_undo { run_editor_buffer } unless editor_search_active?
64
+ when TerminalKeys::CTRL_Z
65
+ @editor_state.undo unless editor_search_active?
66
+ else
67
+ key_name = key_name_for(key)
68
+ named_result = handle_editor_named_key(key_name) if key_name
69
+ return named_result unless named_result == false || named_result.nil?
70
+
71
+ if editor_search_active?
72
+ editor_search_append(key) if printable_key?(key)
73
+ elsif printable_key?(key)
74
+ modern_record_undo { modern_insert_printable(key) }
75
+ end
76
+ end
77
+ end
78
+
79
+ def handle_modern_csi_u_key(key)
80
+ sequence = parse_csi_u_key(key)
81
+ return false unless sequence
82
+
83
+ code = sequence[:code]
84
+ modifier = sequence[:modifier]
85
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
86
+ sequence = sequence.merge(remaining: "")
87
+
88
+ if ctrl_modifier?(modifier) || super_modifier?(modifier)
89
+ handle_modern_modified_key(code, modifier, sequence)
90
+ else
91
+ handle_modern_editor_csi_u_key(sequence)
92
+ end
93
+ end
94
+
95
+ def handle_modern_multi_cursor_key(key)
96
+ return false if editor_search_active?
97
+
98
+ case key
99
+ when TerminalKeys::CTRL_D
100
+ @editor_state.add_next_occurrence_selection
101
+ when *TerminalKeys::ALT_SHIFT_UP
102
+ @editor_state.add_vertical_cursor(:up)
103
+ when *TerminalKeys::ALT_SHIFT_DOWN
104
+ @editor_state.add_vertical_cursor(:down)
105
+ else
106
+ false
107
+ end
108
+ end
109
+
110
+ def handle_modern_indentation_navigation_key(key)
111
+ return false if editor_search_active?
112
+
113
+ case key
114
+ when *modern_indentation_key_sequences(:up)
115
+ modern_move_indentation { @editor_state.move_indentation_up }
116
+ when *modern_indentation_key_sequences(:down)
117
+ modern_move_indentation { @editor_state.move_indentation_down }
118
+ when *modern_indentation_key_sequences(:right)
119
+ modern_move_indentation { @editor_state.move_indentation_right }
120
+ when *modern_indentation_key_sequences(:select_up)
121
+ editor_extending_selection { @editor_state.move_indentation_up }
122
+ when *modern_indentation_key_sequences(:select_down)
123
+ editor_extending_selection { @editor_state.move_indentation_down }
124
+ when *modern_indentation_key_sequences(:select_right)
125
+ editor_extending_selection { @editor_state.move_indentation_right }
126
+ else
127
+ false
128
+ end
129
+ end
130
+
131
+ def modern_move_indentation
132
+ result = yield
133
+ @editor_state.clear_selection
134
+ result
135
+ end
136
+
137
+ def modern_indentation_key_sequences(action)
138
+ case [modern_indentation_modifier, action]
139
+ when [:alt, :up]
140
+ TerminalKeys::ALT_UP
141
+ when [:alt, :down]
142
+ TerminalKeys::ALT_DOWN
143
+ when [:alt, :right]
144
+ TerminalKeys::ALT_RIGHT
145
+ when [:alt, :select_up]
146
+ TerminalKeys::ALT_SHIFT_UP
147
+ when [:alt, :select_down]
148
+ TerminalKeys::ALT_SHIFT_DOWN
149
+ when [:alt, :select_right]
150
+ TerminalKeys::ALT_SHIFT_RIGHT
151
+ when [:ctrl, :up]
152
+ TerminalKeys::CTRL_UP
153
+ when [:ctrl, :down]
154
+ TerminalKeys::CTRL_DOWN
155
+ when [:ctrl, :right]
156
+ TerminalKeys::CTRL_RIGHT
157
+ when [:ctrl, :select_up]
158
+ TerminalKeys::CTRL_SHIFT_UP
159
+ when [:ctrl, :select_down]
160
+ TerminalKeys::CTRL_SHIFT_DOWN
161
+ when [:ctrl, :select_right]
162
+ TerminalKeys::CTRL_SHIFT_RIGHT
163
+ else
164
+ []
165
+ end
166
+ end
167
+
168
+ def modern_indentation_modifier
169
+ RbConfig::CONFIG["host_os"].to_s.downcase.include?("darwin") ? :alt : :ctrl
170
+ end
171
+
172
+ def handle_modern_modified_navigation_key(key)
173
+ return false if editor_search_active?
174
+
175
+ case key
176
+ when *TerminalKeys::CTRL_RIGHT
177
+ @editor_state.move_line_end
178
+ when *TerminalKeys::CTRL_LEFT
179
+ @editor_state.move_line_start
180
+ when *TerminalKeys::CTRL_UP
181
+ @editor_state.move_file_start
182
+ when *TerminalKeys::CTRL_DOWN
183
+ @editor_state.move_file_end
184
+ when *TerminalKeys::ALT_SHIFT_RIGHT
185
+ editor_extending_selection { @editor_state.move_to_next_word }
186
+ when *TerminalKeys::ALT_SHIFT_LEFT
187
+ editor_extending_selection { @editor_state.move_to_previous_word }
188
+ else
189
+ false
190
+ end
191
+ end
192
+
193
+ def handle_modern_key_binding(key)
194
+ case key
195
+ when TerminalKeys::CTRL_SPACE
196
+ true
197
+ when TerminalKeys::CTRL_C
198
+ editor_search_active? ? editor_search_cancel : copy_editor_selection
199
+ when TerminalKeys::CTRL_F
200
+ @editor_state.move_right unless editor_search_active?
201
+ when TerminalKeys::CTRL_V
202
+ modern_record_undo { @editor_state.yank_kill_buffer } unless editor_search_active?
203
+ when TerminalKeys::CTRL_X
204
+ modern_record_undo { cut_editor_selection } unless editor_search_active?
205
+ else
206
+ handle_modern_shared_key_binding(key)
207
+ end
208
+ end
209
+
210
+ def handle_modern_modified_key(code, modifier, sequence)
211
+ normalized_code = ctrl_code(code)
212
+ if super_modifier?(modifier)
213
+ return editor_search_active? ? editor_search_cancel : copy_editor_selection if normalized_code == 99
214
+ return handle_modern_editor_csi_u_key(sequence) unless ctrl_modifier?(modifier)
215
+ end
216
+
217
+ case normalized_code
218
+ when 13
219
+ return false if editor_search_active?
220
+
221
+ modern_record_undo do
222
+ clear_editor_selection_before_edit
223
+ editor_insert_endwise_modifier_newline
224
+ end
225
+ when 99
226
+ editor_search_active? ? editor_search_cancel : copy_editor_selection
227
+ when 100
228
+ return false if editor_search_active?
229
+
230
+ @editor_state.add_next_occurrence_selection
231
+ when 102
232
+ @editor_state.move_right unless editor_search_active?
233
+ when 118
234
+ modern_record_undo { @editor_state.yank_kill_buffer } unless editor_search_active?
235
+ when 120
236
+ modern_record_undo { cut_editor_selection } unless editor_search_active?
237
+ when 108
238
+ return false if editor_search_active?
239
+ return false unless modern_ctrl_shift_key?(code, modifier)
240
+
241
+ @editor_state.selection_to_line_start_cursors
242
+ when 114
243
+ modern_record_undo { run_editor_buffer } unless editor_search_active?
244
+ when 122
245
+ return if editor_search_active?
246
+
247
+ modern_ctrl_shift_key?(code, modifier) ? @editor_state.redo : @editor_state.undo
248
+ else
249
+ return false if normalized_code == 32
250
+
251
+ handle_modern_editor_csi_u_key(sequence)
252
+ end
253
+ end
254
+
255
+ def handle_modern_shared_key_binding(key)
256
+ case key
257
+ when TerminalKeys::CTRL_D, TerminalKeys::CTRL_K, TerminalKeys::CTRL_U, TerminalKeys::CTRL_W, TerminalKeys::CTRL_Y, *TerminalKeys::DELETE, "\ed", "\eD", "\e\b", "\e\x7F"
258
+ modern_record_undo { handle_editor_key_binding(key) }
259
+ else
260
+ handle_editor_key_binding(key)
261
+ end
262
+ end
263
+
264
+ def handle_modern_editor_csi_u_key(key_or_sequence)
265
+ sequence = key_or_sequence.is_a?(Hash) ? key_or_sequence : parse_csi_u_key(key_or_sequence)
266
+ return handle_editor_csi_u_key(key_or_sequence) unless sequence
267
+
268
+ code = sequence[:code]
269
+ modifier = sequence[:modifier]
270
+ if ctrl_modifier?(modifier)
271
+ normalized_code = ctrl_code(code)
272
+ return @editor_state.add_next_occurrence_selection if normalized_code == 100
273
+ if normalized_code == 108 && modern_ctrl_shift_key?(code, modifier)
274
+ return @editor_state.selection_to_line_start_cursors
275
+ end
276
+
277
+ case normalized_code
278
+ when 107, 117, 119, 121
279
+ return modern_record_undo { handle_parsed_editor_csi_u_key(sequence) }
280
+ end
281
+ end
282
+
283
+ case code
284
+ when 9
285
+ return false if editor_search_active?
286
+ return false if ctrl_modifier?(modifier) || alt_modifier?(modifier) || super_modifier?(modifier)
287
+
288
+ shift_modifier?(modifier) ? modern_record_undo { editor_outdent_tab } : modern_record_undo { editor_insert_tab }
289
+ when 13
290
+ return editor_search_confirm if editor_search_active?
291
+
292
+ modern_record_undo { modern_insert_text("\n") }
293
+ when 8, 127
294
+ editor_search_active? ? editor_search_delete_character : modern_record_undo { modern_delete_before_cursor }
295
+ when 4
296
+ modern_record_undo { delete_editor_selection || @editor_state.delete_at_cursor } unless editor_search_active?
297
+ else
298
+ text = csi_u_printable_text(sequence)
299
+ if text
300
+ editor_search_active? ? editor_search_append(text) : modern_record_undo { modern_insert_printable(text) }
301
+ elsif csi_u_text_field?(sequence)
302
+ true
303
+ else
304
+ handle_parsed_editor_csi_u_key(sequence)
305
+ end
306
+ end
307
+ end
308
+
309
+ def handle_modern_bracketed_paste_key(key)
310
+ handle_bracketed_paste(key) do |content|
311
+ modern_record_undo { @editor_state.insert(content) } unless editor_search_active?
312
+ end
313
+ end
314
+
315
+ def modern_insert_printable(text)
316
+ return editor_insert_printable(text) unless @editor_state.multi_cursor?
317
+
318
+ @editor_state.replace_selections(text)
319
+ end
320
+
321
+ def modern_insert_text(text)
322
+ return @editor_state.replace_selections(text) if @editor_state.multi_cursor? || @editor_state.selection_ranges.any?
323
+
324
+ if text == "\n"
325
+ editor_insert_newline
326
+ else
327
+ @editor_state.insert(text)
328
+ end
329
+ end
330
+
331
+ def modern_delete_before_cursor
332
+ return @editor_state.delete_before_selections if @editor_state.multi_cursor?
333
+
334
+ delete_editor_selection || editor_delete_before_cursor
335
+ end
336
+
337
+ def modern_record_undo
338
+ before_buffer = @editor_state.buffer.dup
339
+ before_redo_stack = @editor_state.redo_stack.map { |entry| entry.merge(buffer: entry[:buffer].dup, selections: entry[:selections]&.map(&:dup)) }
340
+ @editor_state.push_undo
341
+ result = yield
342
+ if @editor_state.buffer == before_buffer
343
+ @editor_state.undo_stack.pop
344
+ @editor_state.redo_stack = before_redo_stack
345
+ end
346
+ result
347
+ end
348
+
349
+ def modern_ctrl_shift_key?(code, modifier)
350
+ code.to_i.between?(65, 90) || ((modifier.to_i - 1) & 1).positive?
351
+ end
352
+ end
353
+ end
354
+ end