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.
@@ -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
@@ -18,8 +18,8 @@ module Textbringer
18
18
  :backward_char,
19
19
  :forward_word,
20
20
  :backward_word,
21
- :scroll_up_command,
22
- :scroll_down_command,
21
+ :scroll_up,
22
+ :scroll_down,
23
23
  :beginning_of_buffer,
24
24
  :end_of_buffer,
25
25
  # Search commands
@@ -1,3 +1,3 @@
1
1
  module Textbringer
2
- VERSION = "19"
2
+ VERSION = "24"
3
3
  end
@@ -1,6 +1,16 @@
1
1
  require "curses"
2
2
  require_relative "window/fallback_characters"
3
3
 
4
+ unless Curses::Window.method_defined?(:attr_set)
5
+ using Module.new {
6
+ refine Curses::Window do
7
+ def attr_set(attrs, pair)
8
+ attrset(attrs | Curses.color_pair(pair))
9
+ end
10
+ end
11
+ }
12
+ end
13
+
4
14
  module Textbringer
5
15
  class Window
6
16
  Cursor = Struct.new(:y, :x)
@@ -131,6 +141,8 @@ module Textbringer
131
141
  require_relative "faces/basic"
132
142
  require_relative "faces/programming"
133
143
  require_relative "faces/completion"
144
+ require_relative "faces/dired"
145
+ require_relative "faces/gamegrid"
134
146
  end
135
147
 
136
148
  def self.start
@@ -146,6 +158,10 @@ module Textbringer
146
158
  Curses.start_color
147
159
  Curses.use_default_colors
148
160
  load_faces
161
+ else
162
+ Face.define :mode_line, reverse: true
163
+ Face.define :region, reverse: true
164
+ Face.define :isearch, underline: true
149
165
  end
150
166
  begin
151
167
  window =
@@ -161,6 +177,7 @@ module Textbringer
161
177
  @@started = true
162
178
  yield
163
179
  ensure
180
+ Buffer.current&.input_method&.on_deactivate
164
181
  @@list.each do |win|
165
182
  win.close
166
183
  end
@@ -261,7 +278,7 @@ module Textbringer
261
278
  @cursor = Cursor.new(0, 0)
262
279
  @in_region = false
263
280
  @in_isearch = false
264
- @current_highlight_attrs = 0
281
+ @current_hl_face = nil
265
282
  end
266
283
 
267
284
  def echo_area?
@@ -387,6 +404,10 @@ module Textbringer
387
404
  def highlight
388
405
  @highlight_on = {}
389
406
  @highlight_off = {}
407
+ if (override = @buffer[:highlight_override])
408
+ @highlight_on, @highlight_off = override.call
409
+ return
410
+ end
390
411
  return if !@@has_colors || !CONFIG[:syntax_highlight] || @buffer.binary?
391
412
  syntax_table = @buffer.mode.syntax_table || DEFAULT_SYNTAX_TABLE
392
413
  if @buffer.bytesize < CONFIG[:highlight_buffer_size_limit]
@@ -410,10 +431,10 @@ module Textbringer
410
431
  b = @buffer.point
411
432
  end
412
433
  name = names.find { |n| $~[n] }
413
- attributes = Face[name]&.attributes
414
- if attributes
415
- @highlight_on[b] = attributes
416
- @highlight_off[e] = attributes
434
+ face = Face[name]
435
+ if face
436
+ @highlight_on[b] = face
437
+ @highlight_off[e] = true
417
438
  end
418
439
  end
419
440
  end
@@ -434,45 +455,30 @@ module Textbringer
434
455
  highlight
435
456
  @window.erase
436
457
  @window.setpos(0, 0)
437
- @window.attrset(0)
438
458
  @in_region = false
439
459
  @in_isearch = false
440
- @current_highlight_attrs = 0
460
+ @current_hl_face = nil
441
461
  if current_or_minibuffer_selected? && @buffer.visible_mark &&
442
462
  @buffer.point_after_mark?(@buffer.visible_mark)
443
- @window.attron(region_attr)
444
463
  @in_region = true
445
464
  end
446
465
  if current_or_minibuffer_selected? && @buffer.isearch_mark &&
447
466
  @buffer.point_after_mark?(@buffer.isearch_mark)
448
- # If already in region, switch to isearch (priority)
449
- if @in_region
450
- @window.attroff(region_attr)
451
- end
452
- @window.attron(isearch_attr)
467
+ @in_region = false
453
468
  @in_isearch = true
