Skip to content
14 changes: 14 additions & 0 deletions lib/textbringer/commands/windows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ module Commands
end
end

define_command(:set_foreground_color, doc: <<~EOD) do
Set the default foreground color.
EOD
|color = read_from_minibuffer("Foreground color: ")|
Window.set_default_colors(color, nil)
end

define_command(:set_background_color, doc: <<~EOD) do
Set the default background color.
EOD
|color = read_from_minibuffer("Background color: ")|
Window.set_default_colors(nil, color)
end
Comment on lines +87 to +99

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These commands call read_from_minibuffer without a default value; when the user submits an empty input, read_from_minibuffer returns "" (see lib/textbringer/utils.rb:169-174), which will currently cause Window.set_default_colors to raise and (with the new caching) potentially leave an invalid cached default. Consider providing a default (e.g., "default") or mapping empty input to nil/"default" before calling Window.set_default_colors.

Copilot uses AI. Check for mistakes.

define_command(:list_buffers, doc: <<~EOD) do |buffers = Buffer.list|
List the existing buffers.
EOD
Expand Down
32 changes: 32 additions & 0 deletions lib/textbringer/theme.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def initialize(name)
@name = name
@palettes = {}
@face_definitions = []
@default_colors = nil
end

attr_reader :name
Expand All @@ -87,6 +88,10 @@ def face(name, **attrs)
@face_definitions << [name, attrs]
end

def default_colors(foreground:, background:)
@default_colors = { foreground: foreground, background: background }
end

def activate
mode = self.class.background_mode
tier = self.class.color_tier
Expand All @@ -113,6 +118,33 @@ def activate
Face.define(face_name, **resolved)
end
@@current = self
apply_default_colors(palette, tier)
end

private

def apply_default_colors(palette, tier)
if @default_colors
fg = resolve_default_color(@default_colors[:foreground], palette, tier)
bg = resolve_default_color(@default_colors[:background], palette, tier)
else
fg = "default"
bg = "default"
end
Window.set_default_colors(fg, bg)
end

def resolve_default_color(val, palette, tier)
if val.is_a?(Symbol)
color = palette.resolve(val, tier)
unless color
raise EditorError,
"Unknown palette color :#{val} for default_colors"
end
color
else
val || "default"
end
end

private_class_method def self.detect_background_via_osc11
Expand Down
2 changes: 2 additions & 0 deletions lib/textbringer/themes/catppuccin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
p.color :rosewater, hex: "#d78787", ansi: "red"
end

t.default_colors foreground: :text, background: :base

# Programming faces (from catppuccin/nvim syntax.lua)
t.face :comment, foreground: :overlay2
t.face :preprocessing_directive, foreground: :pink
Expand Down
2 changes: 2 additions & 0 deletions lib/textbringer/themes/github.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
p.color :darkblue, hex: "#c1daec", ansi: "blue" # dcolors.darkblue = blue1
end

t.default_colors foreground: :fg, background: :bg

# Programming faces
t.face :comment, foreground: :comment # Comment = base2
t.face :preprocessing_directive, foreground: :red # PreProc = red
Expand Down
2 changes: 2 additions & 0 deletions lib/textbringer/themes/gruvbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
p.color :orange, hex: "#d75f00", ansi: "red"
end

t.default_colors foreground: :fg1, background: :bg0

# Programming faces
t.face :comment, foreground: :gray
t.face :preprocessing_directive, foreground: :aqua
Expand Down
2 changes: 2 additions & 0 deletions lib/textbringer/themes/molokai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
p.color :search, hex: "#ffd787", ansi: "yellow" # 222 Search bg
end

t.default_colors foreground: :fg, background: :bg

# Programming faces
t.face :comment, foreground: :comment
t.face :preprocessing_directive, foreground: :green
Expand Down
2 changes: 2 additions & 0 deletions lib/textbringer/themes/sonokai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
p.color :purple, hex: "#d787d7", ansi: "magenta"
end

t.default_colors foreground: :fg, background: :bg0

# Programming faces (from sonokai highlight groups)
t.face :comment, foreground: :grey
t.face :preprocessing_directive, foreground: :red
Expand Down
2 changes: 2 additions & 0 deletions lib/textbringer/themes/tokyonight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
p.color :blue5, hex: "#89ddff", ansi: "cyan" # Operator, punctuation delimiters
end

t.default_colors foreground: :fg, background: :bg

# Programming faces
t.face :comment, foreground: :comment
t.face :preprocessing_directive, foreground: :cyan # PreProc = c.cyan
Expand Down
21 changes: 16 additions & 5 deletions lib/textbringer/window.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class Window
@@minibuffer_selected = nil
@@echo_area = nil
@@has_colors = false
@@default_fg = "default"
@@default_bg = "default"

def self.list(include_echo_area: false)
if include_echo_area
Expand Down Expand Up @@ -133,8 +135,13 @@ def self.colors
end

