kward 0.72.0 → 0.73.1

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -0
  3. data/CHANGELOG.md +59 -0
  4. data/Gemfile.lock +2 -2
  5. data/doc/configuration.md +1 -1
  6. data/doc/editor.md +23 -2
  7. data/doc/git.md +1 -0
  8. data/doc/rpc.md +2 -2
  9. data/doc/shell.md +56 -10
  10. data/doc/usage.md +27 -1
  11. data/lib/kward/ansi.rb +62 -23
  12. data/lib/kward/cli/plugins.rb +1 -1
  13. data/lib/kward/cli/rendering.rb +4 -1
  14. data/lib/kward/cli/runtime_helpers.rb +141 -7
  15. data/lib/kward/cli/settings.rb +0 -1
  16. data/lib/kward/cli/slash_commands.rb +213 -0
  17. data/lib/kward/cli/tabs.rb +34 -4
  18. data/lib/kward/cli/tool_summaries.rb +6 -0
  19. data/lib/kward/cli.rb +4 -12
  20. data/lib/kward/clipboard.rb +2 -3
  21. data/lib/kward/compactor.rb +7 -19
  22. data/lib/kward/config_files.rb +26 -4
  23. data/lib/kward/ekwsh.rb +239 -42
  24. data/lib/kward/image_attachments.rb +3 -1
  25. data/lib/kward/interactive_pty_runner.rb +151 -0
  26. data/lib/kward/local_command_runner.rb +155 -0
  27. data/lib/kward/local_pty_command_runner.rb +171 -0
  28. data/lib/kward/model/context_usage.rb +2 -2
  29. data/lib/kward/model/payloads.rb +2 -5
  30. data/lib/kward/prompt_history.rb +5 -3
  31. data/lib/kward/prompt_interface/editor/auto_indent.rb +5 -4
  32. data/lib/kward/prompt_interface/editor/controller.rb +262 -62
  33. data/lib/kward/prompt_interface/editor/modes/emacs.rb +21 -21
  34. data/lib/kward/prompt_interface/editor/modes/modern.rb +38 -37
  35. data/lib/kward/prompt_interface/editor/modes/vibe.rb +23 -173
  36. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  37. data/lib/kward/prompt_interface/editor/renderer.rb +6 -5
  38. data/lib/kward/prompt_interface/editor/state.rb +28 -6
  39. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +5 -3
  40. data/lib/kward/prompt_interface/git_prompt.rb +12 -23
  41. data/lib/kward/prompt_interface/interactive/controller.rb +1 -1
  42. data/lib/kward/prompt_interface/key_handler.rb +93 -51
  43. data/lib/kward/prompt_interface/question_prompt.rb +1 -6
  44. data/lib/kward/prompt_interface/screen.rb +3 -3
  45. data/lib/kward/prompt_interface/selection_prompt.rb +12 -6
  46. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  47. data/lib/kward/prompt_interface.rb +87 -221
  48. data/lib/kward/prompts/commands.rb +4 -0
  49. data/lib/kward/rpc/memory_methods.rb +83 -0
  50. data/lib/kward/rpc/server.rb +130 -83
  51. data/lib/kward/rpc/session_manager.rb +10 -74
  52. data/lib/kward/rpc/tool_metadata.rb +11 -0
  53. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  54. data/lib/kward/scratchpad_runner.rb +56 -0
  55. data/lib/kward/session_diff.rb +20 -3
  56. data/lib/kward/session_naming.rb +11 -0
  57. data/lib/kward/terminal_keys.rb +84 -0
  58. data/lib/kward/terminal_sequences.rb +42 -0
  59. data/lib/kward/tools/context_for_task.rb +2 -0
  60. data/lib/kward/version.rb +1 -1
  61. data/lib/kward/workers/git_guard.rb +25 -0
  62. data/lib/kward/workers/job.rb +99 -0
  63. data/lib/kward/workers/queue_runner.rb +166 -0
  64. data/lib/kward/workers/queue_store.rb +112 -0
  65. data/lib/kward/workers.rb +3 -0
  66. data/lib/kward/workspace.rb +15 -63
  67. data/templates/default/fulldoc/html/css/kward.css +33 -0
  68. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  69. data/templates/default/fulldoc/html/setup.rb +1 -0
  70. data/templates/default/layout/html/layout.erb +19 -32
  71. metadata +15 -1
