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: {
|
||||
maxResults: 15, // Max shown results at one time (i.e. max height of the launcher)
|
||||
actionPrefix: ":", // Prefix for launcher actions
|
||||
apps: {
|
||||
maxResults: 30, // Actual max results, -1 for infinite
|
||||
pins: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
@mixin launcher($mode, $colour) {
|
||||
&.#{$mode} {
|
||||
label.icon {
|
||||
color: $colour;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
.mode {
|
||||
@include lib.border($colour, $width: 2);
|
||||
|
|
@ -87,10 +91,23 @@
|
|||
font-size: lib.s(32);
|
||||
}
|
||||
|
||||
.has-sublabel {
|
||||
padding: lib.s(3) 0;
|
||||
}
|
||||
|
||||
.sublabel {
|
||||
color: scheme.$subtext0;
|
||||
font-size: lib.s(14);
|
||||
}
|
||||
|
||||
& > box {
|
||||
@include lib.spacing(10);
|
||||
}
|
||||
|
||||
&.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: scheme.$surface0;
|
||||
}
|
||||
|
|
@ -98,57 +115,14 @@
|
|||
&:focus {
|
||||
border-bottom: lib.s(2) solid scheme.$red;
|
||||
color: scheme.$red;
|
||||
|
||||
.sublabel {
|
||||
color: color.mix(scheme.$red, scheme.$base, 70%);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
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 { getAppCategoryIcon } from "@/utils/icons";
|
||||
import { launch } from "@/utils/system";
|
||||
import { FlowBox, setupCustomTooltip } from "@/utils/widgets";
|
||||
import PopupWindow from "@/widgets/popupwindow";
|
||||
import { bind, execAsync, Gio, register, Variable } from "astal";
|
||||
import { App, Astal, Gtk, Widget } from "astal/gtk3";
|
||||
import { type FlowBox, setupCustomTooltip } from "@/utils/widgets";
|
||||
import { execAsync, Gio, register } from "astal";
|
||||
import { Astal, Gtk, Widget } from "astal/gtk3";
|
||||
import { launcher as config } from "config";
|
||||
import type AstalApps from "gi://AstalApps";
|
||||
|
||||
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} />;
|
||||
import { close, ContentBox, type LauncherContent, limitLength } from "./util";
|
||||
|
||||
const AppResult = ({ app }: { app: AstalApps.Application }) => (
|
||||
<Gtk.FlowBoxChild visible canFocus={false}>
|
||||
|
|
@ -102,7 +72,7 @@ const FileResult = ({ path }: { path: string }) => (
|
|||
);
|
||||
|
||||
@register()
|
||||
class Apps extends Widget.Box implements ModeContent {
|
||||
class Apps extends Widget.Box implements LauncherContent {
|
||||
#content: FlowBox;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -130,7 +100,7 @@ class Apps extends Widget.Box implements ModeContent {
|
|||
}
|
||||
|
||||
@register()
|
||||
class Files extends Widget.Box implements ModeContent {
|
||||
class Files extends Widget.Box implements LauncherContent {
|
||||
#content: FlowBox;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -162,7 +132,7 @@ class Files extends Widget.Box implements ModeContent {
|
|||
}
|
||||
|
||||
@register()
|
||||
class Math extends Widget.Box implements ModeContent {
|
||||
class Math extends Widget.Box implements LauncherContent {
|
||||
constructor() {
|
||||
super({ name: "math", className: "math" });
|
||||
}
|
||||
|
|
@ -177,7 +147,7 @@ class Math extends Widget.Box implements ModeContent {
|
|||
}
|
||||
|
||||
@register()
|
||||
class Windows extends Widget.Box implements ModeContent {
|
||||
class Windows extends Widget.Box implements LauncherContent {
|
||||
constructor() {
|
||||
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 }) => (
|
||||
<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" />) 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();
|
||||
}
|
||||
}
|
||||
export default () => ({
|
||||
apps: new Apps(),
|
||||
files: new Files(),
|
||||
math: new Math(),
|
||||
windows: new Windows(),
|
||||
});
|
||||
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