454
469
  end
470
+ apply_window_attrs(@window)
455
471
  while !@buffer.end_of_buffer?
456
472
  cury = @window.cury
457
473
  curx = @window.curx
458
474
  update_cursor_and_attr(point, cury, curx)
459
- if attrs = @highlight_off[@buffer.point]
460
- if @in_region || @in_isearch
461
- # In region: only turn off non-color attributes (bold, underline, etc.)
462
- @window.attroff(attrs & ~Curses::A_COLOR)
463
- else
464
- @window.attroff(attrs)
465
- end
466
- @current_highlight_attrs = 0
475
+ if @highlight_off[@buffer.point]
476
+ @current_hl_face = nil
477
+ apply_window_attrs(@window)
467
478
  end
468
- if attrs = @highlight_on[@buffer.point]
469
- if @in_region || @in_isearch
470
- # In region: only turn on non-color attributes (preserve region background)
471
- @window.attron(attrs & ~Curses::A_COLOR)
472
- else
473
- @window.attron(attrs)
474
- end
475
- @current_highlight_attrs = attrs
479
+ if face = @highlight_on[@buffer.point]
480
+ @current_hl_face = face
481
+ apply_window_attrs(@window)
476
482
  end
477
483
  c = @buffer.char_after
478
484
  if c == "\n"
@@ -517,12 +523,7 @@ module Textbringer
517
523
  break if newx == columns && cury == lines - 2
518
524
  @buffer.forward_char
519
525
  end
520
- if current_or_minibuffer_selected? && @buffer.isearch_mark
521
- @window.attroff(isearch_attr)
522
- end
523
- if current_or_minibuffer_selected? && @buffer.visible_mark
524
- @window.attroff(region_attr)
525
- end
526
+ apply_window_attrs(@window)
526
527
  @buffer.mark_to_point(@bottom_of_window)
527
528
  if @buffer.point_at_mark?(point)
528
529
  @cursor.y = @window.cury
@@ -712,8 +713,8 @@ module Textbringer
712
713
  def redisplay_mode_line
713
714
  @mode_line.erase
714
715
  @mode_line.setpos(0, 0)
715
- attrs = @@has_colors ? Face[:mode_line].attributes : Curses::A_REVERSE
716
- @mode_line.attrset(attrs)
716
+ face = Face[:mode_line]
717
+ @mode_line.attr_set(face.text_attrs, face.color_pair)
717
718
  @mode_line.addstr("#{@buffer.input_method_status} #{@buffer.name} ")
718
719
  @mode_line.addstr("[+]") if @buffer.modified?
719
720
  @mode_line.addstr("[RO]") if @buffer.read_only?
@@ -731,7 +732,7 @@ module Textbringer
731
732
  @mode_line.addstr(" #{line},#{column}")
732
733
  @mode_line.addstr(" (#{@buffer.mode_names.join(' ')})")
733
734
  @mode_line.addstr(" " * (columns - @mode_line.curx))
734
- @mode_line.attrset(0)
735
+ @mode_line.attr_set(0, 0)
735
736
  @mode_line.noutrefresh
736
737
  end
737
738
 
@@ -746,45 +747,28 @@ module Textbringer
746
747
  end
747
748
 
748
749
  def update_cursor_and_attr(point, cury, curx)
750
+ changed = false
749
751
  if @buffer.point_at_mark?(point)
750
752
  @cursor.y = cury
751
753
  @cursor.x = curx
752
754
  # Handle visible mark transitions
753
755
  if current_or_minibuffer_selected? && @buffer.visible_mark
754
756
  if @buffer.point_after_mark?(@buffer.visible_mark)
755
- unless @in_isearch
756
- @window.attroff(region_attr)
757
- # Restore syntax highlighting colors after exiting region
758
- if @current_highlight_attrs != 0
759
- @window.attron(@current_highlight_attrs)
760
- end
761
- end
762
757
  @in_region = false
758
+ changed = true
763
759
  elsif @buffer.point_before_mark?(@buffer.visible_mark)
764
- unless @in_isearch
765
- @window.attron(region_attr)
766
- end
767
760
  @in_region = true
761
+ changed = true
768
762
  end
769
763
  end
770
764
  # Handle isearch mark transitions
771
765
  if current_or_minibuffer_selected? && @buffer.isearch_mark
