textbringer 23 → 25
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 +4 -4
- data/exe/txtb +1 -0
- data/lib/textbringer/buffer.rb +19 -11
- data/lib/textbringer/commands/dired.rb +31 -0
- data/lib/textbringer/commands/files.rb +4 -0
- data/lib/textbringer/commands/gamegrid.rb +25 -0
- data/lib/textbringer/commands/misc.rb +19 -0
- data/lib/textbringer/commands/tetris.rb +10 -0
- data/lib/textbringer/commands/windows.rb +14 -0
- data/lib/textbringer/config.rb +2 -1
- data/lib/textbringer/face.rb +71 -15
- data/lib/textbringer/faces/gamegrid.rb +23 -0
- data/lib/textbringer/gamegrid.rb +160 -0
- data/lib/textbringer/highlight_context.rb +21 -0
- data/lib/textbringer/keymap.rb +1 -0
- data/lib/textbringer/mode.rb +25 -0
- data/lib/textbringer/modes/dired_mode.rb +322 -0
- data/lib/textbringer/modes/gamegrid_mode.rb +51 -0
- data/lib/textbringer/modes/ruby_mode.rb +298 -181
- data/lib/textbringer/modes/tetris_mode.rb +316 -0
- data/lib/textbringer/theme.rb +180 -0
- data/lib/textbringer/themes/catppuccin.rb +105 -0
- data/lib/textbringer/themes/github.rb +89 -0
- data/lib/textbringer/themes/gruvbox.rb +84 -0
- data/lib/textbringer/themes/molokai.rb +67 -0
- data/lib/textbringer/themes/sonokai.rb +63 -0
- data/lib/textbringer/themes/tokyonight.rb +70 -0
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer/window.rb +29 -36
- data/lib/textbringer.rb +9 -0
- data/textbringer.gemspec +1 -0
- metadata +32 -5
- data/lib/textbringer/faces/basic.rb +0 -8
- data/lib/textbringer/faces/completion.rb +0 -4
- data/lib/textbringer/faces/programming.rb +0 -6
|
@@ -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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
class Theme
|
|
3
|
+
DEFAULT_THEME = "github"
|
|
4
|
+
|
|
5
|
+
class Palette
|
|
6
|
+
def initialize
|
|
7
|
+
@colors = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def color(name, hex: nil, ansi: nil)
|
|
11
|
+
@colors[name] = { hex: hex, ansi: ansi }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolve(name, tier)
|
|
15
|
+
c = @colors[name]
|
|
16
|
+
return nil unless c
|
|
17
|
+
tier == :ansi ? c[:ansi] : c[:hex]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@@themes = {}
|
|
22
|
+
@@current = nil
|
|
23
|
+
@@background_mode = nil
|
|
24
|
+
|
|
25
|
+
def self.define(name, &block)
|
|
26
|
+
theme = new(name)
|
|
27
|
+
block.call(theme)
|
|
28
|
+
@@themes[name] = theme
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.[](name)
|
|
32
|
+
@@themes[name]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.current
|
|
36
|
+
@@current
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.load(name)
|
|
40
|
+
user_path = File.expand_path("~/.textbringer/themes/#{name}.rb")
|
|
41
|
+
if File.exist?(user_path)
|
|
42
|
+
Kernel.load(user_path)
|
|
43
|
+
else
|
|
44
|
+
require "textbringer/themes/#{name}"
|
|
45
|
+
end
|
|
46
|
+
theme = @@themes[name]
|
|
47
|
+
raise EditorError, "Theme '#{name}' not found" unless theme
|
|
48
|
+
theme.activate
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.load_default
|
|
52
|
+
return if @@current
|
|
53
|
+
load(DEFAULT_THEME)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.background_mode
|
|
57
|
+
mode = CONFIG[:background_mode]
|
|
58
|
+
return mode if mode == :dark || mode == :light
|
|
59
|
+
@@background_mode || :dark
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.detect_background
|
|
63
|
+
@@background_mode = detect_background_via_osc11 ||
|
|
64
|
+
detect_background_via_colorfgbg ||
|
|
65
|
+
:dark
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.color_tier
|
|
69
|
+
Window.colors >= 256 ? :hex : :ansi
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def initialize(name)
|
|
73
|
+
@name = name
|
|
74
|
+
@palettes = {}
|
|
75
|
+
@face_definitions = []
|
|
76
|
+
@default_colors = nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attr_reader :name
|
|
80
|
+
|
|
81
|
+
def palette(mode, &block)
|
|
82
|
+
p = Palette.new
|
|
83
|
+
block.call(p)
|
|
84
|
+
@palettes[mode] = p
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def face(name, **attrs)
|
|
88
|
+
@face_definitions << [name, attrs]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_colors(foreground:, background:)
|
|
92
|
+
@default_colors = { foreground: foreground, background: background }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def activate
|
|
96
|
+
mode = self.class.background_mode
|
|
97
|
+
tier = self.class.color_tier
|
|
98
|
+
palette = @palettes[mode] || @palettes[:dark] || Palette.new
|
|
99
|
+
@face_definitions.each do |face_name, attrs|
|
|
100
|
+
resolved = {}
|
|
101
|
+
[:foreground, :background].each do |key|
|
|
102
|
+
val = attrs[key]
|
|
103
|
+
if val.is_a?(Symbol)
|
|
104
|
+
color = palette.resolve(val, tier)
|
|
105
|
+
if color
|
|
106
|
+
resolved[key] = color
|
|
107
|
+
else
|
|
108
|
+
raise EditorError,
|
|
109
|
+
"Unknown palette color :#{val} for #{key} in face #{face_name}"
|
|
110
|
+
end
|
|
111
|
+
elsif val.is_a?(String)
|
|
112
|
+
resolved[key] = val
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
[:bold, :underline, :reverse, :inherit].each do |key|
|
|
116
|
+
resolved[key] = attrs[key] if attrs.key?(key)
|
|
117
|
+
end
|
|
118
|
+
Face.define(face_name, **resolved)
|
|
119
|
+
end
|
|
120
|
+
@@current = self
|
|
121
|
+
apply_default_colors(palette, tier)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def apply_default_colors(palette, tier)
|
|
127
|
+
if @default_colors
|
|
128
|
+
fg = resolve_default_color(@default_colors[:foreground], palette, tier)
|
|
129
|
+
bg = resolve_default_color(@default_colors[:background], palette, tier)
|
|
130
|
+
else
|
|
131
|
+
fg = "default"
|
|
132
|
+
bg = "default"
|
|
133
|
+
end
|
|
134
|
+
Window.set_default_colors(fg, bg)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def resolve_default_color(val, palette, tier)
|
|
138
|
+
if val.is_a?(Symbol)
|
|
139
|
+
color = palette.resolve(val, tier)
|
|
140
|
+
unless color
|
|
141
|
+
raise EditorError,
|
|
142
|
+
"Unknown palette color :#{val} for default_colors"
|
|
143
|
+
end
|
|
144
|
+
color
|
|
145
|
+
else
|
|
146
|
+
val || "default"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private_class_method def self.detect_background_via_osc11
|
|
151
|
+
return nil unless $stdin.tty? && $stdout.tty?
|
|
152
|
+
require "io/console"
|
|
153
|
+
$stdin.raw(min: 0, time: 1) do |io|
|
|
154
|
+
$stdout.write("\e]11;?\e\\")
|
|
155
|
+
$stdout.flush
|
|
156
|
+
response = +""
|
|
157
|
+
while (c = io.getc)
|
|
158
|
+
response << c
|
|
159
|
+
break if response.include?("\e\\") || response.include?("\a")
|
|
160
|
+
end
|
|
161
|
+
if response =~ /\e\]11;rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i
|
|
162
|
+
r = $1[0..1].to_i(16)
|
|
163
|
+
g = $2[0..1].to_i(16)
|
|
164
|
+
b = $3[0..1].to_i(16)
|
|
165
|
+
luminance = 0.299 * r + 0.587 * g + 0.114 * b
|
|
166
|
+
luminance < 128 ? :dark : :light
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
rescue
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private_class_method def self.detect_background_via_colorfgbg
|
|
174
|
+
colorfgbg = ENV["COLORFGBG"]
|
|
175
|
+
return nil unless colorfgbg
|
|
176
|
+
bg = colorfgbg.split(";").last.to_i
|
|
177
|
+
bg <= 6 || bg == 8 ? :dark : :light
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Catppuccin theme for Textbringer
|
|
2
|
+
# Based on https://github.com/catppuccin/nvim
|
|
3
|
+
#
|
|
4
|
+
# Dark variant: Mocha Light variant: Latte
|
|
5
|
+
|
|
6
|
+
Textbringer::Theme.define "catppuccin" do |t|
|
|
7
|
+
t.palette :dark do |p|
|
|
8
|
+
# Catppuccin Mocha base tones
|
|
9
|
+
p.color :text, hex: "#d7d7ff", ansi: "white"
|
|
10
|
+
p.color :subtext1, hex: "#afafd7", ansi: "white"
|
|
11
|
+
p.color :subtext0, hex: "#afafd7", ansi: "white"
|
|
12
|
+
p.color :overlay2, hex: "#8787af", ansi: "white"
|
|
13
|
+
p.color :overlay1, hex: "#8787af", ansi: "white"
|
|
14
|
+
p.color :overlay0, hex: "#767676", ansi: "brightblack"
|
|
15
|
+
p.color :surface2, hex: "#626262", ansi: "brightblack"
|
|
16
|
+
p.color :surface1, hex: "#4e4e4e", ansi: "brightblack"
|
|
17
|
+
p.color :surface0, hex: "#3a3a3a", ansi: "brightblack"
|
|
18
|
+
p.color :base, hex: "#262626", ansi: "black"
|
|
19
|
+
p.color :mantle, hex: "#1c1c1c", ansi: "black"
|
|
20
|
+
p.color :crust, hex: "#121212", ansi: "black"
|
|
21
|
+
|
|
22
|
+
# Catppuccin Mocha accent colors
|
|
23
|
+
p.color :red, hex: "#ff87af", ansi: "red"
|
|
24
|
+
p.color :maroon, hex: "#d7afaf", ansi: "red"
|
|
25
|
+
p.color :peach, hex: "#ffaf87", ansi: "yellow"
|
|
26
|
+
p.color :yellow, hex: "#ffd7af", ansi: "yellow"
|
|
27
|
+
p.color :green, hex: "#afd7af", ansi: "green"
|
|
28
|
+
p.color :teal, hex: "#87d7d7", ansi: "cyan"
|
|
29
|
+
p.color :sky, hex: "#87d7d7", ansi: "cyan"
|
|
30
|
+
p.color :sapphire, hex: "#87d7ff", ansi: "cyan"
|
|
31
|
+
p.color :blue, hex: "#87afff", ansi: "blue"
|
|
32
|
+
p.color :lavender, hex: "#afafff", ansi: "blue"
|
|
33
|
+
p.color :mauve, hex: "#d7afff", ansi: "magenta"
|
|
34
|
+
p.color :pink, hex: "#ffafd7", ansi: "magenta"
|
|
35
|
+
p.color :flamingo, hex: "#ffd7d7", ansi: "red"
|
|
36
|
+
p.color :rosewater, hex: "#ffd7d7", ansi: "red"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
t.palette :light do |p|
|
|
40
|
+
# Catppuccin Latte base tones
|
|
41
|
+
p.color :text, hex: "#585858", ansi: "black"
|
|
42
|
+
p.color :subtext1, hex: "#5f5f87", ansi: "black"
|
|
43
|
+
p.color :subtext0, hex: "#767676", ansi: "brightblack"
|
|
44
|
+
p.color :overlay2, hex: "#878787", ansi: "brightblack"
|
|
45
|
+
p.color :overlay1, hex: "#949494", ansi: "white"
|
|
46
|
+
p.color :overlay0, hex: "#a8a8a8", ansi: "white"
|
|
47
|
+
p.color :surface2, hex: "#b2b2b2", ansi: "white"
|
|
48
|
+
p.color :surface1, hex: "#c6c6c6", ansi: "white"
|
|
49
|
+
p.color :surface0, hex: "#d0d0d0", ansi: "white"
|
|
50
|
+
p.color :base, hex: "#eeeeee", ansi: "white"
|
|
51
|
+
p.color :mantle, hex: "#eeeeee", ansi: "white"
|
|
52
|
+
p.color :crust, hex: "#e4e4e4", ansi: "white"
|
|
53
|
+
|
|
54
|
+
# Catppuccin Latte accent colors
|
|
55
|
+
p.color :red, hex: "#d7005f", ansi: "red"
|
|
56
|
+
p.color :maroon, hex: "#d75f5f", ansi: "red"
|
|
57
|
+
p.color :peach, hex: "#ff5f00", ansi: "red"
|
|
58
|
+
p.color :yellow, hex: "#d78700", ansi: "yellow"
|
|
59
|
+
p.color :green, hex: "#5faf00", ansi: "green"
|
|
60
|
+
p.color :teal, hex: "#008787", ansi: "cyan"
|
|
61
|
+
p.color :sky, hex: "#00afd7", ansi: "cyan"
|
|
62
|
+
p.color :sapphire, hex: "#00afaf", ansi: "cyan"
|
|
63
|
+
p.color :blue, hex: "#005fff", ansi: "blue"
|
|
64
|
+
p.color :lavender, hex: "#5f87ff", ansi: "blue"
|
|
65
|
+
p.color :mauve, hex: "#875fff", ansi: "magenta"
|
|
66
|
+
p.color :pink, hex: "#d787d7", ansi: "magenta"
|
|
67
|
+
p.color :flamingo, hex: "#d78787", ansi: "red"
|
|
68
|
+
p.color :rosewater, hex: "#d78787", ansi: "red"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
t.default_colors foreground: :text, background: :base
|
|
72
|
+
|
|
73
|
+
# Programming faces (from catppuccin/nvim syntax.lua)
|
|
74
|
+
t.face :comment, foreground: :overlay2
|
|
75
|
+
t.face :preprocessing_directive, foreground: :pink
|
|
76
|
+
t.face :keyword, foreground: :mauve
|
|
77
|
+
t.face :string, foreground: :green
|
|
78
|
+
t.face :number, foreground: :peach
|
|
79
|
+
t.face :constant, foreground: :peach
|
|
80
|
+
t.face :function_name, foreground: :blue
|
|
81
|
+
t.face :type, foreground: :yellow
|
|
82
|
+
t.face :variable, foreground: :flamingo
|
|
83
|
+
t.face :operator, foreground: :sky
|
|
84
|
+
t.face :punctuation
|
|
85
|
+
t.face :builtin, foreground: :red
|
|
86
|
+
t.face :property, foreground: :lavender
|
|
87
|
+
|
|
88
|
+
# Basic faces (from catppuccin/nvim editor.lua)
|
|
89
|
+
t.face :mode_line, foreground: :text, background: :mantle
|
|
90
|
+
t.face :link, foreground: :blue, underline: true
|
|
91
|
+
t.face :control
|
|
92
|
+
t.face :region, background: :surface1
|
|
93
|
+
t.face :isearch, foreground: :mantle, background: :red
|
|
94
|
+
t.face :floating_window, foreground: :text, background: :mantle
|
|
95
|
+
|
|
96
|
+
# Completion faces
|
|
97
|
+
t.face :completion_popup, foreground: :overlay2, background: :mantle
|
|
98
|
+
t.face :completion_popup_selected, background: :surface0
|
|
99
|
+
|
|
100
|
+
# Dired faces
|
|
101
|
+
t.face :dired_directory, foreground: :blue
|
|
102
|
+
t.face :dired_symlink, foreground: :teal
|
|
103
|
+
t.face :dired_executable, foreground: :green
|
|
104
|
+
t.face :dired_flagged, foreground: :red
|
|
105
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# GitHub theme for Textbringer
|
|
2
|
+
# Based on https://github.com/cormacrelf/vim-colors-github
|
|
3
|
+
# Inspired by GitHub's syntax highlighting as of 2018.
|
|
4
|
+
#
|
|
5
|
+
# Light palette: cterm values from source (accurate).
|
|
6
|
+
# Dark palette: GUI hex values — dark-mode cterm values in source are unreliable
|
|
7
|
+
# (e.g. dcolors.blue has cterm=167 which maps to red, overlay has cterm=123
|
|
8
|
+
# which maps to bright cyan).
|
|
9
|
+
|
|
10
|
+
Textbringer::Theme.define "github" do |t|
|
|
11
|
+
t.palette :light do |p|
|
|
12
|
+
# Backgrounds / foreground
|
|
13
|
+
p.color :bg, hex: "#ffffff", ansi: "white" # 231 Normal bg
|
|
14
|
+
p.color :bg1, hex: "#eeeeee", ansi: "white" # 255 overlay/gutter/panels
|
|
15
|
+
p.color :vis, hex: "#afd7ff", ansi: "blue" # 153 Visual selection bg (blue2)
|
|
16
|
+
p.color :search, hex: "#ffffd7", ansi: "yellow" # 230 Search bg (yellow)
|
|
17
|
+
p.color :fg, hex: "#262626", ansi: "black" # 235 Normal fg (base0)
|
|
18
|
+
p.color :comment, hex: "#767676", ansi: "brightblack" # 243 Comment (base2)
|
|
19
|
+
p.color :line_nr, hex: "#bcbcbc", ansi: "white" # 250 LineNr fg (base4)
|
|
20
|
+
|
|
21
|
+
# Syntax colors
|
|
22
|
+
p.color :red, hex: "#d75f5f", ansi: "red" # 167 Statement, Type, PreProc
|
|
23
|
+
p.color :darkred, hex: "#af0000", ansi: "red" # 124 darkred
|
|
24
|
+
p.color :purple, hex: "#8700af", ansi: "magenta" # 91 Function, Define, Special
|
|
25
|
+
p.color :green, hex: "#00875f", ansi: "green" # 29 html/xml tags
|
|
26
|
+
p.color :orange, hex: "#d75f00", ansi: "yellow" # 166 orange
|
|
27
|
+
p.color :blue, hex: "#005fd7", ansi: "blue" # 26 Identifier, Constant, Macro
|
|
28
|
+
p.color :darkblue, hex: "#00005f", ansi: "blue" # 17 String
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
t.palette :dark do |p|
|
|
32
|
+
# Backgrounds / foreground (GUI hex — cterm values are unreliable in source)
|
|
33
|
+
p.color :bg, hex: "#24292e", ansi: "black" # base0 (Normal bg)
|
|
34
|
+
p.color :bg1, hex: "#353a3f", ansi: "brightblack" # dcolors.overlay (panels/popups)
|
|
35
|
+
p.color :vis, hex: "#354a60", ansi: "blue" # dcolors.blue2 / blues[4]
|
|
36
|
+
p.color :search, hex: "#595322", ansi: "yellow" # dcolors.yellow (Search bg)
|
|
37
|
+
p.color :fg, hex: "#fafbfc", ansi: "white" # fafbfc (Normal fg)
|
|
38
|
+
p.color :comment, hex: "#abaeb1", ansi: "brightblack" # darktext[2] (Comment)
|
|
39
|
+
p.color :line_nr, hex: "#76787b", ansi: "brightblack" # numDarkest (base4 in dark)
|
|
40
|
+
|
|
41
|
+
# Syntax colors (GUI hex)
|
|
42
|
+
p.color :red, hex: "#f16636", ansi: "red" # dcolors.red
|
|
43
|
+
p.color :darkred, hex: "#b31d28", ansi: "red" # s:colors.darkred (same both modes)
|
|
44
|
+
p.color :purple, hex: "#a887e6", ansi: "magenta" # dcolors.purple
|
|
45
|
+
p.color :green, hex: "#59b36f", ansi: "green" # dcolors.green
|
|
46
|
+
p.color :orange, hex: "#ffa657", ansi: "yellow" # s:colors.orange (same both modes)
|
|
47
|
+
p.color :blue, hex: "#4dacfd", ansi: "blue" # dcolors.blue
|
|
48
|
+
p.color :darkblue, hex: "#c1daec", ansi: "blue" # dcolors.darkblue = blue1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
t.default_colors foreground: :fg, background: :bg
|
|
52
|
+
|
|
53
|
+
# Programming faces
|
|
54
|
+
t.face :comment, foreground: :comment # Comment = base2
|
|
55
|
+
t.face :preprocessing_directive, foreground: :red # PreProc = red
|
|
56
|
+
t.face :keyword, foreground: :red # Statement = red
|
|
57
|
+
t.face :string, foreground: :darkblue # String = darkblue
|
|
58
|
+
t.face :number, foreground: :blue # Number → Constant = blue
|
|
59
|
+
t.face :constant, foreground: :blue # Constant = blue
|
|
60
|
+
t.face :function_name, foreground: :purple # Function = purple
|
|
61
|
+
t.face :type, foreground: :orange # Type = orange
|
|
62
|
+
t.face :variable, foreground: :blue # Identifier = blue
|
|
63
|
+
t.face :operator # no explicit color in source
|
|
64
|
+
t.face :punctuation # Delimiter = fg (ghNormalNoBg)
|
|
65
|
+
t.face :builtin, foreground: :purple # Special = purple
|
|
66
|
+
t.face :property, foreground: :blue # Identifier = blue
|
|
67
|
+
|
|
68
|
+
# Basic faces
|
|
69
|
+
# StatusLine: fg=grey2 (~bg1), bg=base0 (~fg) — inverted from Normal in both modes
|
|
70
|
+
t.face :mode_line, foreground: :bg1, background: :fg
|
|
71
|
+
t.face :link, foreground: :blue, underline: true
|
|
72
|
+
t.face :control
|
|
73
|
+
t.face :region, background: :vis # Visual = visualblue
|
|
74
|
+
# Search has no explicit fg in source; fg inherits from Normal
|
|
75
|
+
t.face :isearch, foreground: :fg, background: :search
|
|
76
|
+
t.face :floating_window, foreground: :fg, background: :bg1
|
|
77
|
+
|
|
78
|
+
# Completion faces
|
|
79
|
+
# Pmenu: fg=base3 (≈ comment), bg=overlay (≈ bg1)
|
|
80
|
+
# PmenuSel: fg=overlay (≈ bg1), bg=blue; bold
|
|
81
|
+
t.face :completion_popup, foreground: :comment, background: :bg1
|
|
82
|
+
t.face :completion_popup_selected, foreground: :bg1, background: :blue, bold: true
|
|
83
|
+
|
|
84
|
+
# Dired faces
|
|
85
|
+
t.face :dired_directory, foreground: :blue # Directory → ghBlue
|
|
86
|
+
t.face :dired_symlink, foreground: :darkblue
|
|
87
|
+
t.face :dired_executable, foreground: :green
|
|
88
|
+
t.face :dired_flagged, foreground: :red
|
|
89
|
+
end
|