Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ local workspace_picker = wezterm.plugin.require("https://github.com/bugii/worksp
workspace_picker.setup({
{ path = "~/projects/my-project", type = "directory" },
{ path = "~/projects/worktrees", type = "worktreeroot" },
}, {
-- Optional: customize icons, colors, and fuzzy matching
icons = {
directory = "📁",
worktree = "🌳",
zoxide = "⚡",
workspace = "🖥️",
},
colors = {
directory = "#61afef", -- Blue
worktree = "#98c379", -- Green
zoxide = "#e5c07b", -- Yellow
workspace = "#c678dd", -- Purple
},
fuzzy = true, -- Enable/disable fuzzy matching (default: true)
fuzzy_sort = "previous_first",
})

-- Apply default keybinding (LEADER + f)
Expand Down Expand Up @@ -80,11 +96,16 @@ workspace_picker.setup({
worktree = "🌳",
zoxide = "⚡",
workspace = "🖥️",
}
},
colors = {
directory = "#61afef", -- Blue
worktree = "#98c379", -- Green
zoxide = "#e5c07b", -- Yellow
workspace = "#c678dd", -- Purple
},
fuzzy = true, -- Enable/disable fuzzy matching (default: true)
fuzzy_sort = "previous_first",
})

-- Apply to config with custom keybinding
workspace_picker.apply_to_config(config, "f", "CTRL")
```

### Direct Workspace Switching
Expand Down Expand Up @@ -120,6 +141,32 @@ config.keys = {
| `direction` | string | No | Split direction of (child) panes: `"Right"` or `"Bottom"` (default: `"Right"`) |
| `panes` | table | No | Array of pane configurations |

### Color Configuration

You can customize the colors used for different workspace types in the picker:

| Field | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `colors` | table | No | Object mapping workspace types to hex color strings |
| `colors.directory` | string | No | Color for directory workspaces (default: `"#61afef"` - blue) |
| `colors.worktree` | string | No | Color for git worktree workspaces (default: `"#98c379"` - green) |
| `colors.zoxide` | string | No | Color for zoxide directory workspaces (default: `"#e5c07b"` - yellow) |
| `colors.workspace` | string | No | Color for existing Wezterm workspaces (default: `"#c678dd"` - purple) |

### Fuzzy Search Configuration

You can enable or disable fuzzy matching in the workspace selector. When fuzzy mode is enabled, the workspace list can be optionally reordered to prioritize the current and previous workspaces.

| Field | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------------------------------------------------------------- |
| `fuzzy` | boolean | No | Enable/disable fuzzy matching and workspace reordering (default: `true`) |
| `fuzzy_sort` | string | No | Sort order in fuzzy mode: `"previous_first"`, `"current_first"`, or `"none"` (default: `"previous_first"`) |

**`fuzzy_sort` values:**
- `"previous_first"` — Previous active workspace at top, current second
- `"current_first"` — Current workspace at top, previous second
- `"none"` — No reordering (original priority order preserved)

### Pane Configuration

| Field | Type | Required | Description |
Expand Down
205 changes: 167 additions & 38 deletions plugin/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,44 @@ local default_options = {
zoxide = "⚡",
workspace = "🖥️",
},
--- Default colors for different workspace types
colors = {
directory = "#61afef", -- Blue
worktree = "#98c379", -- Green
zoxide = "#e5c07b", -- Yellow
workspace = "#c678dd", -- Purple
},
--- Whether to use fuzzy matching in the workspace selector
fuzzy = true,
--- Sort order in fuzzy mode: "current_first" (current at top, previous second),
--- "previous_first" (previous at top, current second), or "none" (no reordering)
fuzzy_sort = "previous_first",
}

--- Current options (merged with defaults)
local options = {}

-- Track workspace history for "switch to previous workspace" functionality
local last_active_workspace = nil
local current_active_workspace = nil

--- Helper to safely get the active workspace name for a window
--- @param window table: WezTerm window object
--- @return string|nil: active workspace name or nil
local function get_window_active_workspace(window)
if not window then return nil end

-- Try window:active_workspace() if available
local ok, name = pcall(function() return window:active_workspace() end)
if ok and type(name) == "string" and name ~= "" then return name end

-- Fallback to mux.get_active_workspace()
local ok2, name2 = pcall(function() return mux.get_active_workspace() end)
if ok2 and type(name2) == "string" and name2 ~= "" then return name2 end

return nil
end

--- Expand tilde (~) in paths to home directory
--- @param path string: Path that may contain ~
--- @return string|nil: Expanded path or nil if home directory cannot be determined
Expand Down Expand Up @@ -62,10 +95,20 @@ end
--- Create a formatted label for a workspace entry
--- @param path string: Path to format
--- @param icon string: Icon to use
--- @param workspace_type string: Type of workspace (directory, worktree, zoxide, workspace)
--- @return table: WezTerm formatted text
local function format_workspace_label(path, icon)
local function format_workspace_label(path, icon, workspace_type)
local display_path = path:gsub(wezterm.home_dir or "", "~")
return wezterm.format({ { Text = icon .. " " .. display_path } })

-- Get color from options, fallback to defaults, then to light gray
local color = options.colors and options.colors[workspace_type]
or default_options.colors[workspace_type]
or "#abb2bf"

return wezterm.format({
{ Foreground = { Color = color } },
{ Text = icon .. " " .. display_path }
})
end

--- Get static and worktree-based workspace entries from user configuration
Expand All @@ -88,12 +131,12 @@ local function get_config_entries()
if line:match("^worktree ") then
local worktree_path = line:match("^worktree (.+)$")
if worktree_path and worktree_path ~= expanded_path then
table.insert(entries, {
id = worktree_path,
label = format_workspace_label(worktree_path, options.icons.worktree),
type = "worktree",
tabs = entry.tabs,
})
table.insert(entries, {
id = worktree_path,
label = format_workspace_label(worktree_path, options.icons.worktree, "worktree"),
type = "worktree",
tabs = entry.tabs,
})
end
end
end
Expand All @@ -105,12 +148,12 @@ local function get_config_entries()
if not expanded_path then
wezterm.log_error("Failed to expand path '" .. entry.path .. "': " .. (err or "unknown error"))
else
table.insert(entries, {
id = expanded_path,
label = format_workspace_label(expanded_path, options.icons.directory),
type = "directory",
tabs = entry.tabs,
})
table.insert(entries, {
id = expanded_path,
label = format_workspace_label(expanded_path, options.icons.directory, "directory"),
type = "directory",
tabs = entry.tabs,
})
end
end
end
Expand All @@ -131,12 +174,12 @@ local function get_zoxide_sessions()

for line in output:gmatch("[^\r\n]+") do
if line and line ~= "" then
table.insert(sessions, {
id = line,
label = format_workspace_label(line, options.icons.zoxide),
type = "zoxide",
tabs = nil,
})
table.insert(sessions, {
id = line,
label = format_workspace_label(line, options.icons.zoxide, "zoxide"),
type = "zoxide",
tabs = nil,
})
end
end

Expand All @@ -150,12 +193,12 @@ local function get_existing_workspaces()
local names = mux.get_workspace_names()

for _, name in ipairs(names or {}) do
table.insert(workspaces, {
id = name,
label = format_workspace_label(name, options.icons.workspace),
type = "workspace",
tabs = nil,
})
table.insert(workspaces, {
id = name,
label = format_workspace_label(name, options.icons.workspace, "workspace"),
type = "workspace",
tabs = nil,
})
end

return workspaces
Expand Down Expand Up @@ -183,6 +226,38 @@ local function get_all_workspace_choices()
add_unique_items(get_config_entries())
add_unique_items(get_zoxide_sessions())

-- When fuzzy mode is enabled, optionally reorder current/previous workspace
if options.fuzzy and options.fuzzy_sort ~= "none" then
local current_item = nil
local previous_item = nil

for _, item in ipairs(all_items) do
if current_active_workspace and item.id == current_active_workspace and item.type == "workspace" then
current_item = item
elseif last_active_workspace and item.id == last_active_workspace and item.type == "workspace" then
previous_item = item
end
end

local first, second
if options.fuzzy_sort == "previous_first" then
first, second = previous_item, current_item
else
first, second = current_item, previous_item
end

local reordered = {}
if first then table.insert(reordered, first) end
if second then table.insert(reordered, second) end
for _, item in ipairs(all_items) do
if (not first or item.id ~= first.id) and
(not second or item.id ~= second.id) then
table.insert(reordered, item)
end
end
return reordered
end

return all_items
end

Expand Down Expand Up @@ -317,6 +392,16 @@ end
--- @param window table: WezTerm window object
--- @param pane table: WezTerm pane object
local function create_or_switch_workspace(item, window, pane)
-- update history: record current active workspace as last before switching
local active = get_window_active_workspace(window)
if active then
-- only update if different
if current_active_workspace ~= active then
last_active_workspace = current_active_workspace
current_active_workspace = active
end
end

if item.type == "workspace" then
-- just switch in case already existing workspace
window:perform_action(
Expand All @@ -325,6 +410,12 @@ local function create_or_switch_workspace(item, window, pane)
}),
pane
)
-- after switching, update history references
local new_active = item.id
if new_active and new_active ~= current_active_workspace then
last_active_workspace = current_active_workspace
current_active_workspace = new_active
end
else
-- For new workspaces (not existing ones), create the window first
local cwd, err = expand_home_path(item.id)
Expand Down Expand Up @@ -355,6 +446,13 @@ local function create_or_switch_workspace(item, window, pane)
pane
)

-- after switching/creating workspace, update history
local new_active = item.id
if new_active and new_active ~= current_active_workspace then
last_active_workspace = current_active_workspace
current_active_workspace = new_active
end

-- Create tabs and panes if configured
if item.tabs and #item.tabs > 0 then create_tabs_with_panes(new_window, item.tabs) end
end
Expand All @@ -380,7 +478,7 @@ end

--- Create the workspace switcher action
--- @return table: WezTerm action callback
local function switch_workspace_action()
function M.switch_workspace_action()
return wezterm.action_callback(function(window, pane)
local choices = get_all_workspace_choices()

Expand All @@ -397,7 +495,7 @@ local function switch_workspace_action()
act.InputSelector({
title = "Select Workspace",
choices = selector_choices,
fuzzy = true,
fuzzy = options.fuzzy,
action = wezterm.action_callback(function(win, p, id)
if not id then return end

Expand All @@ -423,6 +521,43 @@ local function switch_workspace_action()
end)
end

--- Switch to previously active workspace
--- @return table: WezTerm action callback
function M.switch_to_previous_workspace()
return wezterm.action_callback(function(window, pane)
if not last_active_workspace or last_active_workspace == "" then
wezterm.log_info("No previously active workspace recorded")
return
end

-- Find matching choice among all known workspaces; if not found, treat as name
local choices = get_all_workspace_choices()
local found = nil
for _, choice in ipairs(choices) do
if choice.id == last_active_workspace then
found = choice
break
end
end

if found then
create_or_switch_workspace(found, window, pane)
else
-- If choice isn't found, attempt a direct switch by name
window:perform_action(
act.SwitchToWorkspace({ name = last_active_workspace }),
pane
)
-- update history after switch
local new_active = last_active_workspace
if new_active and new_active ~= current_active_workspace then
last_active_workspace = current_active_workspace
current_active_workspace = new_active
end
end
end)
end

--- Validate a single workspace configuration entry
--- @param entry table: Configuration entry to validate
--- @return boolean: True if valid
Expand Down Expand Up @@ -474,19 +609,13 @@ function M.setup(config, opts)
end
end
end

-- initialize current active workspace if available
current_active_workspace = get_window_active_workspace(wezterm.gui and wezterm.gui.get_window and wezterm.gui.get_window() or nil) or mux.get_active_workspace()
end

--- Apply default keybindings to WezTerm configuration
--- @param config table: WezTerm configuration table
--- @param key string|nil: Key to bind (default: "f")
--- @param mods string|nil: Modifiers for keybinding (default: "LEADER")
function M.apply_to_config(config, key, mods)
config.keys = config.keys or {}
table.insert(config.keys, {
key = key or "f",
mods = mods or "LEADER",
action = switch_workspace_action(),
})
function M.apply_to_config(config)
end

return M