├── .gitignore ├── LICENSE ├── README.md └── plugin ├── init.lua └── utils └── domains.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Rose-Franklin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡ Quick Domains 2 | 3 | A faster way to search and attach to domains in wezterm. Inspired by [smart_workpace_switcher.wezterm](https://github.com/MLFlexer/smart_workspace_switcher.wezterm) 4 | 5 | ## Quick Look 6 | 7 | ![Peek 2024-09-14 06-44](https://github.com/user-attachments/assets/79070918-25fa-47bf-8d8f-35f776c1edfc) 8 | 9 | #### Dependencies 10 | 11 | There are no package dependencies, but you need to configured your 12 | `.ssh/config` [Here](https://wezfurlong.org/wezterm/config/lua/wezterm/enumerate_ssh_hosts.html) to select ssh domains using auto-configuration with this plugin. 13 | 14 | ### 🚀 Install 15 | 16 | This is a wezterm plugin. It can be installed by importing the repo and calling the `apply_to_config` function. It is important that the `apply_to_config` function is called after keys and key_tables have been set. 17 | ```lua 18 | local domains = wezterm.plugin.require("https://github.com/DavidRR-F/quick_domains.wezterm") 19 | domains.apply_to_config(config) 20 | ``` 21 | 22 | ### 🎨 Configuration 23 | 24 | The `apply_to_config` function takes a second parameter opts. To override any options simply pass a table of the desired changes. 25 | 26 | ```lua 27 | domains.apply_to_config( 28 | config, 29 | { 30 | keys = { 31 | attach = { 32 | key = 's', 33 | mods = 'SHIFT', 34 | tbl = 'tmux' 35 | }, 36 | vsplit = { 37 | key = 'v', 38 | mods = 'SHIFT', 39 | tbl = 'tmux' 40 | }, 41 | hsplit = { 42 | key = 'h', 43 | mods = 'SHIFT', 44 | tbl = 'tmux' 45 | } 46 | } 47 | } 48 | ) 49 | ``` 50 | 51 | You can set a custom [wezterm format](https://wezfurlong.org/wezterm/config/lua/wezterm/format.html) for the domain fuzzy selector items 52 | 53 | ```lua 54 | domains.formatter = function(icon, name, label) 55 | return wezterm.format({ 56 | { Attribute = { Italic = true } }, 57 | { Foreground = { AnsiColor = 'Fuchsia' } }, 58 | { Background = { Color = 'blue' } }, 59 | { Text = icon .. ' ' .. name .. ': ' .. label }, 60 | }) 61 | end 62 | ``` 63 | 64 | You can enable auto configuration of [ssh_domains](https://wezfurlong.org/wezterm/config/lua/wezterm/enumerate_ssh_hosts.html?h=ssh) and [exec_domains](https://wezfurlong.org/wezterm/config/lua/ExecDomain.html#example-docker-domains) by disabling the ignore configurations 65 | 66 | ```lua 67 | { 68 | keys = ..., 69 | icons = ..., 70 | auto = { 71 | ssh_ignore = false, 72 | exec_ignore = { 73 | ssh = false, 74 | docker = false, 75 | kubernetes = false 76 | }, 77 | } 78 | } 79 | ``` 80 | 81 | ### 🛠️ Defaults 82 | 83 | These are the current default setting the can be overridden on your `apply_to_config` function 84 | 85 | ```lua 86 | { 87 | keys = { 88 | -- open domain in new tab 89 | attach = { 90 | -- mod keys for fuzzy domain finder 91 | mods = 'CTRL', 92 | -- base key for fuzzy domain finder 93 | key = 'd', 94 | -- key table to insert key map to if any 95 | tbl = '', 96 | }, 97 | -- open domain in split pane 98 | -- excludes remote domains 99 | -- add remote domains as exec domain for split binds 100 | vsplit = { 101 | key = 'v', 102 | mods = 'CTRL', 103 | tbl = '' 104 | }, 105 | hsplit = { 106 | key = 'h', 107 | mods = 'CTRL', 108 | tbl = '' 109 | } 110 | }, 111 | -- swap in and out icons for specific domains 112 | icons = { 113 | hosts = '', 114 | ssh = '󰣀', 115 | tls = '󰢭', 116 | unix = '', 117 | exec = '', 118 | bash = '', 119 | zsh = '', 120 | fish = '', 121 | pwsh = '󰨊', 122 | powershell = '󰨊', 123 | wsl = '', 124 | windows = '', 125 | docker = '', 126 | kubernetes = '󱃾', 127 | }, 128 | -- auto-configuration 129 | auto = { 130 | -- disable ssh multiplex auto config 131 | ssh_ignore = true, 132 | -- disable exec domain auto configs 133 | exec_ignore = { 134 | ssh = true, 135 | docker = true, 136 | kubernetes = true 137 | }, 138 | }, 139 | -- default shells 140 | docker_shell = '/bin/bash', 141 | kubernetes_shell = '/bin/bash' 142 | } 143 | ``` 144 | 145 | This is the current default formatter function that can be overridden 146 | 147 | ```lua 148 | domains.formatter = function(icon, name, _) 149 | return wezterm.format({ 150 | { Text = icon .. ' ' .. string.lower(name) } 151 | }) 152 | end 153 | ``` 154 | ### 🔔 Events 155 | 156 | `quick_domain.fuzzy_selector.opened` 157 | 158 | | parameter | description | 159 | |:----------|:------------| 160 | | window | MuxWindow Object | 161 | | pane | MuxPane Object | 162 | 163 | `quick_domain.fuzzy_selector.selected` 164 | 165 | | parameter | description | 166 | |:----------|:------------| 167 | | window | MuxWindow Object | 168 | | pane | MuxPane Object | 169 | | id | Domain ID | 170 | 171 | `quick_domain.fuzzy_selector.canceled` 172 | 173 | | parameter | description | 174 | |:----------|:------------| 175 | | window | MuxWindow Object | 176 | | pane | MuxPane Object | 177 | -------------------------------------------------------------------------------- /plugin/init.lua: -------------------------------------------------------------------------------- 1 | local wez = require "wezterm" 2 | 3 | local separator = package.config:sub(1, 1) == "\\" and "\\" or "/" 4 | local plugin_dir = wez.plugin.list()[1].plugin_dir:gsub(separator .. "[^" .. separator .. "]*$", "") 5 | 6 | --- Checks if the plugin directory exists 7 | local function directory_exists(path) 8 | local success, result = pcall(wez.read_dir, plugin_dir .. path) 9 | return success and result 10 | end 11 | 12 | --- Returns the name of the package, used when requiring modules 13 | local function get_require_path() 14 | local path = "httpssCssZssZsgithubsDscomsZsDavidRR-FsZsquick_domainssDswezterm" 15 | local path_trailing_slash = "httpssCssZssZsgithubsDscomsZsDavidRR-FsZsquick_domainssDsweztermsZs" 16 | return directory_exists(path_trailing_slash) and path_trailing_slash or path 17 | end 18 | 19 | package.path = package.path 20 | .. ";" 21 | .. plugin_dir 22 | .. separator 23 | .. get_require_path() 24 | .. separator 25 | .. "plugin" 26 | .. separator 27 | .. "?.lua" 28 | 29 | local domains = require 'utils.domains' 30 | local act = wez.action 31 | 32 | local pub = { 33 | formatter = function(icon, name, _) 34 | return wez.format({ 35 | { Text = icon .. ' ' .. string.lower(name) } 36 | }) 37 | end, 38 | } 39 | 40 | local default_settings = { 41 | keys = { 42 | attach = { 43 | mods = 'CTRL', 44 | key = 'd', 45 | tbl = '', 46 | }, 47 | vsplit = { 48 | mods = 'CTRL', 49 | key = 'v', 50 | tbl = '', 51 | }, 52 | hsplit = { 53 | mods = 'CTRL', 54 | key = 'h', 55 | tbl = '', 56 | }, 57 | }, 58 | icons = { 59 | mux = '', 60 | wsl = '', 61 | ssh = '󰣀', 62 | tls = '󰢭', 63 | unix = '', 64 | bash = '', 65 | zsh = '', 66 | fish = '', 67 | pwsh = '󰨊', 68 | powershell = '󰨊', 69 | cmd = '', 70 | docker = '', 71 | kubernetes = '󱃾', 72 | exec = '', 73 | }, 74 | auto = { 75 | ssh_ignore = true, 76 | exec_ignore = { 77 | ssh = true, 78 | docker = true, 79 | kubernetes = true, 80 | } 81 | }, 82 | kubernetes_shell = '/bin/bash', 83 | docker_shell = '/bin/bash', 84 | } 85 | 86 | local function contains_ignore_case(str, pattern) 87 | return string.find(string.lower(str), string.lower(pattern)) ~= nil 88 | end 89 | 90 | local function is_remote_domain(domain) 91 | local remote_domains = { 'ssh mux', 'tls mux', 'unix mux' } 92 | for _, domain_type in ipairs(remote_domains) do 93 | if contains_ignore_case(domain:label(), domain_type) then 94 | return true 95 | end 96 | end 97 | return false 98 | end 99 | 100 | local function filter_remote_domains(domains) 101 | local filtered = {} 102 | for _, domain in ipairs(domains) do 103 | if not is_remote_domain(domain) then 104 | table.insert(filtered, domain) 105 | end 106 | end 107 | return filtered 108 | end 109 | 110 | local function get_choices(domains, opts) 111 | local choices = {} 112 | for _, domain in ipairs(domains) do 113 | local name = domain:name() 114 | local label = domain:label() 115 | local icon = '' 116 | 117 | for domain_type, icon_key in pairs(opts.icons) do 118 | if contains_ignore_case(label, domain_type) then 119 | icon = icon_key 120 | break 121 | end 122 | end 123 | 124 | if name ~= "TermWizTerminalDomain" then 125 | table.insert(choices, { 126 | label = pub.formatter(icon, name, label), 127 | id = name, 128 | }) 129 | end 130 | end 131 | 132 | return choices 133 | end 134 | 135 | local function get_local_domains(opts) 136 | local all_domains = wez.mux.all_domains() 137 | all_domains = filter_remote_domains(all_domains) 138 | return get_choices(all_domains, opts) 139 | end 140 | 141 | local function get_all_domains(opts) 142 | local all_domains = wez.mux.all_domains() 143 | return get_choices(all_domains, opts) 144 | end 145 | 146 | local function fuzzy_attach_tab(opts) 147 | return wez.action_callback(function(window, pane) 148 | local choices = get_all_domains(opts) 149 | wez.emit('quick_domain.fuzzy_selector.opened', window, pane) 150 | window:perform_action( 151 | act.InputSelector({ 152 | action = wez.action_callback(function(inner_window, inner_pane, id, _) 153 | if id then 154 | inner_window:perform_action( 155 | act.SpawnCommandInNewTab { domain = { DomainName = id } }, 156 | inner_pane 157 | ) 158 | wez.emit('quick_domain.fuzzy_selector.selected', window, pane, id) 159 | else 160 | wez.emit('quick_domain.fuzzy_selector.canceled', window, pane) 161 | end 162 | end), 163 | title = "Choose Domain", 164 | description = "Select a host and press Enter = accept, Esc = cancel, / = filter", 165 | fuzzy_description = "Domains: ", 166 | choices = choices, 167 | fuzzy = true, 168 | }), 169 | pane 170 | ) 171 | end) 172 | end 173 | 174 | local function fuzzy_attach_vsplit(opts) 175 | return wez.action_callback(function(window, pane) 176 | local choices = get_local_domains(opts) 177 | wez.emit('quick_domain.fuzzy_selector.opened', window, pane) 178 | window:perform_action( 179 | act.InputSelector({ 180 | action = wez.action_callback(function(inner_window, inner_pane, id, _) 181 | if id then 182 | inner_window:perform_action( 183 | act.SplitVertical { domain = { DomainName = id } }, 184 | inner_pane 185 | ) 186 | wez.emit('quick_domain.fuzzy_selector.selected', window, pane, id) 187 | else 188 | wez.emit('quick_domain.fuzzy_selector.canceled', window, pane) 189 | end 190 | end), 191 | title = "Choose Domain", 192 | description = "Select a host and press Enter = accept, Esc = cancel, / = filter", 193 | fuzzy_description = "Domains: ", 194 | choices = choices, 195 | fuzzy = true, 196 | }), 197 | pane 198 | ) 199 | end) 200 | end 201 | 202 | local function fuzzy_attach_hsplit(opts) 203 | return wez.action_callback(function(window, pane) 204 | local choices = get_local_domains(opts) 205 | wez.emit('quick_domain.fuzzy_selector.opened', window, pane) 206 | window:perform_action( 207 | act.InputSelector({ 208 | action = wez.action_callback(function(inner_window, inner_pane, id, _) 209 | if id then 210 | inner_window:perform_action( 211 | act.SplitHorizontal { domain = { DomainName = id } }, 212 | inner_pane 213 | ) 214 | wez.emit('quick_domain.fuzzy_selector.selected', window, pane, id) 215 | else 216 | wez.emit('quick_domain.fuzzy_selector.canceled', window, pane) 217 | end 218 | end), 219 | title = "Choose Domain", 220 | description = "Select a host and press Enter = accept, Esc = cancel, / = filter", 221 | fuzzy_description = "Domains: ", 222 | choices = choices, 223 | fuzzy = true, 224 | }), 225 | pane 226 | ) 227 | end) 228 | end 229 | 230 | local function all_true(tbl) 231 | for _, value in pairs(tbl) do 232 | if not value then 233 | return false 234 | end 235 | end 236 | return true 237 | end 238 | 239 | local function deep_setmetatable(user, default) 240 | user = user or {} 241 | for k, v in pairs(default) do 242 | if type(v) == "table" then 243 | user[k] = deep_setmetatable(user[k], v) 244 | else 245 | if user[k] == nil then 246 | user[k] = v 247 | end 248 | end 249 | end 250 | 251 | return user 252 | end 253 | 254 | function pub.apply_to_config(config, user_settings) 255 | local opts = deep_setmetatable(user_settings or {}, default_settings) 256 | 257 | if not opts.auto.ssh_ignore then 258 | config.ssh_domains = domains.compute_ssh_domains() 259 | end 260 | 261 | if not all_true(opts.auto.exec_ignore) then 262 | config.exec_domains = domains.compute_exec_domains(opts) 263 | end 264 | 265 | local actions = { 266 | attach = fuzzy_attach_tab(opts), 267 | vsplit = fuzzy_attach_vsplit(opts), 268 | hsplit = fuzzy_attach_hsplit(opts), 269 | } 270 | 271 | for name, key in pairs(opts.keys) do 272 | if key.tbl ~= '' then 273 | config.key_tables = config.key_tables or {} 274 | config.key_tables[key.tbl] = config.key_tables[key.tbl] or {} 275 | table.insert(config.key_tables[key.tbl], 276 | { key = key.key, mods = key.mods, action = actions[name] }) 277 | else 278 | config.keys = config.keys or {} 279 | table.insert(config.keys, { key = key.key, mods = key.mods, action = actions[name] }) 280 | end 281 | end 282 | end 283 | 284 | return pub 285 | -------------------------------------------------------------------------------- /plugin/utils/domains.lua: -------------------------------------------------------------------------------- 1 | local wez = require 'wezterm' 2 | 3 | local M = {} 4 | 5 | local wezterm = require 'wezterm' 6 | 7 | local shells = { 8 | windows = { 9 | "powershell", 10 | "pwsh", 11 | "cmd" 12 | }, 13 | linux = { 14 | "bash", 15 | "zsh", 16 | "fish", 17 | "pwsh", 18 | } 19 | } 20 | 21 | local is_windows = package.config:sub(1, 1) == '\\' 22 | 23 | local function windows_cmd(command) 24 | local windows_cmd = { "cmd", "/c" } 25 | for _, arg in ipairs(command) do 26 | table.insert(windows_cmd, arg) 27 | end 28 | return windows_cmd 29 | end 30 | 31 | local function installed(name) 32 | local command 33 | if is_windows then 34 | command = { "where", name } 35 | else 36 | command = { "which", name } 37 | end 38 | 39 | local success, _, stderr = wez.run_child_process(command) 40 | 41 | if is_windows then 42 | return success and not stderr:find("INFO: Could not find files") 43 | end 44 | 45 | return success 46 | end 47 | 48 | local function make_ssh_label_func() 49 | return function(name) 50 | return "ssh: " .. name 51 | end 52 | end 53 | 54 | local function make_ssh_exec_func(host) 55 | return function(cmd) 56 | cmd.args = { "ssh", host } 57 | return cmd 58 | end 59 | end 60 | 61 | local function kubernetes_pod_list() 62 | local pod_list = {} 63 | local success, stdout, stderr = wez.run_child_process { 64 | 'kubectl', 65 | 'get', 66 | 'pods', 67 | '--no-headers', 68 | '--output', 69 | 'custom-columns=ID:.metadata.uid,Name:.metadata.name' 70 | } 71 | 72 | if not success then 73 | wez.log_error("Failed to run 'kubectl': " .. (stderr or "unknown error")) 74 | return pod_list 75 | end 76 | 77 | for _, line in ipairs(wez.split_by_newlines(stdout)) do 78 | local id, name = line:match '(.-)%s+(.+)' 79 | if id and name then 80 | pod_list[id] = name 81 | end 82 | end 83 | return pod_list 84 | end 85 | 86 | local function make_kubernetes_label_func() 87 | return function(name) 88 | return 'kubernetes: ' .. name 89 | end 90 | end 91 | 92 | local function make_kubernetes_exec_func(name, opts) 93 | return function(cmd) 94 | local wrapped = { 95 | 'kubectl', 96 | 'exec', 97 | '-it', 98 | name, 99 | '--', 100 | opts.kubernetes_shell or '/bin/bash', 101 | } 102 | if is_windows then 103 | wrapped = windows_cmd(wrapped) 104 | wez.log_info(wrapped) 105 | end 106 | cmd.args = wrapped 107 | return cmd 108 | end 109 | end 110 | 111 | local function docker_list() 112 | local container_list = {} 113 | local success, stdout, stderr = wez.run_child_process { 114 | 'docker', 115 | 'container', 116 | 'ls', 117 | '--format', 118 | '{{.ID}}:{{.Names}}', 119 | } 120 | 121 | if not success then 122 | wez.log_error("Failed to run 'kubectl': " .. (stderr or "unknown error")) 123 | return container_list 124 | end 125 | 126 | for _, line in ipairs(wez.split_by_newlines(stdout)) do 127 | local id, name = line:match '(.-):(.+)' 128 | if id and name then 129 | container_list[id] = name 130 | end 131 | end 132 | return container_list 133 | end 134 | 135 | local function make_docker_label_func() 136 | return function(name) 137 | return 'docker: ' .. name 138 | end 139 | end 140 | 141 | local function make_docker_fixup_func(id, opts) 142 | return function(cmd) 143 | local wrapped = { 144 | 'docker', 145 | 'exec', 146 | '-it', 147 | id, 148 | opts.docker_shell or '/bin/bash', 149 | } 150 | if is_windows then 151 | wrapped = windows_cmd(wrapped) 152 | end 153 | cmd.args = wrapped 154 | return cmd 155 | end 156 | end 157 | 158 | function M.compute_ssh_domains() 159 | local ssh_domains = {} 160 | for host, _ in pairs(wez.enumerate_ssh_hosts()) do 161 | table.insert(ssh_domains, { 162 | name = host .. " (ssh domain)", 163 | remote_address = host, 164 | }) 165 | end 166 | return ssh_domains 167 | end 168 | 169 | function M.compute_exec_domains(opts) 170 | local exec_domains = {} 171 | if not opts.auto.exec_ignore.docker and installed("docker") then 172 | for id, name in pairs(docker_list()) do 173 | table.insert( 174 | exec_domains, 175 | wez.exec_domain( 176 | name, 177 | make_docker_fixup_func(id, opts), 178 | make_docker_label_func() 179 | ) 180 | ) 181 | end 182 | end 183 | if not opts.auto.exec_ignore.kubernetes and installed("kubectl") then 184 | for _, name in pairs(kubernetes_pod_list()) do 185 | table.insert( 186 | exec_domains, 187 | wez.exec_domain( 188 | name, 189 | make_kubernetes_exec_func(name, opts), 190 | make_kubernetes_label_func() 191 | ) 192 | ) 193 | end 194 | end 195 | if not opts.auto.exec_ignore.ssh then 196 | for host, _ in pairs(wez.enumerate_ssh_hosts()) do 197 | table.insert(exec_domains, 198 | wez.exec_domain( 199 | host, 200 | make_ssh_exec_func(host), 201 | make_ssh_label_func() 202 | ) 203 | ) 204 | end 205 | end 206 | 207 | if is_windows then 208 | for _, name in ipairs(shells.windows) do 209 | if installed(name) then 210 | table.insert(exec_domains, 211 | wez.exec_domain( 212 | name, 213 | function(cmd) 214 | cmd.args = { name .. '.exe' } 215 | return cmd 216 | end, 217 | function(name) 218 | return 'terminal: ' .. name 219 | end 220 | ) 221 | ) 222 | end 223 | end 224 | else 225 | for _, name in ipairs(shells.linux) do 226 | if installed(name) then 227 | table.insert(exec_domains, 228 | wez.exec_domain( 229 | name, 230 | function(cmd) 231 | cmd.args = { name } 232 | return cmd 233 | end, 234 | function(name) 235 | return 'terminal: ' .. name 236 | end 237 | ) 238 | ) 239 | end 240 | end 241 | end 242 | 243 | return exec_domains 244 | end 245 | 246 | return M 247 | --------------------------------------------------------------------------------