From f9efa56caeeb5289d6b2314e50041a1b7055a3c8 Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Tue, 5 Aug 2025 17:25:30 +0800 Subject: feat: add launcher support and image clipboard history utility - Integrated a new Launcher component into the shell.qml for improved application launching. - Implemented an IPC handler to toggle the launcher visibility. - Added a new script (cliphist-img.sh) to manage image history from the clipboard. - Removed the outdated fuzzysort.js utility to streamline the codebase. - Refactored the Levenshtein distance algorithm for better performance and clarity. --- launcher/ILauncher.qml | 29 +++ launcher/Launcher.qml | 371 ++++++++++++++++++++++++++++++++ launcher/components/App.qml | 143 ++++++++++++ launcher/components/Calc.qml | 80 +++++++ launcher/components/Clip.qml | 0 launcher/components/Finder.qml | 0 launcher/components/Flight.qml | 0 launcher/components/IComponent.qml | 72 +++++++ launcher/components/IInnerComponent.qml | 34 +++ launcher/components/SelectionState.qml | 27 +++ launcher/components/Spell.qml | 224 +++++++++++++++++++ launcher/components/Time.qml | 0 launcher/components/Translate.qml | 0 launcher/components/Weather.qml | 0 launcher/components/Web.qml | 0 launcher/todo.md | 33 +++ 16 files changed, 1013 insertions(+) create mode 100644 launcher/ILauncher.qml create mode 100644 launcher/Launcher.qml create mode 100644 launcher/components/App.qml create mode 100644 launcher/components/Calc.qml create mode 100644 launcher/components/Clip.qml create mode 100644 launcher/components/Finder.qml create mode 100644 launcher/components/Flight.qml create mode 100644 launcher/components/IComponent.qml create mode 100644 launcher/components/IInnerComponent.qml create mode 100644 launcher/components/SelectionState.qml create mode 100644 launcher/components/Spell.qml create mode 100644 launcher/components/Time.qml create mode 100644 launcher/components/Translate.qml create mode 100644 launcher/components/Weather.qml create mode 100644 launcher/components/Web.qml create mode 100644 launcher/todo.md (limited to 'launcher') diff --git a/launcher/ILauncher.qml b/launcher/ILauncher.qml new file mode 100644 index 0000000..8793b97 --- /dev/null +++ b/launcher/ILauncher.qml @@ -0,0 +1,29 @@ +import Quickshell +import Quickshell.Hyprland +import QtQuick.Controls +import QtQuick + +import qs.components +import qs.config + +IWindow { + id: root + required property var parentLoader + // TODO: optional only display on focused screen + focusable: true + + anchors { + top: false + bottom: false + left: false + right: false + } + + HyprlandFocusGrab { + active: true + windows: [root] + onCleared: { + parentLoader.active = false; // or active = true again + } + } +} diff --git a/launcher/Launcher.qml b/launcher/Launcher.qml new file mode 100644 index 0000000..e32dae5 --- /dev/null +++ b/launcher/Launcher.qml @@ -0,0 +1,371 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts + +import "components" +import qs.components +import qs.config + +// TODO: currently, we're relying on the compositor to provide the animation which ig is fine, but we should use our own in the future, however it might look weird combined with the compositors's +ILauncher { + id: launcher + name: "quickshell::launcher::launcher" + + anchors.top: true + margins.top: Launcher.topMargin + aboveWindows: true + exclusionMode: ExclusionMode.Ignore + + IRect { + id: container + property var selection: SelectionState?.selected || null + property var mappedSelection: selection?.mapToItem(null, 0, 0) + onMappedSelectionChanged: { + // SelectionState?.selected may be slow.. smh qt + // my guess is mapToItem is called when its not finished doing whatever it needs to do, + // so it doesnt get the right vars, but it doesn't update after since it already triggered it + selectionSync.running = true; + } + Timer { + id: selectionSync + running: false + repeat: false + // sync w/ IComponent y animation duration + interval: General.animationDuration / 4 + onTriggered: { + parent.mappedSelection = SelectionState?.selected?.mapToItem(null, 0, 0); + } + } + + IRect { + id: activeRect + visible: parent.mappedSelection || false + y: parent.mappedSelection?.y || 0 + x: parent.mappedSelection?.x || 0 + implicitWidth: SelectionState?.selected?.width || 0 + implicitHeight: SelectionState?.selected?.height || 0 + radius: Launcher.borderRadius + color: Qt.rgba(Colors.foreground2.r, Colors.foreground2.g, Colors.foreground2.b, Launcher.bgTransparency) + Behavior on x { + ISpringAnimation {} + } + Behavior on y { + ISpringAnimation {} + } + Behavior on implicitWidth { + ISpringAnimation {} + } + Behavior on height { + ISpringAnimation {} + } + } + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.bgTransparency) + radius: Launcher.borderRadius + + width: Math.max(root.width, Launcher.defaultWidth) + Launcher.innerMargin + height: root.height + + IRect { + id: root + property alias input: textInput.text + + height: searchBar.height + widgets.height + textInput.height / 2 // * 1.5 + width: Math.max(widgets.implicitWidth, Launcher.defaultWidth) + + anchors.centerIn: parent + + color: "transparent" + + RowLayout { + id: searchBar + anchors { + top: parent.top + left: parent.left + right: parent.right + leftMargin: Launcher.innerMargin + rightMargin: Launcher.innerMargin + // try to center it when no widgets are present + topMargin: widgets.height === Launcher.widgets.length * 5 - 5 ? parent.height / 2 - textInput.height / 2 : Launcher.innerMargin // textInput or Icon is the height of this + + Behavior on topMargin { + ISpringAnimation {} + // NumberAnimation { + // duration: General.animationDuration / 2 + // } + } + } + + spacing: Launcher.innerMargin * 2 + + Icon { + text: "" + } + + RowLayout { + spacing: 0 + TextInput { + id: textInput + Layout.fillHeight: true + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + color: Colors.foreground1 + selectionColor: Colors.background3 + text: SelectionState?.input || "" + clip: true + focus: true + renderType: Text.CurveRendering + antialiasing: true + smooth: true + font { + pixelSize: Style.font.size.large + family: Style.font.family.sans + wordSpacing: 5 + } + // move to different file if we have more textinput + cursorDelegate: IRect { + id: cursor + color: Colors.cursor + // width: 1.5 + width: 2 + height: 1 + // blinking animation + SequentialAnimation { + running: true + loops: Animation.Infinite + PauseAnimation { + duration: 500 + } + ParallelAnimation { + PropertyAnimation { + target: cursor + property: "opacity" + to: 0 + duration: General.animationDuration / 5 + } + PropertyAnimation { + target: cursor + property: "scale" + to: 0 + duration: General.animationDuration / 5 + } + } + PauseAnimation { + duration: 500 + } + ParallelAnimation { + PropertyAnimation { + target: cursor + property: "opacity" + to: 1 + duration: General.animationDuration / 5 + } + PropertyAnimation { + target: cursor + property: "scale" + to: 1 + duration: General.animationDuration / 5 + } + } + } + } + onTextChanged: { + if (SelectionState.input !== textInput.text) + SelectionState.input = textInput.text; // TODO: find ways to optimize this, like how i was using an alias + } + onCursorPositionChanged: { + SelectionState.cursorPosition = textInput.cursorPosition; + } + // onActiveFocusChanged: { + // parentLoader.active = activeFocus; + // } + onAccepted: { + SelectionState?.priorities[0]?._exec(); // TODO: kde like waiting animation for app to launch + parentLoader.active = false; + textInput.text = ""; + } + Component.onCompleted: { + selectAll(); + } + } + IRect { + opacity: SelectionState?.priorities[0]?.predictiveCompletion ? 1 : 0 + Layout.fillHeight: true + Layout.preferredWidth: text.width + radius: Launcher.predictiveCompletionRadius + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + IText { + id: text + // animate: true // too jumpy + color: Colors.foreground3 + renderType: Text.CurveRendering + antialiasing: true + smooth: true + font { + pixelSize: Style.font.size.large + family: Style.font.family.sans + wordSpacing: 5 + } + text: SelectionState?.priorities[0]?.predictiveCompletion || "" + } + Behavior on Layout.preferredWidth { + ISpringAnimation {} + // NumberAnimation { + // duration: General.animationDuration / 5 + // } + } + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 2 + easing.type: Easing.InOutQuad + } + } + } + Item { + Layout.fillWidth: true + } + Loader { + active: SelectionState?.priorities[0]?.priority || false // .priority should always be true if it's in priorities + sourceComponent: SelectionState?.priorities[0]?.preview + } + } + } + + ColumnLayout { + id: widgets + anchors { + top: searchBar.bottom + left: parent.left + right: parent.right + topMargin: Launcher.innerMargin * 1.5 // gap in between, maybe seperator FIXME + } + + Repeater { + model: Launcher.widgets + delegate: Loader { + active: true + sourceComponent: modelData + Component.onCompleted: { + SelectionState.widgets[index] = item; + // FIXME: prev predictiveCompletion still exists here + // workaround atm: just clear it or preserve the text manually + } + Connections { + target: item + function onClose() { + parentLoader.active = false; + } + } + } + } + } + } + + // not Keys.onPressed because textinput will steal it + Shortcut { + sequence: "Ctrl+H" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + if (!item) + return; + item.prev(); + } + } + + Shortcut { + sequence: "Ctrl+J" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + if (!item) + return; + item.down(); + } + } + + Shortcut { + sequence: "Ctrl+K" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + if (!item) + return; + item.up(); + } + } + + Shortcut { + sequence: "Ctrl+L" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + if (!item) + return; + item.next(); + } + } + + Shortcut { + sequence: "Escape" + context: Qt.ApplicationShortcut + onActivated: parentLoader.active = false + } + + Shortcut { + sequence: "Down" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + item?.down(); + } + } + + Shortcut { + sequence: "Up" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + item?.up(); + } + } + + Shortcut { + sequence: "PageDown" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + item?.pageDown(); + } + } + + Shortcut { + sequence: "PageUp" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + item?.pageUp(); + } + } + + Shortcut { + sequence: "Home" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + item?.home(); + } + } + + Shortcut { + sequence: "End" + context: Qt.ApplicationShortcut + onActivated: { + const item = SelectionState?.priorities[0]; + item?.end(); + } + } + } + + implicitWidth: container.width + implicitHeight: container.height +} diff --git a/launcher/components/App.qml b/launcher/components/App.qml new file mode 100644 index 0000000..6f0e4ce --- /dev/null +++ b/launcher/components/App.qml @@ -0,0 +1,143 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property list entries: inputCleaned ? AppSearch.fuzzyQuery(inputCleaned) : [] + property int selectedIndex: -1 + + name: "Applications" + + implicitHeight: valid ? layout.height : 0 + + preview: Component { + IconImage { + source: Quickshell.iconPath(answer, "image-missing") + implicitWidth: General.appIconSize + implicitHeight: General.appIconSize + } + } + + property string predictiveCompletion: valid ? entries[selectedIndex].name.slice(input.length) : "" + + process: function () { + const isValid = entries.length > 0; + const selected = isValid ? entries[selectedIndex] : ''; + + return { + valid: isValid, + priority: isValid, + answer: selected ? selected.icon : "" + }; + } + + exec: function () { + SelectionState.selected.modelData.execute(); + } + + up: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + down: function () { + if (selectedIndex + 1 > repeater.count - 1) + return true; + selectedIndex++; + } + + onEntriesChanged: { + selectedIndex = 0; + flickable.contentY = 0; + // selectedIndexChanged dont get called somehow + syncSelectionState(); + } + + onSelectedIndexChanged: { + syncSelectionState(); + if (selectedIndex < 0 || selectedIndex >= repeater.count) + return; + Qt.callLater(() => { + const i = repeater.itemAt(selectedIndex), t = i.y, b = i.y + i.height, v = flickable.contentY, vb = v + flickable.height, maxY = Math.max(0, flickable.contentHeight - flickable.height); + const y = t < v ? t : b > vb ? b - flickable.height : v; + flickable.contentY = Math.max(0, Math.min(maxY, y)); + }); + } + + function syncSelectionState() { + Qt.callLater(() => { + // fucking Qt why tf is repeater not ready when this is called + SelectionState.selected = repeater.itemAt(selectedIndex); + SelectionState.exec = root.exec; + }); + } + + IInnerComponent { + id: layout + fromParent: false + width: parent.width + height: Math.min(innerLayout.height + titleBar.height, Launcher.widgetHeight * 1.5) + + Flickable { + id: flickable + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + contentWidth: innerLayout.width + contentHeight: innerLayout.height + + ColumnLayout { + id: innerLayout + spacing: 0 + + Repeater { + id: repeater + model: entries + + Item { + required property DesktopEntry modelData + required property int index + Layout.margins: Launcher.innerMargin * 2 + implicitWidth: item.width + implicitHeight: item.height + + RowLayout { + id: item + spacing: Launcher.innerMargin * 2 + + IconImage { + source: Quickshell.iconPath(modelData.icon, "image-missing") + implicitWidth: General.appIconSize + implicitHeight: General.appIconSize + } + IText { + text: modelData.name + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + root.selectedIndex = index; + } + onExited: { + root.selectedIndex = 0; + } + onPressed: { + root._exec(); + } + } + } + } + } + } + } +} diff --git a/launcher/components/Calc.qml b/launcher/components/Calc.qml new file mode 100644 index 0000000..ec5b45f --- /dev/null +++ b/launcher/components/Calc.qml @@ -0,0 +1,80 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + property int cursorPosition: SelectionState.cursorPosition + name: "Calculator" + preview: Component { + Icon { + text: "" + } + } + + process: function () { + const isValid = /^(?=.*\d)(?=.*[+\-*\/\^])[0-9+\-*\/\^().\s]+$/.test(input); + const answer = isValid ? String(eval(input.replace(/\^/g, '**').replace(/([\d)])\(/g, '$1*(').replace(/\)([\d])/g, ')*$1'))) : null; + return { + valid: isValid, + priority: isValid, + answer: answer, + predictiveCompletion: isValid ? ' = ' + answer : '' + }; + } + exec: function () { + Clip.copy(answer); + } + + IInnerComponent { + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + + Item { + Layout.fillWidth: true + IText { + anchors.centerIn: parent + elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + clip: true + renderType: Text.CurveRendering + visible: valid + text: input.replace(/ /g, "").replace(/\+/g, " + ").replace(/-/g, " - ").replace(/\*/g, " × ").replace(/\//g, " ÷ ").replace(/%/g, " % ").replace(/\(/g, " ( ").replace(/\)/g, " ) ") + font { + pixelSize: Launcher.widgetFontSize + bold: true + } + } + } + + IText { + visible: valid + text: "→" + font.pixelSize: Launcher.widgetFontSize + font.bold: true + } + + Item { + Layout.fillWidth: true + IText { + animate: true + anchors.centerIn: parent + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + renderType: Text.CurveRendering + visible: valid + text: valid ? answer : '' + font { + pixelSize: Launcher.widgetFontSize + bold: true + } + } + } + } + } +} diff --git a/launcher/components/Clip.qml b/launcher/components/Clip.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/components/Finder.qml b/launcher/components/Finder.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/components/Flight.qml b/launcher/components/Flight.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/components/IComponent.qml b/launcher/components/IComponent.qml new file mode 100644 index 0000000..b8963d2 --- /dev/null +++ b/launcher/components/IComponent.qml @@ -0,0 +1,72 @@ +import Quickshell +import QtQuick + +import qs.components +import qs.config + +IRect { + id: root + // virtual properties + property string name // Component/File name + readonly property string input: SelectionState.input + readonly property string inputCleaned: input.toLowerCase().trim() + property string prefix + property bool valid: processed?.valid || false + property bool priority: valid && processed?.priority || false // please set priority to false if it's invalid + property string answer: processed?.answer || "" + property Component preview: Component { + IText { + animate: true + text: answer + } + } + property string predictiveCompletion: processed?.predictiveCompletion || "" // would technically work with just from answer, but stuff like calc's answer wouldnt have anything to do with the input + property var processed: input && input.startsWith(prefix) ? process() : {} // cached process, thought qml would do that automatically :/ + property var process: () => ({ + valid: valid, + priority: priority, + answer: answer, + preview: preview, + predictiveCompletion: predictiveCompletion + })// use the current IRect if valid is set + // actions + property var up: () => ({ + top: false + }) + property var down: () => ({ + bottom: false + }) + property var prev: () => {} + property var next: () => {} + property var exec: () => {} + + signal close + + function _exec() { + exec(); + close(); + } + + opacity: valid ? 1 : 0 + y: valid ? 0 : -Launcher.widgetHeight + + implicitWidth: valid ? Launcher.widgetWidth : 0 + implicitHeight: valid ? Launcher.widgetHeight : 0 + + // implicitWidth: Launcher.widgetWidth + // implicitHeight: Launcher.widgetHeight + + radius: Launcher.widgetRadius + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetBgTransparency) // TODO when prioritized, highlight + + onPriorityChanged: { + SelectionState.priorities = [...[...SelectionState.priorities, root].reduce((s, x) => (s[(s.has(x) && 'delete') || 'add'](x), s), new Set())]; // sync with SelectionState.priorities + } + + Behavior on y { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } +} diff --git a/launcher/components/IInnerComponent.qml b/launcher/components/IInnerComponent.qml new file mode 100644 index 0000000..7bbc9f0 --- /dev/null +++ b/launcher/components/IInnerComponent.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.config + +ColumnLayout { + property bool fromParent: true + property alias titleBar: titleBar + anchors.fill: fromParent ? parent : undefined + spacing: 0 + IRect { + id: titleBar + Layout.fillWidth: true + Layout.fillHeight: false + visible: Launcher.showWidgetTitle && valid + height: childrenRect.height + width: parent.width + topLeftRadius: Launcher.widgetRadius + topRightRadius: Launcher.widgetRadius + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetTitleBgTransparency) + + IText { + anchors { + top: parent.top + left: parent.left + leftMargin: Launcher.innerMargin + } + visible: valid + font.pixelSize: Launcher.widgetFontSize + text: name + } + } +} diff --git a/launcher/components/SelectionState.qml b/launcher/components/SelectionState.qml new file mode 100644 index 0000000..00d2420 --- /dev/null +++ b/launcher/components/SelectionState.qml @@ -0,0 +1,27 @@ +pragma Singleton +import Quickshell +import QtQuick + +Singleton { + property Item selected: null + property string input: "" + property int cursorPosition: 0 + property var priorities: [] + property var widgets: [] + + // order them based on widgets(which came from order from config) + onPrioritiesChanged: { + // priorities = priorities.filter(item => item.priority === true); // bind loop + // dunno y ts works w/o triggering a binding loop but okay.. + for (let i = 0; i < priorities.length; i++) + if (priorities[i].priority !== true) + priorities.splice(i, 1); + + priorities.sort((a, b) => widgets.indexOf(a) - widgets.indexOf(b)); + priorities.forEach((item, index) => {}); + } + + function clear() { + selected = null; + } +} diff --git a/launcher/components/Spell.qml b/launcher/components/Spell.qml new file mode 100644 index 0000000..4501132 --- /dev/null +++ b/launcher/components/Spell.qml @@ -0,0 +1,224 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property list aspell + property string word: input.slice(prefix.length) + property int selectedIndex: -1 + + name: "Spell" + prefix: name.toLowerCase() + " " + + onValidChanged: { + if (valid) + return; + + aspell = []; + proc.running = false; + selectedIndex = -1; + return; + } + + onAspellChanged: { + selectedIndex = aspell.length > 0 ? 0 : -1; + // selectedIndexChanged dont get called somehow + syncSelectionState(); + if (loader.item && loader.item.flickable) { + loader.item.flickable.contentY = 0; + } + } + + onSelectedIndexChanged: { + syncSelectionState(); + if (!loader.repeater || !loader.item || !loader.item.flickable) + return; + if (selectedIndex < 0 || selectedIndex >= loader.repeater.count) + return; + Qt.callLater(() => { + const flickable = loader.item.flickable; + const i = loader.repeater.itemAt(selectedIndex); + if (!i) + return; + const t = i.y; + const b = i.y + i.height; + const v = flickable.contentY; + const vb = v + flickable.height; + const maxY = Math.max(0, flickable.contentHeight - flickable.height); + const y = t < v ? t : b > vb ? b - flickable.height : v; + flickable.contentY = Math.max(0, Math.min(maxY, y)); + }); + } + + process: function () { + proc.running = true; + + const isValid = selectedIndex >= 0 && aspell.length > 0; + const selected = isValid ? aspell[selectedIndex] : "n"; + const answer = selected === "*" ? word : selected; + + return { + valid: isValid, + priority: isValid, + answer: isValid ? answer : "", + predictiveCompletion: isValid ? answer.slice(word.length) : "" + }; + } + + prev: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + + next: function () { + if (selectedIndex + 1 >= aspell.length) + return true; + selectedIndex++; + } + + up: function () { + const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))); + if (selectedIndex - cols < 0) + return true; + selectedIndex -= cols; + } + + down: function () { + const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))); + if (selectedIndex + cols >= aspell.length) + return true; + selectedIndex += cols; + } + + exec: function () { + if (selectedIndex >= 0) + if (aspell[selectedIndex] !== "*") + Clip.copy(aspell[selectedIndex]); + else + Clip.copy(word); + } + + function syncSelectionState() { + if (!loader.repeater) + return; + + Qt.callLater(() => { + SelectionState.selected = loader.repeater.itemAt(selectedIndex); + SelectionState.exec = root.exec; + }); + } + + IInnerComponent { + id: innerLoader + Loader { + id: loader + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + sourceComponent: aspell[0] === "*" ? correct : show + property var repeater: null + + readonly property Component show: Component { + Item { + property alias flickable: flickable + + Flickable { + id: flickable + anchors.fill: parent + clip: true + contentWidth: grid.width + contentHeight: grid.height + + GridLayout { + id: grid + width: flickable.width + columns: Math.max(1, Math.floor(width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))) + rowSpacing: Launcher.innerMargin + columnSpacing: Launcher.innerMargin + + Repeater { + id: repeater + model: aspell + + IRect { + Layout.fillWidth: true + Layout.preferredWidth: Math.max(chipContent.implicitWidth + Launcher.innerMargin * 2, Math.floor(grid.width / grid.columns) - grid.columnSpacing) + Layout.preferredHeight: chipContent.implicitHeight + Launcher.innerMargin * 2 + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + radius: Launcher.widgetRadius + + IText { + id: chipContent + anchors.centerIn: parent + text: modelData + font.pixelSize: Launcher.widgetFontSize + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + root.selectedIndex = index; + } + onPressed: { + root._exec(); + } + } + } + + Component.onCompleted: { + loader.repeater = repeater; + } + } + } + } + } + } + + readonly property Component correct: Component { + IRect { + Layout.fillWidth: true + color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.widgetBgTransparency / 4) + radius: Launcher.widgetRadius + IText { + anchors.centerIn: parent + text: "Correct" + font.pixelSize: Launcher.widgetFontSize + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + + Process { + id: proc + command: ["sh", "-c", "echo " + word + " | aspell -a"] + property bool found: false + stdout: SplitParser { + onRead: data => { + if (data.startsWith("&")) { + proc.found = true; + aspell = data.split(/\W+/).filter(word => /^[A-Za-z]+$/.test(word)).slice(1); + } else if (data.startsWith("*")) + aspell = ["*"]; + } + } + onStarted: { + found = false; + } + onExited: { + if (!found) + aspell = []; + } + } +} diff --git a/launcher/components/Time.qml b/launcher/components/Time.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/components/Translate.qml b/launcher/components/Translate.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/components/Weather.qml b/launcher/components/Weather.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/components/Web.qml b/launcher/components/Web.qml new file mode 100644 index 0000000..e69de29 diff --git a/launcher/todo.md b/launcher/todo.md new file mode 100644 index 0000000..bfc2103 --- /dev/null +++ b/launcher/todo.md @@ -0,0 +1,33 @@ +when finished: +- [x] reloads dynamically while typing in +- [x] prefix: ... for certain ambiguous queries such as "pi" or "e" for math +- [ ] bang (like ddg's) +- [ ] web search catch all +- [x] height dynamically shrink/grow according to content + +- [ ] web: + - [ ] show multiple options: + - [ ] first - search w/ search engine and open in browser + - [ ] second - fallback/second option + - [ ] custom quick search/fetch + - [ ] e.g. wikipedia: (also stuff like spotify, youtube) + - [ ] match title + - [ ] match first paragraph/description + +- [ ] files: + - [ ] latest modified date + - [ ] file size + - [ ] where it belongs + - [ ] icon/preview/quick look + +- [ ] math: + - [x] expression + - [ ] natural language (i.e. "5 of 3") + - [ ] unit conversion + - [ ] temperature + - [ ] length + - [ ] weight + - [ ] currency + - [ ] color + - [ ] time + -- cgit v1.2.3