kward 0.71.0 → 0.72.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -0
  5. data/doc/agent-tools.md +15 -6
  6. data/doc/authentication.md +22 -1
  7. data/doc/code-search.md +42 -2
  8. data/doc/configuration.md +106 -3
  9. data/doc/context-budgeting.md +136 -0
  10. data/doc/context-tools.md +16 -3
  11. data/doc/editor.md +394 -0
  12. data/doc/extensibility.md +16 -7
  13. data/doc/files.md +100 -0
  14. data/doc/getting-started.md +25 -18
  15. data/doc/git.md +122 -0
  16. data/doc/memory.md +24 -4
  17. data/doc/personas.md +34 -5
  18. data/doc/plugins.md +72 -1
  19. data/doc/releasing.md +37 -9
  20. data/doc/rpc.md +74 -4
  21. data/doc/session-management.md +35 -1
  22. data/doc/shell.md +286 -0
  23. data/doc/tabs.md +122 -0
  24. data/doc/troubleshooting.md +77 -1
  25. data/doc/usage.md +53 -7
  26. data/doc/web-search.md +12 -4
  27. data/doc/workspace-tools.md +51 -12
  28. data/examples/plugins/space_invaders.rb +377 -0
  29. data/lib/kward/agent.rb +1 -1
  30. data/lib/kward/cli/commands.rb +33 -2
  31. data/lib/kward/cli/git.rb +150 -0
  32. data/lib/kward/cli/interactive_turn.rb +73 -9
  33. data/lib/kward/cli/plugins.rb +54 -4
  34. data/lib/kward/cli/prompt_interface.rb +32 -1
  35. data/lib/kward/cli/runtime_helpers.rb +133 -3
  36. data/lib/kward/cli/sessions.rb +2 -2
  37. data/lib/kward/cli/settings.rb +218 -9
  38. data/lib/kward/cli/slash_commands.rb +415 -2
  39. data/lib/kward/cli/tabs.rb +695 -0
  40. data/lib/kward/cli.rb +158 -26
  41. data/lib/kward/config_files.rb +123 -1
  42. data/lib/kward/context_budget_meter.rb +44 -0
  43. data/lib/kward/conversation.rb +12 -4
  44. data/lib/kward/editor_mode.rb +25 -0
  45. data/lib/kward/ekwsh.rb +362 -0
  46. data/lib/kward/plugin_registry.rb +61 -0
  47. data/lib/kward/project_files.rb +52 -0
  48. data/lib/kward/prompt_history.rb +82 -0
  49. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  50. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  51. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  52. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  53. data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
  54. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  55. data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
  56. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  57. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  58. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  59. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  60. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  61. data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
  62. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
  63. data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
  64. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  65. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  66. data/lib/kward/prompt_interface/editor/state.rb +1249 -0
  67. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  68. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
  69. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  70. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  71. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  72. data/lib/kward/prompt_interface/git_prompt.rb +299 -0
  73. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  74. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  75. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  76. data/lib/kward/prompt_interface/key_handler.rb +387 -35
  77. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  78. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  79. data/lib/kward/prompt_interface/question_prompt.rb +98 -50
  80. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  81. data/lib/kward/prompt_interface/screen.rb +16 -0
  82. data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
  83. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  84. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  85. data/lib/kward/prompt_interface.rb +286 -8
  86. data/lib/kward/prompts/commands.rb +5 -0
  87. data/lib/kward/prompts.rb +2 -0
  88. data/lib/kward/rpc/server.rb +42 -3
  89. data/lib/kward/rpc/session_manager.rb +35 -47
  90. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  91. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  92. data/lib/kward/session_store.rb +44 -0
  93. data/lib/kward/session_tree_nodes.rb +136 -0
  94. data/lib/kward/session_tree_renderer.rb +9 -131
  95. data/lib/kward/tab_store.rb +47 -0
  96. data/lib/kward/text_boundary.rb +25 -0
  97. data/lib/kward/tools/context_budget_stats.rb +54 -0
  98. data/lib/kward/tools/context_for_task.rb +202 -0
  99. data/lib/kward/tools/read_file.rb +8 -4
  100. data/lib/kward/tools/registry.rb +62 -16
  101. data/lib/kward/tools/tool_call.rb +10 -0
  102. data/lib/kward/version.rb +1 -1
  103. data/lib/kward/workers/git_guard.rb +68 -0
  104. data/lib/kward/workers/live_view.rb +49 -0
  105. data/lib/kward/workers/manager.rb +288 -0
  106. data/lib/kward/workers/store.rb +72 -0
  107. data/lib/kward/workers/tool_policy.rb +23 -0
  108. data/lib/kward/workers/worker.rb +82 -0
  109. data/lib/kward/workers/write_lock.rb +38 -0
  110. data/lib/kward/workers.rb +7 -0
  111. data/lib/kward/workspace.rb +110 -24
  112. data/templates/default/fulldoc/html/css/kward.css +107 -36
  113. data/templates/default/kward_navigation.rb +12 -1
  114. data/templates/default/layout/html/layout.erb +4 -2
  115. data/templates/default/layout/html/setup.rb +6 -0
  116. metadata +53 -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 "\x00"
