diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index 4136f02..8ba0d96 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index bdd8120..0d1dc77 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -37,6 +37,8 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="move-btn"', body) self.assertIn('id="left-breadcrumbs"', body) self.assertIn('id="right-breadcrumbs"', body) + self.assertIn('id="wildcard-popup"', body) + self.assertIn('id="wildcard-pattern-input"', body) self.assertNotIn('id="bookmarks-panel"', body) self.assertNotIn('id="tasks-panel"', body) diff --git a/webui/html/app.js b/webui/html/app.js index 31abde7..f348ba5 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -22,6 +22,7 @@ let state = { lastTaskCount: 0, }; const ROW_JUMP_STEP = 10; +let wildcardDialogMode = "select"; function paneState(pane) { return state.panes[pane]; @@ -616,6 +617,104 @@ function shouldHandleShortcut(target) { return false; } +function wildcardPopupElements() { + return { + overlay: document.getElementById("wildcard-popup"), + title: document.getElementById("wildcard-popup-title"), + meta: document.getElementById("wildcard-popup-meta"), + input: document.getElementById("wildcard-pattern-input"), + error: document.getElementById("wildcard-popup-error"), + applyButton: document.getElementById("wildcard-apply-btn"), + cancelButton: document.getElementById("wildcard-cancel-btn"), + }; +} + +function isWildcardPopupOpen() { + return !wildcardPopupElements().overlay.classList.contains("hidden"); +} + +function escapeRegExp(text) { + return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"); +} + +function globToRegExp(pattern) { + const escaped = pattern + .split("*") + .map((part) => escapeRegExp(part)) + .join(".*"); + return new RegExp(`^${escaped}$`, "i"); +} + +function applyWildcardSelection(mode, pattern) { + const pane = state.activePane; + const model = paneState(pane); + const matcher = globToRegExp(pattern); + const candidates = model.visibleItems.filter((entry) => !entry.isParent); + const matches = candidates.filter((entry) => matcher.test(entry.name)); + const matchPaths = new Set(matches.map((entry) => entry.path)); + + let changed = 0; + if (mode === "select") { + const existing = new Set(model.selectedItems.map((item) => item.path)); + for (const entry of matches) { + if (existing.has(entry.path)) { + continue; + } + model.selectedItems.push({ path: entry.path, name: entry.name, kind: entry.kind }); + changed += 1; + } + if (matches.length > 0) { + const last = matches[matches.length - 1]; + model.selectedItem = { path: last.path, name: last.name, kind: last.kind }; + } + } else { + const before = model.selectedItems.length; + model.selectedItems = model.selectedItems.filter((item) => !matchPaths.has(item.path)); + changed = before - model.selectedItems.length; + if (!model.selectedItem || matchPaths.has(model.selectedItem.path)) { + model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null; + } + } + + renderPaneItems(pane); + setStatus(`Wildcard ${mode}: ${matches.length} matched, ${changed} changed`); +} + +function closeWildcardPopup() { + const elements = wildcardPopupElements(); + elements.overlay.classList.add("hidden"); + elements.error.textContent = ""; + elements.input.value = ""; +} + +function submitWildcardPopup() { + const elements = wildcardPopupElements(); + const pattern = elements.input.value.trim(); + if (!pattern) { + elements.error.textContent = "Pattern is required"; + return; + } + try { + applyWildcardSelection(wildcardDialogMode, pattern); + closeWildcardPopup(); + } catch (err) { + elements.error.textContent = `Wildcard: ${err.message}`; + } +} + +function openWildcardPopup(mode) { + wildcardDialogMode = mode; + const pane = state.activePane; + const elements = wildcardPopupElements(); + elements.title.textContent = mode === "select" ? "Wildcard Select" : "Wildcard Deselect"; + elements.meta.textContent = `Active pane: ${pane} (visible items only, case-insensitive)`; + elements.applyButton.textContent = mode === "select" ? "Select" : "Deselect"; + elements.error.textContent = ""; + elements.input.value = ""; + elements.overlay.classList.remove("hidden"); + elements.input.focus(); +} + function moveCurrentRow(delta) { const pane = state.activePane; const model = paneState(pane); @@ -671,10 +770,23 @@ function clearSelectionForActivePane() { } function handleKeyboardShortcuts(event) { + if (isWildcardPopupOpen()) { + return; + } if (!shouldHandleShortcut(event.target)) { return; } + if (event.shiftKey && event.key === "+") { + event.preventDefault(); + openWildcardPopup("select"); + return; + } + if (event.shiftKey && event.key === "_") { + event.preventDefault(); + openWildcardPopup("deselect"); + return; + } if (event.metaKey && event.key === "ArrowUp") { event.preventDefault(); jumpCurrentRow("start"); @@ -745,6 +857,26 @@ function setupEvents() { document.getElementById("move-btn").onclick = startMoveSelected; document.getElementById("mkdir-btn").onclick = createFolderForActivePane; document.getElementById("add-bookmark-btn").onclick = addBookmark; + + const wildcard = wildcardPopupElements(); + wildcard.cancelButton.onclick = closeWildcardPopup; + wildcard.applyButton.onclick = submitWildcardPopup; + wildcard.input.onkeydown = (event) => { + if (event.key === "Enter") { + event.preventDefault(); + submitWildcardPopup(); + return; + } + if (event.key === "Escape") { + event.preventDefault(); + closeWildcardPopup(); + } + }; + wildcard.overlay.onclick = (event) => { + if (event.target === wildcard.overlay) { + closeWildcardPopup(); + } + }; } async function init() { diff --git a/webui/html/index.html b/webui/html/index.html index 01a2844..daf9bad 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -73,6 +73,20 @@ +
+