textbringer 23 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fe4d897c03de987989c81559c4c7d354f9d2afd9ec8cc7ff819c5da0a2c1bb4
4
- data.tar.gz: c687cc3726ea8d9ccfd9efcb235590f9d4d447145cb9a0a6eaeb103b133f671f
3
+ metadata.gz: d7426dda13275f37439bdcc360a55971b34cc4b62916eeaee734b127ab9a8a88
4
+ data.tar.gz: fcc6391ac0697659a8b85005766186502731e8473928a5e5165d7c0a97d533f1
5
5
  SHA512:
6
- metadata.gz: c1885974de9ae18238d0a49f436e3300d5971c06b7d3b16fb9c21cfc36c42b15e1beb22ee4608a80252100e202cef01d0943fb8d68a3db6ec9884773aff02093
7
- data.tar.gz: 75bdc11e89da85249a54568e8d23ecd0979934143604918b292c01f4630a59386fd2965c7a260d9fc9b05c8a5f81695ecf23e9306fcb5df04535f97d65a927b7
6
+ metadata.gz: be219b93c87935b98780c5d41430dd3d0760b839e20977dc843fa88aa8253614fb0e68dc7f90e37c08de6a23722daec928294f9702114908707011c33c766dc6
7
+ data.tar.gz: 6f61d4ed9ce65308bf150f24c886e05a1743af8bd38913c80f750db77c64a90b543ae95f60774b9d8ae52953cfa91e5f36e62665e0830b9aab6f6d97925ab289
@@ -0,0 +1,31 @@
1
+ require "fileutils"
2
+
3
+ module Textbringer
4
+ module Commands
5
+ define_command(:dired, doc: "Open a directory browser.") do
6
+ |dir = read_file_name("Dired: ",
7
+ default: (Buffer.current.file_name ?
8
+ File.dirname(Buffer.current.file_name) : Dir.pwd) + "/")|
9
+ dir = File.expand_path(dir)
10
+ raise EditorError, "#{dir} is not a directory" unless File.directory?(dir)
11
+ buf_name = "*Dired: #{dir}*"
12
+ buffer = Buffer.find_or_new(buf_name, undo_limit: 0, read_only: true)
13
+ buffer[:dired_directory] = dir
14
+ buffer.apply_mode(DiredMode) unless buffer.mode.is_a?(DiredMode)
15
+ if buffer.bytesize == 0
16
+ buffer.read_only_edit do
17
+ buffer.insert(DiredMode.generate_listing(dir))
18
+ buffer.beginning_of_buffer
19
+ buffer.forward_line
20
+ until buffer.end_of_buffer?
21
+ buffer.beginning_of_line
22
+ break unless buffer.looking_at?(/^[D ] \S+\s+\d+\s+[\d-]+\s+[\d:]+\s+\.\.?\/$/)
23
+ buffer.forward_line
24
+ end
25
+ end
26
+ end
27
+ switch_to_buffer(buffer)
28
+ dired_move_to_filename_command
29
+ end
30
+ end
31
+ end
@@ -5,6 +5,10 @@ module Textbringer
5
5
  module Commands
6
6
  define_command(:find_file, doc: "Open or create a file.") do
7
7
  |file_name = read_file_name("Find file: ", default: (Buffer.current.file_name ? File.dirname(Buffer.current.file_name) : Dir.pwd) + "/")|
8
+ if File.directory?(file_name)
9
+ dired(file_name)
10
+ next
11
+ end
8
12
  config = EditorConfig.load_file(file_name)
9
13
  buffer = Buffer.find_file(file_name)
10
14
  if buffer.new_file?
