local wezterm = require("wezterm") local state = require("plugins.sessionizer.src.state") local command_builder = require("plugins.sessionizer.src.command_builder") local utils = require("plugins.sessionizer.src.utils") local notify = require("plugins.sessionizer.src.notify") local M = {} local _scan_cache = {} local _meta_lookup = {} local CONTROL_PATTERN = "[%z\r\n]" local function deep_copy(list) local copy = {} for i, v in ipairs(list or {}) do local project_copy = {} for k, val in pairs(v) do project_copy[k] = val end copy[i] = project_copy end return copy end local function clear_scan_cache() for k in pairs(_scan_cache) do _scan_cache[k] = nil end for k in pairs(_meta_lookup) do _meta_lookup[k] = nil end end local function cache_list(list) state.cached_directories = list state.cached_checksum = utils.checksum(list) end local function scan_base(base) local key = tostring(base.path) .. ":" .. tostring(base.max_depth or "default") if _scan_cache[key] then if state.profiler_enabled then wezterm.log_info(("[sessionizer] scan_base %s: cache_hit=true"):format(tostring(base.path))) end return _scan_cache[key] end return utils.profile("scan_base " .. tostring(base.path), function() local paths = utils.resolve_glob_all(base.path) local cmd = command_builder.build_cmd(base) local backend = cmd[1] or "" local ok, out = utils.retry_command(cmd, 3, 200) if not ok then local message = "Failed to scan: " .. tostring(base.path) wezterm.log_error(message) state.last_scan_error = message return {} end local choices = {} local out_lines = 0 local skipped_control = 0 for _, line in ipairs(wezterm.split_by_newlines(out)) do out_lines = out_lines + 1 if line ~= "" then if line:find(CONTROL_PATTERN) then skipped_control = skipped_control + 1 wezterm.log_error("[sessionizer] Skipping path with control characters: " .. line) else local id = line local label = line:gsub(wezterm.home_dir, "~") label = label:gsub(CONTROL_PATTERN, "") table.insert(choices, { id = id, label = label, }) _meta_lookup[id] = { workspace = utils.basename(line), title = utils.basename(line), path = line, } end end end _scan_cache[key] = choices state.last_scan_error = nil if state.profiler_enabled then wezterm.log_info(( "[sessionizer] scan_base %s: cache_hit=false backend=%s glob_paths=%d lines=%d dirs=%d skipped_control=%d" ):format( tostring(base.path), tostring(backend), #paths, out_lines, #choices, skipped_control )) end return choices end) end local function perform_scan() return utils.profile("workspace_scan", function() local list = {} local seen = {} local last_error = nil local bases = #state.project_base local total_dirs = 0 for _, base in ipairs(state.project_base) do local ok, scanned = pcall(scan_base, base) if ok and scanned then total_dirs = total_dirs + #scanned for _, folder in ipairs(scanned) do if folder.id and not seen[folder.id] then table.insert(list, folder) seen[folder.id] = true end end else last_error = "Failed to scan project base: " .. tostring(base.path) wezterm.log_error(last_error) notify.show(last_error) end end cache_list(list) state.last_scan_error = last_error if state.profiler_enabled then wezterm.log_info(( "[sessionizer] workspace_scan: bases=%d total_dirs=%d unique_dirs=%d" ):format(bases, total_dirs, #list)) end return list end) end local function finish_scan(ok, result) if not ok then local message = "[sessionizer] Workspace scan failed: " .. tostring(result) wezterm.log_error(message) state.last_scan_error = message notify.show(message) end state.scan_in_progress = false if state.scan_pending then state.scan_pending = false M.refresh_async() end end --- Kick off a background refresh of project directories without blocking the UI. function M.refresh_async() if state.scan_in_progress then state.scan_pending = true return end if state.config_changed then clear_scan_cache() state.config_changed = false end if #state.project_base == 0 then cache_list({}) state.last_scan_error = "No project bases configured" return end state.scan_in_progress = true wezterm.time.call_after(0, function() local ok, result = pcall(perform_scan) finish_scan(ok, result) end) end --- Get a list of cached project directories. Triggers a refresh if cache is empty/stale. function M.all_dirs() if state.cached_directories and utils.checksum(state.cached_directories) == state.cached_checksum then return deep_copy(state.cached_directories) end if not state.scan_in_progress then M.refresh_async() end return {} end --- Get cached project directories without triggering a refresh. function M.cached_dirs() if state.cached_directories and utils.checksum(state.cached_directories) == state.cached_checksum then return deep_copy(state.cached_directories) end return {} end function M.get_metadata(id) return _meta_lookup[id] end function M.clear_scan_cache() clear_scan_cache() end return M