From 198f30045a9ed3fe953e611918c8ed2bb801051c Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:10:36 -0700 Subject: [PATCH 01/13] Switch back from Ghostty to Wezterm Move wezterm.lua out of no-longer-used/ to wezterm/ and bring it up to parity with the previous Ghostty setup. Highlights: - Use Monaspace variable fonts (Neon/Xenon/Radon/Krypton Var) with Light weight for a thinner stroke - Use Monaspace Neon NF as a scaled fallback so nerd font icons render at the right size (use_cap_height_to_scale_fallback_fonts + scale) - Enable ligatures and texture healing via harfbuzz features (calt, liga, dlig, clig, ss01-ss10) - Apply freetype_load_flags = NO_HINTING to make ligatures actually render with Monaspace Var (per wez/wezterm#4874) - OLED Catppuccin theme and custom tab title formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../wezterm => wezterm}/wezterm.lua | 65 ++++++++++++------- 1 file changed, 43 insertions(+), 22 deletions(-) rename {no-longer-used/wezterm => wezterm}/wezterm.lua (67%) diff --git a/no-longer-used/wezterm/wezterm.lua b/wezterm/wezterm.lua similarity index 67% rename from no-longer-used/wezterm/wezterm.lua rename to wezterm/wezterm.lua index a01da7d..b83457a 100644 --- a/no-longer-used/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -1,12 +1,10 @@ --- I use a custom icon from: git@github.com:mikker/wezterm-icon.git -- Pull in the wezterm API local wezterm = require("wezterm") -- This will hold the configuration. local config = wezterm.config_builder() --- Colour scheme -config.color_scheme = "Catppuccin Mocha" +-- Colour scheme - Catppuccin Mocha with OLED-friendly customizations local custom = wezterm.color.get_builtin_schemes()["Catppuccin Mocha"] custom.background = "#000000" custom.tab_bar.background = "#040404" @@ -17,29 +15,57 @@ config.color_schemes = { } config.color_scheme = "OLEDppuccin" --- Fonts --- config.experimental_svg_fonts = true +-- Fonts - Monaspace variable fonts with NF (Nerd Font) fallback for icons config.font = wezterm.font_with_fallback({ - "Monaspace Neon Var", + { + family = "Monaspace Neon Var", + weight = "Light", + }, + { + family = "Monaspace Neon NF", + scale = 1.2, + }, "JetBrains Mono", "Fira Code", - "Hack Nerd Font", { family = "Apple Color Emoji", assume_emoji_presentation = true, scale = 2, }, }) -config.line_height = 1.2 +-- Use cap-height scaling for fallback fonts (helps nerd font icons match) +config.use_cap_height_to_scale_fallback_fonts = true + +config.font_size = 15.0 +config.line_height = 1.2 config.allow_square_glyphs_to_overflow_width = "Always" +-- Fix for Monaspace ligature rendering issues (per Wezterm maintainer) +-- See: https://github.com/wez/wezterm/issues/4874 +config.freetype_load_flags = "NO_HINTING" + +-- OpenType features - enable ligatures and Monaspace texture healing +config.harfbuzz_features = { + "calt=1", + "liga=1", + "dlig=1", + "ss01=1", + "ss02=1", + "ss03=1", + "ss04=1", + "ss05=1", + "ss06=1", + "ss09=1", +} + +-- Font rules for different styles using Monaspace variants config.font_rules = { { intensity = "Bold", italic = true, font = wezterm.font({ - family = "Monaspace Xenon Var", + family = "Monaspace Krypton Var", weight = "Bold", style = "Italic", }), @@ -67,14 +93,10 @@ config.font_rules = { font = wezterm.font({ family = "Monaspace Xenon Var", weight = "Bold", - style = "Italic", }), }, } -config.font_size = 14.0 -config.harfbuzz_features = { "ss01=1", "ss02=1", "ss03=1", "ss04=1", "ss05=1", "ss06=1", "ss07=1", "dlig=1", "calt=1" } - -- Tabs local function tab_title(tab_info) local title = tab_info.tab_title @@ -82,19 +104,18 @@ local function tab_title(tab_info) if title and #title > 0 then return title end - -- Otherwise, use the title from the active pane - -- in that tab + -- Otherwise, use the title from the active pane in that tab if tab_info.active_pane.title and tab_info.active_pane.title:find("codespaces") ~= nil then - return "" + return "" elseif tab_info.active_pane.title and tab_info.active_pane.title:find("^pt") ~= nil then - return "" + return "" elseif tab_info.active_pane.title and tab_info.active_pane.title:find("server") ~= nil then - return " " .. tab_info.active_pane.title + return " " .. tab_info.active_pane.title elseif tab_info.active_pane.title and tab_info.active_pane.title:find("pi") ~= nil then - return " " .. tab_info.active_pane.title + return " " .. tab_info.active_pane.title end - return " " .. tab_info.active_pane.title + return " " .. tab_info.active_pane.title end wezterm.on("format-tab-title", function(tab) @@ -106,11 +127,11 @@ wezterm.on("format-tab-title", function(tab) end return title end) -config.hide_tab_bar_if_only_one_tab = true +config.hide_tab_bar_if_only_one_tab = true config.window_decorations = "RESIZE" --- Other stuff +-- Window configuration config.quit_when_all_windows_are_closed = false config.window_close_confirmation = "NeverPrompt" config.initial_rows = 40 From 35d37dc4eb88afdb389e2f7b18663a432355ba3b Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:22:18 -0700 Subject: [PATCH 02/13] Add hyperlink rules, kitty keyboard, scrollback, mute bell - Default hyperlink rules so URLs/commit SHAs/issue refs are clickable - enable_kitty_keyboard so nvim/tmux can see Ctrl+Shift+ properly - 10k line scrollback - Disable audible bell Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index b83457a..4051b6a 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -131,6 +131,16 @@ end) config.hide_tab_bar_if_only_one_tab = true config.window_decorations = "RESIZE" +-- Make URLs, commit SHAs, issue refs, etc. clickable +config.hyperlink_rules = wezterm.default_hyperlink_rules() + +-- Better key reporting (CSI u) so nvim/tmux see modifiers like Ctrl+Shift+... +config.enable_kitty_keyboard = true + +-- Quality-of-life +config.scrollback_lines = 10000 +config.audible_bell = "Disabled" + -- Window configuration config.quit_when_all_windows_are_closed = false config.window_close_confirmation = "NeverPrompt" From 1a68313245dea3686af69a1ab5345b7bc9894d48 Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:28:11 -0700 Subject: [PATCH 03/13] Use fancy tab bar with integrated traffic lights, process icons, OSC 9;4 progress - use_fancy_tab_bar = true (native-looking tab bar) - INTEGRATED_BUTTONS|RESIZE so traffic lights live in the tab strip - window_frame styled to match the OLED theme, using Monaspace Neon Var - format-tab-title now shows: * existing host-specific icons (codespaces / pt / server / pi) * a process icon for non-SSH tabs (nvim, fish, git, yazi, docker, etc.) * an OSC 9;4 progress glyph + percentage from pane:get_progress() * an indeterminate spinner / error glyph for non-percent progress states * a bell glyph when a tab has unseen output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 103 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 4051b6a..3bcbf66 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -98,6 +98,35 @@ config.font_rules = { } -- Tabs +local process_icons = { + ["nvim"] = wezterm.nerdfonts.custom_vim, + ["vim"] = wezterm.nerdfonts.custom_vim, + ["node"] = wezterm.nerdfonts.dev_nodejs_small, + ["npm"] = wezterm.nerdfonts.dev_npm, + ["yarn"] = wezterm.nerdfonts.seti_yarn, + ["python"] = wezterm.nerdfonts.dev_python, + ["python3"] = wezterm.nerdfonts.dev_python, + ["ruby"] = wezterm.nerdfonts.dev_ruby, + ["go"] = wezterm.nerdfonts.dev_go, + ["cargo"] = wezterm.nerdfonts.dev_rust, + ["fish"] = wezterm.nerdfonts.dev_terminal, + ["bash"] = wezterm.nerdfonts.dev_terminal, + ["zsh"] = wezterm.nerdfonts.dev_terminal, + ["ssh"] = wezterm.nerdfonts.md_console_network, + ["git"] = wezterm.nerdfonts.dev_git, + ["lazygit"] = wezterm.nerdfonts.dev_git, + ["yazi"] = wezterm.nerdfonts.md_folder_open, + ["btm"] = wezterm.nerdfonts.md_chart_areaspline, + ["docker"] = wezterm.nerdfonts.linux_docker, + ["make"] = wezterm.nerdfonts.seti_makefile, +} + +local function process_icon(pane_info) + local proc = pane_info.foreground_process_name or "" + proc = proc:match("([^/\\]+)$") or proc + return process_icons[proc] or wezterm.nerdfonts.cod_terminal +end + local function tab_title(tab_info) local title = tab_info.tab_title -- if the tab title is explicitly set, take that @@ -105,31 +134,77 @@ local function tab_title(tab_info) return title end -- Otherwise, use the title from the active pane in that tab - if tab_info.active_pane.title and tab_info.active_pane.title:find("codespaces") ~= nil then + local pane_title = tab_info.active_pane.title or "" + if pane_title:find("codespaces") ~= nil then return "" - elseif tab_info.active_pane.title and tab_info.active_pane.title:find("^pt") ~= nil then + elseif pane_title:find("^pt") ~= nil then return "" - elseif tab_info.active_pane.title and tab_info.active_pane.title:find("server") ~= nil then - return " " .. tab_info.active_pane.title - elseif tab_info.active_pane.title and tab_info.active_pane.title:find("pi") ~= nil then - return " " .. tab_info.active_pane.title + elseif pane_title:find("server") ~= nil then + return " " .. pane_title + elseif pane_title:find("pi") ~= nil then + return " " .. pane_title end - return " " .. tab_info.active_pane.title + return process_icon(tab_info.active_pane) .. " " .. pane_title +end + +-- Progress glyphs (filled circle slices) for OSC 9;4 progress +local PCT_GLYPHS = { + wezterm.nerdfonts.md_circle_slice_1, + wezterm.nerdfonts.md_circle_slice_2, + wezterm.nerdfonts.md_circle_slice_3, + wezterm.nerdfonts.md_circle_slice_4, + wezterm.nerdfonts.md_circle_slice_5, + wezterm.nerdfonts.md_circle_slice_6, + wezterm.nerdfonts.md_circle_slice_7, + wezterm.nerdfonts.md_circle_slice_8, +} + +local function progress_suffix(pane_info) + if not pane_info.pane_id then + return "" + end + local mux_pane = wezterm.mux.get_pane(pane_info.pane_id) + if not mux_pane or not mux_pane.get_progress then + return "" + end + local p = mux_pane:get_progress() + if type(p) == "table" then + if p.Percentage then + local idx = math.min(8, math.max(1, math.ceil(p.Percentage / 12.5))) + return " " .. PCT_GLYPHS[idx] .. " " .. p.Percentage .. "%" + elseif p.Error then + return " " .. wezterm.nerdfonts.md_alert_circle + end + elseif p == "Indeterminate" then + return " " .. wezterm.nerdfonts.md_loading + end + return "" end wezterm.on("format-tab-title", function(tab) local title = tab_title(tab) - if tab.is_active then - return { - { Text = " " .. title .. " " }, - } - end - return title + local progress = progress_suffix(tab.active_pane) + local bell = tab.active_pane.has_unseen_output and (" " .. wezterm.nerdfonts.cod_bell) or "" + return { + { Text = " " .. title .. progress .. bell .. " " }, + } end) +-- Use the macOS-native fancy tab bar with integrated traffic lights +config.use_fancy_tab_bar = true +config.window_decorations = "INTEGRATED_BUTTONS|RESIZE" +config.integrated_title_button_style = "MacOsNative" +config.show_new_tab_button_in_tab_bar = true config.hide_tab_bar_if_only_one_tab = true -config.window_decorations = "RESIZE" + +-- Title-bar / fancy tab bar styling (matches OLED Catppuccin theme) +config.window_frame = { + font = wezterm.font({ family = "Monaspace Neon Var", weight = "Medium" }), + font_size = 13.0, + active_titlebar_bg = "#040404", + inactive_titlebar_bg = "#040404", +} -- Make URLs, commit SHAs, issue refs, etc. clickable config.hyperlink_rules = wezterm.default_hyperlink_rules() From 102efdc258c7a492d16e24b53f477dcabca35eaa Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:31:21 -0700 Subject: [PATCH 04/13] Always show wezterm tab bar so integrated traffic lights have a strip With INTEGRATED_BUTTONS, hiding the tab bar leaves the traffic lights overlapping the terminal content. Keep the tab bar visible at all times. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 3bcbf66..99ab34a 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -196,7 +196,8 @@ config.use_fancy_tab_bar = true config.window_decorations = "INTEGRATED_BUTTONS|RESIZE" config.integrated_title_button_style = "MacOsNative" config.show_new_tab_button_in_tab_bar = true -config.hide_tab_bar_if_only_one_tab = true +-- Always show the tab bar so the integrated traffic lights have a place to live +config.hide_tab_bar_if_only_one_tab = false -- Title-bar / fancy tab bar styling (matches OLED Catppuccin theme) config.window_frame = { From 34fbac42aae78ca1d1d532b640f307fdeb47e66f Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:34:28 -0700 Subject: [PATCH 05/13] Smooth/animated OSC 9;4 progress indicators Replace the static circle-slice glyph with a richer progress display: - Percentage progress: smooth 5-cell horizontal bar built from Unicode eighth-blocks (\xe2\x96\x8f \xe2\x96\x8e \xe2\x96\x8d \xe2\x96\x8c \xe2\x96\x8b \xe2\x96\x8a \xe2\x96\x89 \xe2\x96\x88), the look NSProgressIndicator / uv / cargo / starship use, plus a numeric percent - Error progress: red alert glyph + percent - Indeterminate progress: animated braille spinner (\xe2\xa0\x8b \xe2\xa0\x99 \xe2\xa0\xb9 \xe2\xa0\xb8 \xe2\xa0\xbc \xe2\xa0\xb4 \xe2\xa0\xa6 \xe2\xa0\xa7 \xe2\xa0\x87 \xe2\xa0\x8f) advanced via the update-status event, with status_update_interval = 200ms (~5fps) - Catppuccin-mocha colors for each state (blue/red/yellow) format-tab-title now returns a multi-attribute format list so the progress segment gets its own foreground color independent of the tab. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 94 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 99ab34a..ef8c448 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -148,49 +148,97 @@ local function tab_title(tab_info) return process_icon(tab_info.active_pane) .. " " .. pane_title end --- Progress glyphs (filled circle slices) for OSC 9;4 progress -local PCT_GLYPHS = { - wezterm.nerdfonts.md_circle_slice_1, - wezterm.nerdfonts.md_circle_slice_2, - wezterm.nerdfonts.md_circle_slice_3, - wezterm.nerdfonts.md_circle_slice_4, - wezterm.nerdfonts.md_circle_slice_5, - wezterm.nerdfonts.md_circle_slice_6, - wezterm.nerdfonts.md_circle_slice_7, - wezterm.nerdfonts.md_circle_slice_8, +-- Smooth horizontal progress bar using Unicode eighth-blocks +-- (matches the look of NSProgressIndicator / starship / cargo / uv) +local PARTIAL_BLOCKS = { "▏", "▎", "▍", "▌", "▋", "▊", "▉" } +local function progress_bar(pct, width) + width = width or 5 + pct = math.max(0, math.min(100, pct)) + local eighths = math.floor((pct * width * 8 / 100) + 0.5) + local full = math.floor(eighths / 8) + local rem = eighths % 8 + local s = string.rep("█", full) + if rem > 0 and full < width then + s = s .. PARTIAL_BLOCKS[rem] + s = s .. string.rep(" ", width - full - 1) + else + s = s .. string.rep(" ", width - full) + end + return s +end + +-- Animated braille spinner driven by the update-status event +local SPINNER = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } +local spinner_state = { idx = 1 } + +-- Catppuccin-mocha accent colors for progress states +local PROGRESS_COLORS = { + normal = "#89b4fa", -- blue + error = "#f38ba8", -- red + indeterminate = "#f9e2af", -- yellow + dim = "#6c7086", -- overlay (for empty bar slots) } -local function progress_suffix(pane_info) +-- Returns a wezterm format-list (or nil) for the progress portion of a tab title +local function progress_format(pane_info) if not pane_info.pane_id then - return "" + return nil end local mux_pane = wezterm.mux.get_pane(pane_info.pane_id) if not mux_pane or not mux_pane.get_progress then - return "" + return nil end local p = mux_pane:get_progress() if type(p) == "table" then if p.Percentage then - local idx = math.min(8, math.max(1, math.ceil(p.Percentage / 12.5))) - return " " .. PCT_GLYPHS[idx] .. " " .. p.Percentage .. "%" - elseif p.Error then - return " " .. wezterm.nerdfonts.md_alert_circle + return { + { Foreground = { Color = PROGRESS_COLORS.normal } }, + { Text = " " .. progress_bar(p.Percentage) .. " " .. p.Percentage .. "%" }, + "ResetAttributes", + } + elseif p.Error ~= nil then + return { + { Foreground = { Color = PROGRESS_COLORS.error } }, + { Text = " " .. wezterm.nerdfonts.md_alert_circle .. " " .. (p.Error or 0) .. "%" }, + "ResetAttributes", + } end elseif p == "Indeterminate" then - return " " .. wezterm.nerdfonts.md_loading + return { + { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, + { Text = " " .. SPINNER[spinner_state.idx] }, + "ResetAttributes", + } end - return "" + return nil end wezterm.on("format-tab-title", function(tab) local title = tab_title(tab) - local progress = progress_suffix(tab.active_pane) + local progress = progress_format(tab.active_pane) local bell = tab.active_pane.has_unseen_output and (" " .. wezterm.nerdfonts.cod_bell) or "" - return { - { Text = " " .. title .. progress .. bell .. " " }, - } + + local out = { { Text = " " .. title } } + if progress then + for _, item in ipairs(progress) do + table.insert(out, item) + end + end + table.insert(out, { Text = bell .. " " }) + return out end) +-- Drive the spinner animation by advancing the frame on each status tick. +-- Setting the (empty) left status forces a tab-bar redraw so format-tab-title +-- re-runs and picks up the new spinner frame. +wezterm.on("update-status", function(window) + spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 + window:set_left_status("") +end) + +-- ~5 fps spinner animation without burning CPU +config.status_update_interval = 200 + -- Use the macOS-native fancy tab bar with integrated traffic lights config.use_fancy_tab_bar = true config.window_decorations = "INTEGRATED_BUTTONS|RESIZE" From 7c858e18b1c03d41b8c203ae9a6936a301957d3e Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:37:06 -0700 Subject: [PATCH 06/13] Set spinner interval to 67ms (~15 fps) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index ef8c448..7d9635e 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -236,8 +236,8 @@ wezterm.on("update-status", function(window) window:set_left_status("") end) --- ~5 fps spinner animation without burning CPU -config.status_update_interval = 200 +-- ~15 fps spinner animation without burning CPU +config.status_update_interval = 67 -- Use the macOS-native fancy tab bar with integrated traffic lights config.use_fancy_tab_bar = true From 3663d6e0d0e52060bb32c6aa6c0b3960014829f4 Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:40:40 -0700 Subject: [PATCH 07/13] 60fps spinner with idle gating Bump status_update_interval to 16ms (60fps) but only force a tab-bar redraw when at least one pane currently has indeterminate progress. When nothing is animating, update-status iterates panes and returns without calling set_left_status, so idle CPU cost stays at ~0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 7d9635e..7455260 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -228,16 +228,34 @@ wezterm.on("format-tab-title", function(tab) return out end) +-- Returns true if any pane in this window currently has indeterminate progress +local function any_indeterminate(window) + local mux_win = window:mux_window() + if not mux_win then + return false + end + for _, tab in ipairs(mux_win:tabs()) do + for _, pane in ipairs(tab:panes()) do + if pane.get_progress and pane:get_progress() == "Indeterminate" then + return true + end + end + end + return false +end + -- Drive the spinner animation by advancing the frame on each status tick. --- Setting the (empty) left status forces a tab-bar redraw so format-tab-title --- re-runs and picks up the new spinner frame. +-- Only force a redraw when an animation is actually needed; when nothing is +-- spinning, update-status is a near-no-op and idle CPU cost stays at ~0. wezterm.on("update-status", function(window) - spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 - window:set_left_status("") + if any_indeterminate(window) then + spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 + window:set_left_status("") + end end) --- ~15 fps spinner animation without burning CPU -config.status_update_interval = 67 +-- 60 fps spinner animation (only redraws when something is actually animating) +config.status_update_interval = 16 -- Use the macOS-native fancy tab bar with integrated traffic lights config.use_fancy_tab_bar = true From fb04fbc5e681415fe9c7665349c55d1f8b10fefc Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:43:27 -0700 Subject: [PATCH 08/13] Render animated spinner in the left status bar so it actually rotates format-tab-title only fires when tab state changes, so advancing spinner_state.idx in update-status didn't redraw the tab title. Move the animated spinner into the left status bar (set_left_status forces its own re-render) and keep a static loading glyph in the tab title so you can still see which tab is busy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 7455260..fba52ec 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -179,7 +179,9 @@ local PROGRESS_COLORS = { dim = "#6c7086", -- overlay (for empty bar slots) } --- Returns a wezterm format-list (or nil) for the progress portion of a tab title +-- Returns a wezterm format-list (or nil) for the progress portion of a tab title. +-- (Indeterminate state shows a static glyph here; the *animated* spinner lives +-- in the window's left status bar so it can re-render via update-status.) local function progress_format(pane_info) if not pane_info.pane_id then return nil @@ -206,7 +208,7 @@ local function progress_format(pane_info) elseif p == "Indeterminate" then return { { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, - { Text = " " .. SPINNER[spinner_state.idx] }, + { Text = " " .. wezterm.nerdfonts.md_loading }, "ResetAttributes", } end @@ -245,11 +247,21 @@ local function any_indeterminate(window) end -- Drive the spinner animation by advancing the frame on each status tick. --- Only force a redraw when an animation is actually needed; when nothing is --- spinning, update-status is a near-no-op and idle CPU cost stays at ~0. +-- The animated spinner is rendered in the left status bar (set_left_status +-- forces an immediate re-render), so this works regardless of whether +-- format-tab-title would otherwise re-fire. +-- When nothing is animating, we clear the status once and skip further work, +-- so idle CPU cost is negligible even at 60fps. wezterm.on("update-status", function(window) if any_indeterminate(window) then spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 + window:set_left_status(wezterm.format({ + { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, + { Text = " " .. SPINNER[spinner_state.idx] .. " " }, + "ResetAttributes", + })) + elseif spinner_state.idx ~= 0 then + spinner_state.idx = 0 window:set_left_status("") end end) From 81acfaa1387f6834971f67e841dd2a9ffc3044c6 Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:45:53 -0700 Subject: [PATCH 09/13] Animate the spinner inside the tab title via user_var pokes Switch animation strategy: instead of putting the spinner in the window's left status bar, set a 'spinner_frame' user_var on every pane with indeterminate progress on each update-status tick. user_var changes are part of PaneInformation, which forces format-tab-title to re-fire and pick up the new spinner_state.idx. Result: the animated spinner is grouped with the tab title (and with the static glyph for percent/error), and the left status bar stays empty. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index fba52ec..56b0989 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -180,8 +180,9 @@ local PROGRESS_COLORS = { } -- Returns a wezterm format-list (or nil) for the progress portion of a tab title. --- (Indeterminate state shows a static glyph here; the *animated* spinner lives --- in the window's left status bar so it can re-render via update-status.) +-- The animated braille spinner is rendered here for the Indeterminate state; +-- update-status pokes a user_var on each indeterminate pane on every tick, +-- which forces format-tab-title to re-fire and pick up the new spinner frame. local function progress_format(pane_info) if not pane_info.pane_id then return nil @@ -208,7 +209,7 @@ local function progress_format(pane_info) elseif p == "Indeterminate" then return { { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, - { Text = " " .. wezterm.nerdfonts.md_loading }, + { Text = " " .. SPINNER[spinner_state.idx] }, "ResetAttributes", } end @@ -230,39 +231,27 @@ wezterm.on("format-tab-title", function(tab) return out end) --- Returns true if any pane in this window currently has indeterminate progress -local function any_indeterminate(window) +-- Drive the spinner animation by advancing the frame on each status tick. +-- Setting a user_var on each pane that has indeterminate progress is what +-- forces format-tab-title to re-fire so the tab itself shows the new +-- spinner frame. When nothing is animating we do almost no work and idle +-- CPU cost stays at ~0 even at 60fps. +wezterm.on("update-status", function(window) local mux_win = window:mux_window() if not mux_win then - return false + return end + local found = false for _, tab in ipairs(mux_win:tabs()) do for _, pane in ipairs(tab:panes()) do - if pane.get_progress and pane:get_progress() == "Indeterminate" then - return true + if pane:get_progress() == "Indeterminate" then + pane:set_user_var("spinner_frame", tostring(spinner_state.idx)) + found = true end end end - return false -end - --- Drive the spinner animation by advancing the frame on each status tick. --- The animated spinner is rendered in the left status bar (set_left_status --- forces an immediate re-render), so this works regardless of whether --- format-tab-title would otherwise re-fire. --- When nothing is animating, we clear the status once and skip further work, --- so idle CPU cost is negligible even at 60fps. -wezterm.on("update-status", function(window) - if any_indeterminate(window) then + if found then spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 - window:set_left_status(wezterm.format({ - { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, - { Text = " " .. SPINNER[spinner_state.idx] .. " " }, - "ResetAttributes", - })) - elseif spinner_state.idx ~= 0 then - spinner_state.idx = 0 - window:set_left_status("") end end) From 7c269de14bb182177388caefeb0bba4731f24b0f Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:48:19 -0700 Subject: [PATCH 10/13] Simplify progress: use tab.active_pane.progress field and a static indeterminate glyph WezTerm doesn't reliably re-fire format-tab-title on a timer (verified that neither set_left_status nor pane:set_user_var triggers it on update-status), and the published wezterm dotfiles that handle OSC 9;4 (noidilin/wezterm, OSDDQD/wezterm-config) all use a static glyph for the indeterminate state. Match that pattern: - Read progress directly from PaneInformation.progress (cleaner than going through wezterm.mux.get_pane) - Indeterminate state shows a static \xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2 glyph - Drop status_update_interval, the SPINNER table, the spinner_state and the update-status handler entirely Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 50 +++++++-------------------------------------- 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 56b0989..223dce8 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -167,33 +167,24 @@ local function progress_bar(pct, width) return s end --- Animated braille spinner driven by the update-status event -local SPINNER = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } -local spinner_state = { idx = 1 } - -- Catppuccin-mocha accent colors for progress states local PROGRESS_COLORS = { normal = "#89b4fa", -- blue error = "#f38ba8", -- red indeterminate = "#f9e2af", -- yellow - dim = "#6c7086", -- overlay (for empty bar slots) } -- Returns a wezterm format-list (or nil) for the progress portion of a tab title. --- The animated braille spinner is rendered here for the Indeterminate state; --- update-status pokes a user_var on each indeterminate pane on every tick, --- which forces format-tab-title to re-fire and pick up the new spinner frame. +-- WezTerm doesn't reliably re-fire format-tab-title on a timer, so we use a +-- static glyph for the indeterminate state (matches what other wezterm dotfiles +-- do, e.g. noidilin/wezterm and OSDDQD/wezterm-config). local function progress_format(pane_info) - if not pane_info.pane_id then - return nil - end - local mux_pane = wezterm.mux.get_pane(pane_info.pane_id) - if not mux_pane or not mux_pane.get_progress then + local p = pane_info.progress + if not p or p == "None" then return nil end - local p = mux_pane:get_progress() if type(p) == "table" then - if p.Percentage then + if p.Percentage ~= nil then return { { Foreground = { Color = PROGRESS_COLORS.normal } }, { Text = " " .. progress_bar(p.Percentage) .. " " .. p.Percentage .. "%" }, @@ -209,7 +200,7 @@ local function progress_format(pane_info) elseif p == "Indeterminate" then return { { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, - { Text = " " .. SPINNER[spinner_state.idx] }, + { Text = " " .. wezterm.nerdfonts.md_dots_horizontal }, "ResetAttributes", } end @@ -231,33 +222,6 @@ wezterm.on("format-tab-title", function(tab) return out end) --- Drive the spinner animation by advancing the frame on each status tick. --- Setting a user_var on each pane that has indeterminate progress is what --- forces format-tab-title to re-fire so the tab itself shows the new --- spinner frame. When nothing is animating we do almost no work and idle --- CPU cost stays at ~0 even at 60fps. -wezterm.on("update-status", function(window) - local mux_win = window:mux_window() - if not mux_win then - return - end - local found = false - for _, tab in ipairs(mux_win:tabs()) do - for _, pane in ipairs(tab:panes()) do - if pane:get_progress() == "Indeterminate" then - pane:set_user_var("spinner_frame", tostring(spinner_state.idx)) - found = true - end - end - end - if found then - spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 - end -end) - --- 60 fps spinner animation (only redraws when something is actually animating) -config.status_update_interval = 16 - -- Use the macOS-native fancy tab bar with integrated traffic lights config.use_fancy_tab_bar = true config.window_decorations = "INTEGRATED_BUTTONS|RESIZE" From a8ae32a3030acd9b3c6b7ab43193396d439489de Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:53:31 -0700 Subject: [PATCH 11/13] Bring back animated indeterminate spinner in the left status bar Now we have both: - Static \xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2 glyph in the tab title (so you can see *which* tab is busy) - Animated braille spinner in the left status bar (where set_left_status forces a reliable re-render every status tick) format-tab-title only re-fires on pane state changes, so percent OSC sequences animate naturally as the shell streams new values, but the single 'Indeterminate' state can't drive its own animation. The left status bar is the only place where update-status produces a timer- driven re-render. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 47 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 223dce8..3213fb1 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -167,6 +167,10 @@ local function progress_bar(pct, width) return s end +-- Animated braille spinner driven by the update-status event +local SPINNER = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } +local spinner_state = { idx = 1 } + -- Catppuccin-mocha accent colors for progress states local PROGRESS_COLORS = { normal = "#89b4fa", -- blue @@ -175,9 +179,9 @@ local PROGRESS_COLORS = { } -- Returns a wezterm format-list (or nil) for the progress portion of a tab title. --- WezTerm doesn't reliably re-fire format-tab-title on a timer, so we use a --- static glyph for the indeterminate state (matches what other wezterm dotfiles --- do, e.g. noidilin/wezterm and OSDDQD/wezterm-config). +-- The indeterminate spinner uses a static glyph here (format-tab-title doesn't +-- reliably re-fire on a timer); the *animated* spinner lives in the window's +-- left status bar, driven by update-status below. local function progress_format(pane_info) local p = pane_info.progress if not p or p == "None" then @@ -222,6 +226,43 @@ wezterm.on("format-tab-title", function(tab) return out end) +-- Returns true if any pane in this window currently has indeterminate progress +local function any_indeterminate(window) + local mux_win = window:mux_window() + if not mux_win then + return false + end + for _, tab in ipairs(mux_win:tabs()) do + for _, pane in ipairs(tab:panes()) do + if pane.get_progress and pane:get_progress() == "Indeterminate" then + return true + end + end + end + return false +end + +-- Animated spinner in the left status bar. +-- set_left_status with a new value forces the status to re-render, which is +-- what gives us reliable animation. When nothing is spinning we clear the +-- status once and skip further work, so idle CPU cost stays at ~0. +wezterm.on("update-status", function(window) + if any_indeterminate(window) then + spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 + window:set_left_status(wezterm.format({ + { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, + { Text = " " .. SPINNER[spinner_state.idx] .. " " }, + "ResetAttributes", + })) + elseif spinner_state.idx ~= 0 then + spinner_state.idx = 0 + window:set_left_status("") + end +end) + +-- 60 fps spinner animation (only redraws when something is actually animating) +config.status_update_interval = 16 + -- Use the macOS-native fancy tab bar with integrated traffic lights config.use_fancy_tab_bar = true config.window_decorations = "INTEGRATED_BUTTONS|RESIZE" From cea3776ec78fd834f12c49247a5dca1d09450800 Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:54:05 -0700 Subject: [PATCH 12/13] Move animated indeterminate spinner from left to right status bar Less UI to overlap with on the right side; the left sits right next to the integrated traffic lights. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wezterm/wezterm.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/wezterm/wezterm.lua b/wezterm/wezterm.lua index 3213fb1..40ea051 100644 --- a/wezterm/wezterm.lua +++ b/wezterm/wezterm.lua @@ -242,21 +242,23 @@ local function any_indeterminate(window) return false end --- Animated spinner in the left status bar. --- set_left_status with a new value forces the status to re-render, which is --- what gives us reliable animation. When nothing is spinning we clear the --- status once and skip further work, so idle CPU cost stays at ~0. +-- Animated spinner in the right status bar. +-- set_right_status with a new value forces the status to re-render, which is +-- what gives us reliable animation. Right-side has fewer UI elements to +-- overlap with than the left (which sits next to the traffic lights). When +-- nothing is spinning we clear the status once and skip further work, so +-- idle CPU cost stays at ~0. wezterm.on("update-status", function(window) if any_indeterminate(window) then spinner_state.idx = (spinner_state.idx % #SPINNER) + 1 - window:set_left_status(wezterm.format({ + window:set_right_status(wezterm.format({ { Foreground = { Color = PROGRESS_COLORS.indeterminate } }, { Text = " " .. SPINNER[spinner_state.idx] .. " " }, "ResetAttributes", })) elseif spinner_state.idx ~= 0 then spinner_state.idx = 0 - window:set_left_status("") + window:set_right_status("") end end) From de9f023eb1b9ea5e4d21f279b05e4b321534314c Mon Sep 17 00:00:00 2001 From: David Jensenius Date: Wed, 29 Apr 2026 11:56:36 -0700 Subject: [PATCH 13/13] Move ghostty config into no-longer-used/ Switched terminal back to wezterm. Also removes the ~/.config/ghostty symlink locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- {ghostty => no-longer-used/ghostty}/config | 0 {ghostty => no-longer-used/ghostty}/themes/catppuccin-frappe.conf | 0 {ghostty => no-longer-used/ghostty}/themes/catppuccin-latte.conf | 0 .../ghostty}/themes/catppuccin-macchiato.conf | 0 {ghostty => no-longer-used/ghostty}/themes/catppuccin-mocha.conf | 0 {ghostty => no-longer-used/ghostty}/xterm-ghostty.terminfo | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {ghostty => no-longer-used/ghostty}/config (100%) rename {ghostty => no-longer-used/ghostty}/themes/catppuccin-frappe.conf (100%) rename {ghostty => no-longer-used/ghostty}/themes/catppuccin-latte.conf (100%) rename {ghostty => no-longer-used/ghostty}/themes/catppuccin-macchiato.conf (100%) rename {ghostty => no-longer-used/ghostty}/themes/catppuccin-mocha.conf (100%) rename {ghostty => no-longer-used/ghostty}/xterm-ghostty.terminfo (100%) diff --git a/ghostty/config b/no-longer-used/ghostty/config similarity index 100% rename from ghostty/config rename to no-longer-used/ghostty/config diff --git a/ghostty/themes/catppuccin-frappe.conf b/no-longer-used/ghostty/themes/catppuccin-frappe.conf similarity index 100% rename from ghostty/themes/catppuccin-frappe.conf rename to no-longer-used/ghostty/themes/catppuccin-frappe.conf diff --git a/ghostty/themes/catppuccin-latte.conf b/no-longer-used/ghostty/themes/catppuccin-latte.conf similarity index 100% rename from ghostty/themes/catppuccin-latte.conf rename to no-longer-used/ghostty/themes/catppuccin-latte.conf diff --git a/ghostty/themes/catppuccin-macchiato.conf b/no-longer-used/ghostty/themes/catppuccin-macchiato.conf similarity index 100% rename from ghostty/themes/catppuccin-macchiato.conf rename to no-longer-used/ghostty/themes/catppuccin-macchiato.conf diff --git a/ghostty/themes/catppuccin-mocha.conf b/no-longer-used/ghostty/themes/catppuccin-mocha.conf similarity index 100% rename from ghostty/themes/catppuccin-mocha.conf rename to no-longer-used/ghostty/themes/catppuccin-mocha.conf diff --git a/ghostty/xterm-ghostty.terminfo b/no-longer-used/ghostty/xterm-ghostty.terminfo similarity index 100% rename from ghostty/xterm-ghostty.terminfo rename to no-longer-used/ghostty/xterm-ghostty.terminfo