mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(Launcher): ability to have calculator in inline search
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"appLauncher": {
|
||||
"enableClipboardHistory": false,
|
||||
"enableClipPreview": true,
|
||||
"inlineCalculator": false,
|
||||
"position": "center",
|
||||
"pinnedExecs": [],
|
||||
"useApp2Unit": false,
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user