summaryrefslogtreecommitdiffstats
path: root/plugin/50-color.lua
blob: c88e34ba773d9f276351b9688985dc571d084545 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
--[[ 50-color.lua — solarized highlight tweaks, a matching lualine statusline, and the custom tabline.

Registers highlight and statusline appliers into dotfiles.color, which re-applies them on colorscheme
/ 'background' changes and once after startup. Also styles the popup menu and tabpage titles, and sets
the default colorscheme (solarized) and background (dark).

User commands:
  `ColorsBgToggle`:      toggle light/dark background
  `TabRename`:           rename the current tab
  `TabRenameFileName`:   rename the current tab to the active buffer's file name
  `LualineConfig`:       show the resolved lualine configuration
]]

local color = require("dotfiles.color")
local lualine = require("lualine")

local solarized = { blue = "#268bd2" }

----------------------------------------------------------------------------------------------------
-- Highlights --------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------

--[[ Highlight applier: adjust the solarized groups to fit my personal tastes. No-ops unless solarized
is the active colorscheme. ]]
local function apply_solarized_highlights()
	if vim.g.colors_name ~= "solarized" then
		return
	end
	-- Window bar style.
	vim.api.nvim_set_hl(0, "WinBarCwd", { fg = solarized.blue, italic = true })
	vim.api.nvim_set_hl(0, "WinBarFilePath", { italic = true })
	-- Matching delimiter.
	vim.api.nvim_set_hl(0, "MatchParen", { link = "CurSearch" })
	-- Floating windows and popup menu windows follow the editor background, their borders too so that
	-- the popup menu border draws a clean separation.
	vim.opt.pumborder = "rounded"
	vim.opt.pumwidth = 25 -- Minimum popup menu width.
	vim.api.nvim_set_hl(0, "NormalFloat", { link = "Normal" })
	local normal_bg = vim.api.nvim_get_hl(0, { name = "Normal", link = false }).bg
	vim.api.nvim_set_hl(0, "FloatBorder", { bg = normal_bg, update = true })
	vim.api.nvim_set_hl(0, "Pmenu", { bg = normal_bg, update = true })
	vim.api.nvim_set_hl(0, "PmenuSbar", { bg = normal_bg, update = true })
	-- Currently selected tabpage.
	vim.api.nvim_set_hl(0, "TabLineSel", { bold = true, italic = true, update = true })
	-- `Todo` highlight group.
	vim.api.nvim_set_hl(0, "Todo", { bg = "Yellow", fg = "Black", bold = true })
	--[[ Fold lines sharing the cursor-line color (the default) make unfocused windows unreadable, so
	their background is set to blend in; the folds stay visible enough through their dotted lines. ]]
	vim.api.nvim_set_hl(0, "Folded", { bg = normal_bg, underline = true, update = true })
end

--[[ Highlight applier: create custom highlight groups for styling the tabpage title bars.
`TabLineWinCount` is the window count of a tabpage, `TabLineSelWinCount` that of the active one. ]]
local function apply_tabpage_title_highlights()
	local normal = vim.api.nvim_get_hl(0, { name = "Normal", link = false })
	local tabline = vim.api.nvim_get_hl(0, { name = "TabLine", link = false })
	local tablinesel = vim.api.nvim_get_hl(0, { name = "TabLineSel", link = false })
	vim.api.nvim_set_hl(0, "TabLineSel", { fg = normal.fg, update = true })
	vim.api.nvim_set_hl(0, "TabLineWinCount", { fg = solarized.blue, bg = tabline.bg })
	vim.api.nvim_set_hl(
		0,
		"TabLineSelWinCount",
		{ fg = solarized.blue, bg = tablinesel.bg, bold = true }
	)
end

----------------------------------------------------------------------------------------------------
-- Statusline --------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------

-- Spell flag: which spell languages are enabled, or nil when 'spelllang' matches none of these.
local function spell_status()
	if vim.o.spelllang == "fr" then
		return "+S(fr)"
	elseif vim.o.spelllang == "en_us" then
		return "+S(en)"
	elseif vim.o.spelllang == "en_us,fr" then
		return "+S(en,fr)"
	end
end

--[[ Custom status flags: `+F` when formatting is on for the buffer, `+H` when LSP inlay hints are on,
`+R` when disk-change autoreload is enabled (see 50-autoreload.lua), `+T` when terminal follow is on
(see :TerminalFollow), followed by the spell flag. ]]
local function statuses()
	return (vim.b.format and "+F" or "")
		.. (vim.lsp.inlay_hint.is_enabled({ bufnr = 0 }) and "+H" or "")
		.. (vim.g.autoreload and "+R" or "")
		.. (vim.b.terminal_follow and "+T" or "")
		.. (vim.o.spell and spell_status() or "")
end

--[[ lualine builds its own section-b-backed highlights for the diff/diagnostics components
(`lualine_b_diff_added_normal`, `lualine_b_diagnostics_error_normal`, ...). Stamp bold onto them with
`update = true` so only the bold attribute changes — fg and section b's background are left untouched.
Must run after lualine (re)creates the groups: initial setup, theme switch, colorscheme. ]]
local function bold_diff_diag()
	for name in pairs(vim.api.nvim_get_hl(0, {})) do
		if name:find("^lualine_b_diff") or name:find("^lualine_b_diagnostics") then
			vim.api.nvim_set_hl(0, name, { bold = true, update = true })
		end
	end
end

-- Diff counts for lualine's diff component, sourced from gitsigns' status dict.
local function gitsigns_diff()
	local gs = vim.b.gitsigns_status_dict
	if gs then
		return { added = gs.added, modified = gs.changed, removed = gs.removed }
	end
end

local lualine_sections = {
	lualine_b = { "branch", { "diff", source = gitsigns_diff }, "diagnostics" },
	lualine_c = { statuses },
	lualine_x = { "encoding", "fileformat", "filetype", "lsp_status" },
}

local lualine_options = {
	disabled_filetypes = { "netrw", "qf" },
	component_separators = { left = "|", right = "|" },
	section_separators = { left = "", right = "" },
}

-- Pick the lualine theme from 'background' and (re)apply the configuration.
local function configure_lualine()
	lualine_options.theme = vim.o.background == "light" and "solarized_light" or "solarized_dark"
	lualine.setup({ options = lualine_options, sections = lualine_sections })
end

--[[ Statusline applier: re-pick the lualine theme for the current 'background', then re-bold the
diff/diagnostics groups once lualine has rebuilt them. ]]
local function apply_statusline_highlights()
	configure_lualine()
	vim.schedule(bold_diff_diag)
end

----------------------------------------------------------------------------------------------------
-- Tabline -----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------

--[[ Build the window-count segment of a tabpage label, e.g. ` (2)[+] ` (no styling).
`is_modified` appends the `[+]` marker when any window in the tab has pending changes. ]]
local function make_tab_info_label(win_count, is_modified)
	local tab_win_count = "(" .. (win_count >= 10 and ">9" or tostring(win_count)) .. ")"
	local tab_win_count_suffix = is_modified and "[+] " or " "
	return " " .. tab_win_count .. tab_win_count_suffix
end

--[[ Build the title segment of a tabpage label, padded or truncated to `width` (no styling).
Uses the tab's `tabname` if set, else the active buffer's file name, else `[No Name]`, prefixed with
`*` for the current tab and `-` for the last-accessed tab. ]]
local function make_tab_label(id, is_current, width)
	local is_last_accessed = vim.api.nvim_tabpage_get_number(id) == vim.fn.tabpagenr("#")
	local tab_label_prefix = (is_current and "*" or "") .. (is_last_accessed and "-" or "")
	local tab_label = vim.t[id].tabname
	if tab_label == nil or tab_label == "" then
		local win = vim.api.nvim_tabpage_get_win(id)
		local bufname = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(win))
		tab_label = vim.fs.basename(bufname)
	end
	if tab_label == "" then
		tab_label = "[No Name]"
	end
	tab_label = tab_label_prefix .. tab_label
	local display_width = vim.fn.strdisplaywidth(tab_label)
	return (display_width > width) and (vim.fn.strcharpart(tab_label, 0, width - 1) .. "…")
		or (tab_label .. string.rep(" ", width - display_width))
end

--[[ Return a string compatible with the 'tabline' option, used to set the tabline content and style.
Declared as a global function to be available as `v:lua.GetTabLine` in the Vimscript engine for the
'tabline' option. See <nvim-help://%27statusline%27>. ]]
function GetTabLine()
	local max_tab_label_width = 30
	local s = ""
	for _, id in pairs(vim.api.nvim_list_tabpages()) do
		local is_current = id == vim.api.nvim_get_current_tabpage()
		local is_modified = false
		local wins = vim.api.nvim_tabpage_list_wins(id)
		for _, win in ipairs(wins) do
			if vim.bo[vim.api.nvim_win_get_buf(win)].modified then
				is_modified = true
				break
			end
		end

		-- Mark the start of the tabpage label (where the clickable region begins).
		s = s .. "%" .. vim.api.nvim_tabpage_get_number(id) .. "T"

		local tab_info_label = make_tab_info_label(#wins, is_modified)
		local tab_label_width = max_tab_label_width - vim.fn.strdisplaywidth(tab_info_label)
		local tab_label = make_tab_label(id, is_current, tab_label_width)

		s = s .. (is_current and "%#TabLineSelWinCount#" or "%#TabLineWinCount#") .. tab_info_label
		s = s .. (is_current and "%#TabLineSel#" or "%#TabLine#") .. tab_label
	end
	return s .. "%#TabLineFill#"
end

-- Name the current tab from the command argument.
local function rename_tab(opts)
	vim.t.tabname = opts.args
	vim.cmd.redrawtabline()
end

-- Name the current tab after the active buffer's file name (without extension).
local function rename_tab_to_cur_filename()
	vim.t.tabname = vim.fn.expand("%:t:r")
	vim.cmd.redrawtabline()
end

----------------------------------------------------------------------------------------------------
-- Commands & init ---------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------

-- Flip between the light and dark backgrounds (the colorscheme reloads, OptionSet re-themes).
local function toggle_theme()
	vim.opt.background = vim.o.background == "light" and "dark" or "light"
	vim.notify("Color theme set to " .. vim.o.background)
end

-- Show the resolved lualine configuration.
local function show_lualine_config()
	vim.notify(vim.inspect(lualine.get_config()))
end

color.register(apply_solarized_highlights)
color.register(apply_tabpage_title_highlights)
color.register(apply_statusline_highlights)

-- stylua: ignore start
vim.api.nvim_create_user_command("ColorsBgToggle", toggle_theme, { desc = "Toggle light/dark theme" })
vim.api.nvim_create_user_command("TabRename", rename_tab, { nargs = 1 })
vim.api.nvim_create_user_command("TabRenameFileName", rename_tab_to_cur_filename, { nargs = 0 })
vim.api.nvim_create_user_command("LualineConfig", show_lualine_config, { desc = "Show lualine configuration" })
-- stylua: ignore end

-- See <nvim-help://stl-%25%21>.
vim.o.tabline = "%!v:lua.GetTabLine()"
vim.cmd.colorscheme("solarized")
vim.opt.background = "dark"