diff --git a/README.md b/README.md index c51931a..b8be135 100644 --- a/README.md +++ b/README.md @@ -548,6 +548,8 @@ require("diffbandit").setup({ }, theme = { auto_refresh = true, + background = nil, -- reference bg for blending when Normal has none (e.g. "#1e1e2e") + transparent = nil, -- nil: detect from Normal; true/false forces see-through panes semantic_blend = 0.3, change_emphasis_strength = 0.16, min_background_delta = 0.08, @@ -573,6 +575,16 @@ Theme colors are derived from the active colorscheme's diff highlight groups. Use `ui.theme.colors` for semantic color overrides, or `ui.theme.highlights` to override specific `DiffBandit*` highlight groups. +Transparent colorschemes (a `Normal` group with no background) keep the panes +and gutters see-through with no extra configuration: context rows, gutters, +splits, panels and status strips paint no background, while the +add/delete/change bands stay opaque. The bands are still blended against a +background color. Without one, DiffBandit picks a neutral dark or light color +based on `'background'`; set `ui.theme.background` (hex or a color name) to your +terminal's background to tune the bands to it exactly. `ui.theme.transparent = true` forces +see-through panes on an opaque theme, and `false` paints `ui.theme.background` +(or the fallback) instead. + ### Image connectors (Kitty graphics) With `ui.connector_renderer = "auto"` (the default), DiffBandit probes the diff --git a/lua/diffbandit/config.lua b/lua/diffbandit/config.lua index 9b62565..ced44bd 100644 --- a/lua/diffbandit/config.lua +++ b/lua/diffbandit/config.lua @@ -96,6 +96,8 @@ local defaults = { }, theme = { auto_refresh = true, + background = nil, -- reference bg for blending when Normal has none (e.g. "#1e1e2e") + transparent = nil, -- nil: detect from Normal; true/false forces see-through panes semantic_blend = 0.3, change_emphasis_strength = 0.16, min_background_delta = 0.08, diff --git a/lua/diffbandit/highlights.lua b/lua/diffbandit/highlights.lua index 2c4abe4..923c7de 100644 --- a/lua/diffbandit/highlights.lua +++ b/lua/diffbandit/highlights.lua @@ -168,13 +168,46 @@ local function apply_theme_overrides(theme) end end +-- Fallback reference backgrounds for a transparent Normal with no +-- ui.theme.background: a typical dark/light terminal background, chosen by +-- 'background' so light terminals don't blend against black. +local FALLBACK_DARK_BG = 0x1E1E1E +local FALLBACK_LIGHT_BG = 0xFFFFFF + +--- Resolve the two backgrounds the theme derivation needs. +--- `ref_bg` is the colour everything is blended against (always a number): +--- ui.theme.background, then Normal.bg, then a 'background'-based fallback. +--- `paint_bg` is what "page" groups (context rows, gutters, splits) paint: +--- "NONE" when the colorscheme is transparent so the terminal shows through, +--- otherwise ref_bg. ui.theme.transparent forces either mode; nil detects it +--- from Normal having no background (the usual transparent-theme setup). +local function resolve_backgrounds(normal_base, theme) + local normal_bg = color_to_number(normal_base.bg) + local ref_bg = color_to_number(theme.background) + if not ref_bg and type(theme.background) == "string" then + -- Colour names ("Black", "DarkSlateGray"); -1 when unknown. + local named = vim.api.nvim_get_color_by_name(theme.background) + ref_bg = named >= 0 and named or nil + end + ref_bg = ref_bg or normal_bg + if not ref_bg then + ref_bg = vim.o.background == "light" and FALLBACK_LIGHT_BG or FALLBACK_DARK_BG + end + local transparent = theme.transparent + if transparent == nil then + transparent = normal_bg == nil + end + return ref_bg, transparent and "NONE" or ref_bg +end + local function apply_diff_variants(config) local normal_base = adopt_diff_colors("Normal") local ui = (config and config.ui) or {} local theme = ui.theme or {} - local normal_bg = color_to_number(normal_base.bg) or 0x000000 - local normal_fg = color_to_number(normal_base.fg) or 0xFFFFFF + local normal_bg, paint_bg = resolve_backgrounds(normal_base, theme) + local normal_fg = color_to_number(normal_base.fg) + or (luminance(normal_bg) < 0.5 and 0xFFFFFF or 0x000000) local add_bg = semantic_background("add", "DiffAdd", normal_bg, theme) local delete_bg = semantic_background("delete", "DiffDelete", normal_bg, theme) local change_bg = semantic_background("change", "DiffChange", normal_bg, theme) @@ -224,22 +257,28 @@ local function apply_diff_variants(config) -- Context highlight - no sp set to avoid conflicts with separator underlines apply_group("DiffBanditContext", { - bg = normal_bg, + bg = paint_bg, fg = "NONE", }) - local soft_split_fg = blend_color(normal_base.bg or "#000000", get_foreground_color("LineNr", "#808080"), ui.split_blend or 0.3) + local soft_split_fg = blend_color(normal_bg, get_foreground_color("LineNr", "#808080"), ui.split_blend or 0.3) apply_group("DiffBanditSplit", { fg = soft_split_fg, - bg = normal_bg, + bg = paint_bg, }) + -- Hidden separators draw the glyph in the background colour. Transparent: + -- the glyph takes the reference background (invisible when + -- ui.theme.background matches the terminal) over a see-through cell. apply_group("DiffBanditHiddenSplit", { fg = normal_bg, - bg = normal_bg, + bg = paint_bg, }) - local status_bg = blend_color(normal_bg, get_foreground_color("LineNr", "#808080"), 0.08) + -- Status strips and panels (nav/commit panel, log view) get a faint tint on + -- opaque themes; transparent themes keep them see-through like the panes. + local status_bg = paint_bg == "NONE" and "NONE" + or blend_color(normal_bg, get_foreground_color("LineNr", "#808080"), 0.08) local status_muted = get_foreground_color("Comment", "#808080") local status_accent = get_foreground_color("Identifier", normal_fg) apply_group("DiffBanditStatus", { @@ -291,17 +330,17 @@ local function apply_diff_variants(config) -- Filler/placeholder highlights apply_group("DiffBanditGap", { - bg = normal_bg, + bg = paint_bg, fg = get_foreground_color("Comment", "#808080"), }) apply_group("DiffBanditPlaceholder", { - bg = normal_bg, + bg = paint_bg, fg = get_foreground_color("Comment", "#808080"), }) apply_group("DiffBanditEmptyNotice", { - bg = normal_bg, + bg = paint_bg, fg = get_foreground_color("Comment", "#808080"), italic = true, }) @@ -314,25 +353,25 @@ local function apply_diff_variants(config) for kind, color in pairs({ Add = add_bg, Delete = delete_bg, Change = change_bg }) do apply_group("DiffBanditConnector" .. kind, { bg = color, fg = connector_fg }) apply_group("DiffBanditConnector" .. kind .. "Line", { fg = color, bold = true }) - apply_group("DiffBanditConnectorExpansion" .. kind, { fg = color, bg = normal_bg, bold = true }) + apply_group("DiffBanditConnectorExpansion" .. kind, { fg = color, bg = paint_bg, bold = true }) apply_group("DiffBanditOverview" .. kind, { bg = color, fg = color }) end apply_group("DiffBanditConnectorExpansionAddUnderline", { fg = add_bg, - bg = normal_bg, + bg = paint_bg, underline = true, sp = add_bg, bold = true, }) apply_group("DiffBanditConnectorContext", { - bg = normal_bg, + bg = paint_bg, fg = get_foreground_color("Comment", "#808080"), }) apply_group("DiffBanditOverviewContext", { - bg = normal_bg, + bg = paint_bg, fg = normal_bg, }) apply_group("DiffBanditOverviewCursor", { @@ -354,7 +393,7 @@ local function apply_diff_variants(config) apply_group("DiffBanditLineNumberRight", { fg = line_number_fg }) -- The editable right pane's sign column must share the pane background -- instead of the colorscheme's SignColumn bg. - apply_group("DiffBanditSignColumn", { bg = normal_bg }) + apply_group("DiffBanditSignColumn", { bg = paint_bg }) -- Band continuation into the sign column: line_hl_group does not cover the -- sign column, so band rows place a low-priority blank sign carrying the -- band background (real signs outrank it and still display). diff --git a/lua/diffbandit/kitty/canvas.lua b/lua/diffbandit/kitty/canvas.lua index 2c180b0..e2e1c3a 100644 --- a/lua/diffbandit/kitty/canvas.lua +++ b/lua/diffbandit/kitty/canvas.lua @@ -372,8 +372,10 @@ local edge_ns = vim.api.nvim_create_namespace("DiffBanditKittyEdges") local KIND_HL = { add = "DiffBanditConnectorAdd", delete = "DiffBanditConnectorDelete", change = "DiffBanditConnectorChange" } local function vert_char() + -- Global value: the current window may be a hidden-separator pane whose + -- local fillchars blanks `vert` (util/layout.hidden_separator_fillchars). local ok, fc = pcall(function() - return vim.opt.fillchars:get() + return vim.opt_global.fillchars:get() end) local ch = ok and fc and fc.vert or nil return (ch and ch ~= "") and ch or "│" diff --git a/lua/diffbandit/session/layout.lua b/lua/diffbandit/session/layout.lua index 615d699..1b00391 100644 --- a/lua/diffbandit/session/layout.lua +++ b/lua/diffbandit/session/layout.lua @@ -241,9 +241,10 @@ end -- back on desyncs the pane's screen rows from its unwrappable gutters, -- and 'scrolloff'/folds undo ]c alignment. Re-apply after every right-buffer swap. local function apply_right_source_window_options(session) - local right_source_winhl = session.overview_enabled and layout.winhl.hidden_source or layout.winhl.source - set_window_options(session.right_win, layout.win_opts.source(right_source_winhl, { + local hidden = session.overview_enabled + set_window_options(session.right_win, layout.win_opts.source(hidden and layout.winhl.hidden_source or layout.winhl.source, { signcolumn = session.right.editable and "auto" or "no", + fillchars = hidden and layout.hidden_separator_fillchars() or nil, })) end @@ -253,6 +254,7 @@ local function apply_window_options(session) for _, win in ipairs({ session.left_overview_win, session.right_overview_win }) do if win then set_window_options(win, layout.win_opts.gutter(layout.winhl.overview)) + set_window_options(win, { fillchars = layout.hidden_separator_fillchars() }) set_window_width(win, session.overview_width) end end @@ -353,7 +355,9 @@ function M.open(session) finalize_bufhidden(session) -- Set vertical split character to thin line - vim.opt.fillchars:append({ vert = "│" }) + -- Global only: vim.opt would also reset the current window's local + -- fillchars (the hidden-separator windows set their own). + vim.opt_global.fillchars:append({ vert = "│" }) set_initial_focus(session) end diff --git a/lua/diffbandit/util/layout.lua b/lua/diffbandit/util/layout.lua index 6248838..70271e4 100644 --- a/lua/diffbandit/util/layout.lua +++ b/lua/diffbandit/util/layout.lua @@ -47,6 +47,22 @@ M.winhl = { M.win_opts = {} +--- Window-local 'fillchars' for windows whose separator is hidden +--- (winhl.hidden_split): the user's global fillchars with a blank `vert`. +--- DiffBanditHiddenSplit alone paints the glyph fg = bg, which only hides it +--- when the background is opaque; a transparent theme would show the glyph in +--- the reference colour over the terminal background. +function M.hidden_separator_fillchars() + local chars = vim.opt_global.fillchars:get() + chars.vert = " " + local parts = {} + for key, value in pairs(chars) do + parts[#parts + 1] = key .. ":" .. value + end + table.sort(parts) + return table.concat(parts, ",") +end + -- Source/content pane: cursorline on, no gutter columns. function M.win_opts.source(winhl, overrides) local opts = { diff --git a/tests/integration/verify.lua b/tests/integration/verify.lua index 90961c4..1720a26 100644 --- a/tests/integration/verify.lua +++ b/tests/integration/verify.lua @@ -1051,11 +1051,11 @@ local function connector_core_bounds(stripped) from = pos + #"│" end - local has_overview_pane = (separators[1] or 999) <= 3 - if has_overview_pane then - return separators[3], separators[#separators - 2] - end - return separators[2], separators[#separators - 2] + -- Overview strips hide their separators with a blank fillchar (no glyph + -- is invisible on a transparent background), so the visible glyphs are + -- the same with or without overviews: left source, left numbers (core + -- start), [rails], core end, right numbers. + return separators[2], separators[#separators - 1] end local function connector_core_pipe_count(stripped) diff --git a/tests/specs/10_connector.lua b/tests/specs/10_connector.lua index 506bfa9..7dc7122 100644 --- a/tests/specs/10_connector.lua +++ b/tests/specs/10_connector.lua @@ -1543,6 +1543,77 @@ do assert_eq(get_hl("DiffBanditConnectorAddLine").bg, 0x040506, "Per-group highlight override should include background") + -- Transparent colorschemes (Normal without bg): page groups paint no + -- background so the terminal shows through; bands stay opaque and blend + -- against ui.theme.background (or a 'background'-based fallback, never + -- black on light terminals). + local page_groups = { + "DiffBanditContext", "DiffBanditConnectorContext", "DiffBanditOverviewContext", + "DiffBanditSignColumn", "DiffBanditSplit", "DiffBanditHiddenSplit", + "DiffBanditGap", "DiffBanditPlaceholder", "DiffBanditEmptyNotice", + "DiffBanditConnectorExpansionAdd", "DiffBanditConnectorExpansionAddUnderline", + "DiffBanditStatus", "DiffBanditStatusLine", "DiffBanditStatusAccent", "DiffBanditStatusMuted", + } + local transparent_palette = { + Normal = { fg = 0xE0E2EA }, + LineNr = { fg = 0x4F5258 }, + Comment = { fg = 0x9B9EA4 }, + DiffAdd = { fg = 0x40C060 }, + DiffDelete = { fg = 0xE05050 }, + DiffChange = { fg = 0x5090E0 }, + } + local saved_background = vim.o.background + vim.o.background = "dark" + set_palette(transparent_palette) + highlights.apply(config_mod.defaults()) + for _, group in ipairs(page_groups) do + assert_eq(get_hl(group).bg, nil, "Transparent Normal should leave " .. group .. " see-through") + end + assert_eq(get_hl("DiffBanditAdd").bg ~= nil, true, "Transparent Normal should keep opaque add bands") + assert_eq(luminance(get_hl("DiffBanditAdd").bg) < 0.5, true, + "Transparent dark fallback should derive dark add bands") + local fallback_add = get_hl("DiffBanditAdd").bg + + highlights.apply(config_mod.apply({ ui = { theme = { background = "#303446" } } })) + assert_eq(get_hl("DiffBanditContext").bg, nil, + "ui.theme.background must not make a transparent theme opaque") + assert_ne(get_hl("DiffBanditAdd").bg, fallback_add, + "ui.theme.background should be the blend reference for transparent themes") + assert_eq(get_hl("DiffBanditHiddenSplit").fg, 0x303446, + "Hidden splits should draw in the configured reference background") + + highlights.apply(config_mod.apply({ ui = { theme = { background = "DarkSlateGray" } } })) + assert_eq(get_hl("DiffBanditHiddenSplit").fg, vim.api.nvim_get_color_by_name("DarkSlateGray"), + "ui.theme.background should accept colour names") + + local layout_util = require("diffbandit.util.layout") + local saved_fillchars = vim.go.fillchars + vim.go.fillchars = "eob: ,vert:│,fold:-" + assert_eq(layout_util.hidden_separator_fillchars(), "eob: ,fold:-,vert: ", + "Hidden-separator windows should blank vert and keep the user's other fillchars") + vim.go.fillchars = saved_fillchars + + highlights.apply(config_mod.apply({ ui = { theme = { background = "#303446", transparent = false } } })) + assert_eq(get_hl("DiffBanditContext").bg, 0x303446, + "transparent = false should paint the reference background") + + vim.o.background = "light" + set_palette(transparent_palette) + highlights.apply(config_mod.defaults()) + assert_eq(luminance(get_hl("DiffBanditAdd").bg) > 0.5, true, + "Transparent light fallback should derive light add bands, not blend against black") + vim.o.background = saved_background + + set_palette({ + Normal = { fg = 0xE0E2EA, bg = 0x14161B }, + DiffAdd = { bg = 0x005523 }, + }) + highlights.apply(config_mod.apply({ ui = { theme = { transparent = true } } })) + assert_eq(get_hl("DiffBanditContext").bg, nil, "transparent = true should force see-through panes") + assert_eq(get_hl("DiffBanditAdd").bg, 0x005523, "Forced transparency should keep theme bands") + highlights.apply(config_mod.defaults()) + assert_eq(get_hl("DiffBanditContext").bg, 0x14161B, "Opaque themes should keep painting Normal bg") + local diffbandit = require("diffbandit") set_palette({ Normal = { fg = 0xEEEEEE, bg = 0x101010 },