def self.set_default_colors(fg, bg)
Curses.assume_default_colors(Color[fg], Color[bg])
Window.redraw
new_fg = fg || @@default_fg
new_bg = bg || @@default_bg
Curses.assume_default_colors(Color[new_fg], Color[new_bg])
@@default_fg = new_fg
@@default_bg = new_bg
Face.define(:default, foreground: new_fg, background: new_bg)
Window.redraw if @@started
end

def self.load_faces
Expand Down Expand Up @@ -708,7 +715,8 @@ def redisplay_mode_line
@mode_line.addstr(" #{line},#{column}")
@mode_line.addstr(" (#{@buffer.mode_names.join(' ')})")
@mode_line.addstr(" " * (columns - @mode_line.curx))
@mode_line.attr_set(0, 0)
default = Face[:default]
@mode_line.attr_set(default&.text_attrs || 0, default&.color_pair || 0)
@mode_line.noutrefresh
end

Expand Down Expand Up @@ -948,6 +956,7 @@ def get_char
end

def apply_face_attrs(win, face)
face ||= Face[:default]
win.attr_set(face&.text_attrs || 0, face&.color_pair || 0)
end

Expand All @@ -967,7 +976,8 @@ def apply_window_attrs(win)
end
win.attr_set(text_attrs, face.color_pair)
else
win.attr_set(0, 0)
default = Face[:default]
win.attr_set(default&.text_attrs || 0, default&.color_pair || 0)
end
end
end
Expand Down Expand Up @@ -1016,7 +1026,8 @@ def redisplay
@window.addstr(@buffer.input_method_status)
end
@window.setpos(0, 0)
@window.attr_set(0, 0)
default = Face[:default]
@window.attr_set(default&.text_attrs || 0, default&.color_pair || 0)
@in_region = false
@in_isearch = false
@current_hl_face = nil
Expand Down
54 changes: 54 additions & 0 deletions test/textbringer/test_theme.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,60 @@ def test_activate_with_inherit
Face.delete(:derived_kw)
end

def test_activate_sets_default_colors
Theme.define "test_defcol" do |t|
t.palette :dark do |p|
p.color :fg, hex: "#ffffff", ansi: "white"
p.color :bg, hex: "#000000", ansi: "black"
end
t.default_colors foreground: :fg, background: :bg
t.face :test_defcol_face, foreground: :fg
end
Theme["test_defcol"].activate
fg_num = Color["#ffffff"]
bg_num = Color["#000000"]
assert_equal([fg_num, bg_num], Curses.default_colors)
ensure
Face.delete(:test_defcol_face)
end

def test_activate_resets_default_colors_when_not_specified
Theme.define "test_with_defcol" do |t|
t.palette :dark do |p|
p.color :fg, hex: "#ffffff", ansi: "white"
p.color :bg, hex: "#000000", ansi: "black"
end
t.default_colors foreground: :fg, background: :bg
t.face :test_wdc_face, foreground: :fg
end
Theme["test_with_defcol"].activate
assert_not_equal([-1, -1], Curses.default_colors)

Theme.define "test_no_defcol" do |t|
t.palette :dark do |p|
p.color :fg, hex: "#ffffff", ansi: "white"
end
t.face :test_ndc_face, foreground: :fg
end
Theme["test_no_defcol"].activate
assert_equal([-1, -1], Curses.default_colors)
ensure
Face.delete(:test_wdc_face)
Face.delete(:test_ndc_face)
end

def test_activate_default_colors_raises_on_unknown_palette
Theme.define "test_bad_defcol" do |t|
t.palette :dark do |p|
p.color :fg, hex: "#ffffff", ansi: "white"
end
t.default_colors foreground: :nonexistent, background: :fg
end
assert_raise(EditorError) do
Theme["test_bad_defcol"].activate
end
end

def test_load_default_activates_faces
face = Face[:comment]
assert_not_nil(face)
Expand Down
28 changes: 28 additions & 0 deletions test/textbringer/test_window.rb
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,34 @@ def test_s_set_default_colors
assert_equal([-1, -1], Curses.default_colors)
end

def test_s_set_default_colors_partial_update
red = Color["red"]
blue = Color["blue"]
green = Color["green"]
Window.set_default_colors("red", "blue")
assert_equal([red, blue], Curses.default_colors)

# Update only bg, fg should be preserved
Window.set_default_colors(nil, "default")
assert_equal([red, -1], Curses.default_colors)

# Update only fg, bg should be preserved
Window.set_default_colors("green", nil)
assert_equal([green, -1], Curses.default_colors)
end

def test_s_set_default_colors_empty_string_ignored
red = Color["red"]
blue = Color["blue"]
Window.set_default_colors("red", "blue")
assert_equal([red, blue], Curses.default_colors)

# Empty string should be treated as error
assert_raise(EditorError) do
Window.set_default_colors("", "")
end
end

private

def window_string(window)
Expand Down