@@ -1,3 +1,6 @@
1
+ require "fileutils"
2
+ require_relative "../../scratchpad_runner"
3
+
1
4
  # Namespace for the Kward CLI agent runtime.
2
5
  module Kward
3
6
  # Interactive terminal UI used by the CLI frontend.
@@ -43,6 +46,31 @@ module Kward
43
46
  "$#{path}"
44
47
  end
45
48
 
49
+ def open_scratchpad(language = :text, content: "")
50
+ language = normalize_scratchpad_language(language)
51
+ @editor_state = EditorState.new(
52
+ path: scratchpad_display_path(language),
53
+ display_path: scratchpad_display_path(language),
54
+ content: content.to_s,
55
+ new_file: true,
56
+ editor_mode: current_editor_mode,
57
+ virtual: true,
58
+ language: language
59
+ )
60
+ @editor_state.status = scratchpad_status_text(language)
61
+ @prompt_label = "Edit>"
62
+ self.composer_input = ""
63
+ self.composer_cursor = 0
64
+ @composer.clear_attachments
65
+ @pending_keys.clear
66
+ @file_overlay_dismissed_token = nil
67
+ @file_open_dismissed_token = nil
68
+ @asking = true
69
+ set_editor_bar_cursor_locked if current_editor_bar_cursor?
70
+ enable_editor_mouse_reporting
71
+ true
72
+ end
73
+
46
74
  def open_editor(path, allow_new: false, base_dir: Dir.pwd, restrict_to_workspace: true)
47
75
  full_path = File.expand_path(path.to_s, base_dir)
48
76
  root = File.expand_path(Dir.pwd)
@@ -131,6 +159,8 @@ module Kward
131
159
  disable_editor_mouse_reporting(force: true)
132
160
  restore_editor_cursor_shape_locked
133
161
  @editor_text_width = nil
162
+ @editor_save_as_active = false
163
+ @editor_save_as_buffer = ""
134
164
  @editor_state = nil
135
165
  @prompt_label = "You>"
136
166
  self.composer_input = ""
@@ -143,6 +173,7 @@ module Kward
143
173
  return if key.nil?
144
174
  mouse_result = handle_editor_mouse_key(key)
145
175
  return mouse_result unless mouse_result == false
176
+ return handle_editor_save_as_key(key) if @editor_save_as_active
146
177
  return handle_readonly_editor_key(key) if @editor_state&.readonly?
147
178
  return handle_vibe_key(key) if @editor_state&.vibe?
148
179
  return handle_emacs_key(key) if @editor_state&.emacs?
@@ -175,14 +206,14 @@ module Kward
175
206
  editor_insert_tab unless editor_search_active?
176
207
  when "\b", "\x7F"
177
208
  editor_search_active? ? editor_search_delete_character : delete_editor_selection || editor_delete_before_cursor
178
- when "\x03"
209
+ when TerminalKeys::CTRL_C
179
210
  return editor_search_cancel if editor_search_active?
180
211
  when "\e"
181
212
  return editor_search_cancel if editor_search_active?
182
213
  return @editor_state.clear_selection if @editor_state.selection_active?
183
- when "\x11"
214
+ when TerminalKeys::CTRL_Q
184
215
  quit_editor
185
- when "\x13"
216
+ when TerminalKeys::CTRL_S
186
217
  save_editor
187
218
  when "/"
188
219
  clear_editor_selection_before_edit unless editor_search_active?
@@ -270,20 +301,7 @@ module Kward
270
301
  end
271
302
 
272
303
  def parse_editor_mouse_key(key)
273
- match = key.to_s.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])/)
274
- return nil unless match
275
-
276
- code = match[1].to_i
277
- {
278
- code: code,
279
- button: code & 3,
280
- column: match[2].to_i,
281
- row: match[3].to_i,
282
- action: match[4],
283
- release: match[4] == "m",
284
- drag: (code & 32).positive?,
285
- remaining: key.to_s[match[0].length..].to_s
286
- }
304
+ parse_sgr_mouse_event(key)
287
305
  end
288
306
 
289
307
  def handle_editor_mouse_press(event)
@@ -451,13 +469,13 @@ module Kward
451
469
  return false if editor_search_active?
452
470
 
453
471
  case key
