From b4bcb4ca1ddf98bf91ae486bfca7ea6c1be646ab Mon Sep 17 00:00:00 2001 From: Mario Date: Fri, 6 Feb 2026 01:49:51 +0100 Subject: [PATCH] feat: calculadora epica --- modules/services/GlobalShortcuts.qml | 8 + modules/theme/Icons.qml | 1 + .../dashboard/calculator/CalculatorTab.qml | 320 ++++++++++++++++++ modules/widgets/launcher/LauncherView.qml | 38 ++- 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 modules/widgets/dashboard/calculator/CalculatorTab.qml diff --git a/modules/services/GlobalShortcuts.qml b/modules/services/GlobalShortcuts.qml index 7e7f1032..dc04f8aa 100644 --- a/modules/services/GlobalShortcuts.qml +++ b/modules/services/GlobalShortcuts.qml @@ -39,6 +39,7 @@ QtObject { case "emoji": toggleLauncherWithPrefix(2, Config.prefix.emoji + " "); break; case "tmux": toggleLauncherWithPrefix(3, Config.prefix.tmux + " "); break; case "notes": toggleLauncherWithPrefix(4, Config.prefix.notes + " "); break; + case "calculator": toggleLauncherWithPrefix(5, Config.prefix.calculator + " "); break; // Dashboard case "dashboard": toggleDashboardTab(0); break; @@ -280,6 +281,13 @@ QtObject { onPressed: toggleLauncherWithPrefix(4, Config.prefix.notes + " ") } + property GlobalShortcut shortcutCalculator: GlobalShortcut { + appid: root.appId + name: "calculator" + description: "Open launcher calculator" + onPressed: toggleLauncherWithPrefix(5, Config.prefix.calculator + " ") + } + // Dashboard shortcuts property GlobalShortcut shortcutDashboard: GlobalShortcut { appid: root.appId diff --git a/modules/theme/Icons.qml b/modules/theme/Icons.qml index c6a690a5..ecc5b4e7 100644 --- a/modules/theme/Icons.qml +++ b/modules/theme/Icons.qml @@ -49,6 +49,7 @@ QtObject { readonly property string terminalWindow: "" readonly property string clipboard: "" readonly property string emoji: "" + readonly property string calculate: "\ue538" readonly property string shortcut: "" readonly property string launch: "" readonly property string pin: "" diff --git a/modules/widgets/dashboard/calculator/CalculatorTab.qml b/modules/widgets/dashboard/calculator/CalculatorTab.qml new file mode 100644 index 00000000..a20206ed --- /dev/null +++ b/modules/widgets/dashboard/calculator/CalculatorTab.qml @@ -0,0 +1,320 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.modules.theme +import qs.modules.components +import qs.modules.globals +import qs.modules.services +import qs.config + +Item { + id: root + focus: true + + // Prefix support + property string prefixIcon: Icons.calculate + signal backspaceOnEmpty + + property int leftPanelWidth: 0 + + property string searchText: "" + property string mathResult: "" + property var history: [] + + // Model for the ListView + ListModel { id: resultsModel } + + function focusSearchInput() { searchInput.focusInput(); } + + // History management + function loadHistory() { + historyProcess.command = ["bash", "-c", "cat " + Quickshell.dataDir + "/calc_history.json 2>/dev/null || echo '[]'"]; + historyProcess.running = true; + } + + function saveHistory() { + var jsonData = JSON.stringify(history); + saveProcess.command = ["bash", "-c", "echo '" + jsonData.replace(/'/g, "'\\''") + "' > " + Quickshell.dataDir + "/calc_history.json"]; + saveProcess.running = true; + } + + function addToHistory(expression, result) { + // Remove duplicate if exists + history = history.filter(item => item.expression !== expression); + // Add to top + history.unshift({ expression: expression, result: result, timestamp: Date.now() }); + // Limit to 50 + if (history.length > 50) history = history.slice(0, 50); + saveHistory(); + updateModel(); + } + + function updateModel() { + resultsModel.clear(); + + // Add current calculation result if any + if (searchText.trim() !== "" && mathResult !== "") { + resultsModel.append({ + type: "result", + expression: searchText, + result: mathResult, + icon: "calculate" + }); + } + + // Add history + for (var i = 0; i < history.length; i++) { + if (searchText === "" || history[i].expression.includes(searchText) || history[i].result.includes(searchText)) { + resultsModel.append({ + type: "history", + expression: history[i].expression, + result: history[i].result, + icon: "history" + }); + } + } + + // Auto-select first item + if (resultsModel.count > 0) { + resultList.currentIndex = 0; + } else { + resultList.currentIndex = -1; + } + } + + Process { + id: mathProc + property list baseCommand: ["qalc", "-t"] + function calculateExpression(expression) { + if (expression.trim() === "") { + root.mathResult = ""; + updateModel(); + return; + } + mathProc.command = baseCommand.concat(expression); + mathProc.running = true; + } + stdout: SplitParser { + onRead: data => { + root.mathResult = data.trim(); + updateModel(); + } + } + } + + // Timer to debounce calculation + Timer { + id: calcTimer + interval: 10 // Fast response + repeat: false + onTriggered: { + mathProc.calculateExpression(root.searchText); + } + } + + onSearchTextChanged: { + calcTimer.restart(); + } + + Process { + id: historyProcess + stdout: StdioCollector { + onStreamFinished: { + try { + history = JSON.parse(text.trim()); + updateModel(); + } catch (e) { history = []; } + } + } + } + + Process { id: saveProcess } + + Component.onCompleted: { + loadHistory(); + Qt.callLater(() => focusSearchInput()); + } + + Item { + id: mainLayout + anchors.fill: parent + + Row { + id: searchRow + width: parent.width + height: 48 + anchors.top: parent.top + spacing: 8 + + SearchInput { + id: searchInput + width: parent.width + height: 48 + text: root.searchText + placeholderText: "Calculate..." + prefixIcon: root.prefixIcon + + onSearchTextChanged: text => root.searchText = text + onBackspaceOnEmpty: root.backspaceOnEmpty() + onAccepted: { + if (resultList.count > 0 && resultList.currentIndex >= 0) { + let item = resultsModel.get(resultList.currentIndex); + if (item.type === "result") { + root.addToHistory(item.expression, item.result); + Visibilities.setActiveModule(""); + ClipboardService.copy(item.result); + } else { + Visibilities.setActiveModule(""); + ClipboardService.copy(item.result); + } + } + } + + onDownPressed: { + if (resultList.count > 0) { + resultList.currentIndex = Math.min(resultList.currentIndex + 1, resultList.count - 1); + } + } + + onUpPressed: { + if (resultList.count > 0) { + resultList.currentIndex = Math.max(resultList.currentIndex - 1, 0); + } + } + + onEscapePressed: Visibilities.setActiveModule("") + } + } + + ListView { + id: resultList + width: parent.width + anchors.top: searchRow.bottom + anchors.bottom: parent.bottom + anchors.topMargin: 8 + clip: true + model: resultsModel + spacing: 4 + currentIndex: -1 + + highlight: StyledRect { + variant: "primary" + radius: Styling.radius(4) + visible: resultList.currentIndex >= 0 + z: -1 + + Behavior on opacity { + enabled: Config.animDuration > 0 + NumberAnimation { duration: Config.animDuration / 2 } + } + Behavior on y { + enabled: Config.animDuration > 0 + NumberAnimation { duration: Config.animDuration / 2; easing.type: Easing.OutCubic } + } + } + highlightFollowsCurrentItem: true + highlightMoveDuration: Config.animDuration > 0 ? Config.animDuration / 2 : 0 + + delegate: Rectangle { + id: delegateRoot + width: resultList.width + height: 48 + color: "transparent" + radius: Styling.radius(4) + + property bool isHovered: false + property bool isSelected: ListView.isCurrentItem + property color dynamicTextColor: (isHovered || isSelected) ? Colors.overPrimary : Colors.overBackground + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + parent.isHovered = true; + resultList.currentIndex = index; + } + onExited: parent.isHovered = false + onClicked: { + ClipboardService.copy(model.result); + Visibilities.setActiveModule(""); + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 12 + + // Icon + StyledRect { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignVCenter + radius: Styling.radius(-4) + variant: delegateRoot.isSelected ? "overprimary" : "common" + + Text { + anchors.centerIn: parent + text: model.icon === "calculate" ? Icons.calculate : Icons.clock + font.family: Icons.font + font.pixelSize: 20 + color: delegateRoot.isSelected ? Colors.overSurface : delegateRoot.dynamicTextColor + + Behavior on color { + enabled: Config.animDuration > 0 + ColorAnimation { + duration: Config.animDuration / 2 + easing.type: Easing.OutQuart + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + text: model.expression + font.family: Config.theme.font + font.pixelSize: Config.theme.fontSize * 0.9 + color: delegateRoot.dynamicTextColor + opacity: 0.7 + elide: Text.ElideRight + + Behavior on color { + enabled: Config.animDuration > 0 + ColorAnimation { + duration: Config.animDuration / 2 + easing.type: Easing.OutQuart + } + } + } + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + text: "= " + model.result + font.family: Config.theme.font + font.weight: Font.Bold + font.pixelSize: Config.theme.fontSize + color: delegateRoot.dynamicTextColor + elide: Text.ElideRight + + Behavior on color { + enabled: Config.animDuration > 0 + ColorAnimation { + duration: Config.animDuration / 2 + easing.type: Easing.OutQuart + } + } + } + } + } + } + } + } +} diff --git a/modules/widgets/launcher/LauncherView.qml b/modules/widgets/launcher/LauncherView.qml index 39a6390b..738264c8 100644 --- a/modules/widgets/launcher/LauncherView.qml +++ b/modules/widgets/launcher/LauncherView.qml @@ -14,6 +14,7 @@ import "../dashboard/clipboard" import "../dashboard/emoji" import "../dashboard/tmux" import "../dashboard/notes" +import "../dashboard/calculator" Rectangle { id: root @@ -88,14 +89,15 @@ Rectangle { let emojiPrefix = Config.prefix.emoji + " "; let tmuxPrefix = Config.prefix.tmux + " "; let notesPrefix = Config.prefix.notes + " "; + let calculatorPrefix = Config.prefix.calculator + " "; // If prefix was manually disabled, don't re-enable until conditions are met if (prefixDisabled) { // Only re-enable prefix if user deletes the prefix text or adds valid content - if (text === clipPrefix || text === emojiPrefix || text === tmuxPrefix || text === notesPrefix) { + if (text === clipPrefix || text === emojiPrefix || text === tmuxPrefix || text === notesPrefix || text === calculatorPrefix) { // Still at exact prefix - keep disabled return 0; - } else if (!text.startsWith(clipPrefix) && !text.startsWith(emojiPrefix) && !text.startsWith(tmuxPrefix) && !text.startsWith(notesPrefix)) { + } else if (!text.startsWith(clipPrefix) && !text.startsWith(emojiPrefix) && !text.startsWith(tmuxPrefix) && !text.startsWith(notesPrefix) && !text.startsWith(calculatorPrefix)) { // User deleted the prefix - re-enable detection prefixDisabled = false; return 0; @@ -114,6 +116,8 @@ Rectangle { return 3; } else if (text === notesPrefix) { return 4; + } else if (text === calculatorPrefix) { + return 5; } return 0; } @@ -291,6 +295,8 @@ Rectangle { prefixLength = Config.prefix.tmux.length + 1; else if (searchText.startsWith(Config.prefix.notes + " ")) prefixLength = Config.prefix.notes.length + 1; + else if (searchText.startsWith(Config.prefix.calculator + " ")) + prefixLength = Config.prefix.calculator.length + 1; let remainingText = searchText.substring(prefixLength); @@ -307,6 +313,8 @@ Rectangle { targetLoader = tmuxLoader; } else if (detectedTab === 4) { targetLoader = notesLoader; + } else if (detectedTab === 5) { + targetLoader = calculatorLoader; } // If loader is ready, use it immediately @@ -1189,6 +1197,32 @@ Rectangle { } } } + + // Tab 5: Calculator + Loader { + id: calculatorLoader + Layout.fillWidth: true + Layout.fillHeight: true + active: currentTab === 5 || item !== null + sourceComponent: Component { + CalculatorTab { + anchors.fill: parent + leftPanelWidth: root.leftPanelWidth + prefixIcon: Icons.calculate + onBackspaceOnEmpty: { + prefixDisabled = true; + currentTab = 0; + GlobalStates.launcherSearchText = Config.prefix.calculator + " "; + root.focusSearchInput(); + } + } + } + onLoaded: { + if (currentTab === 5 && item && item.focusSearchInput) { + root.focusSearchInput(); + } + } + } } // Process for opening items from clipboard