@@ -0,0 +1,25 @@
1
+ module Textbringer
2
+ module Commands
3
+ define_command(:gamegrid_show_scores,
4
+ doc: "Display high scores for a game.") do
5
+ |game_name = read_from_minibuffer("Game name: ")|
6
+ scores = Gamegrid.load_scores(game_name)
7
+ buffer = Buffer.find_or_new("*Scores*", undo_limit: 0)
8
+ buffer.read_only_edit do
9
+ buffer.clear
10
+ buffer.insert("High Scores for #{game_name}\n")
11
+ buffer.insert("=" * 40 + "\n\n")
12
+ if scores.empty?
13
+ buffer.insert("No scores recorded.\n")
14
+ else
15
+ scores.each_with_index do |entry, i|
16
+ buffer.insert(
17
+ "#{i + 1}. #{entry[:score]} #{entry[:player]} #{entry[:time]}\n"
18
+ )
19
+ end
20
+ end
21
+ end
22
+ switch_to_buffer(buffer)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Textbringer
2
+ module Commands
3
+ define_command(:tetris, doc: "Play Tetris.") do
4
+ buffer = Buffer.find_or_new("*Tetris*", undo_limit: 0)
5
+ buffer.apply_mode(TetrisMode) unless buffer.mode.is_a?(TetrisMode)
6
+ switch_to_buffer(buffer)
7
+ buffer.mode.tetris_new_game
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Textbringer
2
+ Face.define :dired_directory, foreground: "magenta"
3
+ Face.define :dired_symlink, foreground: "cyan"
4
+ Face.define :dired_executable, foreground: "green"
5
+ Face.define :dired_flagged, foreground: "red"
6
+ end
@@ -0,0 +1,23 @@
1
+ module Textbringer
2
+ # Foreground color faces for gamegrid
3
+ Face.define :gamegrid_red, foreground: "red"
4
+ Face.define :gamegrid_green, foreground: "green"
5
+ Face.define :gamegrid_blue, foreground: "blue"
6
+ Face.define :gamegrid_yellow, foreground: "yellow"
7
+ Face.define :gamegrid_cyan, foreground: "cyan"
8
+ Face.define :gamegrid_magenta, foreground: "magenta"
9
+ Face.define :gamegrid_white, foreground: "white"
10
+
11
+ # Block faces (solid background) for Tetris-style solid blocks
12
+ Face.define :gamegrid_block_red, background: "red", foreground: "red"
13
+ Face.define :gamegrid_block_green, background: "green", foreground: "green"
14
+ Face.define :gamegrid_block_blue, background: "blue", foreground: "blue"
15
+ Face.define :gamegrid_block_yellow, background: "yellow", foreground: "yellow"
16
+ Face.define :gamegrid_block_cyan, background: "cyan", foreground: "cyan"
17
+ Face.define :gamegrid_block_magenta, background: "magenta", foreground: "magenta"
18
+ Face.define :gamegrid_block_white, background: "white", foreground: "white"
19
+
20
+ # Utility faces
21
+ Face.define :gamegrid_border, foreground: "white", bold: true
22
+ Face.define :gamegrid_score, foreground: "yellow", bold: true
23
+ end
@@ -0,0 +1,164 @@
1
+ require "fileutils"
2
+ require "time"
3
+
4
+ module Textbringer
5
+ class Gamegrid
6
+ attr_reader :width, :height
7
+ attr_accessor :score
8
+
9
+ def initialize(width, height, margin_left: 0)
10
+ @width = width
11
+ @height = height
12
+ @margin_left = margin_left
13
+ @grid = Array.new(height) { Array.new(width, 0) }
14
+ @faces = Array.new(height) { Array.new(width, nil) }
15
+ @display_options = {}
16
+ @score = 0
17
+ @timer_thread = nil
18
+ end
19
+
20
+ # Cell API
21
+
22
+ def set_cell(x, y, value)
23
+ check_bounds(x, y)
24
+ @grid[y][x] = value
25
+ end
26
+
27
+ def get_cell(x, y)
28
+ check_bounds(x, y)
29
+ @grid[y][x]
30
+ end
31
+
32
+ def set_face(x, y, face_name)
33
+ check_bounds(x, y)
34
+ @faces[y][x] = face_name
35
+ end
36
+
37
+ def get_face(x, y)
38
+ check_bounds(x, y)
39
+ @faces[y][x]
40
+ end
41
+
42
+ def set_display_option(value, char:, face: nil)
43
+ @display_options[value] = { char: char, face: face }
44
+ end
45
+
46
+ def fill(value)
47
+ @height.times do |y|
48
+ @width.times do |x|
49
+ @grid[y][x] = value
50
+ @faces[y][x] = nil
51
+ end
52
+ end
53
+ end
54
+
55
+ # Rendering
56
+
57
+ def render
58
+ margin = " " * @margin_left
59
+ @height.times.map { |y|
60
+ margin + @width.times.map { |x|
61
+ cell_char(@grid[y][x])
62
+ }.join
63
+ }.join("\n")
64
+ end
65
+
66
+ def face_map
67
+ highlight_on = {}
68
+ highlight_off = {}
69
+ offset = 0
70
+ @height.times do |y|
71
+ offset += @margin_left
72
+ @width.times do |x|
73
+ value = @grid[y][x]
74
+ # Priority: explicit set_face > display_option face > nil
75
+ face_name = @faces[y][x]
76
+ if face_name.nil?
77
+ opt = @display_options[value]
78
+ face_name = opt[:face] if opt
79
+ end
80
+ if face_name
81
+ face = Face[face_name]
82
+ if face
83
+ highlight_on[offset] = face
84
+ char_len = cell_char(value).bytesize
85
+ highlight_off[offset + char_len] = true
86
+ end
87
+ end
88
+ offset += cell_char(value).bytesize
89
+ end
90
+ offset += 1 # newline
91
+ end
92
+ [highlight_on, highlight_off]
93
+ end
94
+
95
+ # Timer
96
+
97
+ def start_timer(interval, &callback)
98
+ stop_timer
99
+ @timer_thread = Thread.new do
100
+ loop do
101
+ sleep(interval)
102
+ Controller.current.next_tick(&callback)
103
+ rescue ThreadError
104
+ break
105
+ end
106
+ end
107
+ end
108
+
109
+ def stop_timer
110
+ if @timer_thread
111
+ @timer_thread.kill
112
+ @timer_thread = nil
113
+ end
114
+ end
115
+
116
+ def timer_active?
117
+ !@timer_thread.nil? && @timer_thread.alive?
118
+ end
119
+
120
+ # Score persistence
121
+
122
+ def self.score_file_path(game_name)
123
+ safe_name = File.basename(game_name).gsub(/[^A-Za-z0-9_\-]/, "_")
124
+ File.expand_path("~/.textbringer/scores/#{safe_name}.scores")
125
+ end
126
+
127
+ def self.add_score(game_name, score, player_name: "anonymous")
128
+ path = score_file_path(game_name)
129
+ FileUtils.mkdir_p(File.dirname(path))
130
+ File.open(path, "a") do |f|
131
+ f.puts("#{score}\t#{player_name}\t#{Time.now.iso8601}")
132
+ end
133
+ end
134
+
135
+ def self.load_scores(game_name, limit: 10)
136
+ path = score_file_path(game_name)
137
+ return [] unless File.exist?(path)
138
+ lines = File.readlines(path, chomp: true)
139
+ lines.map { |line|
140
+ parts = line.split("\t")
141
+ { score: parts[0].to_i, player: parts[1], time: parts[2] }
142
+ }.sort_by { |h| -h[:score] }.first(limit)
143
+ end
144
+
145
+ private
146
+
147
+ def check_bounds(x, y)
148
+ if x < 0 || x >= @width || y < 0 || y >= @height
149
+ raise ArgumentError, "coordinates (#{x}, #{y}) out of bounds"
150
+ end
151
+ end
152
+
153
+ def cell_char(value)
154
+ opt = @display_options[value]
155
+ if opt
156
+ opt[:char]
157
+ elsif value.is_a?(String)
158
+ value
159
+ else
160
+ " "
161
+ end
162
+ end
163
+ end
164
+ 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
@@ -0,0 +1,316 @@
1
+ module Textbringer
2
+ class TetrisMode < GamegridMode
3
+ BOARD_WIDTH = 10
4
+ BOARD_HEIGHT = 20
5
+ BORDER_VALUE = 8 # cell value used for the 1-cell border around the board
6
+
7
+ PIECE_COLORS = {
8
+ 1 => :gamegrid_block_cyan,
9
+ 2 => :gamegrid_block_yellow,
10
+ 3 => :gamegrid_block_magenta,
11
+ 4 => :gamegrid_block_green,
12
+ 5 => :gamegrid_block_red,
13
+ 6 => :gamegrid_block_blue,
14
+ 7 => :gamegrid_block_white,
15
+ }.freeze
16
+
17
+ PIECE_NAMES = ["", "I", "O", "T", "S", "Z", "J", "L"].freeze
18
+
19
+ # Pieces[type][rotation][row][col] — 4×4 bounding box, 1-indexed types
20
+ PIECES = [
21
+ nil,
22
+ # 1: I (cyan)
23
+ [
24
+ [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
25
+ [[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],
26
+ [[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],
27
+ [[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]],
28
+ ],
29
+ # 2: O (yellow)
30
+ [
31
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
32
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
33
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
34
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
35
+ ],
36
+ # 3: T (magenta)
37
+ [
38
+ [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
39
+ [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
40
+ [[0,0,0,0],[1,1,1,0],[0,1,0,0],[0,0,0,0]],
41
+ [[0,1,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]],
42
+ ],
43
+ # 4: S (green)
44
+ [
45
+ [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]],
46
+ [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]],
47
+ [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]],
48
+ [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]],
49
+ ],
50
+ # 5: Z (red)
51
+ [
52
+ [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
53
+ [[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
54
+ [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
55
+ [[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
56
+ ],
57
+ # 6: J (blue)
58
+ [
59
+ [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
60
+ [[0,1,1,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]],
61
+ [[0,0,0,0],[1,1,1,0],[0,0,1,0],[0,0,0,0]],
62
+ [[0,1,0,0],[0,1,0,0],[1,1,0,0],[0,0,0,0]],
63
+ ],
64
+ # 7: L (white)
65
+ [
66
+ [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
67
+ [[0,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]],
68
+ [[0,0,0,0],[1,1,1,0],[1,0,0,0],[0,0,0,0]],
69
+ [[1,1,0,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]],
70
+ ],
71
+ ].freeze
72
+
73
+ define_keymap :TETRIS_MODE_MAP
74
+ TETRIS_MODE_MAP.define_key("q", :gamegrid_quit_command)
75
+ TETRIS_MODE_MAP.define_key("n", :tetris_new_game_command)
76
+ TETRIS_MODE_MAP.define_key(:left, :tetris_move_left_command)
77
+ TETRIS_MODE_MAP.define_key("h", :tetris_move_left_command)
78
+ TETRIS_MODE_MAP.define_key(:right, :tetris_move_right_command)
79
+ TETRIS_MODE_MAP.define_key("l", :tetris_move_right_command)
80
+ TETRIS_MODE_MAP.define_key(:down, :tetris_move_down_command)
81
+ TETRIS_MODE_MAP.define_key("j", :tetris_move_down_command)
82
+ TETRIS_MODE_MAP.define_key(:up, :tetris_rotate_command)
83
+ TETRIS_MODE_MAP.define_key("k", :tetris_rotate_command)
84
+ TETRIS_MODE_MAP.define_key(" ", :tetris_drop_command)
85
+ TETRIS_MODE_MAP.define_key("p", :tetris_pause_command)
86
+
87
+ attr_reader :score, :level, :lines_cleared,
88
+ :piece_type, :piece_rot, :piece_x, :piece_y,
89
+ :next_type, :game_over, :paused
90
+
91
+ def initialize(buffer)
92
+ super
93
+ buffer.keymap = TETRIS_MODE_MAP
94
+ @game_over = true
95
+ @paused = false
96
+ @grid = nil
97
+ end
98
+
99
+ define_local_command(:tetris_new_game, doc: "Start a new Tetris game.") do
100
+ @grid&.stop_timer
101
+ @grid = gamegrid_init(BOARD_WIDTH + 2, BOARD_HEIGHT + 2, margin_left: 2)
102
+ @grid.set_display_option(0, char: " ")
103
+ @grid.set_display_option(BORDER_VALUE, char: "[]", face: :gamegrid_border)
104
+ PIECE_COLORS.each { |v, f| @grid.set_display_option(v, char: "[]", face: f) }
105
+
106
+ @board = Array.new(BOARD_HEIGHT) { Array.new(BOARD_WIDTH, 0) }
107
+ @score = 0
108
+ @level = 1
109
+ @lines_cleared = 0
110
+ @game_over = false
111
+ @paused = false
112
+ @next_type = random_piece_type
113
+
114
+ spawn_piece
115
+ start_game_timer unless @game_over
116
+ render_board
117
+ end
118
+
119
+ define_local_command(:tetris_move_left, doc: "Move piece left.") do
120
+ return unless active?
121
+ if valid_position?(@piece_x - 1, @piece_y, @piece_type, @piece_rot)
122
+ @piece_x -= 1
123
+ render_board
124
+ end
125
+ end
126
+
127
+ define_local_command(:tetris_move_right, doc: "Move piece right.") do
128
+ return unless active?
129
+ if valid_position?(@piece_x + 1, @piece_y, @piece_type, @piece_rot)
130
+ @piece_x += 1
131
+ render_board
132
+ end
133
+ end
134
+
135
+ define_local_command(:tetris_move_down, doc: "Soft-drop current piece.") do
136
+ return unless active?
137
+ step_down
138
+ end
139
+
140
+ define_local_command(:tetris_rotate, doc: "Rotate current piece clockwise.") do
141
+ return unless active?
142
+ new_rot = (@piece_rot + 1) % 4
143
+ # Try basic rotation then simple wall-kicks (±1, ±2 columns)
144
+ [0, -1, 1, -2, 2].each do |kick|
145
+ if valid_position?(@piece_x + kick, @piece_y, @piece_type, new_rot)
146
+ @piece_x += kick
147
+ @piece_rot = new_rot
148
+ break
149
+ end
150
+ end
151
+ render_board
152
+ end
153
+
154
+ define_local_command(:tetris_drop, doc: "Hard-drop current piece.") do
155
+ return unless active?
156
+ while valid_position?(@piece_x, @piece_y + 1, @piece_type, @piece_rot)
157
+ @piece_y += 1
158
+ @score += 2
159
+ end
160
+ lock_and_continue
161
+ render_board
162
+ end
163
+
164
+ define_local_command(:tetris_pause, doc: "Toggle game pause.") do
165
+ return if @game_over || !@grid
166
+ @paused = !@paused
167
+ if @paused
168
+ @grid.stop_timer
169
+ else
170
+ start_game_timer
171
+ end
172
+ render_board
173
+ end
174
+
175
+ private
176
+
177
+ def active?
178
+ !@game_over && !@paused && @grid
179
+ end
180
+
181
+ def random_piece_type
182
+ rand(1..7)
183
+ end
184
+
185
+ def spawn_piece
186
+ @piece_type = @next_type
187
+ @next_type = random_piece_type
188
+ @piece_rot = 0
189
+ @piece_x = BOARD_WIDTH / 2 - 2
190
+ @piece_y = 0
191
+ @game_over = !valid_position?(@piece_x, @piece_y, @piece_type, @piece_rot)
192
+ end
193
+
194
+ def valid_position?(x, y, type, rot)
195
+ PIECES[type][rot].each_with_index do |row, row_i|
196
+ row.each_with_index do |cell, col_i|
197
+ next if cell == 0
198
+ bx = x + col_i
199
+ by = y + row_i
200
+ return false if bx < 0 || bx >= BOARD_WIDTH
201
+ return false if by >= BOARD_HEIGHT
202
+ next if by < 0 # piece can start partially above the board
203
+ return false if @board[by][bx] != 0
204
+ end
205
+ end
206
+ true
207
+ end
208
+
209
+ def lock_piece
210
+ PIECES[@piece_type][@piece_rot].each_with_index do |row, row_i|
211
+ row.each_with_index do |cell, col_i|
212
+ next if cell == 0
213
+ by = @piece_y + row_i
214
+ bx = @piece_x + col_i
215
+ next if by < 0 || by >= BOARD_HEIGHT || bx < 0 || bx >= BOARD_WIDTH
216
+ @board[by][bx] = @piece_type
217
+ end
218
+ end
219
+ end
220
+
221
+ def clear_lines
222
+ full = (0...BOARD_HEIGHT).select { |y| @board[y].all? { |c| c != 0 } }
223
+ return 0 if full.empty?
224
+ full.reverse_each { |y| @board.delete_at(y) }
225
+ full.size.times { @board.unshift(Array.new(BOARD_WIDTH, 0)) }
226
+ n = full.size
227
+ @score += [0, 100, 300, 500, 800][n].to_i * @level
228
+ @lines_cleared += n
229
+ @level = @lines_cleared / 10 + 1
230
+ n
231
+ end
232
+
233
+ def step_down
234
+ if valid_position?(@piece_x, @piece_y + 1, @piece_type, @piece_rot)
235
+ @piece_y += 1
236
+ else
237
+ lock_and_continue
238
+ end
239
+ render_board
240
+ end
241
+
242
+ def lock_and_continue
243
+ lock_piece
244
+ clear_lines
245
+ spawn_piece
246
+ if @game_over
247
+ @grid.stop_timer
248
+ else
249
+ start_game_timer
250
+ end
251
+ end
252
+
253
+ def start_game_timer
254
+ @grid.start_timer(timer_interval) { step_down }
255
+ end
256
+
257
+ def timer_interval
258
+ # Level 1 = 1.0 s, each level adds 0.1 s speed, floor at 0.1 s
259
+ [1.0 - (@level - 1) * 0.1, 0.1].max
260
+ end
261
+
262
+ def update_grid
263
+ # Border: top and bottom rows
264
+ (BOARD_WIDTH + 2).times do |x|
265
+ @grid.set_cell(x, 0, BORDER_VALUE)
266
+ @grid.set_cell(x, BOARD_HEIGHT + 1, BORDER_VALUE)
267
+ end
268
+ # Border: left and right columns (inner rows only)
269
+ BOARD_HEIGHT.times do |y|
270
+ @grid.set_cell(0, y + 1, BORDER_VALUE)
271
+ @grid.set_cell(BOARD_WIDTH + 1, y + 1, BORDER_VALUE)
272
+ end
273
+ # Board content, offset by (1, 1)
274
+ BOARD_HEIGHT.times do |y|
275
+ BOARD_WIDTH.times do |x|
276
+ @grid.set_cell(x + 1, y + 1, @board[y][x])
277
+ end
278
+ end
279
+ return if @game_over
280
+ # Current piece, offset by (1, 1)
281
+ PIECES[@piece_type][@piece_rot].each_with_index do |row, row_i|
282
+ row.each_with_index do |cell, col_i|
283
+ next if cell == 0
284
+ bx = @piece_x + col_i + 1
285
+ by = @piece_y + row_i + 1
286
+ next if bx < 1 || bx > BOARD_WIDTH || by < 1 || by > BOARD_HEIGHT
287
+ @grid.set_cell(bx, by, @piece_type)
288
+ end
289
+ end
290
+ end
291
+
292
+ def render_board
293
+ update_grid
294
+ @buffer.read_only_edit do
295
+ @buffer.clear
296
+ @buffer.insert(@grid.render)
297
+ @buffer.insert("\n")
298
+ @buffer.insert(status_text)
299
+ @buffer.beginning_of_buffer
300
+ end
301
+ end
302
+
303
+ def status_text
304
+ if @game_over
305
+ "GAME OVER Score: #{@score} Level: #{@level} Lines: #{@lines_cleared}" \
306
+ " [n]ew game [q]uit\n"
307
+ elsif @paused
308
+ "PAUSED Score: #{@score} Level: #{@level} Lines: #{@lines_cleared}" \
309
+ " [p] resume\n"
310
+ else
311
+ "Score: #{@score} Level: #{@level} Lines: #{@lines_cleared}" \
312
+ " Next: #{PIECE_NAMES[@next_type]}\n"
313
+ end
314
+ end
315
+ end
316
+ end
@@ -1,3 +1,3 @@
1
1
  module Textbringer
2
- VERSION = "23"
2
+ VERSION = "24"
3
3
  end
@@ -141,6 +141,8 @@ module Textbringer
141
141
  require_relative "faces/basic"
142
142
  require_relative "faces/programming"
143
143
  require_relative "faces/completion"
144
+ require_relative "faces/dired"
145
+ require_relative "faces/gamegrid"
144
146
  end
145
147
 
146
148
  def self.start
@@ -402,6 +404,10 @@ module Textbringer
402
404
  def highlight
403
405
  @highlight_on = {}
404
406
  @highlight_off = {}
407
+ if (override = @buffer[:highlight_override])
408
+ @highlight_on, @highlight_off = override.call
409
+ return
410
+ end
405
411
  return if !@@has_colors || !CONFIG[:syntax_highlight] || @buffer.binary?
406
412
  syntax_table = @buffer.mode.syntax_table || DEFAULT_SYNTAX_TABLE
407
413
  if @buffer.bytesize < CONFIG[:highlight_buffer_size_limit]
data/lib/textbringer.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "textbringer/face"
12
12
  require_relative "textbringer/completion_popup"
13
13
  require_relative "textbringer/lsp/client"
14
14
  require_relative "textbringer/lsp/server_registry"
15
+ require_relative "textbringer/gamegrid"
15
16
  require_relative "textbringer/commands"
16
17
  require_relative "textbringer/commands/buffers"
17
18
  require_relative "textbringer/commands/windows"
@@ -33,6 +34,9 @@ require_relative "textbringer/commands/ucs_normalize"
33
34
  require_relative "textbringer/commands/help"
34
35
  require_relative "textbringer/commands/completion"
35
36
  require_relative "textbringer/commands/lsp"
37
+ require_relative "textbringer/commands/dired"
38
+ require_relative "textbringer/commands/gamegrid"
39
+ require_relative "textbringer/commands/tetris"
36
40
  require_relative "textbringer/mode"
37
41
  require_relative "textbringer/modes/fundamental_mode"
38
42
  require_relative "textbringer/modes/programming_mode"
@@ -42,6 +46,9 @@ require_relative "textbringer/modes/backtrace_mode"
42
46
  require_relative "textbringer/modes/completion_list_mode"
43
47
  require_relative "textbringer/modes/buffer_list_mode"
44
48
  require_relative "textbringer/modes/help_mode"
49
+ require_relative "textbringer/modes/dired_mode"
50
+ require_relative "textbringer/modes/gamegrid_mode"
51
+ require_relative "textbringer/modes/tetris_mode"
45
52
  require_relative "textbringer/minor_mode"
46
53
  require_relative "textbringer/global_minor_mode"
47
54
  require_relative "textbringer/modes/overwrite_mode"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textbringer
3
3
  version: !ruby/object:Gem::Version
4
- version: '23'
4
+ version: '24'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -363,8 +363,10 @@ files:
363
363
  - lib/textbringer/commands/completion.rb
364
364
  - lib/textbringer/commands/ctags.rb
365
365
  - lib/textbringer/commands/dabbrev.rb
366
+ - lib/textbringer/commands/dired.rb
366
367
  - lib/textbringer/commands/files.rb
367
368
  - lib/textbringer/commands/fill.rb
369
+ - lib/textbringer/commands/gamegrid.rb
368
370
  - lib/textbringer/commands/help.rb
369
371
  - lib/textbringer/commands/input_method.rb
370
372
  - lib/textbringer/commands/isearch.rb
@@ -376,6 +378,7 @@ files:
376
378
  - lib/textbringer/commands/register.rb
377
379
  - lib/textbringer/commands/replace.rb
378
380
  - lib/textbringer/commands/server.rb
381
+ - lib/textbringer/commands/tetris.rb
379
382
  - lib/textbringer/commands/ucs_normalize.rb
380
383
  - lib/textbringer/commands/windows.rb
381
384
  - lib/textbringer/completion_popup.rb
@@ -386,8 +389,11 @@ files:
386
389
  - lib/textbringer/face.rb
387
390
  - lib/textbringer/faces/basic.rb
388
391
  - lib/textbringer/faces/completion.rb
392
+ - lib/textbringer/faces/dired.rb
393
+ - lib/textbringer/faces/gamegrid.rb
389
394
  - lib/textbringer/faces/programming.rb
390
395
  - lib/textbringer/floating_window.rb
396
+ - lib/textbringer/gamegrid.rb
391
397
  - lib/textbringer/global_minor_mode.rb
392
398
  - lib/textbringer/input_method.rb
393
399
  - lib/textbringer/input_methods/hangul_input_method.rb
@@ -404,11 +410,14 @@ files:
404
410
  - lib/textbringer/modes/buffer_list_mode.rb
405
411
  - lib/textbringer/modes/c_mode.rb
406
412
  - lib/textbringer/modes/completion_list_mode.rb
413
+ - lib/textbringer/modes/dired_mode.rb
407
414
  - lib/textbringer/modes/fundamental_mode.rb
415
+ - lib/textbringer/modes/gamegrid_mode.rb
408
416
  - lib/textbringer/modes/help_mode.rb
409
417
  - lib/textbringer/modes/overwrite_mode.rb
410
418
  - lib/textbringer/modes/programming_mode.rb
411
419
  - lib/textbringer/modes/ruby_mode.rb
420
+ - lib/textbringer/modes/tetris_mode.rb
412
421
  - lib/textbringer/modes/transient_mark_mode.rb
413
422
  - lib/textbringer/plugin.rb
414
423
  - lib/textbringer/ring.rb
@@ -488,7 +497,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
488
497
  - !ruby/object:Gem::Version
489
498
  version: '0'
490
499
  requirements: []
491
- rubygems_version: 4.0.3
500
+ rubygems_version: 4.0.6
492
501
  specification_version: 4
493
502
  summary: An Emacs-like text editor
494
503
  test_files: []