454
- when "\e[1;2D", "\e[2D"
472
+ when *TerminalKeys::SHIFT_LEFT
455
473
  editor_extending_selection { @editor_state.move_left }
456
- when "\e[1;2C", "\e[2C"
474
+ when *TerminalKeys::SHIFT_RIGHT
457
475
  editor_extending_selection { @editor_state.move_right }
458
- when "\e[1;2A", "\e[2A"
476
+ when *TerminalKeys::SHIFT_UP
459
477
  editor_extending_selection { editor_move_up }
460
- when "\e[1;2B", "\e[2B"
478
+ when *TerminalKeys::SHIFT_DOWN
461
479
  editor_extending_selection { editor_move_down }
462
480
  else
463
481
  false
@@ -466,39 +484,39 @@ module Kward
466
484
 
467
485
  def handle_editor_key_binding(key)
468
486
  case key
469
- when "\x01"
487
+ when TerminalKeys::CTRL_A
470
488
  @editor_state.move_line_start unless editor_search_active?
471
- when "\x02"
489
+ when TerminalKeys::CTRL_B
472
490
  @editor_state.move_left unless editor_search_active?
473
- when "\x04"
491
+ when TerminalKeys::CTRL_D
474
492
  @editor_state.delete_at_cursor unless editor_search_active?
475
- when "\x05"
493
+ when TerminalKeys::CTRL_E
476
494
  @editor_state.move_line_end unless editor_search_active?
477
- when "\x06"
495
+ when TerminalKeys::CTRL_F
478
496
  @editor_state.move_right unless editor_search_active?
479
- when "\x00"
497
+ when TerminalKeys::CTRL_SPACE
480
498
  @editor_state.begin_selection unless editor_search_active?
481
- when "\x0B"
499
+ when TerminalKeys::CTRL_K
482
500
  @editor_state.kill_line_after_cursor unless editor_search_active?
483
- when "\x0E"
501
+ when TerminalKeys::CTRL_N
484
502
  editor_move_down unless editor_search_active?
485
- when "\x10"
503
+ when TerminalKeys::CTRL_P
486
504
  editor_move_up unless editor_search_active?
487
- when "\x15"
505
+ when TerminalKeys::CTRL_U
488
506
  @editor_state.kill_line_before_cursor unless editor_search_active?
489
- when "\x17"
507
+ when TerminalKeys::CTRL_W
490
508
  @editor_state.delete_word_before_cursor unless editor_search_active?
491
- when "\x19"
509
+ when TerminalKeys::CTRL_Y
492
510
  editor_selection_active? ? copy_editor_selection : @editor_state.yank_kill_buffer unless editor_search_active?
493
- when "\e[D", "\eOD"
511
+ when *TerminalKeys::LEFT
494
512
  @editor_state.move_left unless editor_search_active?
495
- when "\e[C", "\eOC"
513
+ when *TerminalKeys::RIGHT
496
514
  @editor_state.move_right unless editor_search_active?
497
- when "\e[H", "\eOH", "\e[1~", "\e[7~"
515
+ when *TerminalKeys::HOME
498
516
  @editor_state.move_line_start unless editor_search_active?
499
- when "\e[F", "\eOF", "\e[4~", "\e[8~"
517
+ when *TerminalKeys::END_KEY
500
518
  @editor_state.move_line_end unless editor_search_active?
501
- when "\e[3~"
519
+ when *TerminalKeys::DELETE
502
520
  delete_editor_selection || @editor_state.delete_at_cursor unless editor_search_active?
503
521
  when "\eb", "\eB"
504
522
  @editor_state.move_to_previous_word unless editor_search_active?
@@ -600,12 +618,9 @@ module Kward
600
618
  end
601
619
 
602
620
  def handle_editor_bracketed_paste_key(key)
603
- paste = read_bracketed_paste(key)
604
- return false unless paste
605
-
606
- @editor_state.insert(normalize_paste(paste[:content])) unless editor_search_active?
607
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
608
- true
621
+ handle_bracketed_paste(key) do |content|
622
+ @editor_state.insert(content) unless editor_search_active?
623
+ end
609
624
  end
610
625
 
611
626
  def ctrl_code(code)
@@ -669,12 +684,12 @@ module Kward
669
684
  return named_result unless named_result == false || named_result.nil?
670
685
 
671
686
  case key
672
- when "\x11"
687
+ when TerminalKeys::CTRL_Q
673
688
  close_editor
674
- when "\x06"
689
+ when TerminalKeys::CTRL_F
675
690
  editor_search_active? ? editor_search_append(key) : editor_search_begin
676
- when "\x03"
677
- editor_search_cancel if editor_search_active?
691
+ when TerminalKeys::CTRL_C
692
+ editor_search_active? ? editor_search_cancel : copy_editor_selection
678
693
  when "/"
679
694
  editor_search_active? ? editor_search_append(key) : editor_search_begin
680
695
  when "\b", "\x7F"
@@ -707,6 +722,10 @@ module Kward
707
722
  return editor_search_active? ? editor_search_append(key) : editor_search_begin
708
723
  end
709
724
 
725
+ if (ctrl_modifier?(modifier) || super_modifier?(modifier)) && ctrl_code(code) == 99
726
+ return editor_search_active? ? editor_search_cancel : copy_editor_selection
727
+ end
728
+
710
729
  case code
711
730
  when 13
712
731
  editor_search_confirm if editor_search_active?
@@ -726,12 +745,9 @@ module Kward
726
745
  end
727
746
 
728
747
  def handle_readonly_bracketed_paste_key(key)
729
- paste = read_bracketed_paste(key)
730
- return false unless paste
731
-
732
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
733
- @editor_state.status = "Read-only diff" unless editor_search_active?
734
- true
748
+ handle_bracketed_paste(key) do |_content|
749
+ @editor_state.status = "Read-only diff" unless editor_search_active?
750
+ end
735
751
  end
736
752
 
737
753
  def handle_readonly_named_key(key_name)
@@ -786,12 +802,18 @@ module Kward
786
802
  line, column = @editor_state.cursor_line_and_column
787
803
  text_width = current_editor_text_width
788
804
  row_start = editor_visual_row_start_column(line, column, text_width)
805
+ visual_column = column - row_start
789
806
  if row_start.positive?
790
- target_column = row_start - text_width + column - row_start
807
+ target_column = row_start - text_width + visual_column
791
808
  return @editor_state.set_cursor_line_and_column(line, target_column)
792
809
  end
793
810
 
794
- @editor_state.move_up
811
+ return @editor_state.move_up if line.zero?
812
+
813
+ previous_line = @editor_state.lines[line - 1].to_s
814
+ previous_row_start = editor_last_visual_row_start_column(previous_line, text_width)
815
+ target_column = [previous_row_start + visual_column, previous_line.length].min
816
+ @editor_state.set_cursor_line_and_column(line - 1, target_column)
795
817
  end
796
818
 
797
819
  def editor_move_down
@@ -800,14 +822,26 @@ module Kward
800
822
  line, column = @editor_state.cursor_line_and_column
801
823
  text_width = current_editor_text_width
802
824
  row_start = editor_visual_row_start_column(line, column, text_width)
825
+ visual_column = column - row_start
803
826
  next_start = row_start + text_width
804
827
  current_line = @editor_state.lines[line].to_s
805
828
  if next_start < current_line.length
806
- target_column = [next_start + column - row_start, current_line.length].min
829
+ target_column = [next_start + visual_column, current_line.length].min
807
830
  return @editor_state.set_cursor_line_and_column(line, target_column)
808
831
  end
809
832
 
810
- @editor_state.move_down
833
+ return @editor_state.move_down if line >= @editor_state.lines.length - 1
834
+
835
+ next_line = @editor_state.lines[line + 1].to_s
836
+ target_column = [visual_column, next_line.length].min
837
+ @editor_state.set_cursor_line_and_column(line + 1, target_column)
838
+ end
839
+
840
+ def editor_last_visual_row_start_column(line, text_width)
841
+ length = line.to_s.length
842
+ return 0 if length.zero?
843
+
844
+ ((length - 1) / text_width) * text_width
811
845
  end
812
846
 
813
847
  def current_editor_text_width
@@ -845,7 +879,7 @@ module Kward
845
879
  return false if text.empty?
846
880
 
847
881
  @editor_state.push_kill(text)
848
- @output_io.print("\e]52;c;#{Base64.strict_encode64(text)}\a")
882
+ @output_io.print(TerminalSequences.osc52(text))
849
883
  @output_io.flush if @output_io.respond_to?(:flush)
850
884
  @editor_state.clear_selection
851
885
  @editor_state.status = "Copied selection"
@@ -923,7 +957,7 @@ module Kward
923
957
  def enable_editor_mouse_reporting
924
958
  return if @editor_mouse_reporting_enabled
925
959
 
926
- @output_io.print("\e[?1003h\e[?1006h")
960
+ @output_io.print(TerminalSequences::MOUSE_REPORTING_ENABLE)
927
961
  @output_io.flush if @output_io.respond_to?(:flush)
928
962
  @editor_mouse_reporting_enabled = true
929
963
  end
@@ -931,7 +965,7 @@ module Kward
931
965
  def disable_editor_mouse_reporting(force: false)
932
966
  return unless force || @editor_mouse_reporting_enabled
933
967
 
934
- @output_io.print("\e[?1006l\e[?1003l")
968
+ @output_io.print(TerminalSequences::MOUSE_REPORTING_DISABLE)
935
969
  @output_io.flush if @output_io.respond_to?(:flush)
936
970
  @editor_mouse_reporting_enabled = false
937
971
  end
@@ -989,19 +1023,33 @@ module Kward
989
1023
  true
990
1024
  end
991
1025
 
992
- def save_editor
1026
+ def save_editor(path = nil, prompt_for_path: true)
993
1027
  return false unless @editor_state
994
1028
  if @editor_state.readonly?
995
1029
  @editor_state.status = "Read-only diff"
996
1030
  return true
997
1031
  end
998
1032
 
1033
+ if path.to_s.strip.empty? && @editor_state.path.to_s.empty?
1034
+ if prompt_for_path
1035
+ begin_editor_save_as
1036
+ return true
1037
+ end
1038
+
1039
+ @editor_state.status = "Use :w filename"
1040
+ return false
1041
+ end
1042
+
1043
+ return false if !path.to_s.strip.empty? && !bind_editor_save_path(path)
1044
+
999
1045
  if @editor_state.file_changed_on_disk? && !@editor_state.overwrite_confirmed
1000
1046
  @editor_state.overwrite_confirmed = true
1001
1047
  @editor_state.status = "File changed on disk. Press Ctrl+S again to overwrite."
1002
1048
  return true
1003
1049
  end
1004
1050
 
1051
+ parent = File.dirname(@editor_state.path)
1052
+ FileUtils.mkdir_p(parent) unless Dir.exist?(parent)
1005
1053
  File.write(@editor_state.path, @editor_state.buffer)
1006
1054
  @editor_state.refresh_after_save(@editor_state.buffer)
1007
1055
  true
@@ -1010,6 +1058,158 @@ module Kward
1010
1058
  false
1011
1059
  end
1012
1060
 
