Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lua/diffbandit/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 54 additions & 15 deletions lua/diffbandit/highlights.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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", {
Expand Down Expand Up @@ -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,
})
Expand All @@ -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", {
Expand All @@ -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).
Expand Down
4 changes: 3 additions & 1 deletion lua/diffbandit/kitty/canvas.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 "│"
Expand Down
10 changes: 7 additions & 3 deletions lua/diffbandit/session/layout.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions lua/diffbandit/util/layout.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
10 changes: 5 additions & 5 deletions tests/integration/verify.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
71 changes: 71 additions & 0 deletions tests/specs/10_connector.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Loading