launcher: use scrollable
This commit is contained in:
parent
2293da9789
commit
ba4a879cc7
3 changed files with 116 additions and 54 deletions
20
config.ts
20
config.ts
|
|
@ -7,8 +7,9 @@ export const bar = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const launcher = {
|
export const launcher = {
|
||||||
maxResults: 15,
|
maxResults: 15, // Max shown results at one time (i.e. max height of the launcher)
|
||||||
fdOpts: ["-a", "-t", "f"],
|
apps: {
|
||||||
|
maxResults: 30, // Actual max results, -1 for infinite
|
||||||
pins: [
|
pins: [
|
||||||
["firefox", "waterfox", "google-chrome", "chromium", "brave-browser", "vivaldi-stable", "vivaldi-snapshot"],
|
["firefox", "waterfox", "google-chrome", "chromium", "brave-browser", "vivaldi-stable", "vivaldi-snapshot"],
|
||||||
["foot", "alacritty", "kitty", "wezterm"],
|
["foot", "alacritty", "kitty", "wezterm"],
|
||||||
|
|
@ -16,13 +17,24 @@ export const launcher = {
|
||||||
["codium", "code", "clion", "intellij-idea-ultimate-edition"],
|
["codium", "code", "clion", "intellij-idea-ultimate-edition"],
|
||||||
["spotify-adblock", "spotify", "audacious", "elisa"],
|
["spotify-adblock", "spotify", "audacious", "elisa"],
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
maxResults: 40, // Actual max results, -1 for infinite
|
||||||
|
fdOpts: ["-a", "-t", "f"], // Options to pass to `fd`
|
||||||
|
},
|
||||||
|
math: {
|
||||||
|
maxResults: 40, // Actual max results, -1 for infinite
|
||||||
|
},
|
||||||
windows: {
|
windows: {
|
||||||
|
maxResults: -1, // Actual max results, -1 for infinite
|
||||||
|
weights: {
|
||||||
// Weights for fuzzy sort
|
// Weights for fuzzy sort
|
||||||
title: 1,
|
title: 1,
|
||||||
class: 1,
|
class: 1,
|
||||||
initialTitle: 0.5,
|
initialTitle: 0.5,
|
||||||
initialClass: 0.5,
|
initialClass: 0.5,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notifpopups = {
|
export const notifpopups = {
|
||||||
|
|
@ -55,6 +67,10 @@ export const osds = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
export const math = {
|
||||||
|
maxHistory: 100,
|
||||||
|
};
|
||||||
|
|
||||||
export const updates = {
|
export const updates = {
|
||||||
interval: 900000,
|
interval: 900000,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ const getEmptyTextFromMode = (mode: Mode) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const limitLength = <T,>(arr: T[], cfg: { maxResults: number }) =>
|
||||||
|
cfg.maxResults > 0 && arr.length > cfg.maxResults ? arr.slice(0, cfg.maxResults) : arr;
|
||||||
|
|
||||||
const close = (self: JSX.Element) => {
|
const close = (self: JSX.Element) => {
|
||||||
const toplevel = self.get_toplevel();
|
const toplevel = self.get_toplevel();
|
||||||
if (toplevel instanceof Widget.Window) toplevel.hide();
|
if (toplevel instanceof Widget.Window) toplevel.hide();
|
||||||
|
|
@ -114,7 +117,7 @@ const PinnedApp = (names: string[]) => {
|
||||||
return widget;
|
return widget;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PinnedApps = () => <box homogeneous>{config.pins.map(PinnedApp)}</box>;
|
const PinnedApps = () => <box homogeneous>{config.apps.pins.map(PinnedApp)}</box>;
|
||||||
|
|
||||||
const SearchEntry = ({ entry }: { entry: Widget.Entry }) => (
|
const SearchEntry = ({ entry }: { entry: Widget.Entry }) => (
|
||||||
<stack
|
<stack
|
||||||
|
|
@ -141,6 +144,7 @@ const Result = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
onClicked,
|
onClicked,
|
||||||
onSecondaryClick,
|
onSecondaryClick,
|
||||||
|
onMiddleClick,
|
||||||
onDestroy,
|
onDestroy,
|
||||||
}: {
|
}: {
|
||||||
icon?: string | Gio.Icon | null;
|
icon?: string | Gio.Icon | null;
|
||||||
|
|
@ -150,6 +154,7 @@ const Result = ({
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
onClicked: (self: Widget.Button) => void;
|
onClicked: (self: Widget.Button) => void;
|
||||||
onSecondaryClick?: (self: Widget.Button) => void;
|
onSecondaryClick?: (self: Widget.Button) => void;
|
||||||
|
onMiddleClick?: (self: Widget.Button) => void;
|
||||||
onDestroy?: () => void;
|
onDestroy?: () => void;
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -157,7 +162,10 @@ const Result = ({
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
tooltipText={tooltip}
|
tooltipText={tooltip}
|
||||||
onClicked={onClicked}
|
onClicked={onClicked}
|
||||||
onClick={(self, event) => event.button === Astal.MouseButton.SECONDARY && onSecondaryClick?.(self)}
|
onClick={(self, event) => {
|
||||||
|
if (event.button === Astal.MouseButton.SECONDARY) onSecondaryClick?.(self);
|
||||||
|
else if (event.button === Astal.MouseButton.MIDDLE) onMiddleClick?.(self);
|
||||||
|
}}
|
||||||
onDestroy={onDestroy}
|
onDestroy={onDestroy}
|
||||||
>
|
>
|
||||||
<box>
|
<box>
|
||||||
|
|
@ -167,14 +175,16 @@ const Result = ({
|
||||||
) : (
|
) : (
|
||||||
<icon valign={Gtk.Align.START} className="icon" gicon={icon} />
|
<icon valign={Gtk.Align.START} className="icon" gicon={icon} />
|
||||||
))}
|
))}
|
||||||
{!icon && materialIcon && <label valign={Gtk.Align.START} className="icon" label={materialIcon} />}
|
{materialIcon && (!icon || (typeof icon === "string" && !Astal.Icon.lookup_icon(icon))) && (
|
||||||
|
<label valign={Gtk.Align.START} className="icon" label={materialIcon} />
|
||||||
|
)}
|
||||||
{sublabel ? (
|
{sublabel ? (
|
||||||
<box vertical valign={Gtk.Align.CENTER} className="has-sublabel">
|
<box vertical valign={Gtk.Align.CENTER} className="has-sublabel">
|
||||||
<label hexpand truncate maxWidthChars={1} xalign={0} label={label} />
|
<label hexpand truncate maxWidthChars={1} xalign={0} label={label} />
|
||||||
<label hexpand truncate maxWidthChars={1} className="sublabel" xalign={0} label={sublabel} />
|
<label hexpand truncate maxWidthChars={1} className="sublabel" xalign={0} label={sublabel} />
|
||||||
</box>
|
</box>
|
||||||
) : (
|
) : (
|
||||||
<label xalign={0} label={label} />
|
<label hexpand truncate maxWidthChars={1} xalign={0} label={label} />
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -388,18 +398,43 @@ const WindowResult = ({ client, reload }: { client: Client; reload: () => void }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const classOrTitle = (prop: "Class" | "Title", header = true) => {
|
||||||
|
const lower = prop.toLowerCase() as "class" | "title";
|
||||||
|
return (
|
||||||
|
(header ? `${prop}: ` : "") +
|
||||||
|
(client[lower] || (client[`initial${prop}`] ? `${client[`initial${prop}`]} (initial)` : `No ${lower}`))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const workspace = (header = false) =>
|
||||||
|
(header ? "Workspace: " : "") + `${client.workspace.name} (${client.workspace.id})`;
|
||||||
|
const prop = (prop: keyof typeof client, header?: string) =>
|
||||||
|
`${header ?? prop.slice(0, 1).toUpperCase() + prop.slice(1)}: ${client[prop]}`;
|
||||||
|
|
||||||
const result = (
|
const result = (
|
||||||
<Result
|
<Result
|
||||||
icon={app.iconName}
|
icon={app.iconName}
|
||||||
materialIcon={getAppCategoryIcon(app)}
|
materialIcon={getAppCategoryIcon(app)}
|
||||||
label={client.title || (client.initialTitle ? `${client.initialTitle} (initial)` : "No title")}
|
label={
|
||||||
sublabel={client.class || (client.initialClass ? `${client.initialClass} (initial)` : "No class")}
|
classOrTitle("Title", false).length < 5
|
||||||
tooltip={`Address: ${client.address}\nWorkspace: ${client.workspace.name} (${client.workspace.id})\nProcess ID: ${client.pid}\nFloating: ${client.floating}\nInhibiting idle: ${client.inhibitingIdle}`}
|
? `${classOrTitle("Class", false)}: ${classOrTitle("Title", false)}`
|
||||||
|
: classOrTitle("Title", false)
|
||||||
|
}
|
||||||
|
sublabel={`Workspace ${workspace()} on ${hyprland.get_monitor(client.monitor).name}`}
|
||||||
|
tooltip={`${classOrTitle("Title")}\n${classOrTitle("Class")}\n${prop("address")}\n${workspace(
|
||||||
|
true
|
||||||
|
)}\n${prop("pid", "Process ID")}\n${prop("floating")}\n${prop("inhibitingIdle", "Inhibiting idle")}`}
|
||||||
onClicked={self => {
|
onClicked={self => {
|
||||||
close(self);
|
close(self);
|
||||||
astalClient?.focus();
|
astalClient?.focus();
|
||||||
}}
|
}}
|
||||||
onSecondaryClick={() => menu.popup_at_pointer(null)}
|
onSecondaryClick={() => menu.popup_at_pointer(null)}
|
||||||
|
onMiddleClick={() => {
|
||||||
|
astalClient?.kill();
|
||||||
|
const id = hyprland.connect("client-removed", () => {
|
||||||
|
hyprland.disconnect(id);
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
}}
|
||||||
onDestroy={() => menu.destroy()}
|
onDestroy={() => menu.destroy()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -409,24 +444,10 @@ const WindowResult = ({ client, reload }: { client: Client; reload: () => void }
|
||||||
const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> }) => {
|
const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> }) => {
|
||||||
const empty = Variable(true);
|
const empty = Variable(true);
|
||||||
|
|
||||||
return (
|
const scrollable = (
|
||||||
<stack
|
<scrollable name="list" hscroll={Gtk.PolicyType.NEVER}>
|
||||||
className="results"
|
|
||||||
transitionType={Gtk.StackTransitionType.CROSSFADE}
|
|
||||||
transitionDuration={150}
|
|
||||||
shown={bind(empty).as(t => (t ? "empty" : "list"))}
|
|
||||||
>
|
|
||||||
<box name="empty" className="empty" halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
|
||||||
<label className="icon" label="bug_report" />
|
|
||||||
<label
|
|
||||||
label={bind(entry, "text").as(t =>
|
|
||||||
t.startsWith(">") ? "No matching subcommands" : getEmptyTextFromMode(mode.get())
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
<box
|
<box
|
||||||
vertical
|
vertical
|
||||||
name="list"
|
|
||||||
setup={self => {
|
setup={self => {
|
||||||
const subcommands: Record<string, Subcommand> = {
|
const subcommands: Record<string, Subcommand> = {
|
||||||
apps: {
|
apps: {
|
||||||
|
|
@ -470,11 +491,16 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
};
|
};
|
||||||
const subcommandList = Object.keys(subcommands);
|
const subcommandList = Object.keys(subcommands);
|
||||||
|
|
||||||
const updateEmpty = () => empty.set(self.get_children().length === 0);
|
const afterUpdate = () => {
|
||||||
|
empty.set(self.get_children().length === 0);
|
||||||
|
|
||||||
|
const children = limitLength(self.get_children(), config);
|
||||||
|
const height = children.reduce((a, b) => a + b.get_preferred_height()[1], 0);
|
||||||
|
scrollable.css = `min-height: ${height}px;`;
|
||||||
|
};
|
||||||
|
|
||||||
const appSearch = () => {
|
const appSearch = () => {
|
||||||
const apps = Apps.fuzzy_query(entry.text);
|
const apps = limitLength(Apps.fuzzy_query(entry.text), config.apps);
|
||||||
if (apps.length > config.maxResults) apps.length = config.maxResults;
|
|
||||||
for (const app of apps) self.add(<AppResult app={app} />);
|
for (const app of apps) self.add(<AppResult app={app} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -485,23 +511,23 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
);
|
);
|
||||||
self.add(<box className="separator" />);
|
self.add(<box className="separator" />);
|
||||||
}
|
}
|
||||||
for (const item of MathService.get_default().history)
|
for (const item of limitLength(MathService.get_default().history, config.math))
|
||||||
self.add(<MathResult isHistory math={item} entry={entry} />);
|
self.add(<MathResult isHistory math={item} entry={entry} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileSearch = () =>
|
const fileSearch = () =>
|
||||||
execAsync(["fd", ...config.fdOpts, entry.text, HOME])
|
execAsync(["fd", ...config.files.fdOpts, entry.text, HOME])
|
||||||
.then(out => {
|
.then(out => {
|
||||||
const paths = out.split("\n").filter(path => path);
|
const paths = out.split("\n").filter(path => path);
|
||||||
if (paths.length > config.maxResults) paths.length = config.maxResults;
|
|
||||||
self.foreach(ch => ch.destroy());
|
self.foreach(ch => ch.destroy());
|
||||||
for (const path of paths) self.add(<FileResult path={path} />);
|
for (const path of limitLength(paths, config.files))
|
||||||
|
self.add(<FileResult path={path} />);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
// Ignore execAsync error
|
// Ignore execAsync error
|
||||||
if (!(e instanceof Gio.IOErrorEnum || e instanceof GLib.SpawnError)) console.error(e);
|
if (!(e instanceof Gio.IOErrorEnum || e instanceof GLib.SpawnError)) console.error(e);
|
||||||
})
|
})
|
||||||
.finally(updateEmpty);
|
.finally(afterUpdate);
|
||||||
|
|
||||||
const listWindows = () => {
|
const listWindows = () => {
|
||||||
const hyprland = AstalHyprland.get_default();
|
const hyprland = AstalHyprland.get_default();
|
||||||
|
|
@ -512,13 +538,13 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
if (entry.text) {
|
if (entry.text) {
|
||||||
const clients = fuzzysort.go(entry.text, unsortedClients, {
|
const clients = fuzzysort.go(entry.text, unsortedClients, {
|
||||||
all: true,
|
all: true,
|
||||||
limit: config.maxResults,
|
limit: config.windows.maxResults,
|
||||||
keys: ["title", "class", "initialTitle", "initialClass"],
|
keys: ["title", "class", "initialTitle", "initialClass"],
|
||||||
scoreFn: r =>
|
scoreFn: r =>
|
||||||
r[0].score * config.windows.title +
|
r[0].score * config.windows.weights.title +
|
||||||
r[1].score * config.windows.class +
|
r[1].score * config.windows.weights.class +
|
||||||
r[2].score * config.windows.initialTitle +
|
r[2].score * config.windows.weights.initialTitle +
|
||||||
r[3].score * config.windows.initialClass,
|
r[3].score * config.windows.weights.initialClass,
|
||||||
});
|
});
|
||||||
self.foreach(ch => ch.destroy());
|
self.foreach(ch => ch.destroy());
|
||||||
for (const { obj } of clients)
|
for (const { obj } of clients)
|
||||||
|
|
@ -526,13 +552,13 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
} else {
|
} else {
|
||||||
const clients = unsortedClients.sort((a, b) => a.focusHistoryID - b.focusHistoryID);
|
const clients = unsortedClients.sort((a, b) => a.focusHistoryID - b.focusHistoryID);
|
||||||
self.foreach(ch => ch.destroy());
|
self.foreach(ch => ch.destroy());
|
||||||
for (const client of clients)
|
for (const client of limitLength(clients, config.windows))
|
||||||
self.add(<WindowResult reload={listWindows} client={client} />);
|
self.add(<WindowResult reload={listWindows} client={client} />);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
updateEmpty();
|
afterUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -572,10 +598,29 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
else if (mode.get() === "files") fileSearch();
|
else if (mode.get() === "files") fileSearch();
|
||||||
else if (mode.get() === "windows") listWindows();
|
else if (mode.get() === "windows") listWindows();
|
||||||
|
|
||||||
if (ignoreFileAsync) updateEmpty();
|
if (ignoreFileAsync) afterUpdate();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</scrollable>
|
||||||
|
) as Widget.Scrollable;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<stack
|
||||||
|
className="results"
|
||||||
|
transitionType={Gtk.StackTransitionType.CROSSFADE}
|
||||||
|
transitionDuration={150}
|
||||||
|
shown={bind(empty).as(t => (t ? "empty" : "list"))}
|
||||||
|
>
|
||||||
|
<box name="empty" className="empty" halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
||||||
|
<label className="icon" label="bug_report" />
|
||||||
|
<label
|
||||||
|
label={bind(entry, "text").as(t =>
|
||||||
|
t.startsWith(">") ? "No matching subcommands" : getEmptyTextFromMode(mode.get())
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
{scrollable}
|
||||||
</stack>
|
</stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
|
import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
|
||||||
import { derivative, evaluate, rationalize, simplify } from "mathjs/number";
|
import { derivative, evaluate, rationalize, simplify } from "mathjs/number";
|
||||||
|
import { math as config } from "../../config";
|
||||||
|
|
||||||
export interface HistoryItem {
|
export interface HistoryItem {
|
||||||
equation: string;
|
equation: string;
|
||||||
|
|
@ -16,7 +17,7 @@ export default class Math extends GObject.Object {
|
||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly #maxHistory = 20;
|
readonly #maxHistory = config.maxHistory;
|
||||||
readonly #path = `${CACHE}/math-history.json`;
|
readonly #path = `${CACHE}/math-history.json`;
|
||||||
readonly #history: HistoryItem[] = [];
|
readonly #history: HistoryItem[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue