├── .github └── workflows │ └── check.yml ├── .stylua.toml ├── LICENSE ├── README.md └── plugin └── init.lua /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: 3 | push: 4 | paths: 5 | - "**.lua" 6 | pull_request: 7 | paths: 8 | - "**.lua" 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Luacheck lint 17 | uses: lunarmodules/luacheck@v1 18 | - uses: JohnnyMorganz/stylua-action@v2 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | version: latest 22 | args: --check . 23 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 80 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferDouble" 6 | collapse_simple_statement = "Never" 7 | call_parentheses = "Always" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nekowinston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wezterm.bar 2 | 3 | A [WezTerm](https://wezfurlong.org/wezterm/) [plugin](https://github.com/wez/wezterm/commit/e4ae8a844d8feaa43e1de34c5cc8b4f07ce525dd) 4 | that makes your retro bar look nice. 5 | 6 | ```lua 7 | local wezterm = require("wezterm") 8 | local c = wezterm.config_builder() 9 | 10 | -- build your config according to 11 | -- https://wezfurlong.org/wezterm/config/lua/wezterm/config_builder.html 12 | 13 | -- the plugin is currently made for Catppuccin only 14 | c.color_scheme = "Catppuccin Mocha" 15 | 16 | -- then finally apply the plugin 17 | -- these are currently the defaults: 18 | wezterm.plugin.require("https://github.com/nekowinston/wezterm-bar").apply_to_config(c, { 19 | position = "bottom", 20 | max_width = 32, 21 | dividers = "slant_right", -- or "slant_left", "arrows", "rounded", false 22 | indicator = { 23 | leader = { 24 | enabled = true, 25 | off = " ", 26 | on = " ", 27 | }, 28 | mode = { 29 | enabled = true, 30 | names = { 31 | resize_mode = "RESIZE", 32 | copy_mode = "VISUAL", 33 | search_mode = "SEARCH", 34 | }, 35 | }, 36 | }, 37 | tabs = { 38 | numerals = "arabic", -- or "roman" 39 | pane_count = "superscript", -- or "subscript", false 40 | brackets = { 41 | active = { "", ":" }, 42 | inactive = { "", ":" }, 43 | }, 44 | }, 45 | clock = { -- note that this overrides the whole set_right_status 46 | enabled = true, 47 | format = "%H:%M", -- use https://wezfurlong.org/wezterm/config/lua/wezterm.time/Time/format.html 48 | }, 49 | }) 50 | 51 | return c 52 | ``` 53 | -------------------------------------------------------------------------------- /plugin/init.lua: -------------------------------------------------------------------------------- 1 | local wezterm = require("wezterm") 2 | 3 | local M = {} 4 | 5 | -- default configuration 6 | local config = { 7 | position = "bottom", 8 | max_width = 32, 9 | dividers = "slant_right", 10 | indicator = { 11 | leader = { 12 | enabled = true, 13 | off = " ", 14 | on = " ", 15 | }, 16 | mode = { 17 | enabled = true, 18 | names = { 19 | resize_mode = "RESIZE", 20 | copy_mode = "VISUAL", 21 | search_mode = "SEARCH", 22 | }, 23 | }, 24 | }, 25 | tabs = { 26 | numerals = "arabic", 27 | pane_count = "superscript", 28 | brackets = { 29 | active = { "", ":" }, 30 | inactive = { "", ":" }, 31 | }, 32 | }, 33 | clock = { 34 | enabled = true, 35 | format = "%H:%M", 36 | }, 37 | } 38 | 39 | -- parsed config 40 | local C = {} 41 | 42 | local function tableMerge(t1, t2) 43 | for k, v in pairs(t2) do 44 | if type(v) == "table" then 45 | if type(t1[k] or false) == "table" then 46 | tableMerge(t1[k] or {}, t2[k] or {}) 47 | else 48 | t1[k] = v 49 | end 50 | else 51 | t1[k] = v 52 | end 53 | end 54 | return t1 55 | end 56 | 57 | local dividers = { 58 | slant_right = { 59 | left = utf8.char(0xe0be), 60 | right = utf8.char(0xe0bc), 61 | }, 62 | slant_left = { 63 | left = utf8.char(0xe0ba), 64 | right = utf8.char(0xe0b8), 65 | }, 66 | arrows = { 67 | left = utf8.char(0xe0b2), 68 | right = utf8.char(0xe0b0), 69 | }, 70 | rounded = { 71 | left = utf8.char(0xe0b6), 72 | right = utf8.char(0xe0b4), 73 | }, 74 | } 75 | 76 | -- conforming to https://github.com/wez/wezterm/commit/e4ae8a844d8feaa43e1de34c5cc8b4f07ce525dd 77 | -- exporting an apply_to_config function, even though we don't change the users config 78 | M.apply_to_config = function(c, opts) 79 | -- make the opts arg optional 80 | if not opts then 81 | opts = {} 82 | end 83 | 84 | -- combine user config with defaults 85 | config = tableMerge(config, opts) 86 | C.div = { 87 | l = "", 88 | r = "", 89 | } 90 | 91 | if config.dividers then 92 | C.div.l = dividers[config.dividers].left 93 | C.div.r = dividers[config.dividers].right 94 | end 95 | 96 | C.leader = { 97 | enabled = config.indicator.leader.enabled and true, 98 | off = config.indicator.leader.off, 99 | on = config.indicator.leader.on, 100 | } 101 | 102 | C.mode = { 103 | enabled = config.indicator.mode.enabled, 104 | names = config.indicator.mode.names, 105 | } 106 | 107 | C.tabs = { 108 | numerals = config.tabs.numerals, 109 | pane_count_style = config.tabs.pane_count, 110 | brackets = { 111 | active = config.tabs.brackets.active, 112 | inactive = config.tabs.brackets.inactive, 113 | }, 114 | } 115 | 116 | C.clock = { 117 | enabled = config.clock.enabled, 118 | format = config.clock.format, 119 | } 120 | 121 | -- set the right-hand padding to 0 spaces, if the rounded style is active 122 | C.p = (config.dividers == "rounded") and "" or " " 123 | 124 | -- set wezterm config options according to the parsed config 125 | c.use_fancy_tab_bar = false 126 | c.tab_bar_at_bottom = config.position == "bottom" 127 | c.tab_max_width = config.max_width 128 | end 129 | 130 | -- superscript/subscript 131 | local function numberStyle(number, script) 132 | local scripts = { 133 | superscript = { 134 | "⁰", 135 | "¹", 136 | "²", 137 | "³", 138 | "⁴", 139 | "⁵", 140 | "⁶", 141 | "⁷", 142 | "⁸", 143 | "⁹", 144 | }, 145 | subscript = { 146 | "₀", 147 | "₁", 148 | "₂", 149 | "₃", 150 | "₄", 151 | "₅", 152 | "₆", 153 | "₇", 154 | "₈", 155 | "₉", 156 | }, 157 | } 158 | local numbers = scripts[script] 159 | local number_string = tostring(number) 160 | local result = "" 161 | for i = 1, #number_string do 162 | local char = number_string:sub(i, i) 163 | local num = tonumber(char) 164 | if num then 165 | result = result .. numbers[num + 1] 166 | else 167 | result = result .. char 168 | end 169 | end 170 | return result 171 | end 172 | 173 | local roman_numerals = { 174 | "Ⅰ", 175 | "Ⅱ", 176 | "Ⅲ", 177 | "Ⅳ", 178 | "Ⅴ", 179 | "Ⅵ", 180 | "Ⅶ", 181 | "Ⅷ", 182 | "Ⅸ", 183 | "Ⅹ", 184 | "Ⅺ", 185 | "Ⅻ", 186 | } 187 | 188 | -- custom tab bar 189 | wezterm.on( 190 | "format-tab-title", 191 | function(tab, tabs, _panes, conf, _hover, _max_width) 192 | local colours = conf.resolved_palette.tab_bar 193 | 194 | local active_tab_index = 0 195 | for _, t in ipairs(tabs) do 196 | if t.is_active == true then 197 | active_tab_index = t.tab_index 198 | end 199 | end 200 | 201 | -- TODO: make colors configurable 202 | local rainbow = { 203 | conf.resolved_palette.ansi[2], 204 | conf.resolved_palette.indexed[16], 205 | conf.resolved_palette.ansi[4], 206 | conf.resolved_palette.ansi[3], 207 | conf.resolved_palette.ansi[5], 208 | conf.resolved_palette.ansi[6], 209 | } 210 | 211 | local i = tab.tab_index % 6 212 | local active_bg = rainbow[i + 1] 213 | local active_fg = colours.background 214 | local inactive_bg = colours.inactive_tab.bg_color 215 | local inactive_fg = colours.inactive_tab.fg_color 216 | local new_tab_bg = colours.new_tab.bg_color 217 | 218 | local s_bg, s_fg, e_bg, e_fg 219 | 220 | -- the last tab 221 | if tab.tab_index == #tabs - 1 then 222 | if tab.is_active then 223 | s_bg = active_bg 224 | s_fg = active_fg 225 | e_bg = new_tab_bg 226 | e_fg = active_bg 227 | else 228 | s_bg = inactive_bg 229 | s_fg = inactive_fg 230 | e_bg = new_tab_bg 231 | e_fg = inactive_bg 232 | end 233 | elseif tab.tab_index == active_tab_index - 1 then 234 | s_bg = inactive_bg 235 | s_fg = inactive_fg 236 | e_bg = rainbow[(i + 1) % 6 + 1] 237 | e_fg = inactive_bg 238 | elseif tab.is_active then 239 | s_bg = active_bg 240 | s_fg = active_fg 241 | e_bg = inactive_bg 242 | e_fg = active_bg 243 | else 244 | s_bg = inactive_bg 245 | s_fg = inactive_fg 246 | e_bg = inactive_bg 247 | e_fg = inactive_bg 248 | end 249 | 250 | local pane_count = "" 251 | if C.tabs.pane_count_style then 252 | local tabi = wezterm.mux.get_tab(tab.tab_id) 253 | local muxpanes = tabi:panes() 254 | local count = #muxpanes == 1 and "" or tostring(#muxpanes) 255 | pane_count = numberStyle(count, C.tabs.pane_count_style) 256 | end 257 | 258 | local index_i 259 | if C.tabs.numerals == "roman" then 260 | index_i = roman_numerals[tab.tab_index + 1] 261 | else 262 | index_i = tab.tab_index + 1 263 | end 264 | 265 | local index 266 | if tab.is_active then 267 | index = string.format( 268 | "%s%s%s ", 269 | C.tabs.brackets.active[1], 270 | index_i, 271 | C.tabs.brackets.active[2] 272 | ) 273 | else 274 | index = string.format( 275 | "%s%s%s ", 276 | C.tabs.brackets.inactive[1], 277 | index_i, 278 | C.tabs.brackets.inactive[2] 279 | ) 280 | end 281 | 282 | -- start and end hardcoded numbers are the Powerline + " " padding 283 | local fillerwidth = 2 + string.len(index) + string.len(pane_count) + 2 284 | 285 | local tabtitle = tab.active_pane.title 286 | local width = conf.tab_max_width - fillerwidth - 1 287 | if (#tabtitle + fillerwidth) > conf.tab_max_width then 288 | tabtitle = wezterm.truncate_right(tabtitle, width) .. "…" 289 | end 290 | 291 | local title = string.format(" %s%s%s%s", index, tabtitle, pane_count, C.p) 292 | 293 | return { 294 | { Background = { Color = s_bg } }, 295 | { Foreground = { Color = s_fg } }, 296 | { Text = title }, 297 | { Background = { Color = e_bg } }, 298 | { Foreground = { Color = e_fg } }, 299 | { Text = C.div.r }, 300 | } 301 | end 302 | ) 303 | 304 | wezterm.on("update-status", function(window, _pane) 305 | local active_kt = window:active_key_table() ~= nil 306 | local show = C.leader.enabled or (active_kt and C.mode.enabled) 307 | if not show then 308 | window:set_left_status("") 309 | return 310 | end 311 | 312 | local present, conf = pcall(window.effective_config, window) 313 | if not present then 314 | return 315 | end 316 | local palette = conf.resolved_palette 317 | 318 | local leader = "" 319 | if C.leader.enabled then 320 | local leader_text = C.leader.off 321 | if window:leader_is_active() then 322 | leader_text = C.leader.on 323 | end 324 | leader = wezterm.format({ 325 | { Foreground = { Color = palette.background } }, 326 | { Background = { Color = palette.ansi[5] } }, 327 | { Text = " " .. leader_text .. C.p }, 328 | }) 329 | end 330 | 331 | local mode = "" 332 | if C.mode.enabled then 333 | local mode_text = "" 334 | local active = window:active_key_table() 335 | if C.mode.names[active] ~= nil then 336 | mode_text = C.mode.names[active] .. "" 337 | end 338 | mode = wezterm.format({ 339 | { Foreground = { Color = palette.background } }, 340 | { Background = { Color = palette.ansi[5] } }, 341 | { Attribute = { Intensity = "Bold" } }, 342 | { Text = mode_text }, 343 | "ResetAttributes", 344 | }) 345 | end 346 | 347 | local first_tab_active = window:mux_window():tabs_with_info()[1].is_active 348 | local divider_bg = first_tab_active and palette.ansi[2] 349 | or palette.tab_bar.inactive_tab.bg_color 350 | 351 | local divider = wezterm.format({ 352 | { Background = { Color = divider_bg } }, 353 | { Foreground = { Color = palette.ansi[5] } }, 354 | { Text = C.div.r }, 355 | }) 356 | 357 | window:set_left_status(leader .. mode .. divider) 358 | 359 | if C.clock.enabled then 360 | local time = wezterm.time.now():format(C.clock.format) 361 | window:set_right_status(wezterm.format({ 362 | { Background = { Color = palette.tab_bar.background } }, 363 | { Foreground = { Color = palette.ansi[6] } }, 364 | { Text = time }, 365 | })) 366 | end 367 | end) 368 | 369 | return M 370 | --------------------------------------------------------------------------------