feat(calculator): replace mathjs with SoulverCore

This commit replaces the `mathjs` library with the native SoulverCore engine for all calculations within the command palette. It updates the Swift wrapper to expose SoulverCore's functionality and modified the frontend to use the `calculate_soulver` command for evaluating expressions.
This commit is contained in:
ByteAtATime 2025-06-28 20:22:44 -07:00
parent 5f1ad1c84a
commit 5a5e609992
No known key found for this signature in database
4 changed files with 102 additions and 89 deletions

View file

@ -27,7 +27,6 @@
"@tauri-apps/plugin-shell": "~2.2.1",
"embla-carousel-svelte": "^8.6.0",
"fuse.js": "^7.1.0",
"mathjs": "^14.5.2",
"msgpackr": "^1.11.4",
"svelte-marked": "^0.8.0",
"virtua": "^0.41.5",

68
pnpm-lock.yaml generated
View file

@ -44,9 +44,6 @@ importers:
fuse.js:
specifier: ^7.1.0
version: 7.1.0
mathjs:
specifier: ^14.5.2
version: 14.5.2
msgpackr:
specifier: ^1.11.4
version: 1.11.4
@ -224,10 +221,6 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/types@7.27.6':
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'}
@ -1327,9 +1320,6 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
complex.js@2.4.2:
resolution: {integrity: sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -1383,9 +1373,6 @@ packages:
supports-color:
optional: true
decimal.js@10.5.0:
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@ -1485,9 +1472,6 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-latex@1.2.0:
resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@ -1631,10 +1615,6 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
fraction.js@5.2.2:
resolution: {integrity: sha512-uXBDv5knpYmv/2gLzWQ5mBHGBRk9wcKTeWu6GLTUEQfjCxO09uM/mHDrojlL+Q1mVGIIFo149Gba7od1XPgSzQ==}
engines: {node: '>= 12'}
from2@2.3.0:
resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
@ -1852,9 +1832,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
javascript-natural-sort@0.7.1:
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
@ -1994,11 +1971,6 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mathjs@14.5.2:
resolution: {integrity: sha512-51U6hp7j4M4Rj+l+q2KbmXAV9EhQVQzUdw1wE67RnUkKKq5ibxdrl9Ky2YkSUEIc2+VU8/IsThZNu6QSHUoyTA==}
engines: {node: '>= 18'}
hasBin: true
md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
@ -2453,9 +2425,6 @@ packages:
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
seedrandom@3.0.5:
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
@ -2638,9 +2607,6 @@ packages:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
engines: {node: '>=0.6.0'}
tiny-emitter@2.1.0:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
@ -2686,10 +2652,6 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
typed-function@4.2.1:
resolution: {integrity: sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==}
engines: {node: '>= 18'}
typescript-eslint@8.34.0:
resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -2904,8 +2866,6 @@ snapshots:
dependencies:
'@babel/types': 7.27.6
'@babel/runtime@7.27.6': {}
'@babel/types@7.27.6':
dependencies:
'@babel/helper-string-parser': 7.27.1
@ -3954,8 +3914,6 @@ snapshots:
color-name@1.1.4: {}
complex.js@2.4.2: {}
concat-map@0.0.1: {}
console-browserify@1.2.0: {}
@ -4021,8 +3979,6 @@ snapshots:
optionalDependencies:
supports-color: 8.1.1
decimal.js@10.5.0: {}
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@ -4147,8 +4103,6 @@ snapshots:
escalade@3.2.0: {}
escape-latex@1.2.0: {}
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.5(eslint@9.28.0(jiti@2.4.2)):
@ -4321,8 +4275,6 @@ snapshots:
dependencies:
is-callable: 1.2.7
fraction.js@5.2.2: {}
from2@2.3.0:
dependencies:
inherits: 2.0.4
@ -4525,8 +4477,6 @@ snapshots:
filelist: 1.0.4
minimatch: 3.1.2
javascript-natural-sort@0.7.1: {}
jiti@2.4.2: {}
js-yaml@4.1.0:
@ -4629,18 +4579,6 @@ snapshots:
math-intrinsics@1.1.0: {}
mathjs@14.5.2:
dependencies:
'@babel/runtime': 7.27.6
complex.js: 2.4.2
decimal.js: 10.5.0
escape-latex: 1.2.0
fraction.js: 5.2.2
javascript-natural-sort: 0.7.1
seedrandom: 3.0.5
tiny-emitter: 2.1.0
typed-function: 4.2.1
md5.js@1.3.5:
dependencies:
hash-base: 3.0.5
@ -5079,8 +5017,6 @@ snapshots:
scheduler@0.26.0: {}
seedrandom@3.0.5: {}
semver@7.7.2: {}
set-cookie-parser@2.7.1: {}
@ -5309,8 +5245,6 @@ snapshots:
dependencies:
setimmediate: 1.0.5
tiny-emitter@2.1.0: {}
tinyglobby@0.2.14:
dependencies:
fdir: 6.4.6(picomatch@4.0.2)
@ -5348,8 +5282,6 @@ snapshots:
type-fest@0.21.3: {}
typed-function@4.2.1: {}
typescript-eslint@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.6.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.6.3)

