textbringer 19 → 24

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.
@@ -1,4 +1,13 @@
1
+ require "open-uri"
2
+ require "fileutils"
3
+ require "socket"
4
+ require "timeout"
5
+
1
6
  module Textbringer
7
+ CONFIG[:skk_dictionary_path] = File.expand_path("~/.textbringer/skk/SKK-JISYO.L")
8
+ CONFIG[:skk_server_host] = nil # nil = disabled (default)
9
+ CONFIG[:skk_server_port] = 1178
10
+
2
11
  class SkkInputMethod < InputMethod
3
12
  HIRAGANA_TABLE = {
4
13
  "a" => "あ", "i" => "い", "u" => "う", "e" => "え", "o" => "お",
@@ -41,7 +50,11 @@ module Textbringer
41
50
  "xtu" => "っ", "xtsu" => "っ",
42
51
  "xya" => "ゃ", "xyu" => "ゅ", "xyo" => "ょ",
43
52
  "xa" => "ぁ", "xi" => "ぃ", "xu" => "ぅ", "xe" => "ぇ", "xo" => "ぉ",
44
- "," => "、", "." => "。",
53
+ "-" => "ー", "," => "、", "." => "。", "[" => "「", "]" => "」",
54
+ "z-" => "~", "z." => "…", "z/" => "・", "z," => "‥",
55
+ "z(" => "(", "z)" => ")", "z[" => "『", "z]" => "』",
56
+ "zh" => "←", "zj" => "↓", "zk" => "↑", "zl" => "→", "zL" => "⇒",
57
+ "z " => " ",
45
58
  }
46
59
 
47
60
  HIRAGANA_PREFIXES = HIRAGANA_TABLE.keys.flat_map { |s|
@@ -94,7 +107,13 @@ module Textbringer
94
107
  (s.size - 1).times.map { |i| s[0, i + 1] }
95
108
  }.uniq
96
109
 
97
- DICTIONARY_PATH = File.expand_path("~/.textbringer/skk/SKK-JISYO.L")
110
+ STATUS_NAMES = {
111
+ hiragana: "かな",
112
+ katakana: "カナ",
113
+ hankaku_katakana: "半カナ",
114
+ zenkaku_ascii: "全英",
115
+ ascii: "SKK:"
116
+ }
98
117
 