1061
+ def begin_editor_save_as
1062
+ @editor_save_as_active = true
1063
+ @editor_save_as_buffer = ""
1064
+ @editor_state.status = "Save as: "
1065
+ true
1066
+ end
1067
+
1068
+ def handle_editor_save_as_key(key)
1069
+ return true if handle_bundled_key(key) { |token| handle_editor_save_as_key(token) }
1070
+
1071
+ csi_result = handle_editor_save_as_csi_u_key(key)
1072
+ return csi_result unless csi_result == false
1073
+
1074
+ case key
1075
+ when "\n", "\r"
1076
+ finish_editor_save_as
1077
+ when "\e", TerminalKeys::CTRL_C
1078
+ cancel_editor_save_as
1079
+ when "\b", "\x7F"
1080
+ @editor_save_as_buffer = @editor_save_as_buffer.to_s[0...-1]
1081
+ update_editor_save_as_status
1082
+ else
1083
+ key_name = key_name_for(key)
1084
+ case key_name
1085
+ when :return, :enter
1086
+ finish_editor_save_as
1087
+ when :backspace
1088
+ @editor_save_as_buffer = @editor_save_as_buffer.to_s[0...-1]
1089
+ update_editor_save_as_status
1090
+ else
1091
+ if printable_key?(key)
1092
+ @editor_save_as_buffer = @editor_save_as_buffer.to_s + key
1093
+ update_editor_save_as_status
1094
+ end
1095
+ end
1096
+ end
1097
+ true
1098
+ end
1099
+
1100
+ def handle_editor_save_as_csi_u_key(key)
1101
+ sequence = parse_csi_u_key(key)
1102
+ return false unless sequence
1103
+
1104
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
1105
+ code = sequence[:code]
1106
+ modifier = sequence[:modifier]
1107
+ normalized_code = ctrl_code(code)
1108
+ if code == 13
1109
+ return finish_editor_save_as
1110
+ elsif [8, 127].include?(code)
1111
+ @editor_save_as_buffer = @editor_save_as_buffer.to_s[0...-1]
1112
+ return update_editor_save_as_status
1113
+ elsif code == 27 || (ctrl_modifier?(modifier) && normalized_code == 99)
1114
+ return cancel_editor_save_as
1115
+ end
1116
+
1117
+ text = csi_u_printable_text(sequence)
1118
+ if text
1119
+ @editor_save_as_buffer = @editor_save_as_buffer.to_s + text
1120
+ return update_editor_save_as_status
1121
+ end
1122
+
1123
+ true
1124
+ end
1125
+
1126
+ def finish_editor_save_as
1127
+ path = @editor_save_as_buffer.to_s.strip
1128
+ @editor_save_as_active = false
1129
+ @editor_save_as_buffer = ""
1130
+ if path.empty?
1131
+ @editor_state.status = "Save canceled"
1132
+ return true
1133
+ end
1134
+
1135
+ save_editor(path, prompt_for_path: false)
1136
+ end
1137
+
1138
+ def cancel_editor_save_as
1139
+ @editor_save_as_active = false
1140
+ @editor_save_as_buffer = ""
1141
+ @editor_state.status = "Save canceled"
1142
+ true
1143
+ end
1144
+
1145
+ def update_editor_save_as_status
1146
+ @editor_state.status = "Save as: #{@editor_save_as_buffer}"
1147
+ true
1148
+ end
1149
+
1150
+ def bind_editor_save_path(path)
1151
+ full_path = File.expand_path(path.to_s.strip, Dir.pwd)
1152
+ root = File.expand_path(Dir.pwd)
1153
+ unless full_path == root || full_path.start_with?("#{root}/")
1154
+ @editor_state.status = "Cannot save outside workspace"
1155
+ return false
1156
+ end
1157
+
1158
+ @editor_state.bind_path(full_path)
1159
+ @editor_syntax_language_path = nil
1160
+ @editor_indent_unit_path = nil
1161
+ true
1162
+ end
1163
+
1164
+ def run_editor_buffer
1165
+ return false unless @editor_state
1166
+
1167
+ language = @editor_state.language || editor_syntax_language
1168
+ result = ScratchpadRunner.run(language, @editor_state.buffer)
1169
+ @editor_state.replace_range(0, @editor_state.buffer.length, result.buffer)
1170
+ @editor_state.status = "Ran #{language} (exit #{result.exit_status})"
1171
+ true
1172
+ rescue StandardError => e
1173
+ @editor_state.status = "Run failed: #{e.message}" if @editor_state
1174
+ false
1175
+ end
1176
+
1177
+ def normalize_scratchpad_language(language)
1178
+ case language.to_s.strip.downcase
1179
+ when "", "text", "txt"
1180
+ :text
1181
+ when "markdown", "md"
1182
+ :markdown
1183
+ when "ruby", "rb"
1184
+ :ruby
1185
+ else
1186
+ :text
1187
+ end
1188
+ end
1189
+
1190
+ def scratchpad_display_path(language)
1191
+ case language.to_sym
1192
+ when :markdown
1193
+ "scratchpad.md"
1194
+ when :ruby
1195
+ "scratchpad.rb"
1196
+ else
1197
+ "scratchpad.txt"
1198
+ end
1199
+ end
1200
+
1201
+ def scratchpad_status_text(language)
1202
+ runnable = language.to_sym == :ruby
1203
+ case current_editor_mode
1204
+ when "vibe"
1205
+ runnable ? "NORMAL · i insert · :w filename save · :q quit · :run run" : "NORMAL · i insert · :w filename save · :q quit"
1206
+ when "emacs"
1207
+ runnable ? "C-x C-s save as · C-x C-c quit · C-r run" : "C-x C-s save as · C-x C-c quit"
1208
+ else
1209
+ runnable ? "Ctrl+S save as · Ctrl+Q quit · Ctrl+R run" : "Ctrl+S save as · Ctrl+Q quit"
1210
+ end
1211
+ end
1212
+
1013
1213
  def printable_key?(key)
