launcher: actions + refactor into multi file
Action prefix is configurable
This commit is contained in:
parent
e16bf9a9ad
commit
62602465ce
6 changed files with 392 additions and 182 deletions
|
|
@ -89,7 +89,7 @@ const DEFAULTS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
launcher: {
|
launcher: {
|
||||||
maxResults: 15, // Max shown results at one time (i.e. max height of the launcher)
|
actionPrefix: ":", // Prefix for launcher actions
|
||||||
apps: {
|
apps: {
|
||||||
maxResults: 30, // Actual max results, -1 for infinite
|
maxResults: 30, // Actual max results, -1 for infinite
|
||||||
pins: [
|
pins: [
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
|
|
||||||
@mixin launcher($mode, $colour) {
|
@mixin launcher($mode, $colour) {
|
||||||
&.#{$mode} {
|
&.#{$mode} {
|
||||||
|
label.icon {
|
||||||
|
color: $colour;
|
||||||
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
.mode {
|
.mode {
|
||||||
@include lib.border($colour, $width: 2);
|
@include lib.border($colour, $width: 2);
|
||||||
|
|
@ -87,10 +91,23 @@
|
||||||
font-size: lib.s(32);
|
font-size: lib.s(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-sublabel {
|
||||||
|
padding: lib.s(3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sublabel {
|
||||||
|
color: scheme.$subtext0;
|
||||||
|
font-size: lib.s(14);
|
||||||
|
}
|
||||||
|
|
||||||
& > box {
|
& > box {
|
||||||
@include lib.spacing(10);
|
@include lib.spacing(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: scheme.$surface0;
|
background-color: scheme.$surface0;
|
||||||
}
|
}
|
||||||
|
|
@ -98,57 +115,14 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
border-bottom: lib.s(2) solid scheme.$red;
|
border-bottom: lib.s(2) solid scheme.$red;
|
||||||
color: scheme.$red;
|
color: scheme.$red;
|
||||||
|
|
||||||
|
.sublabel {
|
||||||
|
color: color.mix(scheme.$red, scheme.$base, 70%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: scheme.$surface1;
|
background-color: scheme.$surface1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned-app {
|
|
||||||
@include lib.rounded(5);
|
|
||||||
|
|
||||||
font-size: lib.s(64);
|
|
||||||
}
|
|
||||||
|
|
||||||
.results {
|
|
||||||
.icon {
|
|
||||||
font-size: lib.s(32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
color: scheme.$subtext0;
|
|
||||||
font-size: lib.s(18);
|
|
||||||
padding: lib.s(10) 0;
|
|
||||||
|
|
||||||
@include lib.spacing;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: scheme.$subtext0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.result {
|
|
||||||
@include lib.rounded(10);
|
|
||||||
|
|
||||||
font-size: lib.s(18);
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
@include lib.spacing(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-sublabel {
|
|
||||||
font-size: lib.s(16);
|
|
||||||
|
|
||||||
.sublabel {
|
|
||||||
color: scheme.$subtext0;
|
|
||||||
font-size: lib.s(14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
201
src/modules/launcher/actions.tsx
Normal file
201
src/modules/launcher/actions.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { Apps } from "@/services/apps";
|
||||||
|
import { notify } from "@/utils/system";
|
||||||
|
import { setupCustomTooltip, type FlowBox } from "@/utils/widgets";
|
||||||
|
import { execAsync, GLib, readFile, register, type Variable } from "astal";
|
||||||
|
import { Gtk, Widget } from "astal/gtk3";
|
||||||
|
import { launcher as config } from "config";
|
||||||
|
import fuzzysort from "fuzzysort";
|
||||||
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
import { close, ContentBox, type LauncherContent, type Mode } from "./util";
|
||||||
|
|
||||||
|
interface IAction {
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
action: (...args: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionMap {
|
||||||
|
[k: string]: IAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({
|
||||||
|
apps: {
|
||||||
|
icon: "apps",
|
||||||
|
name: "Apps",
|
||||||
|
description: "Search for apps",
|
||||||
|
action: () => {
|
||||||
|
mode.set("apps");
|
||||||
|
entry.set_text("");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
icon: "folder",
|
||||||
|
name: "Files",
|
||||||
|
description: "Search for files",
|
||||||
|
action: () => {
|
||||||
|
mode.set("files");
|
||||||
|
entry.set_text("");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
math: {
|
||||||
|
icon: "calculate",
|
||||||
|
name: "Math",
|
||||||
|
description: "Do math calculations",
|
||||||
|
action: () => {
|
||||||
|
mode.set("math");
|
||||||
|
entry.set_text("");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
icon: "select_window",
|
||||||
|
name: "Windows",
|
||||||
|
description: "Manage open windows",
|
||||||
|
action: () => {
|
||||||
|
mode.set("windows");
|
||||||
|
entry.set_text("");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scheme: {
|
||||||
|
icon: "palette",
|
||||||
|
name: "Scheme",
|
||||||
|
description: "Change the current colour scheme",
|
||||||
|
action: (...args) => {
|
||||||
|
// If no args, autocomplete cmd
|
||||||
|
if (args.length === 0) {
|
||||||
|
entry.set_text(">scheme ");
|
||||||
|
entry.set_position(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
execAsync(`caelestia scheme ${args[0]}`).catch(console.error);
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
todo: {
|
||||||
|
icon: "checklist",
|
||||||
|
name: "Todo",
|
||||||
|
description: "Create a todo in Todoist",
|
||||||
|
action: (...args) => {
|
||||||
|
// If no args, autocomplete cmd
|
||||||
|
if (args.length === 0) {
|
||||||
|
entry.set_text(">todo ");
|
||||||
|
entry.set_position(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If tod not installed, notify
|
||||||
|
if (!GLib.find_program_in_path("tod")) {
|
||||||
|
notify({
|
||||||
|
summary: "Tod not installed",
|
||||||
|
body: "The launcher todo subcommand requires `tod`. Install it with `yay -S tod-bin`",
|
||||||
|
icon: "dialog-warning-symbolic",
|
||||||
|
urgency: "critical",
|
||||||
|
actions: {
|
||||||
|
Install: () => execAsync("uwsm app -T -- yay -S tod-bin").catch(console.error),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If tod not configured, notify
|
||||||
|
let token = null;
|
||||||
|
try {
|
||||||
|
token = JSON.parse(readFile(GLib.get_user_config_dir() + "/tod.cfg")).token;
|
||||||
|
} catch {} // Ignore
|
||||||
|
if (!token) {
|
||||||
|
notify({
|
||||||
|
summary: "Tod not configured",
|
||||||
|
body: "You need to configure tod first. Run any tod command to do this.",
|
||||||
|
icon: "dialog-warning-symbolic",
|
||||||
|
urgency: "critical",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create todo and notify if configured
|
||||||
|
execAsync(`tod t q -c ${args.join(" ")}`).catch(console.error);
|
||||||
|
if (config.todo.notify.get())
|
||||||
|
notify({
|
||||||
|
summary: "Todo created",
|
||||||
|
body: `Created todo with content: ${args.join(" ")}`,
|
||||||
|
icon: "view-list-bullet-symbolic",
|
||||||
|
urgency: "low",
|
||||||
|
transient: true,
|
||||||
|
actions: {
|
||||||
|
"Copy content": () => execAsync(`wl-copy -- ${args.join(" ")}`).catch(console.error),
|
||||||
|
View: () => {
|
||||||
|
const client = AstalHyprland.get_default().clients.find(c => c.class === "Todoist");
|
||||||
|
if (client) client.focus();
|
||||||
|
else execAsync("uwsm app -- todoist").catch(console.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reload: {
|
||||||
|
icon: "refresh",
|
||||||
|
name: "Reload",
|
||||||
|
description: "Reload app list",
|
||||||
|
action: () => {
|
||||||
|
Apps.reload();
|
||||||
|
entry.set_text("");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Action = ({ args, icon, name, description, action }: IAction & { args: string[] }) => (
|
||||||
|
<Gtk.FlowBoxChild visible canFocus={false}>
|
||||||
|
<button
|
||||||
|
className="result"
|
||||||
|
cursor="pointer"
|
||||||
|
onClicked={() => action(...args)}
|
||||||
|
setup={self => setupCustomTooltip(self, description)}
|
||||||
|
>
|
||||||
|
<box>
|
||||||
|
<label className="icon" label={icon} />
|
||||||
|
<box vertical className="has-sublabel">
|
||||||
|
<label truncate xalign={0} label={name} />
|
||||||
|
<label truncate xalign={0} label={description} className="sublabel" />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</button>
|
||||||
|
</Gtk.FlowBoxChild>
|
||||||
|
);
|
||||||
|
|
||||||
|
@register()
|
||||||
|
export default class Actions extends Widget.Box implements LauncherContent {
|
||||||
|
#map: ActionMap;
|
||||||
|
#list: string[];
|
||||||
|
|
||||||
|
#content: FlowBox;
|
||||||
|
|
||||||
|
constructor(mode: Variable<Mode>, entry: Widget.Entry) {
|
||||||
|
super({ name: "actions", className: "actions" });
|
||||||
|
|
||||||
|
this.#map = actions(mode, entry);
|
||||||
|
this.#list = Object.keys(this.#map);
|
||||||
|
|
||||||
|
this.#content = (<ContentBox />) as FlowBox;
|
||||||
|
|
||||||
|
this.add(
|
||||||
|
<scrollable expand hscroll={Gtk.PolicyType.NEVER}>
|
||||||
|
{this.#content}
|
||||||
|
</scrollable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent(search: string): void {
|
||||||
|
this.#content.foreach(c => c.destroy());
|
||||||
|
const args = search.split(" ");
|
||||||
|
for (const { target } of fuzzysort.go(args[0].slice(1), this.#list, { all: true }))
|
||||||
|
this.#content.add(<Action {...this.#map[target]} args={args.slice(1)} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActivate(): void {
|
||||||
|
this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
|
||||||
|
this.#content.get_child_at_index(0)?.get_child()?.activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/modules/launcher/index.tsx
Normal file
136
src/modules/launcher/index.tsx
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,42 +1,12 @@
|
||||||
import { Apps as AppsService } from "@/services/apps";
|
import { Apps as AppsService } from "@/services/apps";
|
||||||
import { getAppCategoryIcon } from "@/utils/icons";
|
import { getAppCategoryIcon } from "@/utils/icons";
|
||||||
import { launch } from "@/utils/system";
|
import { launch } from "@/utils/system";
|
||||||
import { FlowBox, setupCustomTooltip } from "@/utils/widgets";
|
import { type FlowBox, setupCustomTooltip } from "@/utils/widgets";
|
||||||
import PopupWindow from "@/widgets/popupwindow";
|
import { execAsync, Gio, register } from "astal";
|
||||||
import { bind, execAsync, Gio, register, Variable } from "astal";
|
import { Astal, Gtk, Widget } from "astal/gtk3";
|
||||||
import { App, Astal, Gtk, Widget } from "astal/gtk3";
|
|
||||||
import { launcher as config } from "config";
|
import { launcher as config } from "config";
|
||||||
import type AstalApps from "gi://AstalApps";
|
import type AstalApps from "gi://AstalApps";
|
||||||
|
import { close, ContentBox, type LauncherContent, limitLength } from "./util";
|
||||||
type Mode = "apps" | "files" | "math" | "windows";
|
|
||||||
|
|
||||||
interface ModeContent {
|
|
||||||
updateContent(search: string): void;
|
|
||||||
handleActivate(search: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => App.get_window("launcher")?.hide();
|
|
||||||
|
|
||||||
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 limitLength = <T,>(arr: T[], cfg: { maxResults: Variable<number> }) =>
|
|
||||||
cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr;
|
|
||||||
|
|
||||||
const ContentBox = () => <FlowBox homogeneous valign={Gtk.Align.START} minChildrenPerLine={2} maxChildrenPerLine={2} />;
|
|
||||||
|
|
||||||
const AppResult = ({ app }: { app: AstalApps.Application }) => (
|
const AppResult = ({ app }: { app: AstalApps.Application }) => (
|
||||||
<Gtk.FlowBoxChild visible canFocus={false}>
|
<Gtk.FlowBoxChild visible canFocus={false}>
|
||||||
|
|
@ -102,7 +72,7 @@ const FileResult = ({ path }: { path: string }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
class Apps extends Widget.Box implements ModeContent {
|
class Apps extends Widget.Box implements LauncherContent {
|
||||||
#content: FlowBox;
|
#content: FlowBox;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -130,7 +100,7 @@ class Apps extends Widget.Box implements ModeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
class Files extends Widget.Box implements ModeContent {
|
class Files extends Widget.Box implements LauncherContent {
|
||||||
#content: FlowBox;
|
#content: FlowBox;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -162,7 +132,7 @@ class Files extends Widget.Box implements ModeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
class Math extends Widget.Box implements ModeContent {
|
class Math extends Widget.Box implements LauncherContent {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ name: "math", className: "math" });
|
super({ name: "math", className: "math" });
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +147,7 @@ class Math extends Widget.Box implements ModeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
class Windows extends Widget.Box implements ModeContent {
|
class Windows extends Widget.Box implements LauncherContent {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ name: "windows", className: "windows" });
|
super({ name: "windows", className: "windows" });
|
||||||
}
|
}
|
||||||
|
|
@ -191,99 +161,9 @@ class Windows extends Widget.Box implements ModeContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBar = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => (
|
export default () => ({
|
||||||
<box className="search-bar">
|
apps: new Apps(),
|
||||||
<label className="mode" label={bind(mode)} />
|
files: new Files(),
|
||||||
{entry}
|
math: new Math(),
|
||||||
</box>
|
windows: new Windows(),
|
||||||
);
|
});
|
||||||
|
|
||||||
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" />) as Widget.Entry;
|
|
||||||
const mode = Variable<Mode>("apps");
|
|
||||||
const content = {
|
|
||||||
apps: new Apps(),
|
|
||||||
files: new Files(),
|
|
||||||
math: new Math(),
|
|
||||||
windows: new Windows(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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.SLIDE_LEFT_RIGHT}
|
|
||||||
transitionDuration={200}
|
|
||||||
shown={bind(mode)}
|
|
||||||
>
|
|
||||||
{Object.values(content)}
|
|
||||||
</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", () => content[mode.get()].updateContent(entry.get_text()));
|
|
||||||
this.hook(entry, "activate", () => 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
src/modules/launcher/util.tsx
Normal file
19
src/modules/launcher/util.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { FlowBox } from "@/utils/widgets";
|
||||||
|
import type { Variable } from "astal";
|
||||||
|
import { App, Gtk } from "astal/gtk3";
|
||||||
|
|
||||||
|
export type Mode = "apps" | "files" | "math" | "windows";
|
||||||
|
|
||||||
|
export interface LauncherContent {
|
||||||
|
updateContent(search: string): void;
|
||||||
|
handleActivate(search: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const close = () => App.get_window("launcher")?.hide();
|
||||||
|
|
||||||
|
export const limitLength = <T,>(arr: T[], cfg: { maxResults: Variable<number> }) =>
|
||||||
|
cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr;
|
||||||
|
|
||||||
|
export const ContentBox = () => (
|
||||||
|
<FlowBox homogeneous valign={Gtk.Align.START} minChildrenPerLine={2} maxChildrenPerLine={2} />
|
||||||
|
);
|
||||||
Loading…
Add table
Reference in a new issue