feat(Launcher): ability to have calculator in inline search

This commit is contained in:
DuckySoLucky
2025-12-30 15:11:44 +01:00
parent 015ff66fbd
commit 15e92a2752
7 changed files with 81 additions and 32 deletions
+4
View File
@@ -1789,6 +1789,10 @@
"description": "Show a preview of the clipboard content when using the >clip command.",
"label": "Enable clip preview"
},
"inline-calculator": {
"description": "Show calculator results directly in search when typing math expressions (e.g., '17 * 6').",
"label": "Inline calculator"
},
"clipboard-history": {
"description": "Access previously copied items from the launcher.",
"label": "Enable clipboard history"
+1
View File
@@ -178,6 +178,7 @@
"appLauncher": {
"enableClipboardHistory": false,
"enableClipPreview": true,
"inlineCalculator": false,
"position": "center",
"pinnedExecs": [],
"useApp2Unit": false,
+2 -1
View File
@@ -25,7 +25,7 @@ Singleton {
- Default cache directory: ~/.cache/noctalia
*/
readonly property alias data: adapter // Used to access via Settings.data.xxx.yyy
readonly property int settingsVersion: 35
readonly property int settingsVersion: 36
readonly property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1"
readonly property string shellName: "noctalia"
readonly property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
@@ -393,6 +393,7 @@ Singleton {
property JsonObject appLauncher: JsonObject {
property bool enableClipboardHistory: false
property bool enableClipPreview: true
property bool inlineCalculator: false
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property list<string> pinnedExecs: []
+3
View File
@@ -80,6 +80,9 @@ function evaluate(expression) {
.replace(/\bcosd\s*\(/g, '(function(x) { return Math.cos(' + (Math.PI / 180) + ' * x); })(')
.replace(/\btand\s*\(/g, '(function(x) { return Math.tan(' + (Math.PI / 180) + ' * x); })(');
// Handle ^ for exponentiation: convert 2^3 to Math.pow(2,3)
processed = processed.replace(/([\d.]+|\))\^([\d.]+|\([^)]*\))/g, 'Math.pow($1,$2)');
// Sanitize expression (only allow safe characters)
if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) {
throw new Error("Invalid characters in expression");
+8
View File
@@ -268,6 +268,14 @@ SmartPanel {
results = results.concat(pluginResults);
}
}
// Add inline calculator result at the end if available
if (searchText.trim() && calcPlugin.getInlineResult) {
const inlineResult = calcPlugin.getInlineResult(searchText);
if (inlineResult) {
results = results.concat([inlineResult]);
}
}
}
selectedIndex = 0;
@@ -12,6 +12,39 @@ Item {
return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression(query.substring(1)));
}
function getInlineResult(query) {
if (!Settings.data.appLauncher.inlineCalculator) {
return null;
}
if (query.startsWith(">")) {
return null;
}
if (!isMathExpression(query)) {
return null;
}
try {
let result = AdvancedMath.evaluate(query.trim());
return {
"name": AdvancedMath.formatResult(result),
"description": `${query} = ${result}`,
"icon": iconMode === "tabler" ? "calculator" : "accessories-calculator",
"isTablerIcon": true,
"isImage": false,
"isCalculatorResult": true,
"onActivate": function () {
// TODO: copy entry to clipboard via ClipHist
launcher.close();
}
};
} catch (error) {
return null;
}
}
function commands() {
return [
{
@@ -81,38 +114,28 @@ Item {
}
}
function evaluateExpression(expr) {
// Sanitize input - only allow safe characters
const sanitized = expr.replace(/[^0-9\+\-\*\/\(\)\.\s\%]/g, '');
if (sanitized !== expr) {
throw new Error("Invalid characters in expression");
}
// Don't allow empty expressions
if (!sanitized.trim()) {
throw new Error("Empty expression");
}
try {
// Use Function constructor for safe evaluation
// This is safer than eval() but still evaluate math
const result = Function('"use strict"; return (' + sanitized + ')')();
// Check for valid result
if (!isFinite(result)) {
throw new Error("Result is not a finite number");
}
// Round to reasonable precision to avoid floating point issues
return Math.round(result * 1000000000) / 1000000000;
} catch (e) {
throw new Error("Invalid mathematical expression");
}
}
function isMathExpression(expr) {
// Check if string looks like a math expression
// Allow digits, operators, parentheses, decimal points, and whitespace
return /^[\d\s\+\-\*\/\(\)\.\%]+$/.test(expr);
// Allow: digits, operators, parentheses, decimal points, whitespace, letters (for functions), commas
if (!/^[\d\s\+\-\*\/\(\)\.\%\^a-zA-Z,]+$/.test(expr)) {
return false;
}
// Must contain at least one operator OR a function call (letter followed by parenthesis)
if (!/[+\-*/%\^]/.test(expr) && !/[a-zA-Z]\s*\(/.test(expr)) {
return false;
}
// Reject if ends with an operator (incomplete expression)
if (/[+\-*/%\^]\s*$/.test(expr)) {
return false;
}
// Reject if it's just letters (would match app names)
if (/^[a-zA-Z\s]+$/.test(expr)) {
return false;
}
return true;
}
}
@@ -97,6 +97,15 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview")
}
NToggle {
label: I18n.tr("settings.launcher.settings.inline-calculator.label")
description: I18n.tr("settings.launcher.settings.inline-calculator.description")
checked: Settings.data.appLauncher.inlineCalculator
onToggled: checked => Settings.data.appLauncher.inlineCalculator = checked
isSettings: true
defaultValue: Settings.getDefaultValue("appLauncher.inlineCalculator")
}
NToggle {
label: I18n.tr("settings.launcher.settings.sort-by-usage.label")
description: I18n.tr("settings.launcher.settings.sort-by-usage.description")