Add .config/DankMaterialShell/firefox.css
Add .config/DankMaterialShell/plugin_settings.json Add .config/DankMaterialShell/plugins/calculator/CalculatorLauncher.qml Add .config/DankMaterialShell/plugins/calculator/CalculatorSettings.qml Add .config/DankMaterialShell/plugins/calculator/README.md Add .config/DankMaterialShell/plugins/calculator/calculator.js Add .config/DankMaterialShell/plugins/calculator/.git/HEAD Add .config/DankMaterialShell/plugins/calculator/.git/config Add .config/DankMaterialShell/plugins/calculator/.git/index Add .config/DankMaterialShell/plugins/calculator/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/calculator/.git/objects/pack/pack-67f644835e660794f65c9273e46788b0c3da57cf.idx Add .config/DankMaterialShell/plugins/calculator/.git/objects/pack/pack-67f644835e660794f65c9273e46788b0c3da57cf.rev Add .config/DankMaterialShell/plugins/calculator/.git/objects/pack/pack-67f644835e660794f65c9273e46788b0c3da57cf.pack Add .config/DankMaterialShell/plugins/calculator/.git/refs/heads/main Add .config/DankMaterialShell/plugins/calculator/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/calculator/.git/refs/remotes/origin/qalc Add .config/DankMaterialShell/plugins/calculator/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/calculator/plugin.json Add .config/DankMaterialShell/plugins/calculator/screenshot.png Add .config/DankMaterialShell/plugins/calculator/test_precision.js Add .config/DankMaterialShell/plugins/calculator/test_single.js Add .config/DankMaterialShell/plugins/commandRunner/CommandRunner.qml Add .config/DankMaterialShell/plugins/commandRunner/CommandRunnerSettings.qml Add .config/DankMaterialShell/plugins/commandRunner/LICENSE Add .config/DankMaterialShell/plugins/commandRunner/README.md Add .config/DankMaterialShell/plugins/commandRunner/.git/HEAD Add .config/DankMaterialShell/plugins/commandRunner/.git/config Add .config/DankMaterialShell/plugins/commandRunner/.git/index Add .config/DankMaterialShell/plugins/commandRunner/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/commandRunner/.git/objects/pack/pack-5a720f795fd2994ef506cd867d86ca7df8a25a31.idx Add .config/DankMaterialShell/plugins/commandRunner/.git/objects/pack/pack-5a720f795fd2994ef506cd867d86ca7df8a25a31.rev Add .config/DankMaterialShell/plugins/commandRunner/.git/objects/pack/pack-5a720f795fd2994ef506cd867d86ca7df8a25a31.pack Add .config/DankMaterialShell/plugins/commandRunner/.git/refs/heads/main Add .config/DankMaterialShell/plugins/commandRunner/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/commandRunner/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/commandRunner/plugin.json Add .config/DankMaterialShell/plugins/commandRunner/screenshot.png Add .config/DankMaterialShell/plugins/dankActions.meta Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsWidget.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlerts.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlertsSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooks.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooksSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/README.md Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroWidget.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/LICENSE Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/README.md Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/HEAD Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/config Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/index Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/heads/master Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/remotes/origin/master Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.gitignore Add .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncher.qml Add .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncherSettings.qml Add .config/DankMaterialShell/plugins/emojiLauncher/LICENSE Add .config/DankMaterialShell/plugins/emojiLauncher/README.md Add .config/DankMaterialShell/plugins/emojiLauncher/catalog.js Add .config/DankMaterialShell/plugins/emojiLauncher/data/emojis.txt Add .config/DankMaterialShell/plugins/emojiLauncher/data/math.txt Add .config/DankMaterialShell/plugins/emojiLauncher/data/nerdfont.txt Add .config/DankMaterialShell/plugins/emojiLauncher/.git/HEAD Add .config/DankMaterialShell/plugins/emojiLauncher/.git/config Add .config/DankMaterialShell/plugins/emojiLauncher/.git/index Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.idx Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.rev Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.pack Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/heads/main Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/emojiLauncher/plugin.json Add .config/DankMaterialShell/plugins/emojiLauncher/screenshot.png Add .config/DankMaterialShell/plugins/emojiLauncher/scripts/generate_catalog.py Add .config/DankMaterialShell/plugins/dankActions Add .config/DankMaterialShell/plugins/webSearch/LICENSE Add .config/DankMaterialShell/plugins/webSearch/README.md Add .config/DankMaterialShell/plugins/webSearch/WebSearch.qml Add .config/DankMaterialShell/plugins/webSearch/WebSearchSettings.qml Add .config/DankMaterialShell/plugins/webSearch/.git/HEAD Add .config/DankMaterialShell/plugins/webSearch/.git/config Add .config/DankMaterialShell/plugins/webSearch/.git/index Add .config/DankMaterialShell/plugins/webSearch/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/webSearch/.git/objects/pack/pack-6a60c736f418e5b4b1f0505f66c1e2a371d46fed.idx Add .config/DankMaterialShell/plugins/webSearch/.git/objects/pack/pack-6a60c736f418e5b4b1f0505f66c1e2a371d46fed.rev Add .config/DankMaterialShell/plugins/webSearch/.git/objects/pack/pack-6a60c736f418e5b4b1f0505f66c1e2a371d46fed.pack Add .config/DankMaterialShell/plugins/webSearch/.git/refs/heads/main Add .config/DankMaterialShell/plugins/webSearch/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/webSearch/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/webSearch/plugin.json Add .config/DankMaterialShell/plugins/webSearch/screenshot.png Add .config/DankMaterialShell/settings.json
This commit is contained in:
parent
b22161e0ca
commit
809b12203c
103 changed files with 25020 additions and 0 deletions
|
|
@ -0,0 +1,118 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
import "calculator.js" as Calculator
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Plugin properties
|
||||
property var pluginService: null
|
||||
property string trigger: ""
|
||||
|
||||
// Plugin interface signals
|
||||
signal itemsChanged()
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Calculator: Plugin loaded")
|
||||
|
||||
// Load custom trigger from settings (default is "=")
|
||||
if (pluginService) {
|
||||
trigger = pluginService.loadPluginData("calculator", "trigger", "=")
|
||||
}
|
||||
}
|
||||
|
||||
// Required function: Get items for launcher
|
||||
function getItems(query) {
|
||||
console.log("Calculator: getItems called with query:", query)
|
||||
|
||||
// If query is empty, return nothing
|
||||
if (!query || query.trim().length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
// Check if it looks like a math expression
|
||||
if (!Calculator.isMathExpression(trimmedQuery)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Try to evaluate the expression
|
||||
const result = Calculator.evaluate(trimmedQuery)
|
||||
|
||||
if (!result.success) {
|
||||
// Don't show error items, just return empty
|
||||
return []
|
||||
}
|
||||
|
||||
// Format the result nicely
|
||||
let resultString = result.result.toString()
|
||||
|
||||
// Only apply scientific notation formatting for actual numbers, not BigInt strings
|
||||
if (typeof result.result === 'number') {
|
||||
// For very long decimals or very large/small numbers, use scientific notation
|
||||
if (resultString.length > 15 && Math.abs(result.result) >= 1e6) {
|
||||
resultString = result.result.toExponential(6)
|
||||
} else if (resultString.length > 15 && Math.abs(result.result) < 1e-6) {
|
||||
resultString = result.result.toExponential(6)
|
||||
}
|
||||
}
|
||||
// For BigInt string results, use as-is (already formatted)
|
||||
|
||||
return [
|
||||
{
|
||||
name: resultString,
|
||||
icon: "material:equal",
|
||||
comment: trimmedQuery + " = " + resultString,
|
||||
action: "copy:" + resultString,
|
||||
categories: ["Calculator"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Required function: Execute item action
|
||||
function executeItem(item) {
|
||||
if (!item || !item.action) {
|
||||
console.warn("Calculator: Invalid item or action")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Calculator: Executing item:", item.name, "with action:", item.action)
|
||||
|
||||
const actionParts = item.action.split(":")
|
||||
const actionType = actionParts[0]
|
||||
const actionData = actionParts.slice(1).join(":")
|
||||
|
||||
switch (actionType) {
|
||||
case "copy":
|
||||
copyToClipboard(actionData)
|
||||
break
|
||||
default:
|
||||
console.warn("Calculator: Unknown action type:", actionType)
|
||||
showToast("Unknown action: " + actionType)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to copy to clipboard
|
||||
function copyToClipboard(text) {
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
|
||||
showToast("Copied to clipboard: " + text)
|
||||
}
|
||||
|
||||
// Helper function to show toast notification
|
||||
function showToast(message) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("Calculator", message)
|
||||
} else {
|
||||
console.log("Calculator Toast:", message)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for trigger changes
|
||||
onTriggerChanged: {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("calculator", "trigger", trigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
|
||||
implicitHeight: settingsColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Calculator Plugin Settings"
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Bold
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "This plugin evaluates mathematical expressions and copies the result to your clipboard."
|
||||
font.pixelSize: 14
|
||||
color: "#CCFFFFFF"
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - 32
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 32
|
||||
height: 1
|
||||
color: "#30FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 12
|
||||
width: parent.width - 32
|
||||
|
||||
Text {
|
||||
text: "Trigger Configuration"
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: noTriggerToggle.checked ? "Calculator is always active. Simply type a math expression like '3 + 3' in the launcher." : "Set a trigger prefix to activate the calculator. Type the trigger before your expression."
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 12
|
||||
|
||||
CheckBox {
|
||||
id: noTriggerToggle
|
||||
text: "No trigger (always active)"
|
||||
checked: loadSettings("noTrigger", false)
|
||||
|
||||
contentItem: Text {
|
||||
text: noTriggerToggle.text
|
||||
font.pixelSize: 14
|
||||
color: "#FFFFFF"
|
||||
leftPadding: noTriggerToggle.indicator.width + 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
radius: 4
|
||||
border.color: noTriggerToggle.checked ? "#4CAF50" : "#60FFFFFF"
|
||||
border.width: 2
|
||||
color: noTriggerToggle.checked ? "#4CAF50" : "transparent"
|
||||
|
||||
Rectangle {
|
||||
width: 12
|
||||
height: 12
|
||||
anchors.centerIn: parent
|
||||
radius: 2
|
||||
color: "#FFFFFF"
|
||||
visible: noTriggerToggle.checked
|
||||
}
|
||||
}
|
||||
|
||||
onCheckedChanged: {
|
||||
saveSettings("noTrigger", checked)
|
||||
if (checked) {
|
||||
saveSettings("trigger", "")
|
||||
} else {
|
||||
const currentTrigger = triggerField.text || "="
|
||||
saveSettings("trigger", currentTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 12
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: !noTriggerToggle.checked
|
||||
|
||||
Text {
|
||||
text: "Trigger:"
|
||||
font.pixelSize: 14
|
||||
color: "#FFFFFF"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: triggerField
|
||||
width: 100
|
||||
height: 40
|
||||
text: loadSettings("trigger", "=")
|
||||
placeholderText: "="
|
||||
backgroundColor: "#30FFFFFF"
|
||||
textColor: "#FFFFFF"
|
||||
|
||||
onTextEdited: {
|
||||
const newTrigger = text.trim()
|
||||
saveSettings("trigger", newTrigger || "=")
|
||||
saveSettings("noTrigger", newTrigger === "")
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Examples: =, calc, c"
|
||||
font.pixelSize: 12
|
||||
color: "#AAFFFFFF"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 32
|
||||
height: 1
|
||||
color: "#30FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 8
|
||||
width: parent.width - 32
|
||||
|
||||
Text {
|
||||
text: "Supported Operations:"
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
leftPadding: 16
|
||||
|
||||
Text {
|
||||
text: "• Addition: 3 + 3"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Subtraction: 10 - 5"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Multiplication: 4 * 7"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Division: 20 / 4"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Exponentiation: 2 ^ 8"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Modulo: 17 % 5"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Parentheses: (5 + 3) * 2"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "• Decimals: 3.14 * 2"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 32
|
||||
height: 1
|
||||
color: "#30FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 8
|
||||
width: parent.width - 32
|
||||
|
||||
Text {
|
||||
text: "Usage:"
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
leftPadding: 16
|
||||
bottomPadding: 24
|
||||
|
||||
Text {
|
||||
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: noTriggerToggle.checked ? "2. Type a mathematical expression (e.g., '3 + 3')" : "2. Type your trigger followed by the expression (e.g., '= 3 + 3')"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "3. The result will appear as a launcher item"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "4. Press Enter to copy the result to clipboard"
|
||||
font.pixelSize: 12
|
||||
color: "#CCFFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(key, value) {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("calculator", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings(key, defaultValue) {
|
||||
if (pluginService) {
|
||||
return pluginService.loadPluginData("calculator", key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# Calculator Plugin for DMS Launcher
|
||||
|
||||
|
||||
[](https://plugins.danklinux.com/calculator.html)
|
||||
|
||||
A launcher plugin that evaluates mathematical expressions and copies results to the clipboard.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time calculation**: Type mathematical expressions directly in the launcher
|
||||
- **Default prefix**: Uses `=` as the default trigger prefix (configurable)
|
||||
- **Safe evaluation**: Only allows mathematical operations, preventing code injection
|
||||
- **Clipboard integration**: Press Enter to copy the result to clipboard
|
||||
- **Multiple operations**: Supports +, -, *, /, ^, %, and parentheses
|
||||
|
||||
## Installation
|
||||
|
||||
### Via DMS
|
||||
|
||||
```bash
|
||||
dms plugins install Calculator
|
||||
```
|
||||
|
||||
### Via DMS GUI
|
||||
- Mod + ,
|
||||
- Go to Plugins Tab
|
||||
- Choose Browse
|
||||
- Enable third party
|
||||
- install Calculator
|
||||
|
||||
### Manually
|
||||
|
||||
```
|
||||
cd ~/.config/DankMaterialShell/plugins
|
||||
git clone https://github.com/rochacbruno/DankCalculator Calculator
|
||||
```
|
||||
|
||||
1. Open DMS Settings (Ctrl+,)
|
||||
2. Navigate to Plugins tab
|
||||
3. Click "Scan for Plugins"
|
||||
4. Enable the "Calculator" plugin with the toggle switch
|
||||
|
||||
## Usage
|
||||
|
||||
### With Default Settings (= Prefix)
|
||||
|
||||
1. Open the launcher (Ctrl+Space)
|
||||
2. Type the `=` prefix followed by a mathematical expression: `= 3 + 3`
|
||||
3. The result (`6`) appears as a launcher item
|
||||
4. Press Enter to copy the result to clipboard
|
||||
|
||||
### Customizing the Trigger
|
||||
|
||||
You can configure a different trigger prefix or disable it entirely in the settings:
|
||||
|
||||
1. Open Settings → Plugins → Calculator
|
||||
2. Change the trigger to a custom value (e.g., `calc`, `c`, `math`)
|
||||
3. Or check "No trigger (always active)" to remove the prefix requirement
|
||||
4. In the launcher, type your configured trigger: `calc 3 + 3` or just `3 + 3` (if no trigger)
|
||||
5. Press Enter to copy the result
|
||||
|
||||
### Adding a keybinding (niri)
|
||||
|
||||
```kdl
|
||||
binds {
|
||||
Mod+Shift+C hotkey-overlay-title="Calculator" {
|
||||
spawn "dms" "ipc" "call" "spotlight" "openQuery" "=";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Operations
|
||||
|
||||
- **Addition**: `= 3 + 3` → `6`
|
||||
- **Subtraction**: `= 10 - 5` → `5`
|
||||
- **Multiplication**: `= 4 * 7` → `28`
|
||||
- **Division**: `= 20 / 4` → `5`
|
||||
- **Exponentiation**: `= 2 ^ 8` → `256`
|
||||
- **Modulo**: `= 17 % 5` → `2`
|
||||
- **Parentheses**: `= (5 + 3) * 2` → `16`
|
||||
- **Decimals**: `= 3.14 * 2` → `6.28`
|
||||
- **Complex**: `= (10 + 5) * 2 - 3 / 3` → `29`
|
||||
|
||||
## Examples
|
||||
|
||||
| Expression | Result |
|
||||
|------------|--------|
|
||||
| `= 3 + 3` | `6` |
|
||||
| `= 100 / 4` | `25` |
|
||||
| `= 2 ^ 10` | `1024` |
|
||||
| `= (5 + 3) * 2` | `16` |
|
||||
| `= 3.14159 * 2` | `6.28318` |
|
||||
| `= 16 ^ 0.5` | `4` (square root) |
|
||||
|
||||
## Security
|
||||
|
||||
The calculator uses safe expression evaluation:
|
||||
- Only allows numbers, operators (+, -, *, /, ^, %), parentheses, and dots
|
||||
- Rejects any expressions with letters or special characters (except operators)
|
||||
- Prevents code injection by validating input before evaluation
|
||||
|
||||
## Files
|
||||
|
||||
- `plugin.json` - Plugin manifest
|
||||
- `CalculatorLauncher.qml` - Main launcher component
|
||||
- `CalculatorSettings.qml` - Settings UI
|
||||
- `calculator.js` - Safe expression evaluation logic
|
||||
- `README.md` - This file
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are stored in `~/.config/DankMaterialShell/plugin_settings.json` under the `calculator` plugin key:
|
||||
|
||||
```json
|
||||
{
|
||||
"pluginSettings": {
|
||||
"calculator": {
|
||||
"trigger": "=",
|
||||
"noTrigger": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Calculator items don't appear:**
|
||||
- Make sure the plugin is enabled in Settings → Plugins
|
||||
- Check that you're typing a valid mathematical expression
|
||||
- Try disabling "No trigger" and setting a specific trigger
|
||||
|
||||
**Result shows wrong value:**
|
||||
- JavaScript has floating-point precision limitations
|
||||
- Very large or very small numbers may use scientific notation
|
||||
|
||||
**Copy to clipboard doesn't work:**
|
||||
- Make sure your system clipboard is accessible
|
||||
- Check console for error messages
|
||||
|
||||
## Version
|
||||
|
||||
1.0.0
|
||||
|
||||
## Author
|
||||
|
||||
Bruno Cesar Rocha
|
||||
|
||||
## License
|
||||
|
||||
Same as DankMaterialShell
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
// Calculator utility for safe mathematical expression evaluation
|
||||
.pragma library
|
||||
|
||||
/**
|
||||
* Checks if expression contains only integers (no decimals)
|
||||
*/
|
||||
function isIntegerOnly(expression) {
|
||||
return !/\./.test(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates integer expression using BigInt for precision
|
||||
*/
|
||||
function evaluateInteger(expression) {
|
||||
try {
|
||||
// Replace operators with BigInt-safe versions
|
||||
let expr = expression.replace(/\s/g, '');
|
||||
|
||||
// Handle exponentiation separately (BigInt doesn't support **)
|
||||
if (expr.includes('^')) {
|
||||
return evaluateWithExponentiation(expr, true);
|
||||
}
|
||||
|
||||
// For modulo, division, and basic arithmetic, try BigInt
|
||||
// Note: BigInt division truncates, so we need to handle / carefully
|
||||
if (expr.includes('/')) {
|
||||
// If division exists, fall back to regular number for accuracy
|
||||
return null;
|
||||
}
|
||||
|
||||
// Replace numbers with BigInt literals
|
||||
expr = expr.replace(/(\d+)/g, '$1n');
|
||||
|
||||
// Evaluate
|
||||
const result = eval(expr);
|
||||
|
||||
// Convert back to string then number for display
|
||||
// Check if result fits in safe integer range
|
||||
const numResult = Number(result);
|
||||
if (Number.isSafeInteger(numResult)) {
|
||||
return numResult;
|
||||
}
|
||||
|
||||
// Return as string for very large integers
|
||||
return result.toString().replace(/n$/, '');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exponentiation for both BigInt and regular numbers
|
||||
*/
|
||||
function evaluateWithExponentiation(expression, useBigInt) {
|
||||
// Find exponentiation operations and evaluate them
|
||||
let expr = expression;
|
||||
|
||||
// Handle ^ operator by converting to **
|
||||
expr = expr.replace(/\^/g, '**');
|
||||
|
||||
if (useBigInt) {
|
||||
// For BigInt, we need custom exponentiation
|
||||
// This is complex, so fall back to regular evaluation
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = eval(expr);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs precise decimal arithmetic by working with scaled integers
|
||||
*/
|
||||
function evaluatePrecise(expression) {
|
||||
try {
|
||||
// Replace ^ with ** for exponentiation
|
||||
let cleaned = expression.replace(/\^/g, '**');
|
||||
|
||||
// Evaluate using JavaScript's eval (safe because we validated the input)
|
||||
let result = eval(cleaned);
|
||||
|
||||
// Check if result is a valid number
|
||||
if (typeof result !== 'number' || !isFinite(result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle floating point precision issues
|
||||
// Round to 15 significant digits (JavaScript's max precision)
|
||||
if (Math.abs(result) < 1e-10 && result !== 0) {
|
||||
// Very small number, keep in scientific notation
|
||||
return result;
|
||||
}
|
||||
|
||||
// For regular decimals, use toPrecision to avoid floating point errors
|
||||
// But only if the number has decimal places
|
||||
if (result % 1 !== 0) {
|
||||
// Count significant digits in result
|
||||
const resultStr = result.toString();
|
||||
if (resultStr.includes('e')) {
|
||||
// Already in scientific notation
|
||||
return result;
|
||||
}
|
||||
|
||||
// Round to 15 significant figures to eliminate floating point errors
|
||||
// e.g., 0.1 + 0.2 = 0.30000000000000004 becomes 0.3
|
||||
const precision = 15;
|
||||
const rounded = parseFloat(result.toPrecision(precision));
|
||||
|
||||
// If rounding made it a whole number, return as integer
|
||||
if (rounded % 1 === 0 && Math.abs(rounded) < Number.MAX_SAFE_INTEGER) {
|
||||
return Math.round(rounded);
|
||||
}
|
||||
|
||||
return rounded;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely evaluates a mathematical expression
|
||||
* @param {string} expression - The mathematical expression to evaluate
|
||||
* @returns {object} - {success: boolean, result: number|string|null, error: string|null}
|
||||
*/
|
||||
function evaluate(expression) {
|
||||
if (!expression || typeof expression !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Invalid expression"
|
||||
};
|
||||
}
|
||||
|
||||
// Clean the expression
|
||||
let cleaned = expression.trim();
|
||||
|
||||
// Check if it's empty
|
||||
if (cleaned.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Empty expression"
|
||||
};
|
||||
}
|
||||
|
||||
// Only allow numbers, basic operators, parentheses, dots, and spaces
|
||||
const allowedChars = /^[0-9+\-*/().\s%^]+$/;
|
||||
if (!allowedChars.test(cleaned)) {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Invalid characters in expression"
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it looks like a mathematical expression
|
||||
// Must contain at least one operator or be a simple number
|
||||
const hasOperator = /[+\-*/^%]/.test(cleaned);
|
||||
const isSimpleNumber = /^-?\d+\.?\d*$/.test(cleaned);
|
||||
|
||||
if (!hasOperator && !isSimpleNumber) {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Not a valid mathematical expression"
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
// Try BigInt evaluation for integer-only expressions (better precision for large integers)
|
||||
if (isIntegerOnly(cleaned) && !cleaned.includes('/')) {
|
||||
result = evaluateInteger(cleaned);
|
||||
}
|
||||
|
||||
// Fall back to precise decimal evaluation
|
||||
if (result === null || result === undefined) {
|
||||
result = evaluatePrecise(cleaned);
|
||||
}
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Evaluation failed"
|
||||
};
|
||||
}
|
||||
|
||||
// Check if result is valid
|
||||
if (typeof result === 'number' && !isFinite(result)) {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Invalid result"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: result,
|
||||
error: null
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: "Evaluation error: " + e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string looks like it could be a mathematical expression
|
||||
* @param {string} query - The query to check
|
||||
* @returns {boolean} - True if it looks like a math expression
|
||||
*/
|
||||
function isMathExpression(query) {
|
||||
if (!query || typeof query !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cleaned = query.trim();
|
||||
|
||||
// Must contain only allowed characters
|
||||
const allowedChars = /^[0-9+\-*/().\s%^]+$/;
|
||||
if (!allowedChars.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have at least one digit
|
||||
if (!/\d/.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must be at least 3 characters for an expression (e.g., "1+1")
|
||||
// or be a simple number
|
||||
const hasOperator = /[+\-*/^%]/.test(cleaned);
|
||||
const isSimpleNumber = /^-?\d+\.?\d*$/.test(cleaned);
|
||||
|
||||
return (hasOperator && cleaned.length >= 3) || isSimpleNumber;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[core]
|
||||
bare = false
|
||||
filemode = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/rochacbruno/DankCalculator
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
[branch "main"]
|
||||
remote = origin
|
||||
merge = refs/heads/main
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
de6dbd59b7630e897a50e107f704c1cd4a131128
|
||||
|
|
@ -0,0 +1 @@
|
|||
de6dbd59b7630e897a50e107f704c1cd4a131128
|
||||
|
|
@ -0,0 +1 @@
|
|||
ea2857f89d1a0d00e01e6697f11da2d9f267e071
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id": "calculator",
|
||||
"name": "Calculator",
|
||||
"description": "A calculator plugin that evaluates mathematical expressions and copies results to clipboard",
|
||||
"version": "0.1.3",
|
||||
"author": "Bruno Cesar Rocha <rochacbruno>",
|
||||
"icon": "calculate",
|
||||
"type": "launcher",
|
||||
"capabilities": ["launcher"],
|
||||
"component": "./CalculatorLauncher.qml",
|
||||
"settings": "./CalculatorSettings.qml",
|
||||
"trigger": "=",
|
||||
"requires_dms": ">0.1.18",
|
||||
"requires": ["wl-copy"],
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Test cases for calculator precision improvements
|
||||
// Note: This is a Node.js test file for demonstration purposes
|
||||
// The actual calculator.js is a QML JavaScript module
|
||||
|
||||
// Mock QML .pragma library behavior
|
||||
const Calculator = {
|
||||
isIntegerOnly: function(expression) {
|
||||
return !/\./.test(expression);
|
||||
},
|
||||
|
||||
evaluateInteger: function(expression) {
|
||||
try {
|
||||
let expr = expression.replace(/\s/g, '');
|
||||
|
||||
if (expr.includes('^') || expr.includes('/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
expr = expr.replace(/(\d+)/g, '$1n');
|
||||
const result = eval(expr);
|
||||
const numResult = Number(result);
|
||||
|
||||
if (Number.isSafeInteger(numResult)) {
|
||||
return numResult;
|
||||
}
|
||||
|
||||
return result.toString().replace(/n$/, '');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
evaluatePrecise: function(expression) {
|
||||
try {
|
||||
let cleaned = expression.replace(/\^/g, '**');
|
||||
let result = eval(cleaned);
|
||||
|
||||
if (typeof result !== 'number' || !isFinite(result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Math.abs(result) < 1e-10 && result !== 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result % 1 !== 0) {
|
||||
const resultStr = result.toString();
|
||||
if (resultStr.includes('e')) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const precision = 15;
|
||||
const rounded = parseFloat(result.toPrecision(precision));
|
||||
|
||||
if (rounded % 1 === 0 && Math.abs(rounded) < Number.MAX_SAFE_INTEGER) {
|
||||
return Math.round(rounded);
|
||||
}
|
||||
|
||||
return rounded;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
evaluate: function(expression) {
|
||||
if (!expression || typeof expression !== 'string') {
|
||||
return { success: false, result: null, error: "Invalid expression" };
|
||||
}
|
||||
|
||||
let cleaned = expression.trim();
|
||||
|
||||
if (cleaned.length === 0) {
|
||||
return { success: false, result: null, error: "Empty expression" };
|
||||
}
|
||||
|
||||
const allowedChars = /^[0-9+\-*/().\s%^]+$/;
|
||||
if (!allowedChars.test(cleaned)) {
|
||||
return { success: false, result: null, error: "Invalid characters" };
|
||||
}
|
||||
|
||||
const hasOperator = /[+\-*/^%]/.test(cleaned);
|
||||
const isSimpleNumber = /^-?\d+\.?\d*$/.test(cleaned);
|
||||
|
||||
if (!hasOperator && !isSimpleNumber) {
|
||||
return { success: false, result: null, error: "Not a math expression" };
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (this.isIntegerOnly(cleaned) && !cleaned.includes('/')) {
|
||||
result = this.evaluateInteger(cleaned);
|
||||
}
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
result = this.evaluatePrecise(cleaned);
|
||||
}
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
return { success: false, result: null, error: "Evaluation failed" };
|
||||
}
|
||||
|
||||
if (typeof result === 'number' && !isFinite(result)) {
|
||||
return { success: false, result: null, error: "Invalid result" };
|
||||
}
|
||||
|
||||
return { success: true, result: result, error: null };
|
||||
} catch (e) {
|
||||
return { success: false, result: null, error: "Evaluation error: " + e.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Test cases
|
||||
console.log("=== Calculator Precision Test Suite ===\n");
|
||||
|
||||
const tests = [
|
||||
// Floating point precision tests
|
||||
{ expr: "0.1 + 0.2", expected: "0.3", description: "Classic floating point precision issue" },
|
||||
{ expr: "0.1 + 0.2 + 0.3", expected: "0.6", description: "Multiple decimal additions" },
|
||||
{ expr: "1.1 + 2.2", expected: "3.3", description: "Simple decimal addition" },
|
||||
{ expr: "3.3 - 1.1", expected: "2.2", description: "Decimal subtraction" },
|
||||
{ expr: "0.3 - 0.1", expected: "0.2", description: "Another precision issue" },
|
||||
|
||||
// BigInt large integer tests
|
||||
{ expr: "999999999999999999 + 1", expected: "1000000000000000000", description: "Large integer addition (BigInt)" },
|
||||
{ expr: "123456789012345678 * 2", expected: "246913578024691356", description: "Large integer multiplication (BigInt)" },
|
||||
{ expr: "999999999999999999 - 999999999999999998", expected: "1", description: "Large integer subtraction" },
|
||||
|
||||
// Regular operations
|
||||
{ expr: "2 + 2", expected: "4", description: "Simple addition" },
|
||||
{ expr: "10 - 3", expected: "7", description: "Simple subtraction" },
|
||||
{ expr: "5 * 6", expected: "30", description: "Simple multiplication" },
|
||||
{ expr: "20 / 4", expected: "5", description: "Simple division" },
|
||||
{ expr: "2 ^ 10", expected: "1024", description: "Exponentiation" },
|
||||
{ expr: "17 % 5", expected: "2", description: "Modulo" },
|
||||
{ expr: "(5 + 3) * 2", expected: "16", description: "Parentheses" },
|
||||
|
||||
// Edge cases
|
||||
{ expr: "100 / 3", expected: "33.3333333333333", description: "Repeating decimal (rounded to 15 sig figs)" },
|
||||
{ expr: "1.23456789012345678", expected: "1.23456789012346", description: "High precision decimal (rounded to 15 sig figs)" }
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
tests.forEach((test, index) => {
|
||||
const result = Calculator.evaluate(test.expr);
|
||||
const resultStr = result.success ? result.result.toString() : "ERROR";
|
||||
const matches = resultStr.startsWith(test.expected.substring(0, Math.min(test.expected.length, 10)));
|
||||
|
||||
if (matches) {
|
||||
console.log(`✓ Test ${index + 1}: ${test.description}`);
|
||||
console.log(` Expression: ${test.expr}`);
|
||||
console.log(` Result: ${resultStr}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`✗ Test ${index + 1}: ${test.description}`);
|
||||
console.log(` Expression: ${test.expr}`);
|
||||
console.log(` Expected: ${test.expected}`);
|
||||
console.log(` Got: ${resultStr}`);
|
||||
failed++;
|
||||
}
|
||||
console.log();
|
||||
});
|
||||
|
||||
console.log(`=== Test Results ===`);
|
||||
console.log(`Passed: ${passed}/${tests.length}`);
|
||||
console.log(`Failed: ${failed}/${tests.length}`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log("\n✓ All tests passed!");
|
||||
} else {
|
||||
console.log(`\n✗ ${failed} test(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Quick test for the specific case
|
||||
const fs = require('fs');
|
||||
|
||||
// Load and parse the calculator.js file (QML pragma library style)
|
||||
const calcCode = fs.readFileSync('./calculator.js', 'utf8')
|
||||
.replace('.pragma library', ''); // Remove QML pragma
|
||||
|
||||
// Create a function wrapper to execute the code
|
||||
const Calculator = new Function(calcCode + '\nreturn { isMathExpression, evaluate };')();
|
||||
|
||||
// Test the specific case
|
||||
const testValue = '99999999999';
|
||||
console.log('Testing:', testValue);
|
||||
console.log('---');
|
||||
|
||||
const isMath = Calculator.isMathExpression(testValue);
|
||||
console.log('isMathExpression:', isMath);
|
||||
|
||||
if (isMath) {
|
||||
const result = Calculator.evaluate(testValue);
|
||||
console.log('Evaluation success:', result.success);
|
||||
console.log('Result value:', result.result);
|
||||
console.log('Result type:', typeof result.result);
|
||||
|
||||
if (result.success) {
|
||||
const str = result.result.toString();
|
||||
console.log('String representation:', str);
|
||||
console.log('String length:', str.length);
|
||||
console.log('---');
|
||||
console.log('Would convert to scientific notation?');
|
||||
console.log(' Length > 15?', str.length > 15, `(actual: ${str.length})`);
|
||||
console.log(' Abs value >= 1e6?', Math.abs(result.result) >= 1e6, `(actual: ${result.result})`);
|
||||
console.log(' RESULT:', (str.length > 15 && Math.abs(result.result) >= 1e6) ? 'YES - would use scientific' : 'NO - would display normally');
|
||||
|
||||
if (str.length > 15 && Math.abs(result.result) >= 1e6) {
|
||||
console.log(' Scientific notation:', result.result.toExponential(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue