diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index 18a0ab955..7570e0751 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -17,6 +17,7 @@ bind = Super, mouse_up, global, quickshell:overviewToggleReleaseInterrupt # [hi bind = Super, mouse_down,global, quickshell:overviewToggleReleaseInterrupt # [hidden] bindit = ,Super_L, global, quickshell:workspaceNumber # [hidden] +bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard bindd = Super, Tab, Toggle overview, global, quickshell:overviewToggle # [hidden] Toggle overview/launcher (alt) bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] bindd = Super, A, Toggle left sidebar, global, quickshell:sidebarLeftToggle # Toggle left sidebar @@ -47,7 +48,7 @@ bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || logi ##! Utilities # Screenshot, Record, OCR, Color picker, Clipboard history -bindd = Super, V, Copy clipboard history entry, exec, pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # Clipboard history >> clipboard +bindd = Super, V, Copy clipboard history entry, exec, qs ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # Clipboard history >> clipboard bindd = Super, Period, Copy an emoji, exec, pkill fuzzel || ~/.local/bin/fuzzel-emoji # Emoji bindd = Super+Shift, S, Screen snip, exec, grimblast --freeze copy area # Screen snip >> clipboard bindd = Super+Shift+Alt, S, Screen snip and annotate, exec, grim -g "$(slurp)" - | swappy -f - # Screen snip and annotate diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml index ce4ddc0f9..f38d8f056 100644 --- a/.config/quickshell/modules/common/ConfigOptions.qml +++ b/.config/quickshell/modules/common/ConfigOptions.qml @@ -62,6 +62,7 @@ Singleton { property list excludedSites: [ "quora.com" ] property QtObject prefix: QtObject { property string action: "/" + property string clipboard: ":" } } diff --git a/.config/quickshell/modules/overview/Overview.qml b/.config/quickshell/modules/overview/Overview.qml index 9625f6c7b..8f0ca8a8d 100644 --- a/.config/quickshell/modules/overview/Overview.qml +++ b/.config/quickshell/modules/overview/Overview.qml @@ -10,7 +10,10 @@ import Quickshell.Wayland import Quickshell.Hyprland Scope { + id: overviewScope + property bool dontAutoCancelSearch: false Variants { + id: overviewVariants model: Quickshell.screens PanelWindow { id: root @@ -50,7 +53,14 @@ Scope { Connections { target: GlobalStates function onOverviewOpenChanged() { - delayedGrabTimer.start() + if (!GlobalStates.overviewOpen) { + overviewScope.dontAutoCancelSearch = false; + } else { + if (!overviewScope.dontAutoCancelSearch) { + searchWidget.cancelSearch() + } + delayedGrabTimer.start() + } } } @@ -67,6 +77,10 @@ Scope { implicitWidth: columnLayout.width implicitHeight: columnLayout.height + function setSearchingText(text) { + searchWidget.setSearchingText(text); + } + ColumnLayout { id: columnLayout visible: GlobalStates.overviewOpen @@ -84,6 +98,7 @@ Scope { } SearchWidget { + id: searchWidget panelWindow: root Layout.alignment: Qt.AlignHCenter onSearchingTextChanged: (text) => { @@ -163,5 +178,27 @@ Scope { GlobalStates.superReleaseMightTrigger = false } } + GlobalShortcut { + name: "overviewClipboardToggle" + description: qsTr("Toggle clipboard query on overview widget") + + onPressed: { + if (GlobalStates.overviewOpen) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText( + ConfigOptions.search.prefix.clipboard + ); + GlobalStates.overviewOpen = true; + return + } + } + } + } } diff --git a/.config/quickshell/modules/overview/SearchItem.qml b/.config/quickshell/modules/overview/SearchItem.qml index 95e4c744f..7008ad8d9 100644 --- a/.config/quickshell/modules/overview/SearchItem.qml +++ b/.config/quickshell/modules/overview/SearchItem.qml @@ -67,7 +67,7 @@ RippleButton { RowLayout { id: rowLayout - spacing: 10 + spacing: iconLoader.sourceComponent === null ? 0 : 10 anchors.fill: parent anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding @@ -76,7 +76,9 @@ RippleButton { Loader { id: iconLoader active: true - sourceComponent: root.materialSymbol == "" ? iconImageComponent : materialSymbolComponent + sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent : + root.itemIcon !== "" ? iconImageComponent : + null } Component { @@ -111,6 +113,7 @@ RippleButton { StyledText { Layout.fillWidth: true id: nameText + textFormat: Text.PlainText // TODO: make cliphist entry highlighting working font.pixelSize: Appearance.font.pixelSize.normal font.family: Appearance.font.family[root.fontType] color: Appearance.m3colors.m3onSurface diff --git a/.config/quickshell/modules/overview/SearchWidget.qml b/.config/quickshell/modules/overview/SearchWidget.qml index 4320a96a1..1af0c6b9a 100644 --- a/.config/quickshell/modules/overview/SearchWidget.qml +++ b/.config/quickshell/modules/overview/SearchWidget.qml @@ -24,6 +24,21 @@ Item { // Wrapper implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2 property string mathResult: "" + property bool lastQueryWasClipboard: false + + onShowResultsChanged: { + lastQueryWasClipboard = false; + } + function cancelSearch() { + searchInput.selectAll() + root.searchingText = "" + } + + function setSearchingText(text) { + searchInput.text = text; + root.searchingText = text; + } + property var searchActions: [ { action: "img", @@ -194,7 +209,7 @@ Item { // Wrapper Layout.leftMargin: 15 iconSize: Appearance.font.pixelSize.huge color: Appearance.m3colors.m3onSurface - text: "search" + text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : 'search' } TextField { // Search box id: searchInput @@ -219,13 +234,6 @@ Item { // Wrapper } onTextChanged: root.searchingText = text - Connections { - target: root - function onVisibleChanged() { - searchInput.selectAll() - root.searchingText = "" - } - } onAccepted: { if (appResults.count > 0) { @@ -279,10 +287,31 @@ Item { // Wrapper model: ScriptModel { id: model - values: { + values: { // Search results are handled here + ////////////////// Skip? ////////////////// if(root.searchingText == "") return []; - // Start math calculation, declare special result objects + ///////////// Special cases /////////////// + if (root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard)) { // Clipboard + if (!root.lastQueryWasClipboard) { + root.lastQueryWasClipboard = true; + Cliphist.refresh(); // Refresh clipboard entries + } + const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length); + return Cliphist.fuzzyQuery(searchString).map(entry => { + return { + name: entry.replace(/^\s*\S+\s+/, ""), + clickActionName: qsTr("Copy"), + type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, + execute: () => { + Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`); + } + }; + }).filter(Boolean); + } + + + ////////////////// Init /////////////////// nonAppResultsTimer.restart(); const mathResultObject = { name: root.mathResult, @@ -322,10 +351,9 @@ Item { // Wrapper }) .filter(Boolean); - // Init result array let result = []; - // Add filtered application entries + //////////////// Apps ////////////////// result = result.concat( AppSearch.fuzzyQuery(root.searchingText) .map((entry) => { @@ -335,23 +363,20 @@ Item { // Wrapper }) ); - // Add launcher actions + ////////// Launcher actions //////////// result = result.concat(launcherActionObjects); - // Insert math result before command if search starts with a number + /////////// Math result & command ////////// const startsWithNumber = /^\d/.test(root.searchingText); - if (startsWithNumber) + if (startsWithNumber) { result.push(mathResultObject); - - // Command - result.push(commandResultObject); - - // If not already added, add math result after command - if (!startsWithNumber) { + result.push(commandResultObject); + } else { + result.push(commandResultObject); result.push(mathResultObject); } - // Web search + ///////////////// Web search //////////////// result.push({ name: root.searchingText, clickActionName: qsTr("Search"), @@ -369,7 +394,8 @@ Item { // Wrapper return result; } } - delegate: SearchItem { + + delegate: SearchItem { // The selectable item for each search result entry: modelData } } diff --git a/.config/quickshell/services/Cliphist.qml b/.config/quickshell/services/Cliphist.qml new file mode 100644 index 000000000..f835a5f96 --- /dev/null +++ b/.config/quickshell/services/Cliphist.qml @@ -0,0 +1,57 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common/functions/fuzzysort.js" as Fuzzy +import "root:/modules/common" +import "root:/" +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property list entries: [] + property string highlightPrefix: `` + property string highlightSuffix: `` + readonly property var preparedEntries: entries.map(a => ({ + name: Fuzzy.prepare(`${a}`), + entry: a + })) + function fuzzyQuery(search: string): var { + return Fuzzy.go(search, preparedEntries, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry + // console.log(JSON.stringify(r)) + // return r.highlight(highlightPrefix, highlightSuffix); + }); + } + + function refresh() { + readProc.buffer = [] + readProc.running = false + readProc.running = true + } + + Process { + id: readProc + property list buffer: [] + + command: ["cliphist", "list"] + + stdout: SplitParser { + onRead: (line) => { + readProc.buffer.push(line) + } + } + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + root.entries = readProc.buffer + } else { + console.error("[Cliphist] Failed to refresh with code", exitCode, "and status", exitStatus) + } + } + } +} diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml index af14d6b74..ec506a3b9 100644 --- a/.config/quickshell/shell.qml +++ b/.config/quickshell/shell.qml @@ -24,6 +24,7 @@ ShellRoot { MaterialThemeLoader.reapplyTheme() ConfigLoader.loadConfig() PersistentStateManager.loadStates() + Cliphist.refresh() } Bar {}