summaryrefslogtreecommitdiff
path: root/src/modules/launcher/index.tsx
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-01 16:35:18 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-01 16:35:18 +1100
commit62602465ced5f65d4626f3cdf54b5fa30ba7c9dd (patch)
tree50223190f6d9c5e6dfc72f6d6492e26290521a16 /src/modules/launcher/index.tsx
parentlauncher: better files (diff)
downloadcaelestia-shell-62602465ced5f65d4626f3cdf54b5fa30ba7c9dd.tar.gz
caelestia-shell-62602465ced5f65d4626f3cdf54b5fa30ba7c9dd.tar.bz2
caelestia-shell-62602465ced5f65d4626f3cdf54b5fa30ba7c9dd.zip
launcher: actions + refactor into multi file
Action prefix is configurable
Diffstat (limited to 'src/modules/launcher/index.tsx')
-rw-r--r--src/modules/launcher/index.tsx136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/modules/launcher/index.tsx b/src/modules/launcher/index.tsx
new file mode 100644
index 0000000..2821050
--- /dev/null
+++ b/src/modules/launcher/index.tsx
@@ -0,0 +1,136 @@
+import PopupWindow from "@/widgets/popupwindow";
+import { bind, register, Variable } from "astal";
+import { Astal, Gtk, Widget } from "astal/gtk3";
+import { launcher as config } from "config";
+import Actions from "./actions";
+import Modes from "./modes";
+import type { Mode } from "./util";
+
+const getModeIcon = (mode: Mode) => {
+ if (mode === "apps") return "apps";
+ if (mode === "files") return "folder";
+ if (mode === "math") return "calculate";
+ if (mode === "windows") return "select_window";
+ return "search";
+};
+
+const getPrettyMode = (mode: Mode) => {
+ if (mode === "apps") return "Apps";
+ if (mode === "files") return "Files";
+ if (mode === "math") return "Math";
+ if (mode === "windows") return "Windows";
+ return mode;
+};
+
+const isAction = (text: string) => text.startsWith(config.actionPrefix.get());
+
+const SearchBar = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => (
+ <box className="search-bar">
+ <label className="mode" label={bind(mode)} />
+ {entry}
+ </box>
+);
+
+const ModeSwitcher = ({ mode, modes }: { mode: Variable<Mode>; modes: Mode[] }) => (
+ <box homogeneous hexpand className="mode-switcher">
+ {modes.map(m => (
+ <button
+ className={bind(mode).as(c => `mode ${c === m ? "selected" : ""}`)}
+ cursor="pointer"
+ onClicked={() => mode.set(m)}
+ >
+ <box halign={Gtk.Align.CENTER}>
+ <label className="icon" label={getModeIcon(m)} />
+ <label label={getPrettyMode(m)} />
+ </box>
+ </button>
+ ))}
+ </box>
+);
+
+@register()
+export default class Launcher extends PopupWindow {
+ readonly mode: Variable<Mode>;
+
+ constructor() {
+ const entry = (
+ <entry
+ hexpand
+ className="entry"
+ placeholderText={bind(config.actionPrefix).as(p => `Type "${p}" for subcommands`)}
+ />
+ ) as Widget.Entry;
+ const mode = Variable<Mode>("apps");
+ const content = Modes();
+ const actions = new Actions(mode, entry);
+
+ super({
+ name: "launcher",
+ anchor:
+ Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
+ keymode: Astal.Keymode.EXCLUSIVE,
+ borderWidth: 0,
+ onKeyPressEvent(_, event) {
+ const keyval = event.get_keyval()[1];
+ // Focus entry on typing
+ if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
+ entry.text += String.fromCharCode(keyval);
+ entry.grab_focus();
+ entry.set_position(-1);
+
+ // Consume event, if not consumed it will duplicate character in entry
+ return true;
+ }
+ },
+ child: (
+ <box
+ vertical
+ halign={Gtk.Align.CENTER}
+ valign={Gtk.Align.CENTER}
+ className={bind(mode).as(m => `launcher ${m}`)}
+ >
+ <SearchBar mode={mode} entry={entry} />
+ <stack
+ expand
+ transitionType={Gtk.StackTransitionType.CROSSFADE}
+ transitionDuration={100}
+ shown={bind(entry, "text").as(t => (isAction(t) ? "actions" : "content"))}
+ >
+ <stack
+ name="content"
+ transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
+ transitionDuration={200}
+ shown={bind(mode)}
+ >
+ {Object.values(content)}
+ </stack>
+ {actions}
+ </stack>
+ <ModeSwitcher mode={mode} modes={Object.keys(content) as Mode[]} />
+ </box>
+ ),
+ });
+
+ this.mode = mode;
+
+ content[mode.get()].updateContent(entry.get_text());
+ this.hook(mode, (_, v: Mode) => {
+ entry.set_text("");
+ content[v].updateContent(entry.get_text());
+ });
+ this.hook(entry, "changed", () =>
+ (isAction(entry.get_text()) ? actions : content[mode.get()]).updateContent(entry.get_text())
+ );
+ this.hook(entry, "activate", () =>
+ (isAction(entry.get_text()) ? actions : content[mode.get()]).handleActivate(entry.get_text())
+ );
+
+ // Clear search on hide if not in math mode or creating a todo
+ this.connect("hide", () => mode.get() !== "math" && !entry.text.startsWith(">todo") && entry.set_text(""));
+ }
+
+ open(mode: Mode) {
+ this.mode.set(mode);
+ this.show();
+ }
+}