View file

@ -1,6 +1,19 @@
import SoulverCore
import Foundation
struct SoulverResult: Codable {
let value: String
let type: String
let error: String?
init(value: String, type: String, error: String? = nil) {
self.value = value
self.type = type
self.error = error
}
}
@MainActor
private var globalCalculator: Calculator?
@ -32,17 +45,53 @@ public func initialize_soulver(resourcesPath: UnsafePointer<CChar>) {
@MainActor
@_cdecl("evaluate")
public func evaluate(expression: UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar>? {
let encoder = JSONEncoder()
guard let calculator = globalCalculator else {
let errorMsg = "Error: SoulverCore not initialized. Call initialize_soulver() first."
print("❌ Soulver Wrapper: \(errorMsg)")
return strdup(errorMsg)
let errorResult = SoulverResult(value: "", type: "error", error: errorMsg)
if let jsonData = try? encoder.encode(errorResult), let jsonString = String(data: jsonData, encoding: .utf8) {
return strdup(jsonString)
}
return nil
}
let swiftExpression = String(cString: expression)
let result = calculator.calculate(swiftExpression)
let resultString = result.stringValue
return strdup(resultString)
if swiftExpression.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
let emptyResult = SoulverResult(value: "", type: "none", error: nil)
if let jsonData = try? encoder.encode(emptyResult), let jsonString = String(data: jsonData, encoding: .utf8) {
return strdup(jsonString)
}
return nil
}
let result = calculator.calculate(swiftExpression)
if result.isEmptyResult {
let emptyResult = SoulverResult(value: "", type: "none", error: nil)
if let jsonData = try? encoder.encode(emptyResult), let jsonString = String(data: jsonData, encoding: .utf8) {
return strdup(jsonString)
}
return nil
}
let resultType = String(describing: result.evaluationResult.equivalentTokenType)
let soulverResult = SoulverResult(value: result.stringValue, type: resultType)
if let jsonData = try? encoder.encode(soulverResult),
let jsonString = String(data: jsonData, encoding: .utf8) {
return strdup(jsonString)
}
let errorMsg = "Failed to encode Soulver result to JSON."
let errorResult = SoulverResult(value: "", type: "error", error: errorMsg)
if let jsonData = try? encoder.encode(errorResult), let jsonString = String(data: jsonData, encoding: .utf8) {
return strdup(jsonString)
}
return nil
}
@_cdecl("free_string")

View file

@ -1,10 +1,9 @@
import type { PluginInfo } from '@raycast-linux/protocol';
import { invoke } from '@tauri-apps/api/core';
import Fuse from 'fuse.js';
import { create, all } from 'mathjs';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import type { Quicklink } from '$lib/quicklinks.svelte';
import { frecencyStore } from '$lib/frecency.svelte';
import { frecencyStore } from './frecency.svelte';
import { viewManager } from './viewManager.svelte';
import type { App } from './apps.svelte';
@ -24,8 +23,6 @@ type UseCommandPaletteItemsArgs = {
selectedQuicklinkForArgument: () => Quicklink | null;
};
const math = create(all);
export function useCommandPaletteItems({
searchText,
plugins,
@ -59,21 +56,58 @@ export function useCommandPaletteItems({
})
);
const calculatorResult = $derived.by(() => {
let calculatorResult = $state<{ value: string; type: string } | null>(null);
let calculationId = 0;
$effect(() => {
const term = searchText();
calculationId++;
const currentCalculationId = calculationId;
if (!term.trim() || selectedQuicklinkForArgument()) {
return null;
calculatorResult = null;
return;
}
try {
const result = math.evaluate(term.trim());
if (typeof result === 'function' || typeof result === 'undefined') return null;
const resultString = math.format(result, { precision: 14 });
if (resultString === term.trim()) return null;
return { value: resultString, type: math.typeOf(result) };
} catch {
return null;
}
(async () => {
try {
const resultJson = await invoke<string>('calculate_soulver', { expression: term.trim() });
if (currentCalculationId !== calculationId) {
return; // Stale request
}
const result = JSON.parse(resultJson) as {
value: string;
type: string;
error?: string;
};
if (result.error) {
console.error('Soulver error:', result.error);
calculatorResult = null;
return;
}
if (result.type === 'none' || !result.value) {
calculatorResult = null;
return;
}
if (result.value === term.trim()) {
calculatorResult = null;
return;
}
calculatorResult = { value: result.value, type: result.type };
} catch (e) {
if (currentCalculationId !== calculationId) {
return; // Stale request
}
console.error('Soulver invocation failed:', e);
calculatorResult = null;
}
})();
});
const displayItems = $derived.by(() => {
@ -86,7 +120,6 @@ export function useCommandPaletteItems({
score: 0,
fuseScore: result.score
}));
console.log(items);
} else {
items = allSearchableItems.map((item) => ({ ...item, score: 0, fuseScore: 1 }));
}