99
118
  DEFAULT_CURSOR_COLORS = {
100
119
  hiragana: "pink",
@@ -117,6 +136,7 @@ module Textbringer
117
136
  @marker_pos = nil
118
137
  @okuriiari = nil
119
138
  @okurinasi = nil
139
+ @skk_server_socket = nil
120
140
  end
121
141
 
122
142
  def toggle
@@ -125,22 +145,26 @@ module Textbringer
125
145
  update_cursor_color
126
146
  else
127
147
  reset_cursor_color
148
+ close_skk_server
128
149
  end
129
150
  end
130
151
 
131
152
  def disable
132
153
  super
133
154
  reset_cursor_color
155
+ close_skk_server
156
+ end
157
+
158
+ def on_activate
159
+ update_cursor_color if @enabled
160
+ end
161
+
162
+ def on_deactivate
163
+ reset_cursor_color if @enabled
134
164
  end
135
165
 
136
166
  def status
137
- case @phase
138
- when :converting then "▽"
139
- when :selecting then "▼"
140
- else
141
- { hiragana: "あ", katakana: "ア", hankaku_katakana: "ア",
142
- zenkaku_ascii: "A", ascii: "A" }[@mode]
143
- end
167
+ STATUS_NAMES[@mode]
144
168
  end
145
169
 
146
170
  def handle_event(event)
@@ -198,7 +222,7 @@ module Textbringer
198
222
  process_romaji(event)
199
223
  end
200
224
  when "l"
201
- if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
225
+ if @roman_buffer != "z" && [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
202
226
  @roman_buffer = +""
203
227
  @mode = :ascii
204
228
  Window.redisplay
@@ -208,7 +232,7 @@ module Textbringer
208
232
  process_romaji(event)
209
233
  end
210
234
  when "L"
211
- if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
235
+ if @roman_buffer != "z" && [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
212
236
  @roman_buffer = +""
213
237
  @mode = :zenkaku_ascii
214
238
  Window.redisplay
@@ -386,8 +410,8 @@ module Textbringer
386
410
  if kana
387
411
  @roman_buffer = +""
388
412
  if @okuri_roman
389
- # Completing okurigana
390
- @okuri_kana = kana
413
+ # Completing okurigana (accumulate in case a vowel kana was already prepended)
414
+ @okuri_kana = (@okuri_kana || "") + kana
391
415
  with_target_buffer do |buffer|
392
416
  buffer.insert(kana)
393
417
  end
@@ -426,7 +450,14 @@ module Textbringer
426
450
  first_char = @roman_buffer[0]
427
451
  last_char = @roman_buffer[-1]
428
452
  @roman_buffer = +""
429
- append_yomi_kana(first_char)
453
+ if @okuri_roman && (kana = HIRAGANA_TABLE[first_char])
454
+ # Vowel starts okurigana: accumulate kana and continue buffering the rest
455
+ @okuri_kana = (@okuri_kana || "") + kana
456
+ with_target_buffer { |b| b.insert(kana) }
457
+ Window.redisplay
458
+ else
459
+ append_yomi_kana(first_char)
460
+ end
430
461
  process_converting_romaji(last_char)
431
462
  end
432
463
 
@@ -454,9 +485,20 @@ module Textbringer
454
485
  nil
455
486
  end
456
487
 
457
- def start_okurigana(consonant)
458
- @okuri_roman = consonant.dup
459
- @roman_buffer = consonant.dup
488
+ def start_okurigana(c)
489
+ @okuri_roman = c.dup
490
+ kana = HIRAGANA_TABLE[c]
491
+ if kana
492
+ # Vowel okurigana: insert the kana immediately and record it in @okuri_kana.
493
+ # (A vowel is never a prefix of a longer romaji sequence, so it's always complete.)
494
+ @okuri_kana = kana
495
+ @roman_buffer = +""
496
+ with_target_buffer { |b| b.insert(kana) }
497
+ Window.redisplay
498
+ start_selecting
499
+ else
500
+ @roman_buffer = c.dup
501
+ end
460
502
  end
461
503
 
462
504
  def cancel_converting
@@ -489,16 +531,19 @@ module Textbringer
489
531
  end
490
532
 
491
533
  def start_selecting
492
- ensure_dictionary_loaded
493
-
494
534
  lookup_key = if @okuri_roman
495
535
  @yomi + @okuri_roman
496
536
  else
497
537
  @yomi
498
538
  end
499
539
 
500
- dict = @okuri_roman ? @okuriiari : @okurinasi
501
- candidates = dict[lookup_key]
540
+ candidates = if CONFIG[:skk_server_host]
541
+ skk_server_lookup(lookup_key)
542
+ else
543
+ ensure_dictionary_loaded
544
+ dict = @okuri_roman ? @okuriiari : @okurinasi
545
+ dict[lookup_key]
546
+ end
502
547
 
503
548
  if candidates.nil? || candidates.empty?
504
549
  message("No conversion: #{@yomi}")
@@ -569,7 +614,7 @@ module Textbringer
569
614
  def ensure_dictionary_loaded
570
615
  return if @okuriiari
571
616
 
572
- path = CONFIG[:skk_dictionary] || DICTIONARY_PATH
617
+ path = CONFIG[:skk_dictionary_path]
573
618
  @okuriiari = {}
574
619
  @okurinasi = {}
575
620
  section = :okuriiari
@@ -747,5 +792,70 @@ module Textbringer
747
792
  STDOUT.write("\e]112\a")
748
793
  STDOUT.flush
749
794
  end
795
+
796
+ SKK_SERVER_TIMEOUT = 3 # seconds
797
+
798
+ def close_skk_server
799
+ return unless @skk_server_socket
800
+ begin
801
+ @skk_server_socket.write("0\n")
802
+ rescue IOError, Errno::EPIPE
803
+ end
804
+ @skk_server_socket.close rescue nil
805
+ @skk_server_socket = nil
806
+ end
807
+
808
+ def skk_server_connect
809
+ return true if @skk_server_socket && !@skk_server_socket.closed?
810
+ host = CONFIG[:skk_server_host]
811
+ return false unless host
812
+ port = CONFIG[:skk_server_port] || 1178
813
+ Timeout.timeout(SKK_SERVER_TIMEOUT) do
814
+ @skk_server_socket = TCPSocket.new(host, port)
815
+ end
816
+ true
817
+ end
818
+
819
+ def skk_server_lookup(lookup_key)
820
+ skk_server_connect
821
+ begin
822
+ Timeout.timeout(SKK_SERVER_TIMEOUT) do
823
+ @skk_server_socket.write("1#{lookup_key} \n".encode("EUC-JP"))
824
+ response = @skk_server_socket.gets("\n")
825
+ return nil unless response
826
+ response = response.encode("UTF-8", "EUC-JP", invalid: :replace, undef: :replace).chomp
827
+ return nil unless response.start_with?("1/")
828
+ body = response[2..]
829
+ candidates = body.split("/").map { |c| c.split(";").first&.strip }.compact.reject(&:empty?)
830
+ candidates.empty? ? nil : candidates
831
+ end
832
+ rescue Timeout::Error, IOError, Errno::EPIPE, Errno::ECONNRESET
833
+ @skk_server_socket.close rescue nil
834
+ @skk_server_socket = nil
835
+ nil
836
+ end
837
+ end
838
+ end
839
+
840
+ SKK_DICTIONARY_URL = "https://github.com/skk-dev/dict/raw/090619ac57ef230a0506c191b569fc8c82b1025b/SKK-JISYO.L"
841
+
842
+ module Commands
843
+ define_command(:skk_download_dictionary, doc: "Download SKK dictionary") do
844
+ path = CONFIG[:skk_dictionary_path]
845
+ if File.exist?(path) && !yes_or_no?("#{path} already exists. Overwrite it?")
846
+ return
847
+ end
848
+ background do
849
+ URI.open(SKK_DICTIONARY_URL) do |f|
850
+ FileUtils.mkdir_p(File.dirname(path))
851
+ File.open(path, "wb") do |out|
852
+ IO.copy_stream(f, out)
853
+ end
854
+ end
855
+ foreground do
856
+ message("Downloaded to #{path}")
857
+ end
858
+ end
859
+ end
750
860
  end
751
861
  end
@@ -177,6 +177,7 @@ module Textbringer
177
177
  GLOBAL_MAP.define_key("\C-z", :suspend_textbringer)
178
178
  GLOBAL_MAP.define_key("\C-x\C-f", :find_file)
179
179
  GLOBAL_MAP.define_key("\C-x\C-r", :find_file_read_only)
180
+ GLOBAL_MAP.define_key("\C-xd", :dired)
180
181
  GLOBAL_MAP.define_key("\C-x\C-v", :find_alternate_file)
181
182
  GLOBAL_MAP.define_key("\C-xb", :switch_to_buffer)
182
183
  GLOBAL_MAP.define_key("\C-x\C-b", :list_buffers)
@@ -0,0 +1,322 @@
1
+ module Textbringer
2
+ class DiredMode < Mode
3
+ define_keymap :DIRED_MODE_MAP
4
+ DIRED_MODE_MAP.define_key("n", :dired_next_line_command)
5
+ DIRED_MODE_MAP.define_key(" ", :dired_next_line_command)
6
+ DIRED_MODE_MAP.define_key("\C-n", :dired_next_line_command)
7
+ DIRED_MODE_MAP.define_key("p", :dired_previous_line_command)
8
+ DIRED_MODE_MAP.define_key("\C-p", :dired_previous_line_command)
9
+ DIRED_MODE_MAP.define_key("^", :dired_up_directory_command)
10
+ DIRED_MODE_MAP.define_key("\C-m", :dired_find_file_command)
11
+ DIRED_MODE_MAP.define_key("f", :dired_find_file_command)
12
+ DIRED_MODE_MAP.define_key("o", :dired_find_file_other_window_command)
13
+ DIRED_MODE_MAP.define_key("d", :dired_flag_file_deletion_command)
14
+ DIRED_MODE_MAP.define_key("u", :dired_unmark_command)
15
+ DIRED_MODE_MAP.define_key("U", :dired_unmark_all_command)
16
+ DIRED_MODE_MAP.define_key("x", :dired_do_flagged_delete_command)
17
+ DIRED_MODE_MAP.define_key("R", :dired_do_rename_command)
18
+ DIRED_MODE_MAP.define_key("C", :dired_do_copy_command)
19
+ DIRED_MODE_MAP.define_key("+", :dired_create_directory_command)
20
+ DIRED_MODE_MAP.define_key("g", :dired_revert_command)
21
+ DIRED_MODE_MAP.define_key("q", :bury_buffer)
22
+
23
+ # Deletion-flagged lines
24
+ define_syntax :dired_flagged, /^D .+$/
25
+ # Symlinks
26
+ define_syntax :dired_symlink, /^[ D] \S+\s+\d+\s+[\d-]+ [\d:]+ .+ -> .+$/
27
+ # Directories
28
+ define_syntax :dired_directory, /^[ D] d\S+\s+\d+\s+[\d-]+ [\d:]+ .+\/$/
29
+ # Executables
30
+ define_syntax :dired_executable, /^[ D] -[r-][w-]x.+$/
31
+
32
+ PERM_BITS = [
33
+ ["r", 0400], ["w", 0200], ["x", 0100],
34
+ ["r", 0040], ["w", 0020], ["x", 0010],
35
+ ["r", 0004], ["w", 0002], ["x", 0001]
36
+ ]
37
+
38
+ def self.format_permissions(stat)
39
+ type = if stat.directory? then "d"
40
+ elsif stat.symlink? then "l"
41
+ elsif stat.pipe? then "p"
42
+ elsif stat.socket? then "s"
43
+ elsif stat.chardev? then "c"
44
+ elsif stat.blockdev? then "b"
45
+ else "-"
46
+ end
47
+ type + PERM_BITS.map { |ch, mask| stat.mode & mask != 0 ? ch : "-" }.join
48
+ end
49
+
50
+ def self.generate_listing(dir)
51
+ entries = []
52
+ Dir.foreach(dir) do |name|
53
+ path = File.join(dir, name)
54
+ begin
55
+ stat = File.lstat(path)
56
+ perms = format_permissions(stat)
57
+ size = stat.size
58
+ mtime = stat.mtime.strftime("%Y-%m-%d %H:%M")
59
+ if stat.symlink?
60
+ begin
61
+ target = File.readlink(path)
62
+ rescue SystemCallError
63
+ target = "?"
64
+ end
65
+ display = "#{name} -> #{target}"
66
+ elsif stat.directory?
67
+ display = "#{name}/"
68
+ else
69
+ display = name
70
+ end
71
+ entries << {
72
+ name: name,
73
+ display: display,
74
+ perms: perms,
75
+ size: size,
76
+ mtime: mtime,
77
+ directory: stat.directory?
78
+ }
79
+ rescue SystemCallError => e
80
+ entries << {
81
+ name: name,
82
+ display: name,
83
+ perms: "??????????",
84
+ size: 0,
85
+ mtime: "????-??-?? ??:??",
86
+ directory: false,
87
+ error: e.message
88
+ }
89
+ end
90
+ end
91
+
92
+ entries.sort_by! { |e| [e[:directory] ? 0 : 1, e[:name].downcase] }
93
+
94
+ lines = [" #{dir}:\n"]
95
+ entries.each do |e|
96
+ line = " #{e[:perms]} #{e[:size].to_s.rjust(8)} #{e[:mtime]} #{e[:display]}\n"
97
+ lines << line
98
+ end
99
+ lines.join
100
+ end
101
+
102
+ def initialize(buffer)
103
+ super(buffer)
104
+ buffer.keymap = DIRED_MODE_MAP
105
+ end
106
+
107
+ define_local_command(:dired_move_to_filename,
108
+ doc: "Move point to the filename on the current line.") do
109
+ @buffer.beginning_of_line
110
+ if @buffer.looking_at?(/^[D ] \S+\s+\d+\s+[\d-]+\s+[\d:]+\s+/)
111
+ @buffer.forward_char(@buffer.match_string(0).length)
112
+ end
113
+ end
114
+
115
+ define_local_command(:dired_next_line, doc: "Move to next file line.") do
116
+ @buffer.next_line
117
+ dired_move_to_filename
118
+ end
119
+
120
+ define_local_command(:dired_previous_line, doc: "Move to previous file line.") do
121
+ first_file_line = @buffer.save_excursion {
122
+ @buffer.beginning_of_buffer
123
+ @buffer.current_line
124
+ }
125
+ if @buffer.current_line > first_file_line
126
+ @buffer.previous_line
127
+ end
128
+ dired_move_to_filename
129
+ end
130
+
131
+ define_local_command(:dired_up_directory, doc: "Go up to parent directory.") do
132
+ dir = @buffer[:dired_directory]
133
+ parent = File.dirname(dir)
134
+ dired(parent)
135
+ end
136
+
137
+ define_local_command(:dired_find_file, doc: "Visit file or directory at point.") do
138
+ name = current_file_name
139
+ return unless name
140
+ dir = @buffer[:dired_directory]
141
+ path = File.join(dir, name)
142
+ if File.directory?(path)
143
+ dired(path)
144
+ else
145
+ find_file(path)
146
+ end
147
+ end
148
+
149
+ define_local_command(:dired_find_file_other_window,
150
+ doc: "Visit file at point in other window.") do
151
+ name = current_file_name
152
+ return unless name
153
+ dir = @buffer[:dired_directory]
154
+ path = File.join(dir, name)
155
+ if Window.list.size == 1
156
+ split_window
157
+ end
158
+ other_window
159
+ if File.directory?(path)
160
+ dired(path)
161
+ else
162
+ find_file(path)
163
+ end
164
+ end
165
+
166
+ define_local_command(:dired_flag_file_deletion,
167
+ doc: "Flag file at point for deletion.") do
168
+ set_flag("D")
169
+ dired_next_line
170
+ end
171
+
172
+ define_local_command(:dired_unmark, doc: "Remove deletion flag from file at point.") do
173
+ set_flag(" ")
174
+ dired_next_line
175
+ end
176
+
177
+ define_local_command(:dired_unmark_all, doc: "Remove all deletion flags.") do
178
+ @buffer.save_excursion do
179
+ @buffer.beginning_of_buffer
180
+ while !@buffer.end_of_buffer?
181
+ set_flag(" ")
182
+ @buffer.next_line
183
+ end
184
+ end
185
+ end
186
+
187
+ define_local_command(:dired_do_flagged_delete,
188
+ doc: "Delete files flagged for deletion.") do
189
+ files = collect_flagged_files
190
+ return if files.empty?
191
+ list = files.map { |f| " #{f}" }.join("\n")
192
+ if yes_or_no?("Delete these files?")
193
+ files.each do |name|
194
+ next if name == "." || name == ".."
195
+ path = File.join(@buffer[:dired_directory], name)
196
+ begin
197
+ if File.directory?(path) && !File.symlink?(path)
198
+ FileUtils.rm_rf(path)
199
+ else
200
+ File.delete(path)
201
+ end
202
+ rescue SystemCallError => e
203
+ message("Error deleting #{name}: #{e.message}")
204
+ end
205
+ end
206
+ dired_revert
207
+ end
208
+ end
209
+
210
+ define_local_command(:dired_do_rename, doc: "Rename/move file at point.") do
211
+ name = current_file_name
212
+ return unless name
213
+ dir = @buffer[:dired_directory]
214
+ src = File.join(dir, name)
215
+ dest = read_file_name("Rename #{name} to: ", default: dir + "/")
216
+ dest = File.expand_path(dest, dir)
217
+ FileUtils.mv(src, dest)
218
+ dired_revert
219
+ end
220
+
221
+ define_local_command(:dired_do_copy, doc: "Copy file at point.") do
222
+ name = current_file_name
223
+ return unless name
224
+ dir = @buffer[:dired_directory]
225
+ src = File.join(dir, name)
226
+ dest = read_file_name("Copy #{name} to: ", default: dir + "/")
227
+ dest = File.expand_path(dest, dir)
228
+ FileUtils.cp_r(src, dest)
229
+ dired_revert
230
+ end
231
+
232
+ define_local_command(:dired_create_directory, doc: "Create a new directory.") do
233
+ dir = @buffer[:dired_directory]
234
+ name = read_from_minibuffer("Create directory: ", default: dir + "/")
235
+ name = File.expand_path(name, dir)
236
+ FileUtils.mkdir_p(name)
237
+ dired_revert
238
+ end
239
+
240
+ define_local_command(:dired_revert, doc: "Refresh directory listing.") do
241
+ dir = @buffer[:dired_directory]
242
+ saved_name = current_file_name
243
+ saved_line = @buffer.current_line
244
+ @buffer.read_only_edit do
245
+ @buffer.clear
246
+ @buffer.insert(DiredMode.generate_listing(dir))
247
+ @buffer.beginning_of_buffer
248
+ @buffer.forward_line
249
+ end
250
+ if saved_name
251
+ @buffer.beginning_of_buffer
252
+ found = false
253
+ while !@buffer.end_of_buffer?
254
+ if current_file_name == saved_name
255
+ found = true
256
+ break
257
+ end
258
+ @buffer.next_line
259
+ end
260
+ unless found
261
+ goto_line(saved_line)
262
+ @buffer.beginning_of_line
263
+ if @buffer.end_of_buffer? || current_file_name.nil?
264
+ @buffer.end_of_buffer
265
+ @buffer.previous_line while current_file_name.nil? && @buffer.current_line > 2
266
+ end
267
+ end
268
+ end
269
+ dired_move_to_filename
270
+ end
271
+
272
+ private
273
+
274
+ def current_file_name
275
+ @buffer.save_excursion do
276
+ @buffer.beginning_of_line
277
+ # Line format: " perms size date time display_name"
278
+ # or: "D perms size date time display_name"
279
+ if @buffer.looking_at?(/^[D ] (\S+)\s+\d+\s+[\d-]+\s+[\d:]+\s+(.+)$/)
280
+ perms = @buffer.match_string(1)
281
+ display = @buffer.match_string(2)
282
+ # Strip symlink target: "name -> target" -> "name" (only for symlinks)
283
+ display = display.sub(/ -> .+$/, "") if perms.start_with?("l")
284
+ # Strip trailing slash for directories: "name/" -> "name"
285
+ display = display.chomp("/")
286
+ display
287
+ end
288
+ end
289
+ end
290
+
291
+ def set_flag(char)
292
+ @buffer.read_only_edit do
293
+ @buffer.save_excursion do
294
+ @buffer.beginning_of_line
295
+ if @buffer.looking_at?(/^[D ]/)
296
+ @buffer.delete_char(1)
297
+ @buffer.insert(char)
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ def collect_flagged_files
304
+ files = []
305
+ @buffer.save_excursion do
306
+ @buffer.beginning_of_buffer
307
+ while !@buffer.end_of_buffer?
308
+ @buffer.beginning_of_line
309
+ if @buffer.looking_at?(/^D (\S+)\s+\d+\s+[\d-]+\s+[\d:]+\s+(.+)$/)
310
+ perms = @buffer.match_string(1)
311
+ display = @buffer.match_string(2)
312
+ display = display.sub(/ -> .+$/, "") if perms.start_with?("l")
313
+ display = display.chomp("/")
314
+ files << display
315
+ end
316
+ @buffer.next_line
317
+ end
318
+ end
319
+ files
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,46 @@
1
+ module Textbringer
2
+ class GamegridMode < Mode
3
+ @syntax_table = {}
4
+
5
+ def self.inherited(child)
6
+ super
7
+ child.instance_variable_set(:@syntax_table, {})
8
+ end
9
+
10
+ define_keymap :GAMEGRID_MODE_MAP
11
+ GAMEGRID_MODE_MAP.define_key("q", :gamegrid_quit_command)
12
+
13
+ def initialize(buffer)
14
+ super(buffer)
15
+ buffer.keymap = GAMEGRID_MODE_MAP
16
+ end
17
+
18
+
19
+ define_local_command(:gamegrid_init,
20
+ doc: "Initialize a gamegrid in the current buffer.") do |width, height, margin_left: 0|
21
+ grid = Gamegrid.new(width, height, margin_left: margin_left)
22
+ @buffer[:gamegrid] = grid
23
+ @buffer.read_only = true
24
+ @buffer[:highlight_override] = -> { grid.face_map }
25
+ grid
26
+ end
27
+
28
+ define_local_command(:gamegrid_refresh,
29
+ doc: "Refresh the gamegrid display.") do
30
+ grid = @buffer[:gamegrid]
31
+ return unless grid
32
+ @buffer.read_only_edit do
33
+ @buffer.clear
34
+ @buffer.insert(grid.render)
35
+ @buffer.beginning_of_buffer
36
+ end
37
+ end
38
+
39
+ define_local_command(:gamegrid_quit,
40
+ doc: "Quit the current game.") do
41
+ grid = @buffer[:gamegrid]
42
+ grid&.stop_timer
43
+ bury_buffer
44
+ end
45
+ end
46
+ end