From cb487aefb15ff2c59f8ff4dc329040e5d8f7ad12 Mon Sep 17 00:00:00 2001
From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Date: Tue, 14 Jan 2025 17:51:58 +1100
Subject: launcher: better math

---
 modules/launcher.tsx | 80 ++++++++++++++++++++++++++++++++++------------------
 1 file changed, 52 insertions(+), 28 deletions(-)

(limited to 'modules/launcher.tsx')

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) {
-- 
cgit v1.2.3-freya

