summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-14 17:51:58 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-14 17:51:58 +1100
commitcb487aefb15ff2c59f8ff4dc329040e5d8f7ad12 (patch)
tree55a4972aa6a09ac1ac15b485b7a03470d9a15af6
parentlauncher modes + player controls IPC (diff)
downloadcaelestia-shell-cb487aefb15ff2c59f8ff4dc329040e5d8f7ad12.tar.gz
caelestia-shell-cb487aefb15ff2c59f8ff4dc329040e5d8f7ad12.tar.bz2
caelestia-shell-cb487aefb15ff2c59f8ff4dc329040e5d8f7ad12.zip
launcher: better math
-rw-r--r--modules/launcher.tsx80
-rw-r--r--package-lock.json110
-rw-r--r--package.json2
-rw-r--r--scss/launcher.scss29
-rw-r--r--services/math.ts159
-rw-r--r--services/players.ts6
-rw-r--r--utils/constants.ts3
7 files changed, 338 insertions, 51 deletions
diff --git a/modules/launcher.tsx b/modules/launcher.tsx
index 8f01596..4b42328 100644
--- a/modules/launcher.tsx
+++ b/modules/launcher.tsx
@@ -3,8 +3,8 @@ import { Astal, Gtk, Widget } from "astal/gtk3";
import fuzzysort from "fuzzysort";
import type AstalApps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland";
-import Mexp from "math-expression-evaluator";
import { Apps } from "../services/apps";
+import Math, { type HistoryItem } from "../services/math";
import { getAppCategoryIcon } from "../utils/icons";
import { launch } from "../utils/system";
import { PopupWindow, setupCustomTooltip, TransitionType } from "../utils/widgets";
@@ -104,7 +104,6 @@ const SearchEntry = ({ entry }: { entry: Widget.Entry }) => (
</stack>
);
-// TODO: description field
const Result = ({
icon,
materialIcon,
@@ -167,6 +166,25 @@ const AppResult = ({ app }: { app: AstalApps.Application }) => (
/>
);
+const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?: boolean; entry: Widget.Entry }) => (
+ <Result
+ materialIcon={math.icon}
+ label={math.equation}
+ sublabel={math.result}
+ onClicked={() => {
+ if (isHistory) {
+ Math.get_default().select(math);
+ entry.set_text(math.equation);
+ entry.grab_focus();
+ entry.set_position(-1);
+ } else {
+ execAsync(`wl-copy -- ${math.result}`).catch(console.error);
+ entry.set_text("");
+ }
+ }}
+ />
+);
+
const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> }) => {
const empty = Variable(true);
@@ -198,10 +216,10 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
description: "Search for files",
command: () => mode.set("files"),
},
- calc: {
+ math: {
icon: "calculate",
- name: "Calculator",
- description: "A calculator...",
+ name: "Math",
+ description: "Do math calculations",
command: () => mode.set("math"),
},
todo: {
@@ -214,7 +232,6 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
},
};
const subcommandList = Object.keys(subcommands);
- const mexp = new Mexp();
const appSearch = () => {
const apps = Apps.fuzzy_query(entry.text);
@@ -224,27 +241,24 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
};
const calculate = () => {
- // TODO: allow defs, history
- let math = null;
- try {
- math = mexp.eval(entry.text);
- } catch (e) {
- // Ignore
+ if (entry.text) {
+ self.add(<MathResult math={Math.get_default().evaluate(entry.text)} entry={entry} />);
+ self.add(<box className="separator" />);
}
- if (math !== null)
- self.add(
- <Result
- materialIcon="calculate"
- label={entry.text}
- sublabel={String(math)}
- onClicked={() => execAsync(`wl-copy -- ${math}`).catch(console.error)}
- />
- );
+ for (const item of Math.get_default().history)
+ self.add(<MathResult isHistory math={item} entry={entry} />);
};
- self.hook(entry, "activate", () => entry.text && self.get_children()[0].activate());
- self.hook(entry, "changed", () => {
+ self.hook(entry, "activate", () => {
if (!entry.text) return;
+ if (mode.get() === "math") {
+ if (entry.text.startsWith("clear")) Math.get_default().clear();
+ else Math.get_default().commit();
+ }
+ self.get_children()[0]?.activate();
+ });
+ self.hook(entry, "changed", () => {
+ if (!entry.text && mode.get() === "apps") return;
self.foreach(ch => ch.destroy());
if (entry.text.startsWith(">")) {
@@ -268,7 +282,15 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
);
};
-const LauncherContent = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => (
+const LauncherContent = ({
+ mode,
+ showResults,
+ entry,
+}: {
+ mode: Variable<Mode>;
+ showResults: Variable<boolean>;
+ entry: Widget.Entry;
+}) => (
<box
vertical
className={bind(mode).as(m => `launcher ${m}`)}
@@ -280,14 +302,14 @@ const LauncherContent = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.
<label className="icon" label={bind(mode).as(getIconFromMode)} />
</box>
<revealer
- revealChild={bind(entry, "textLength").as(t => t === 0)}
+ revealChild={bind(showResults).as(s => !s)}
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={150}
>
<PinnedApps />
</revealer>
<revealer
- revealChild={bind(entry, "textLength").as(t => t > 0)}
+ revealChild={bind(showResults)}
transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
transitionDuration={150}
>
@@ -303,6 +325,7 @@ export default class Launcher extends PopupWindow {
constructor() {
const entry = (<entry name="entry" />) as Widget.Entry;
const mode = Variable<Mode>("apps");
+ const showResults = Variable.derive([bind(entry, "textLength"), mode], (t, m) => t > 0 || m !== "apps");
super({
name: "launcher",
@@ -323,12 +346,13 @@ export default class Launcher extends PopupWindow {
transitionType: TransitionType.SLIDE_DOWN,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.START,
- child: <LauncherContent mode={mode} entry={entry} />,
+ child: <LauncherContent mode={mode} showResults={showResults} entry={entry} />,
});
this.mode = mode;
- this.connect("hide", () => entry.set_text(""));
+ // Clear search on hide if not in math mode
+ this.connect("hide", () => mode.get() !== "math" && entry.set_text(""));
}
open(mode: Mode) {
diff --git a/package-lock.json b/package-lock.json
index c5de752..0e638a9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,12 +6,24 @@
"": {
"dependencies": {
"fuzzysort": "^3.1.0",
- "math-expression-evaluator": "^2.0.6"
+ "mathjs": "^14.0.1"
},
"devDependencies": {
"esbuild": "0.24.2"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+ "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
@@ -437,6 +449,25 @@
"node": ">=18"
}
},
+ "node_modules/complex.js": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
+ "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
+ "license": "MIT"
+ },
"node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
@@ -478,17 +509,86 @@
"@esbuild/win32-x64": "0.24.2"
}
},
+ "node_modules/escape-latex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
+ "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==",
+ "license": "MIT"
+ },
+ "node_modules/fraction.js": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz",
+ "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
"node_modules/fuzzysort": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz",
"integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==",
"license": "MIT"
},
- "node_modules/math-expression-evaluator": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-2.0.6.tgz",
- "integrity": "sha512-DRung1qNcKbgkhFeQ0fBPUFB6voRUMY7KyRyp1TRQ2v95Rp2egC823xLRooM1mDx1rmbkY7ym6ZWmpaE/VimOA==",
+ "node_modules/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
+ "license": "MIT"
+ },
+ "node_modules/mathjs": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.0.1.tgz",
+ "integrity": "sha512-yyJgLwC6UXuve724np8tHRMYaTtb5UqiOGQkjwbSXgH8y1C/LcJ0pvdNDZLI2LT7r+iExh2Y5HwfAY+oZFtGIQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "complex.js": "^2.2.5",
+ "decimal.js": "^10.4.3",
+ "escape-latex": "^1.2.0",
+ "fraction.js": "^5.2.1",
+ "javascript-natural-sort": "^0.7.1",
+ "seedrandom": "^3.0.5",
+ "tiny-emitter": "^2.1.0",
+ "typed-function": "^4.2.1"
+ },
+ "bin": {
+ "mathjs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "license": "MIT"
+ },
+ "node_modules/seedrandom": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
+ "license": "MIT"
+ },
+ "node_modules/tiny-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
+ },
+ "node_modules/typed-function": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
+ "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
}
}
}
diff --git a/package.json b/package.json
index f0cb26d..c530938 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"dependencies": {
"fuzzysort": "^3.1.0",
- "math-expression-evaluator": "^2.0.6"
+ "mathjs": "^14.0.1"
},
"devDependencies": {
"esbuild": "0.24.2"
diff --git a/scss/launcher.scss b/scss/launcher.scss
index 8e710e9..a72389a 100644
--- a/scss/launcher.scss
+++ b/scss/launcher.scss
@@ -2,6 +2,19 @@
@use "lib";
@use "font";
+@mixin launcher($mode, $colour) {
+ &.#{$mode} {
+ label.icon {
+ color: $colour;
+ }
+
+ .separator {
+ background-color: $colour;
+ margin: lib.s(5) 0;
+ }
+ }
+}
+
.launcher-wrapper {
@include lib.ease-in-out;
@@ -21,6 +34,10 @@
color: scheme.$text;
padding: lib.s(10) lib.s(14);
+ @include launcher(apps, scheme.$sapphire);
+ @include launcher(files, scheme.$peach);
+ @include launcher(math, scheme.$green);
+
.search-bar {
margin-bottom: lib.s(5);
font-size: lib.s(16);
@@ -87,16 +104,4 @@
}
}
}
-
- &.apps label.icon {
- color: scheme.$sapphire;
- }
-
- &.files label.icon {
- color: scheme.$peach;
- }
-
- &.math label.icon {
- color: scheme.$green;
- }
}
diff --git a/services/math.ts b/services/math.ts
new file mode 100644
index 0000000..9920044
--- /dev/null
+++ b/services/math.ts
@@ -0,0 +1,159 @@
+import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
+import {
+ create,
+ derivativeDependencies,
+ evaluateDependencies,
+ rationalizeDependencies,
+ simplifyDependencies,
+ type MathNode,
+} from "mathjs/number";
+import { CACHE_DIR } from "../utils/constants";
+
+export interface HistoryItem {
+ equation: string;
+ result: string;
+ icon: string;
+}
+
+@register({ GTypeName: "Math" })
+export default class Math extends GObject.Object {
+ static instance: Math;
+ static get_default() {
+ if (!this.instance) this.instance = new Math();
+
+ return this.instance;
+ }
+
+ static math = create({
+ simplifyDependencies,
+ derivativeDependencies,
+ rationalizeDependencies,
+ evaluateDependencies,
+ });
+
+ readonly #maxHistory = 20;
+ readonly #path = `${CACHE_DIR}/math-history.json`;
+ readonly #history: HistoryItem[] = [];
+
+ #variables: Record<string, number | MathNode> = {};
+ #lastExpression: HistoryItem | null = null;
+
+ @property(Object)
+ get history() {
+ return this.#history;
+ }
+
+ #save() {
+ writeFileAsync(this.#path, JSON.stringify(this.#history)).catch(console.error);
+ }
+
+ /**
+ * Commits the last evaluated expression to the history
+ */
+ commit() {
+ if (!this.#lastExpression) return;
+
+ // Try select first to prevent duplicates, if it fails, add it
+ if (!this.select(this.#lastExpression)) {
+ this.#history.unshift(this.#lastExpression);
+ if (this.#history.length > this.#maxHistory) this.#history.pop();
+ this.notify("history");
+ this.#save();
+ }
+ this.#lastExpression = null;
+ }
+
+ /**
+ * Moves an item in the history to the top
+ * @param item The item to select
+ * @returns If the item was successfully selected
+ */
+ select(item: HistoryItem) {
+ const idx = this.#history.indexOf(item);
+ if (idx >= 0) {
+ this.#history.splice(idx, 1);
+ this.#history.unshift(item);
+ this.notify("history");
+ this.#save();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Clears the history and variables
+ */
+ clear() {
+ if (this.#history.length > 0) {
+ this.#history.length = 0;
+ this.notify("history");
+ this.#save();
+ }
+ this.#lastExpression = null;
+ this.#variables = {};
+ }
+
+ /**
+ * Evaluates an equation and adds it to the history
+ * @param equation The equation to evaluate
+ * @returns A {@link HistoryItem} representing the result of the equation
+ */
+ evaluate(equation: string): HistoryItem {
+ if (equation.startsWith("clear"))
+ return {
+ equation: "Clear history",
+ result: "Delete history and previously set variables",
+ icon: "delete_forever",
+ };
+
+ let result: string, icon: string;
+ try {
+ if (equation.includes("=")) {
+ const [left, right] = equation.split("=");
+ try {
+ this.#variables[left] = Math.math.simplify(right);
+ } catch {
+ this.#variables[left] = parseFloat(right);
+ }
+ result = this.#variables[left].toString();
+ icon = "equal";
+ } else if (equation.startsWith("simplify")) {
+ result = Math.math.simplify(equation.slice(8), this.#variables).toString();
+ icon = "function";
+ } else if (equation.startsWith("derive")) {
+ const respectTo = equation.slice(6).split(" ")[0];
+ result = Math.math.derivative(equation.slice(7 + respectTo.length), respectTo).toString();
+ icon = "function";
+ } else if (equation.startsWith("rationalize")) {
+ result = Math.math.rationalize(equation.slice(11), this.#variables).toString();
+ icon = "function";
+ } else {
+ result = Math.math.evaluate(equation, this.#variables).toString();
+ icon = "calculate";
+ }
+ } catch {
+ result = equation;
+ icon = "error";
+ equation = "Invalid equation";
+ }
+
+ return (this.#lastExpression = { equation, result, icon });
+ }
+
+ constructor() {
+ super();
+
+ // Load history
+ if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) {
+ try {
+ this.#history = JSON.parse(readFile(this.#path));
+ // Init eval to create variables and last expression
+ for (const item of this.#history) this.evaluate(item.equation);
+ } catch (e) {
+ console.error("Math - Unable to load history", e);
+ }
+ }
+ }
+}
diff --git a/services/players.ts b/services/players.ts
index 2a32960..3313595 100644
--- a/services/players.ts
+++ b/services/players.ts
@@ -1,4 +1,4 @@
-import { execAsync, GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
+import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
import AstalMpris from "gi://AstalMpris";
import { CACHE_DIR } from "../utils/constants";
import { isRealPlayer } from "../utils/mpris";
@@ -89,9 +89,7 @@ export default class Players extends GObject.Object {
}
#save() {
- execAsync(`mkdir -p ${CACHE_DIR}`)
- .then(() => writeFileAsync(this.#path, this.#players.map(p => p.busName).join("\n")).catch(console.error))
- .catch(console.error);
+ writeFileAsync(this.#path, this.#players.map(p => p.busName).join("\n")).catch(console.error);
}
#connectPlayerSignals(player: AstalMpris.Player) {
diff --git a/utils/constants.ts b/utils/constants.ts
index d907014..205315e 100644
--- a/utils/constants.ts
+++ b/utils/constants.ts
@@ -1,3 +1,4 @@
-import { GLib } from "astal";
+import { exec, GLib } from "astal";
export const CACHE_DIR = GLib.get_user_cache_dir() + "/caelestia";
+exec(`mkdir -p '${CACHE_DIR}'`);