diff --git a/README.md b/README.md index ee35650..6bc1c9e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 | diff --git a/plugin/init.lua b/plugin/init.lua index 45a64f1..0c86ca3 100644 --- a/plugin/init.lua +++ b/plugin/init.lua @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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( @@ -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) @@ -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 @@ -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() @@ -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 @@ -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 @@ -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