1014
1214
  key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
1015
1215
  end
@@ -37,44 +37,44 @@ module Kward
37
37
  when "\b", "\x7F"
38
38
  clear_editor_selection_before_edit unless editor_search_active?
39
39
  editor_search_active? ? editor_search_delete_character : editor_delete_before_cursor
40
- when "\x00"
40
+ when TerminalKeys::CTRL_SPACE
41
41
  @editor_state.begin_selection unless editor_search_active?
42
- when "\x01"
42
+ when TerminalKeys::CTRL_A
43
43
  @editor_state.move_line_start unless editor_search_active?
44
- when "\x02"
44
+ when TerminalKeys::CTRL_B
45
45
  @editor_state.move_left unless editor_search_active?
46
- when "\x04"
46
+ when TerminalKeys::CTRL_D
47
47
  @editor_state.delete_at_cursor unless editor_search_active?
48
- when "\x05"
48
+ when TerminalKeys::CTRL_E
49
49
  @editor_state.move_line_end unless editor_search_active?
50
- when "\x06"
50
+ when TerminalKeys::CTRL_F
51
51
  @editor_state.move_right unless editor_search_active?
52
52
  when "\x07"
53
53
  emacs_cancel
54
- when "\x0B"
54
+ when TerminalKeys::CTRL_K
55
55
  if editor_selection_active?
56
56
  emacs_kill_selection
57
57
  else
58
58
  @editor_state.kill_line_after_cursor unless editor_search_active?
59
59
  end
60
- when "\x0E"
60
+ when TerminalKeys::CTRL_N
61
61
  editor_move_down unless editor_search_active?
62
- when "\x10"
62
+ when TerminalKeys::CTRL_P
63
63
  editor_move_up unless editor_search_active?
64
- when "\x12"
64
+ when TerminalKeys::CTRL_R
65
65
  editor_search_active? ? editor_search_append(key) : editor_search_begin(:backward)
66
- when "\x13"
66
+ when TerminalKeys::CTRL_S
67
67
  editor_search_active? ? editor_search_append(key) : editor_search_begin(:forward)
68
- when "\x15"
68
+ when TerminalKeys::CTRL_U
69
69
  @editor_state.kill_line_before_cursor unless editor_search_active?
70
- when "\x16"
70
+ when TerminalKeys::CTRL_V
71
71
  @editor_state.page_down(editor_page_rows) unless editor_search_active?
72
- when "\x17"
72
+ when TerminalKeys::CTRL_W
73
73
  editor_selection_active? ? emacs_kill_selection : @editor_state.delete_word_before_cursor unless editor_search_active?
74
- when "\x18"
74
+ when TerminalKeys::CTRL_X
75
75
  @editor_state.emacs_pending = "C-x"
76
76
  @editor_state.status = "C-x"
77
- when "\x19"
77
+ when TerminalKeys::CTRL_Y
78
78
  @editor_state.yank_from_kill_ring unless editor_search_active?
79
79
  when "\e"
80
80
  return editor_search_cancel if editor_search_active?
@@ -192,9 +192,9 @@ module Kward
192
192
  @editor_state.emacs_pending = nil
193
193
  key = emacs_ctrl_x_csi_u_key(key)
194
194
  case key
195
- when "\x13"
195
+ when TerminalKeys::CTRL_S
196
196
  save_editor
197
- when "\x03"
197
+ when TerminalKeys::CTRL_C
198
198
  quit_editor("Unsaved changes. Press C-x C-c again to discard.")
199
199
  else
200
200
  @editor_state.status = "Unknown C-x command"
@@ -209,9 +209,9 @@ module Kward
209
209
  queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
210
210
  case ctrl_code(sequence[:code])
211
211
  when 99
212
- "\x03"
212
+ TerminalKeys::CTRL_C
213
213
  when 115
214
- "\x13"
214
+ TerminalKeys::CTRL_S
215
215
  else
216
216
  key
217
217
  end
@@ -236,7 +236,7 @@ module Kward
236
236
  end
237
237
 
238
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")
239
+ @output_io.print(TerminalSequences.osc52(@editor_state.kill_buffer))
240
240
  @output_io.flush if @output_io.respond_to?(:flush)
241
241
  @editor_state.clear_selection
242
242
  @editor_state.status = "Copied region"