772
766
  if @buffer.point_after_mark?(@buffer.isearch_mark)
773
- @window.attroff(isearch_attr)
774
767
  @in_isearch = false
775
- # If we were covering a region, restore it
776
- if @in_region
777
- @window.attron(region_attr)
778
- elsif @current_highlight_attrs != 0
779
- @window.attron(@current_highlight_attrs)
780
- end
768
+ changed = true
781
769
  elsif @buffer.point_before_mark?(@buffer.isearch_mark)
782
- # Entering isearch - turn off region if active
783
- if @in_region
784
- @window.attroff(region_attr)
785
- end
786
- @window.attron(isearch_attr)
787
770
  @in_isearch = true
771
+ changed = true
788
772
  end
789
773
  end
790
774
  end
@@ -792,42 +776,25 @@ module Textbringer
792
776
  if current_or_minibuffer_selected? && @buffer.isearch_mark &&
793
777
  @buffer.point_at_mark?(@buffer.isearch_mark)
794
778
  if @buffer.point_after_mark?(point)
795
- @window.attroff(isearch_attr)
796
779
  @in_isearch = false
797
- # If we have a region underneath, restore it
798
- if @in_region
799
- @window.attron(region_attr)
800
- elsif @current_highlight_attrs != 0
801
- @window.attron(@current_highlight_attrs)
802
- end
780
+ changed = true
803
781
  elsif @buffer.point_before_mark?(point)
804
- # Entering isearch - turn off region if active
805
- if @in_region
806
- @window.attroff(region_attr)
807
- end
808
- @window.attron(isearch_attr)
809
782
  @in_isearch = true
783
+ changed = true
810
784
  end
811
785
  end
812
786
  # Handle transitions when point crosses visible mark
813
787
  if current_or_minibuffer_selected? && @buffer.visible_mark &&
814
788
  @buffer.point_at_mark?(@buffer.visible_mark)
815
789
  if @buffer.point_after_mark?(point)
816
- unless @in_isearch
817
- @window.attroff(region_attr)
818
- # Restore syntax highlighting colors after exiting region
819
- if @current_highlight_attrs != 0
820
- @window.attron(@current_highlight_attrs)
821
- end
822
- end
823
790
  @in_region = false
791
+ changed = true
824
792
  elsif @buffer.point_before_mark?(point)
825
- unless @in_isearch
826
- @window.attron(region_attr)
827
- end
828
793
  @in_region = true
794
+ changed = true
829
795
  end
830
796
  end
797
+ apply_window_attrs(@window) if changed
831
798
  end
832
799
 
833
800
  def compose_character(point, cury, curx, c)
@@ -1004,12 +971,28 @@ module Textbringer
1004
971
  end
1005
972
  end
1006
973
 
1007
- def region_attr
1008
- @@has_colors ? Face[:region].attributes : Curses::A_REVERSE
974
+ def apply_face_attrs(win, face)
975
+ win.attr_set(face&.text_attrs || 0, face&.color_pair || 0)
1009
976
  end
1010
977
 
1011
- def isearch_attr
1012
- @@has_colors ? Face[:isearch].attributes : Curses::A_UNDERLINE
978
+ def apply_window_attrs(win)
979
+ if @in_isearch
980
+ face = Face[:isearch]
981
+ elsif @in_region
982
+ face = Face[:region]
983
+ else
984
+ face = @current_hl_face
985
+ end
986
+
987
+ if face
988
+ text_attrs = face.text_attrs
989
+ if (@in_isearch || @in_region) && @current_hl_face
990
+ text_attrs |= @current_hl_face.text_attrs
991
+ end
992
+ win.attr_set(text_attrs, face.color_pair)
993
+ else
994
+ win.attr_set(0, 0)
995
+ end
1013
996
  end
1014
997
  end
1015
998
 
@@ -1057,10 +1040,10 @@ module Textbringer
1057
1040
  @window.addstr(@buffer.input_method_status)
1058
1041
  end
1059
1042
  @window.setpos(0, 0)
1060
- @window.attrset(0)
1043
+ @window.attr_set(0, 0)
1061
1044
  @in_region = false
1062
1045
  @in_isearch = false
1063
- @current_highlight_attrs = 0
1046
+ @current_hl_face = nil
1064
1047
  if @message
1065
1048
  @window.addstr(escape(@message))
1066
1049
  else
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"