Last active 1729607880

My wezterm configuration

README.md Raw

My Wezterm Setup

This is my Wezterm setup. My goal was to allow Wezterm to replace tmux, as I wanted a way to essentially have the features I use most from tmux always available — pane splitting, tab creation, and session management. My keybindings mimic those I used in tmux for creating new panes and tabs.

I'd previously used tmux-resurrect to manage my tmux sessions, and relied on its ability to save state periodically. For wezterm, I'm using resurrect.wezterm, with a number of configurations based on my own usage, and I use the same keybindings for naming sessions, saving sessions, and loading sessions, as I did in tmux.

In my own setup, the smart-splits and resurrect configurations are in separate subdirectories; I've adapted the gist to a flat file structure.

appearance.lua Raw
1-- From https://alexplescan.com/posts/2024/08/10/wezterm/
2local wezterm = require 'wezterm'
3
4local module = {}
5
6-- Returns a bool based on whether the host operating system's
7-- appearance is light or dark.
8function module.is_dark()
9 -- wezterm.gui is not always available, depending on what
10 -- environment wezterm is operating in. Just return true
11 -- if it's not defined.
12 if wezterm.gui then
13 -- Some systems report appearance like "Dark High Contrast"
14 -- so let's just look for the string "Dark" and if we find
15 -- it assume appearance is dark.
16 return wezterm.gui.get_appearance():find("Dark")
17 end
18 return true
19end
20
21return module
merge.lua Raw
1-- Provide generalized functionality for merging tables
2
3local merge = {}
4
5function merge.all(base, overrides)
6 local ret = base or {}
7 local second = overrides or {}
8 for _, v in pairs(second) do table.insert(ret, v) end
9 return ret
10end
11
12return merge
notify.lua Raw
1local wezterm = require 'wezterm'
2local module = {}
3
4local function has_value (tab, val)
5 for index, value in ipairs(tab) do -- luacheck: ignore 213
6 if value == val then
7 return true
8 end
9 end
10
11 return false
12end
13
14local function notify (subject, msg, urgency)
15 local allowed_urgency = { 'low', 'normal', 'critical' }
16 urgency = urgency or 'normal'
17 if not has_value(allowed_urgency, urgency) then
18 urgency = 'normal'
19 end
20
21 wezterm.run_child_process {
22 'notify-send',
23 '-i',
24 'org.wezfurlong.wezterm',
25 '-a',
26 'wezterm',
27 '-u',
28 urgency,
29 subject,
30 msg
31 }
32end
33
34module.send = notify
35
36return module
powerline.lua Raw
1-- From https://alexplescan.com/posts/2024/08/10/wezterm/
2local wezterm = require 'wezterm'
3local appearance = require 'appearance'
4
5local function segments_for_right_status(window)
6 return {
7 window:active_workspace() .. ' ',
8 wezterm.strftime('%a %Y-%m-%d %H:%M '),
9 wezterm.hostname(),
10 }
11end
12
13wezterm.on('update-status', function(window, _)
14 local SOLID_LEFT_ARROW = utf8.char(0xe0b2)
15 local segments = segments_for_right_status(window)
16
17 local color_scheme = window:effective_config().resolved_palette
18 -- Note the use of wezterm.color.parse here, this returns
19 -- a Color object, which comes with functionality for lightening
20 -- or darkening the colour (amongst other things).
21 local bg = wezterm.color.parse(color_scheme.background)
22 local fg = color_scheme.foreground
23
24 -- Each powerline segment is going to be coloured progressively
25 -- darker/lighter depending on whether we're on a dark/light colour
26 -- scheme. Let's establish the "from" and "to" bounds of our gradient.
27 local gradient_to, gradient_from = bg
28
29 if appearance.is_dark() then
30 gradient_from = gradient_to:lighten(0.2)
31 else
32 gradient_from = gradient_to:darken(0.2)
33 end
34
35 -- Yes, WezTerm supports creating gradients, because why not?! Although
36 -- they'd usually be used for setting high fidelity gradients on your terminal's
37 -- background, we'll use them here to give us a sample of the powerline segment
38 -- colours we need.
39 local gradient = wezterm.color.gradient(
40 {
41 orientation = 'Horizontal',
42 colors = { gradient_from, gradient_to },
43 },
44 #segments -- only gives us as many colours as we have segments.
45 )
46
47 -- We'll build up the elements to send to wezterm.format in this table.
48 local elements = {}
49
50 for i, seg in ipairs(segments) do
51 local is_first = i == 1
52
53 if is_first then
54 table.insert(elements, { Background = { Color = 'none' } })
55 end
56 table.insert(elements, { Foreground = { Color = gradient[i] } })
57 table.insert(elements, { Text = SOLID_LEFT_ARROW })
58
59 table.insert(elements, { Foreground = { Color = fg } })
60 table.insert(elements, { Background = { Color = gradient[i] } })
61 table.insert(elements, { Text = ' ' .. seg .. ' ' })
62 end
63
64 window:set_right_status(wezterm.format(elements))
65end)
resurrect-config.lua Raw
1-- resurrect.wezterm configuration and settings
2--
3-- This module:
4-- * Configures the resurrect.wezterm plugin
5-- * Configures event listener configuration (via an additional required file)
6-- * Returns wezterm keybinding configuration for resurrect-related actions.
7--
8-- The main wezterm configuration is then responsible for merging the
9-- keybindings with other keybindings, or setting up its own.
10
11local config = {}
12local wezterm = require 'wezterm'
13local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")
14
15-- resurrect.wezterm encryption
16-- Uncomment the following to use encryption.
17-- If you do, ensure you have the age tool installed, you have created an
18-- encryption key at ~/.config/age/wezterm-resurrect.txt, and that you supply
19-- the associated public_key below
20resurrect.set_encryption({
21 enable = true,
22 method = "age",
23 private_key = wezterm.home_dir .. "/.config/age/wezterm-resurrect.txt",
24 public_key = "PUBLIC-KEY-GOES-HERE",
25})
26
27-- resurrect.wezterm periodic save every 5 minutes
28resurrect.periodic_save({
29 interval_seconds = 300,
30 save_tabs = true,
31 save_windows = true,
32 save_workspaces = true,
33})
34
35-- Save only 5000 lines per pane
36resurrect.set_max_nlines(5000)
37
38-- Default keybindings
39-- These will need to be merged with the main wezterm keys.
40config.keys = {
41 {
42 -- Save current and window state
43 -- See https://github.com/MLFlexer/resurrect.wezterm for options around
44 -- saving workspace and window state separately
45 key = 'S',
46 mods = 'LEADER|SHIFT',
47 action = wezterm.action_callback(function(win, pane) -- luacheck: ignore 212
48 local state = resurrect.workspace_state.get_workspace_state()
49 resurrect.save_state(state)
50 resurrect.window_state.save_window_action()
51 end),
52 },
53 {
54 -- Load workspace or window state, using a fuzzy finder
55 key = 'L',
56 mods = 'LEADER|SHIFT',
57 action = wezterm.action_callback(function(win, pane)
58 resurrect.fuzzy_load(win, pane, function(id, label) -- luacheck: ignore 212
59 local type = string.match(id, "^([^/]+)") -- match before '/'
60 id = string.match(id, "([^/]+)$") -- match after '/'
61 id = string.match(id, "(.+)%..+$") -- remove file extension
62
63 local opts = {
64 window = win:mux_window(),
65 relative = true,
66 restore_text = true,
67 on_pane_restore = resurrect.tab_state.default_on_pane_restore,
68 }
69
70 if type == "workspace" then
71 local state = resurrect.load_state(id, "workspace")
72 resurrect.workspace_state.restore_workspace(state, opts)
73 elseif type == "window" then
74 local state = resurrect.load_state(id, "window")
75 -- opts.tab = win:active_tab()
76 resurrect.window_state.restore_window(pane:window(), state, opts)
77 elseif type == "tab" then
78 local state = resurrect.load_state(id, "tab")
79 resurrect.tab_state.restore_tab(pane:tab(), state, opts)
80 end
81 end)
82 end),
83 },
84 {
85 -- Delete a saved session using a fuzzy finder
86 key = 'd',
87 mods = 'LEADER|SHIFT',
88 action = wezterm.action_callback(function(win, pane)
89 resurrect.fuzzy_load(
90 win,
91 pane,
92 function(id)
93 resurrect.delete_state(id)
94 end,
95 {
96 title = 'Delete State',
97 description = 'Select session to delete and press Enter = accept, Esc = cancel, / = filter',
98 fuzzy_description = 'Search session to delete: ',
99 is_fuzzy = true,
100 }
101 )
102 end),
103 }
104}
105
106require 'resurrect-events'
107
108return config
resurrect-events.lua Raw
1-- resurrect.wezterm event listener configuration
2--
3-- This module configures event listeners for the resurrect.wezterm plugin.
4
5local wezterm = require 'wezterm'
6local notify = require '../notify'
7local suppress_notification = false
8
9wezterm.on('resurrect.error', function (error)
10 notify.send("Wezterm - ERROR", error, 'critical')
11end)
12
13wezterm.on('resurrect.periodic_save', function ()
14 suppress_notification = true
15end)
16
17wezterm.on('resurrect.save_state.finished', function (session_path)
18 local is_workspace_save = session_path:find("state/workspace")
19
20 if is_workspace_save == nil then
21 return
22 end
23
24 if suppress_notification then
25 suppress_notification = false
26 return
27 end
28
29 local path = session_path:match(".+/([^+]+)$")
30 local name = path:match("^(.+)%.json$")
31 notify.send("Wezterm - Save workspace", 'Saved workspace ' .. name .. "\n\n" .. session_path)
32end)
33
34wezterm.on('resurrect.load_state.finished', function(name, type)
35 local msg = 'Completed loading ' .. type .. ' state: ' .. name
36 notify.send("Wezterm - Restore session", msg)
37end)
smart-splits-setup.lua Raw
1local w = require('wezterm')
2
3-- if you are *NOT* lazy-loading smart-splits.nvim (recommended)
4local function is_vim(pane)
5 -- this is set by the plugin, and unset on ExitPre in Neovim
6 return pane:get_user_vars().IS_NVIM == 'true'
7end
8
9local direction_keys = {
10 h = 'Left',
11 j = 'Down',
12 k = 'Up',
13 l = 'Right',
14}
15
16local function split_nav(resize_or_move, key)
17 return {
18 key = key,
19 mods = resize_or_move == 'resize' and 'META' or 'CTRL',
20 action = w.action_callback(function(win, pane)
21 if is_vim(pane) then
22 -- pass the keys through to vim/nvim
23 win:perform_action({
24 SendKey = { key = key, mods = resize_or_move == 'resize' and 'META' or 'CTRL' },
25 }, pane)
26 else
27 if resize_or_move == 'resize' then
28 win:perform_action({ AdjustPaneSize = { direction_keys[key], 3 } }, pane)
29 else
30 win:perform_action({ ActivatePaneDirection = direction_keys[key] }, pane)
31 end
32 end
33 end),
34 }
35end
36
37return {
38 keys = {
39 -- move between split panes
40 split_nav('move', 'h'),
41 split_nav('move', 'j'),
42 split_nav('move', 'k'),
43 split_nav('move', 'l'),
44
45 -- resize panes
46 split_nav('resize', 'h'),
47 split_nav('resize', 'j'),
48 split_nav('resize', 'k'),
49 split_nav('resize', 'l'),
50 },
51}
tab-status.lua Raw
1local wezterm = require 'wezterm'
2
3wezterm.on(
4 'format-tab-title',
5 function(tab, tabs, panes, config, hover, max_width)
6 if tab.is_active then
7 -- Do nothing; normal active style is fine, so just return the text
8 return tab.active_pane.title
9 end
10
11 local has_unseen_output = false
12
13 for _, pane in ipairs(tab.panes) do
14 if pane.has_unseen_output then
15 has_unseen_output = true
16 break
17 end
18 end
19
20 if has_unseen_output then
21 -- Set the background to Solarized's yellow, and foreground to
22 -- Solarized's base02
23 return {
24 { Background = { Color = '#b58900' } },
25 { Foreground = { Color = '#073642' } },
26 { Text = ' ' .. tab.active_pane.title .. ' ' },
27 }
28 end
29
30 -- Do nothing different, as there's no activity; just return the text
31 return tab.active_pane.title
32 end
33)
wezterm.lua Raw
1-- Pull in the wezterm API, some of its modules, and plugins
2local wezterm = require 'wezterm'
3local act = wezterm.action
4local merge = require 'merge'
5local mux = wezterm.mux
6local resurrect = require 'resurrect-config'
7local smart_splits = require 'smart-splits-setup'
8
9-- --------------------------------------------------------------------
10-- CONFIGURATION
11-- --------------------------------------------------------------------
12
13-- This table will hold the configuration.
14local config = {}
15
16-- In newer versions of wezterm, use the config_builder which will
17-- help provide clearer error messages
18if wezterm.config_builder then
19 config = wezterm.config_builder()
20end
21
22config.adjust_window_size_when_changing_font_size = false
23config.automatically_reload_config = true
24config.color_scheme = 'Solarized (dark) (terminal.sexy)'
25config.enable_scroll_bar = true
26config.enable_wayland = true
27-- config.font = wezterm.font('Hack')
28config.font = wezterm.font('MonaspiceNe NFP')
29config.font_size = 12.0
30config.hide_tab_bar_if_only_one_tab = false
31-- The leader is similar to how tmux defines a set of keys to hit in order to
32-- invoke tmux bindings. Binding to ctrl-a here to mimic tmux
33config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 2000 }
34config.mouse_bindings = {
35 -- Open URLs with Ctrl+Click
36 {
37 event = { Up = { streak = 1, button = 'Left' } },
38 mods = 'CTRL',
39 action = act.OpenLinkAtMouseCursor,
40 }
41}
42config.pane_focus_follows_mouse = true
43config.scrollback_lines = 5000
44config.tiling_desktop_environments = {
45 'Wayland',
46}
47config.use_dead_keys = false
48config.warn_about_missing_glyphs = false
49config.window_decorations = "TITLE | RESIZE"
50config.window_padding = {
51 left = 0,
52 right = 0,
53 top = 0,
54 bottom = 0,
55}
56
57-- Tab bar
58config.use_fancy_tab_bar = true
59config.tab_bar_at_bottom = true
60config.switch_to_last_active_tab_when_closing_tab = true
61config.tab_max_width = 32
62config.colors = {
63 quick_select_label_bg = { Color = '#fdf6e3' },
64 quick_select_label_fg = { Color = '#073642' },
65 tab_bar = {
66 active_tab = {
67 fg_color = '#073642',
68 bg_color = '#2aa198',
69 }
70 }
71}
72
73-- Add items to launch menu
74config.launch_menu = {
75 {
76 label = 'mwop',
77 cwd = wezterm.home_dir .. '/git/weierophinney/mwop.net',
78 },
79 {
80 label = 'onedrive',
81 cwd = wezterm.home_dir .. '/OneDrive',
82 },
83 {
84 label = 'top',
85 args = { 'top' },
86 },
87}
88
89-- Custom key bindings
90config.keys = {
91 -- Show the launcher
92 {
93 key = 'm',
94 mods = 'LEADER',
95 action = act.ShowLauncher,
96 },
97
98 -- Copy mode
99 {
100 key = '[',
101 mods = 'LEADER',
102 action = act.ActivateCopyMode,
103 },
104
105 -- ----------------------------------------------------------------
106 -- TABS
107 --
108 -- Where possible, I'm using the same combinations as I would in tmux
109 -- ----------------------------------------------------------------
110
111 -- Show tab navigator; similar to listing panes in tmux
112 {
113 key = 'w',
114 mods = 'LEADER',
115 action = act.ShowTabNavigator,
116 },
117
118 -- Create a tab (alternative to Ctrl-Shift-Tab)
119 {
120 key = 'c',
121 mods = 'LEADER',
122 action = act.SpawnTab 'CurrentPaneDomain',
123 },
124
125 -- Rename current tab; analagous to command in tmux
126 {
127 key = ',',
128 mods = 'LEADER',
129 action = act.PromptInputLine {
130 description = 'Enter new name for tab',
131 action = wezterm.action_callback(
132 function(window, pane, line) -- luacheck: ignore 212
133 if line then
134 window:active_tab():set_title(line)
135 end
136 end
137 ),
138 },
139 },
140
141 -- Move to next/previous TAB
142 {
143 key = 'n',
144 mods = 'LEADER',
145 action = act.ActivateTabRelative(1),
146 },
147 {
148 key = 'p',
149 mods = 'LEADER',
150 action = act.ActivateTabRelative(-1),
151 },
152
153 -- Close tab
154 {
155 key = '&',
156 mods = 'LEADER|SHIFT',
157 action = act.CloseCurrentTab{ confirm = true },
158 },
159
160 -- ----------------------------------------------------------------
161 -- PANES
162 --
163 -- These are great and get me most of the way to replacing tmux
164 -- entirely, particularly as you can use "wezterm ssh" to ssh to another
165 -- server, and still retain Wezterm as your terminal there.
166 --
167 -- Note that these only define creating splits, relative motion
168 -- (next/previous), zooming, swapping, and killing panes; actual directional
169 -- motions between panes or resizing them are handled by smart splits.
170 -- ----------------------------------------------------------------
171
172 -- Vertical split
173 {
174 -- |
175 key = '|',
176 mods = 'LEADER|SHIFT',
177 action = act.SplitPane {
178 direction = 'Right',
179 size = { Percent = 50 },
180 },
181 },
182
183 -- Horizontal split
184 {
185 -- -
186 key = '-',
187 mods = 'LEADER',
188 action = act.SplitPane {
189 direction = 'Down',
190 size = { Percent = 50 },
191 },
192 },
193
194 -- Close/kill active pane
195 {
196 key = 'x',
197 mods = 'LEADER',
198 action = act.CloseCurrentPane { confirm = true },
199 },
200
201 -- Swap active pane with another one
202 {
203 key = '{',
204 mods = 'LEADER|SHIFT',
205 action = act.PaneSelect { mode = "SwapWithActiveKeepFocus" },
206 },
207
208 -- Zoom current pane (toggle)
209 {
210 key = 'z',
211 mods = 'LEADER',
212 action = act.TogglePaneZoomState,
213 },
214 {
215 key = 'f',
216 mods = 'ALT',
217 action = act.TogglePaneZoomState,
218 },
219
220 -- Move to next/previous pane
221 {
222 key = ';',
223 mods = 'LEADER',
224 action = act.ActivatePaneDirection('Prev'),
225 },
226 {
227 key = 'o',
228 mods = 'LEADER',
229 action = act.ActivatePaneDirection('Next'),
230 },
231
232 -- ----------------------------------------------------------------
233 -- Workspaces
234 --
235 -- These are roughly equivalent to tmux sessions.
236 -- ----------------------------------------------------------------
237
238 -- Attach to muxer
239 {
240 key = 'a',
241 mods = 'LEADER',
242 action = act.AttachDomain 'unix',
243 },
244
245 -- Detach from muxer
246 {
247 key = 'd',
248 mods = 'LEADER',
249 action = act.DetachDomain { DomainName = 'unix' },
250 },
251
252 -- Show list of workspaces
253 {
254 key = 's',
255 mods = 'LEADER',
256 action = act.ShowLauncherArgs { flags = 'WORKSPACES' },
257 },
258
259 -- Rename current session; analagous to command in tmux
260 {
261 key = '$',
262 mods = 'LEADER|SHIFT',
263 action = act.PromptInputLine {
264 description = 'Enter new name for session',
265 action = wezterm.action_callback(
266 function(window, pane, line) -- luacheck: ignore 212
267 if line then
268 mux.rename_workspace(
269 window:mux_window():get_workspace(),
270 line
271 )
272 end
273 end
274 ),
275 },
276 },
277}
278
279-- --------------------------------------------------------------------
280-- Smart splits
281--
282-- See https://github.com/mrjones2014/smart-splits.nvim
283--
284-- Allows moving and resizing panes easily, as well as navigation between
285-- wezterm and nvim panes
286-- --------------------------------------------------------------------
287config.keys = merge.all(config.keys, smart_splits.keys)
288
289-- --------------------------------------------------------------------
290-- resurrect.wezterm
291--
292-- See https://github.com/MLFlexer/resurrect.wezterm
293-- See resurrect.lua
294-- --------------------------------------------------------------------
295config.keys = merge.all(config.keys, resurrect.keys)
296
297-- Powerline for tab bar
298require 'powerline'
299
300-- Tab status
301require 'tab-status'
302
303-- Plugin management
304-- Automatically update plugins
305-- wezterm.plugin.update_all()
306
307-- and finally, return the configuration to wezterm
308return config