Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions modules/services/GlobalShortcuts.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions modules/theme/Icons.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down
320 changes: 320 additions & 0 deletions modules/widgets/dashboard/calculator/CalculatorTab.qml
Original file line number Diff line number Diff line change
@@ -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<string> 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
}
}
}
}
}
}
}
}
}
Loading