Last active 1729607880

My wezterm configuration

matthew revised this gist 1729607880. Go to revision

10 files changed, 375 insertions, 2 deletions

README.md(file created)

@@ -0,0 +1,10 @@
1 + # My Wezterm Setup
2 +
3 + This is my Wezterm setup.
4 + 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.
5 + My keybindings mimic those I used in tmux for creating new panes and tabs.
6 +
7 + I'd previously used tmux-resurrect to manage my tmux sessions, and relied on its ability to save state periodically.
8 + 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.
9 +
10 + 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(file created)

@@ -0,0 +1,21 @@
1 + -- From https://alexplescan.com/posts/2024/08/10/wezterm/
2 + local wezterm = require 'wezterm'
3 +
4 + local module = {}
5 +
6 + -- Returns a bool based on whether the host operating system's
7 + -- appearance is light or dark.
8 + function 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
19 + end
20 +
21 + return module

merge.lua(file created)

@@ -0,0 +1,12 @@
1 + -- Provide generalized functionality for merging tables
2 +
3 + local merge = {}
4 +
5 + function 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
10 + end
11 +
12 + return merge

notify.lua(file created)

@@ -0,0 +1,36 @@
1 + local wezterm = require 'wezterm'
2 + local module = {}
3 +
4 + local 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
12 + end
13 +
14 + local 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 + }
32 + end
33 +
34 + module.send = notify
35 +
36 + return module

powerline.lua(file created)

@@ -0,0 +1,65 @@
1 + -- From https://alexplescan.com/posts/2024/08/10/wezterm/
2 + local wezterm = require 'wezterm'
3 + local appearance = require 'appearance'
4 +
5 + local 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 + }
11 + end
12 +
13 + wezterm.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))
65 + end)

resurrect-config.lua(file created)

@@ -0,0 +1,108 @@
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 +
11 + local config = {}
12 + local wezterm = require 'wezterm'
13 + local 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
20 + resurrect.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
28 + resurrect.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
36 + resurrect.set_max_nlines(5000)
37 +
38 + -- Default keybindings
39 + -- These will need to be merged with the main wezterm keys.
40 + config.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 +
106 + require 'resurrect-events'
107 +
108 + return config

resurrect-events.lua(file created)

@@ -0,0 +1,37 @@
1 + -- resurrect.wezterm event listener configuration
2 + --
3 + -- This module configures event listeners for the resurrect.wezterm plugin.
4 +
5 + local wezterm = require 'wezterm'
6 + local notify = require '../notify'
7 + local suppress_notification = false
8 +
9 + wezterm.on('resurrect.error', function (error)
10 + notify.send("Wezterm - ERROR", error, 'critical')
11 + end)
12 +
13 + wezterm.on('resurrect.periodic_save', function ()
14 + suppress_notification = true
15 + end)
16 +
17 + wezterm.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)
32 + end)
33 +
34 + wezterm.on('resurrect.load_state.finished', function(name, type)
35 + local msg = 'Completed loading ' .. type .. ' state: ' .. name
36 + notify.send("Wezterm - Restore session", msg)
37 + end)

smart-splits-setup.lua(file created)

@@ -0,0 +1,51 @@
1 + local w = require('wezterm')
2 +
3 + -- if you are *NOT* lazy-loading smart-splits.nvim (recommended)
4 + local 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'
7 + end
8 +
9 + local direction_keys = {
10 + h = 'Left',
11 + j = 'Down',
12 + k = 'Up',
13 + l = 'Right',
14 + }
15 +
16 + local 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 + }
35 + end
36 +
37 + return {
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(file created)

@@ -0,0 +1,33 @@
1 + local wezterm = require 'wezterm'
2 +
3 + wezterm.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

@@ -3,8 +3,8 @@ local wezterm = require 'wezterm'
3 3 local act = wezterm.action
4 4 local merge = require 'merge'
5 5 local mux = wezterm.mux
6 - local resurrect = require 'resurrect/config'
7 - local smart_splits = require 'smart-splits/setup'
6 + local resurrect = require 'resurrect-config'
7 + local smart_splits = require 'smart-splits-setup'
8 8
9 9 -- --------------------------------------------------------------------
10 10 -- CONFIGURATION

matthew revised this gist 1729606336. Go to revision

1 file changed, 308 insertions

wezterm.lua(file created)

@@ -0,0 +1,308 @@
1 + -- Pull in the wezterm API, some of its modules, and plugins
2 + local wezterm = require 'wezterm'
3 + local act = wezterm.action
4 + local merge = require 'merge'
5 + local mux = wezterm.mux
6 + local resurrect = require 'resurrect/config'
7 + local smart_splits = require 'smart-splits/setup'
8 +
9 + -- --------------------------------------------------------------------
10 + -- CONFIGURATION
11 + -- --------------------------------------------------------------------
12 +
13 + -- This table will hold the configuration.
14 + local config = {}
15 +
16 + -- In newer versions of wezterm, use the config_builder which will
17 + -- help provide clearer error messages
18 + if wezterm.config_builder then
19 + config = wezterm.config_builder()
20 + end
21 +
22 + config.adjust_window_size_when_changing_font_size = false
23 + config.automatically_reload_config = true
24 + config.color_scheme = 'Solarized (dark) (terminal.sexy)'
25 + config.enable_scroll_bar = true
26 + config.enable_wayland = true
27 + -- config.font = wezterm.font('Hack')
28 + config.font = wezterm.font('MonaspiceNe NFP')
29 + config.font_size = 12.0
30 + config.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
33 + config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 2000 }
34 + config.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 + }
42 + config.pane_focus_follows_mouse = true
43 + config.scrollback_lines = 5000
44 + config.tiling_desktop_environments = {
45 + 'Wayland',
46 + }
47 + config.use_dead_keys = false
48 + config.warn_about_missing_glyphs = false
49 + config.window_decorations = "TITLE | RESIZE"
50 + config.window_padding = {
51 + left = 0,
52 + right = 0,
53 + top = 0,
54 + bottom = 0,
55 + }
56 +
57 + -- Tab bar
58 + config.use_fancy_tab_bar = true
59 + config.tab_bar_at_bottom = true
60 + config.switch_to_last_active_tab_when_closing_tab = true
61 + config.tab_max_width = 32
62 + config.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
74 + config.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
90 + config.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 + -- --------------------------------------------------------------------
287 + config.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 + -- --------------------------------------------------------------------
295 + config.keys = merge.all(config.keys, resurrect.keys)
296 +
297 + -- Powerline for tab bar
298 + require 'powerline'
299 +
300 + -- Tab status
301 + require 'tab-status'
302 +
303 + -- Plugin management
304 + -- Automatically update plugins
305 + -- wezterm.plugin.update_all()
306 +
307 + -- and finally, return the configuration to wezterm
308 + return config
Newer Older