41
+ @editor_state.begin_selection unless editor_search_active?
42
+ when "\x01"
43
+ @editor_state.move_line_start unless editor_search_active?
44
+ when "\x02"
45
+ @editor_state.move_left unless editor_search_active?
46
+ when "\x04"
47
+ @editor_state.delete_at_cursor unless editor_search_active?
48
+ when "\x05"
49
+ @editor_state.move_line_end unless editor_search_active?
50
+ when "\x06"
51
+ @editor_state.move_right unless editor_search_active?
52
+ when "\x07"
53
+ emacs_cancel
54
+ when "\x0B"
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 "\x0E"
61
+ editor_move_down unless editor_search_active?
62
+ when "\x10"
63
+ editor_move_up unless editor_search_active?
64
+ when "\x12"
65
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:backward)
66
+ when "\x13"
67
+ editor_search_active? ? editor_search_append(key) : editor_search_begin(:forward)
68
+ when "\x15"
69
+ @editor_state.kill_line_before_cursor unless editor_search_active?
70
+ when "\x16"
71
+ @editor_state.page_down(editor_page_rows) unless editor_search_active?
72
+ when "\x17"
73
+ editor_selection_active? ? emacs_kill_selection : @editor_state.delete_word_before_cursor unless editor_search_active?
74
+ when "\x18"
75
+ @editor_state.emacs_pending = "C-x"
76
+ @editor_state.status = "C-x"
77
+ when "\x19"
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 "\x13"
196
+ save_editor
197
+ when "\x03"
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
+ "\x03"
213
+ when 115
214
+ "\x13"
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("\e]52;c;#{Base64.strict_encode64(@editor_state.kill_buffer)}\a")
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,353 @@
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 "\x03"
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 "\x11"
59
+ quit_editor
60
+ when "\x13"
61
+ save_editor
62
+ when "\x1A"
63
+ @editor_state.undo unless editor_search_active?
64
+ else
65
+ key_name = key_name_for(key)
66
+ named_result = handle_editor_named_key(key_name) if key_name
67
+ return named_result unless named_result == false || named_result.nil?
68
+
69
+ if editor_search_active?
70
+ editor_search_append(key) if printable_key?(key)
71
+ elsif printable_key?(key)
72
+ modern_record_undo { modern_insert_printable(key) }
73
+ end
74
+ end
75
+ end
76
+
77
+ def handle_modern_csi_u_key(key)
78
+ sequence = parse_csi_u_key(key)
79
+ return false unless sequence
80
+
81
+ code = sequence[:code]
82
+ modifier = sequence[:modifier]
83
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
84
+ sequence = sequence.merge(remaining: "")
85
+
86
+ if ctrl_modifier?(modifier) || super_modifier?(modifier)
87
+ handle_modern_modified_key(code, modifier, sequence)
88
+ else
89
+ handle_modern_editor_csi_u_key(sequence)
90
+ end
91
+ end
92
+
93
+ def handle_modern_multi_cursor_key(key)
94
+ return false if editor_search_active?
95
+
96
+ case key
97
+ when "\x04"
98
+ @editor_state.add_next_occurrence_selection
99
+ when "\e[1;4A", "\e[4A"
100
+ @editor_state.add_vertical_cursor(:up)
101
+ when "\e[1;4B", "\e[4B"
102
+ @editor_state.add_vertical_cursor(:down)
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ def handle_modern_indentation_navigation_key(key)
109
+ return false if editor_search_active?
110
+
111
+ case key
112
+ when *modern_indentation_key_sequences(:up)
113
+ modern_move_indentation { @editor_state.move_indentation_up }
114
+ when *modern_indentation_key_sequences(:down)
115
+ modern_move_indentation { @editor_state.move_indentation_down }
116
+ when *modern_indentation_key_sequences(:right)
117
+ modern_move_indentation { @editor_state.move_indentation_right }
118
+ when *modern_indentation_key_sequences(:select_up)
119
+ editor_extending_selection { @editor_state.move_indentation_up }
120
+ when *modern_indentation_key_sequences(:select_down)
121
+ editor_extending_selection { @editor_state.move_indentation_down }
122
+ when *modern_indentation_key_sequences(:select_right)
123
+ editor_extending_selection { @editor_state.move_indentation_right }
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ def modern_move_indentation
130
+ result = yield
131
+ @editor_state.clear_selection
132
+ result
133
+ end
134
+
135
+ def modern_indentation_key_sequences(action)
136
+ case [modern_indentation_modifier, action]
137
+ when [:alt, :up]
138
+ ["\e[1;3A", "\e[3A"]
139
+ when [:alt, :down]
140
+ ["\e[1;3B", "\e[3B"]
141
+ when [:alt, :right]
142
+ ["\e[1;3C", "\e[3C"]
143
+ when [:alt, :select_up]
144
+ ["\e[1;4A", "\e[4A"]
145
+ when [:alt, :select_down]
146
+ ["\e[1;4B", "\e[4B"]
147
+ when [:alt, :select_right]
148
+ ["\e[1;4C", "\e[4C"]
149
+ when [:ctrl, :up]
150
+ ["\e[1;5A", "\e[5A"]
151
+ when [:ctrl, :down]
152
+ ["\e[1;5B", "\e[5B"]
153
+ when [:ctrl, :right]
154
+ ["\e[1;5C", "\e[5C"]
155
+ when [:ctrl, :select_up]
156
+ ["\e[1;6A", "\e[6A"]
157
+ when [:ctrl, :select_down]
158
+ ["\e[1;6B", "\e[6B"]
159
+ when [:ctrl, :select_right]
160
+ ["\e[1;6C", "\e[6C"]
161
+ else
162
+ []
163
+ end
164
+ end
165
+
166
+ def modern_indentation_modifier
167
+ RbConfig::CONFIG["host_os"].to_s.downcase.include?("darwin") ? :alt : :ctrl
168
+ end
169
+
170
+ def handle_modern_modified_navigation_key(key)
171
+ return false if editor_search_active?
172
+
173
+ case key
174
+ when "\e[1;5C", "\e[5C"
175
+ @editor_state.move_line_end
176
+ when "\e[1;5D", "\e[5D"
177
+ @editor_state.move_line_start
178
+ when "\e[1;5A", "\e[5A"
179
+ @editor_state.move_file_start
180
+ when "\e[1;5B", "\e[5B"
181
+ @editor_state.move_file_end
182
+ when "\e[1;4C", "\e[4C"
183
+ editor_extending_selection { @editor_state.move_to_next_word }
184
+ when "\e[1;4D", "\e[4D"
185
+ editor_extending_selection { @editor_state.move_to_previous_word }
186
+ else
187
+ false
188
+ end
189
+ end
190
+
191
+ def handle_modern_key_binding(key)
192
+ case key
193
+ when "\x00"
194
+ true
195
+ when "\x03"
196
+ editor_search_active? ? editor_search_cancel : copy_editor_selection
197
+ when "\x06"
198
+ @editor_state.move_right unless editor_search_active?
199
+ when "\x16"
200
+ modern_record_undo { @editor_state.yank_kill_buffer } unless editor_search_active?
201
+ when "\x18"
202
+ modern_record_undo { cut_editor_selection } unless editor_search_active?
203
+ else
204
+ handle_modern_shared_key_binding(key)
205
+ end
206
+ end
207
+
208
+ def handle_modern_modified_key(code, modifier, sequence)
209
+ normalized_code = ctrl_code(code)
210
+ if super_modifier?(modifier)
211
+ return editor_search_active? ? editor_search_cancel : copy_editor_selection if normalized_code == 99
212
+ return handle_modern_editor_csi_u_key(sequence) unless ctrl_modifier?(modifier)
213
+ end
214
+
215
+ case normalized_code
216
+ when 13
217
+ return false if editor_search_active?
218
+
219
+ modern_record_undo do
220
+ clear_editor_selection_before_edit
221
+ editor_insert_endwise_modifier_newline
222
+ end
223
+ when 99
224
+ editor_search_active? ? editor_search_cancel : copy_editor_selection
225
+ when 100
226
+ return false if editor_search_active?
227
+
228
+ @editor_state.add_next_occurrence_selection
229
+ when 102
230
+ @editor_state.move_right unless editor_search_active?
231
+ when 118
232
+ modern_record_undo { @editor_state.yank_kill_buffer } unless editor_search_active?
233
+ when 120
234
+ modern_record_undo { cut_editor_selection } unless editor_search_active?
235
+ when 108
236
+ return false if editor_search_active?
237
+ return false unless modern_ctrl_shift_key?(code, modifier)
238
+
239
+ @editor_state.selection_to_line_start_cursors
240
+ when 122
241
+ return if editor_search_active?
242
+
243
+ modern_ctrl_shift_key?(code, modifier) ? @editor_state.redo : @editor_state.undo
244
+ else
245
+ return false if normalized_code == 32
246
+
247
+ handle_modern_editor_csi_u_key(sequence)
248
+ end
249
+ end
250
+
251
+ def handle_modern_shared_key_binding(key)
252
+ case key
253
+ when "\x04", "\x0B", "\x15", "\x17", "\x19", "\e[3~", "\ed", "\eD", "\e\b", "\e\x7F"
254
+ modern_record_undo { handle_editor_key_binding(key) }
255
+ else
256
+ handle_editor_key_binding(key)
257
+ end
258
+ end
259
+
260
+ def handle_modern_editor_csi_u_key(key_or_sequence)
261
+ sequence = key_or_sequence.is_a?(Hash) ? key_or_sequence : parse_csi_u_key(key_or_sequence)
262
+ return handle_editor_csi_u_key(key_or_sequence) unless sequence
263
+
264
+ code = sequence[:code]
265
+ modifier = sequence[:modifier]
266
+ if ctrl_modifier?(modifier)
267
+ normalized_code = ctrl_code(code)
268
+ return @editor_state.add_next_occurrence_selection if normalized_code == 100
269
+ if normalized_code == 108 && modern_ctrl_shift_key?(code, modifier)
270
+ return @editor_state.selection_to_line_start_cursors
271
+ end
272
+
273
+ case normalized_code
274
+ when 107, 117, 119, 121
275
+ return modern_record_undo { handle_parsed_editor_csi_u_key(sequence) }
276
+ end
277
+ end
278
+
279
+ case code
280
+ when 9
281
+ return false if editor_search_active?
282
+ return false if ctrl_modifier?(modifier) || alt_modifier?(modifier) || super_modifier?(modifier)
283
+
284
+ shift_modifier?(modifier) ? modern_record_undo { editor_outdent_tab } : modern_record_undo { editor_insert_tab }
285
+ when 13
286
+ return editor_search_confirm if editor_search_active?
287
+
288
+ modern_record_undo { modern_insert_text("\n") }
289
+ when 8, 127
290
+ editor_search_active? ? editor_search_delete_character : modern_record_undo { modern_delete_before_cursor }
291
+ when 4
292
+ modern_record_undo { delete_editor_selection || @editor_state.delete_at_cursor } unless editor_search_active?
293
+ else
294
+ text = csi_u_printable_text(sequence)
295
+ if text
296
+ editor_search_active? ? editor_search_append(text) : modern_record_undo { modern_insert_printable(text) }
297
+ elsif csi_u_text_field?(sequence)
298
+ true
299
+ else
300
+ handle_parsed_editor_csi_u_key(sequence)
301
+ end
302
+ end
303
+ end
304
+
305
+ def handle_modern_bracketed_paste_key(key)
306
+ paste = read_bracketed_paste(key)
307
+ return false unless paste
308
+
309
+ modern_record_undo { @editor_state.insert(normalize_paste(paste[:content])) } unless editor_search_active?
310
+ queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
311
+ true
312
+ end
313
+
314
+ def modern_insert_printable(text)
315
+ return editor_insert_printable(text) unless @editor_state.multi_cursor?
316
+
317
+ @editor_state.replace_selections(text)
318
+ end
319
+
320
+ def modern_insert_text(text)
321
+ return @editor_state.replace_selections(text) if @editor_state.multi_cursor? || @editor_state.selection_ranges.any?
322
+
323
+ if text == "\n"
324
+ editor_insert_newline
325
+ else
326
+ @editor_state.insert(text)
327
+ end
328
+ end
329
+
330
+ def modern_delete_before_cursor
331
+ return @editor_state.delete_before_selections if @editor_state.multi_cursor?
332
+
333
+ delete_editor_selection || editor_delete_before_cursor
334
+ end
335
+
336
+ def modern_record_undo
337
+ before_buffer = @editor_state.buffer.dup
338
+ before_redo_stack = @editor_state.redo_stack.map { |entry| entry.merge(buffer: entry[:buffer].dup, selections: entry[:selections]&.map(&:dup)) }
339
+ @editor_state.push_undo
340
+ result = yield
341
+ if @editor_state.buffer == before_buffer
342
+ @editor_state.undo_stack.pop
343
+ @editor_state.redo_stack = before_redo_stack
344
+ end
345
+ result
346
+ end
347
+
348
+ def modern_ctrl_shift_key?(code, modifier)
349
+ code.to_i.between?(65, 90) || ((modifier.to_i - 1) & 1).positive?
350
+ end
351
+ end
352
+ end
353
+ end