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.
@@ -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