launcher: windows mode
Also files mode icons
This commit is contained in:
parent
3cfb189e90
commit
2293da9789
4 changed files with 262 additions and 16 deletions
|
|
@ -16,6 +16,13 @@ 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"],
|
||||||
],
|
],
|
||||||
|
windows: {
|
||||||
|
// Weights for fuzzysort
|
||||||
|
title: 1,
|
||||||
|
class: 1,
|
||||||
|
initialTitle: 0.5,
|
||||||
|
initialClass: 0.5,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notifpopups = {
|
export const notifpopups = {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
@include launcher(apps, scheme.$sapphire);
|
@include launcher(apps, scheme.$sapphire);
|
||||||
@include launcher(files, scheme.$peach);
|
@include launcher(files, scheme.$peach);
|
||||||
@include launcher(math, scheme.$green);
|
@include launcher(math, scheme.$green);
|
||||||
|
@include launcher(windows, scheme.$teal);
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
margin-bottom: lib.s(5);
|
margin-bottom: lib.s(5);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { bind, execAsync, Gio, GLib, register, timeout, Variable } from "astal";
|
import { bind, execAsync, Gio, GLib, register, timeout, Variable } from "astal";
|
||||||
import { Astal, Gtk, Widget } from "astal/gtk3";
|
import { App, Astal, Gtk, Widget } from "astal/gtk3";
|
||||||
import fuzzysort from "fuzzysort";
|
import fuzzysort from "fuzzysort";
|
||||||
import type AstalApps from "gi://AstalApps";
|
import type AstalApps from "gi://AstalApps";
|
||||||
import AstalHyprland from "gi://AstalHyprland";
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
import { launcher as config } from "../../config";
|
import { launcher as config } from "../../config";
|
||||||
import { Apps } from "../services/apps";
|
import { Apps } from "../services/apps";
|
||||||
import Math, { type HistoryItem } from "../services/math";
|
import MathService, { type HistoryItem } from "../services/math";
|
||||||
import { getAppCategoryIcon } from "../utils/icons";
|
import { getAppCategoryIcon } from "../utils/icons";
|
||||||
import { launch } from "../utils/system";
|
import { launch } from "../utils/system";
|
||||||
|
import type { Client } from "../utils/types";
|
||||||
import { MenuItem, setupCustomTooltip } from "../utils/widgets";
|
import { MenuItem, setupCustomTooltip } from "../utils/widgets";
|
||||||
import PopupWindow from "../widgets/popupwindow";
|
import PopupWindow from "../widgets/popupwindow";
|
||||||
|
|
||||||
type Mode = "apps" | "files" | "math";
|
type Mode = "apps" | "files" | "math" | "windows";
|
||||||
|
|
||||||
interface Subcommand {
|
interface Subcommand {
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
@ -28,6 +29,8 @@ const getIconFromMode = (mode: Mode) => {
|
||||||
return "folder";
|
return "folder";
|
||||||
case "math":
|
case "math":
|
||||||
return "calculate";
|
return "calculate";
|
||||||
|
case "windows":
|
||||||
|
return "select_window_2";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -39,6 +42,8 @@ const getEmptyTextFromMode = (mode: Mode) => {
|
||||||
return GLib.find_program_in_path("fd") === null ? "File search requires `fd`" : "No files found";
|
return GLib.find_program_in_path("fd") === null ? "File search requires `fd`" : "No files found";
|
||||||
case "math":
|
case "math":
|
||||||
return "Type an expression";
|
return "Type an expression";
|
||||||
|
case "windows":
|
||||||
|
return "No windows found";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -133,14 +138,16 @@ const Result = ({
|
||||||
materialIcon,
|
materialIcon,
|
||||||
label,
|
label,
|
||||||
sublabel,
|
sublabel,
|
||||||
|
tooltip,
|
||||||
onClicked,
|
onClicked,
|
||||||
onSecondaryClick,
|
onSecondaryClick,
|
||||||
onDestroy,
|
onDestroy,
|
||||||
}: {
|
}: {
|
||||||
icon?: string;
|
icon?: string | Gio.Icon | null;
|
||||||
materialIcon?: string;
|
materialIcon?: string;
|
||||||
label: string;
|
label: string;
|
||||||
sublabel?: string;
|
sublabel?: string;
|
||||||
|
tooltip?: string;
|
||||||
onClicked: (self: Widget.Button) => void;
|
onClicked: (self: Widget.Button) => void;
|
||||||
onSecondaryClick?: (self: Widget.Button) => void;
|
onSecondaryClick?: (self: Widget.Button) => void;
|
||||||
onDestroy?: () => void;
|
onDestroy?: () => void;
|
||||||
|
|
@ -148,16 +155,19 @@ const Result = ({
|
||||||
<button
|
<button
|
||||||
className="result"
|
className="result"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
|
tooltipText={tooltip}
|
||||||
onClicked={onClicked}
|
onClicked={onClicked}
|
||||||
onClick={(self, event) => event.button === Astal.MouseButton.SECONDARY && onSecondaryClick?.(self)}
|
onClick={(self, event) => event.button === Astal.MouseButton.SECONDARY && onSecondaryClick?.(self)}
|
||||||
onDestroy={onDestroy}
|
onDestroy={onDestroy}
|
||||||
>
|
>
|
||||||
<box>
|
<box>
|
||||||
{icon && Astal.Icon.lookup_icon(icon) ? (
|
{icon &&
|
||||||
<icon valign={Gtk.Align.START} className="icon" icon={icon} />
|
(typeof icon === "string" ? (
|
||||||
) : (
|
Astal.Icon.lookup_icon(icon) && <icon valign={Gtk.Align.START} className="icon" icon={icon} />
|
||||||
<label valign={Gtk.Align.START} className="icon" label={materialIcon} />
|
) : (
|
||||||
)}
|
<icon valign={Gtk.Align.START} className="icon" gicon={icon} />
|
||||||
|
))}
|
||||||
|
{!icon && materialIcon && <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} />
|
||||||
|
|
@ -229,7 +239,7 @@ const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?:
|
||||||
sublabel={math.result}
|
sublabel={math.result}
|
||||||
onClicked={() => {
|
onClicked={() => {
|
||||||
if (isHistory) {
|
if (isHistory) {
|
||||||
Math.get_default().select(math);
|
MathService.get_default().select(math);
|
||||||
entry.set_text(math.equation);
|
entry.set_text(math.equation);
|
||||||
entry.grab_focus();
|
entry.grab_focus();
|
||||||
entry.set_position(-1);
|
entry.set_position(-1);
|
||||||
|
|
@ -243,12 +253,159 @@ const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?:
|
||||||
|
|
||||||
const FileResult = ({ path }: { path: string }) => (
|
const FileResult = ({ path }: { path: string }) => (
|
||||||
<Result
|
<Result
|
||||||
|
icon={Gio.File.new_for_path(path)
|
||||||
|
.query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, null)
|
||||||
|
.get_icon()}
|
||||||
label={path.split("/").pop()!}
|
label={path.split("/").pop()!}
|
||||||
sublabel={path.startsWith(HOME) ? "~" + path.slice(HOME.length) : path}
|
sublabel={path.startsWith(HOME) ? "~" + path.slice(HOME.length) : path}
|
||||||
onClicked={self => openFileAndClose(self, path)}
|
onClicked={self => openFileAndClose(self, path)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const WindowResult = ({ client, reload }: { client: Client; reload: () => void }) => {
|
||||||
|
const hyprland = AstalHyprland.get_default();
|
||||||
|
const app = Apps.fuzzy_query(client.class)[0];
|
||||||
|
const astalClient = hyprland.get_client(client.address);
|
||||||
|
|
||||||
|
const menu = new Gtk.Menu();
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: "Focus",
|
||||||
|
onActivate: () => {
|
||||||
|
close(result);
|
||||||
|
astalClient?.focus();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
menu.append(new Gtk.SeparatorMenuItem({ visible: true }));
|
||||||
|
|
||||||
|
const addSubmenus = (silent: boolean) => {
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: `Move to workspace${silent ? " (silent)" : ""}`,
|
||||||
|
setup: self => {
|
||||||
|
const submenu = new Gtk.Menu();
|
||||||
|
const start = Math.floor((hyprland.focusedWorkspace.id - 1) / 10) * 10;
|
||||||
|
for (let i = 1; i <= 10; i++)
|
||||||
|
submenu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: `Workspace ${start + i}`,
|
||||||
|
onActivate: () => {
|
||||||
|
if (!silent) close(result);
|
||||||
|
hyprland.dispatch(
|
||||||
|
`movetoworkspace${silent ? "silent" : ""}`,
|
||||||
|
`${start + i},address:${client.address}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.set_submenu(submenu);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: `Move to special workspace${silent ? " (silent)" : ""}`,
|
||||||
|
setup: self => {
|
||||||
|
const submenu = new Gtk.Menu();
|
||||||
|
submenu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: "special",
|
||||||
|
onActivate: () => {
|
||||||
|
if (!silent) close(result);
|
||||||
|
hyprland.dispatch(
|
||||||
|
`movetoworkspace${silent ? "silent" : ""}`,
|
||||||
|
`special,address:${client.address}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
hyprland.message_async("j/workspaces", (_, res) => {
|
||||||
|
const workspaces = JSON.parse(hyprland.message_finish(res));
|
||||||
|
for (const workspace of workspaces)
|
||||||
|
if (workspace.name.startsWith("special:"))
|
||||||
|
submenu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: workspace.name.slice(8),
|
||||||
|
onActivate: () => {
|
||||||
|
if (!silent) close(result);
|
||||||
|
hyprland.dispatch(
|
||||||
|
`movetoworkspace${silent ? "silent" : ""}`,
|
||||||
|
`${workspace.name},address:${client.address}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
self.set_submenu(submenu);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
addSubmenus(false);
|
||||||
|
addSubmenus(true);
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: "Copy property",
|
||||||
|
setup: self => {
|
||||||
|
const addSubmenu = (self: MenuItem, obj: object) => {
|
||||||
|
const submenu = new Gtk.Menu();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj))
|
||||||
|
if (typeof value === "object") submenu.append(addSubmenu(new MenuItem({ label: key }), value));
|
||||||
|
else
|
||||||
|
submenu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: key,
|
||||||
|
onActivate: () => {
|
||||||
|
close(result);
|
||||||
|
execAsync(`wl-copy -- ${value}`).catch(console.error);
|
||||||
|
},
|
||||||
|
tooltipText: String(value),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
self.set_submenu(submenu);
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
addSubmenu(self, client);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
menu.append(new Gtk.SeparatorMenuItem({ visible: true }));
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: "Kill",
|
||||||
|
onActivate: () => {
|
||||||
|
astalClient?.kill();
|
||||||
|
const id = hyprland.connect("client-removed", () => {
|
||||||
|
hyprland.disconnect(id);
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = (
|
||||||
|
<Result
|
||||||
|
icon={app.iconName}
|
||||||
|
materialIcon={getAppCategoryIcon(app)}
|
||||||
|
label={client.title || (client.initialTitle ? `${client.initialTitle} (initial)` : "No title")}
|
||||||
|
sublabel={client.class || (client.initialClass ? `${client.initialClass} (initial)` : "No class")}
|
||||||
|
tooltip={`Address: ${client.address}\nWorkspace: ${client.workspace.name} (${client.workspace.id})\nProcess ID: ${client.pid}\nFloating: ${client.floating}\nInhibiting idle: ${client.inhibitingIdle}`}
|
||||||
|
onClicked={self => {
|
||||||
|
close(self);
|
||||||
|
astalClient?.focus();
|
||||||
|
}}
|
||||||
|
onSecondaryClick={() => menu.popup_at_pointer(null)}
|
||||||
|
onDestroy={() => menu.destroy()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
|
@ -290,6 +447,12 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
description: "Do math calculations",
|
description: "Do math calculations",
|
||||||
command: () => mode.set("math"),
|
command: () => mode.set("math"),
|
||||||
},
|
},
|
||||||
|
windows: {
|
||||||
|
icon: "select_window_2",
|
||||||
|
name: "Windows",
|
||||||
|
description: "Manage open windows",
|
||||||
|
command: () => mode.set("windows"),
|
||||||
|
},
|
||||||
todo: {
|
todo: {
|
||||||
icon: "checklist",
|
icon: "checklist",
|
||||||
name: "Todo",
|
name: "Todo",
|
||||||
|
|
@ -317,10 +480,12 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
|
|
||||||
const calculate = () => {
|
const calculate = () => {
|
||||||
if (entry.text) {
|
if (entry.text) {
|
||||||
self.add(<MathResult math={Math.get_default().evaluate(entry.text)} entry={entry} />);
|
self.add(
|
||||||
|
<MathResult math={MathService.get_default().evaluate(entry.text)} entry={entry} />
|
||||||
|
);
|
||||||
self.add(<box className="separator" />);
|
self.add(<box className="separator" />);
|
||||||
}
|
}
|
||||||
for (const item of Math.get_default().history)
|
for (const item of MathService.get_default().history)
|
||||||
self.add(<MathResult isHistory math={item} entry={entry} />);
|
self.add(<MathResult isHistory math={item} entry={entry} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -338,18 +503,58 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
})
|
})
|
||||||
.finally(updateEmpty);
|
.finally(updateEmpty);
|
||||||
|
|
||||||
|
const listWindows = () => {
|
||||||
|
const hyprland = AstalHyprland.get_default();
|
||||||
|
// Use message cause AstalHyprland is buggy (inconsistent prop updating)
|
||||||
|
hyprland.message_async("j/clients", (_, res) => {
|
||||||
|
try {
|
||||||
|
const unsortedClients: Client[] = JSON.parse(hyprland.message_finish(res));
|
||||||
|
if (entry.text) {
|
||||||
|
const clients = fuzzysort.go(entry.text, unsortedClients, {
|
||||||
|
all: true,
|
||||||
|
limit: config.maxResults,
|
||||||
|
keys: ["title", "class", "initialTitle", "initialClass"],
|
||||||
|
scoreFn: r =>
|
||||||
|
r[0].score * config.windows.title +
|
||||||
|
r[1].score * config.windows.class +
|
||||||
|
r[2].score * config.windows.initialTitle +
|
||||||
|
r[3].score * config.windows.initialClass,
|
||||||
|
});
|
||||||
|
self.foreach(ch => ch.destroy());
|
||||||
|
for (const { obj } of clients)
|
||||||
|
self.add(<WindowResult reload={listWindows} client={obj} />);
|
||||||
|
} else {
|
||||||
|
const clients = unsortedClients.sort((a, b) => a.focusHistoryID - b.focusHistoryID);
|
||||||
|
self.foreach(ch => ch.destroy());
|
||||||
|
for (const client of clients)
|
||||||
|
self.add(<WindowResult reload={listWindows} client={client} />);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
updateEmpty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update windows on open
|
||||||
|
self.hook(App, "window-toggled", (_, window) => {
|
||||||
|
if (window.name === "launcher" && window.visible && mode.get() === "windows") listWindows();
|
||||||
|
});
|
||||||
|
|
||||||
self.hook(entry, "activate", () => {
|
self.hook(entry, "activate", () => {
|
||||||
if (mode.get() === "math") {
|
if (mode.get() === "math") {
|
||||||
if (entry.text.startsWith("clear")) Math.get_default().clear();
|
if (entry.text.startsWith("clear")) MathService.get_default().clear();
|
||||||
else Math.get_default().commit();
|
else MathService.get_default().commit();
|
||||||
}
|
}
|
||||||
self.get_children()[0]?.activate();
|
self.get_children()[0]?.activate();
|
||||||
});
|
});
|
||||||
self.hook(entry, "changed", () => {
|
self.hook(entry, "changed", () => {
|
||||||
if (!entry.text && mode.get() === "apps") return;
|
if (!entry.text && mode.get() === "apps") return;
|
||||||
|
|
||||||
// Files has delay cause async so it does some stuff by itself
|
// Files and windows have delay cause async so they do some stuff by themselves
|
||||||
const ignoreFileAsync = entry.text.startsWith(">") || mode.get() !== "files";
|
const ignoreFileAsync =
|
||||||
|
entry.text.startsWith(">") || (mode.get() !== "files" && mode.get() !== "windows");
|
||||||
if (ignoreFileAsync) self.foreach(ch => ch.destroy());
|
if (ignoreFileAsync) self.foreach(ch => ch.destroy());
|
||||||
|
|
||||||
if (entry.text.startsWith(">")) {
|
if (entry.text.startsWith(">")) {
|
||||||
|
|
@ -365,6 +570,7 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
||||||
} else if (mode.get() === "apps") appSearch();
|
} else if (mode.get() === "apps") appSearch();
|
||||||
else if (mode.get() === "math") calculate();
|
else if (mode.get() === "math") calculate();
|
||||||
else if (mode.get() === "files") fileSearch();
|
else if (mode.get() === "files") fileSearch();
|
||||||
|
else if (mode.get() === "windows") listWindows();
|
||||||
|
|
||||||
if (ignoreFileAsync) updateEmpty();
|
if (ignoreFileAsync) updateEmpty();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
32
src/utils/types.ts
Normal file
32
src/utils/types.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type AstalHyprland from "gi://AstalHyprland";
|
||||||
|
|
||||||
|
export type Address = `0x${string}`;
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
address: Address;
|
||||||
|
mapped: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
at: [number, number];
|
||||||
|
size: [number, number];
|
||||||
|
workspace: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
floating: boolean;
|
||||||
|
pseudo: boolean;
|
||||||
|
monitor: number;
|
||||||
|
class: string;
|
||||||
|
title: string;
|
||||||
|
initialClass: string;
|
||||||
|
initialTitle: string;
|
||||||
|
pid: number;
|
||||||
|
xwayland: boolean;
|
||||||
|
pinned: boolean;
|
||||||
|
fullscreen: AstalHyprland.Fullscreen;
|
||||||
|
fullscreenClient: AstalHyprland.Fullscreen;
|
||||||
|
grouped: Address[];
|
||||||
|
tags: string[];
|
||||||
|
swallowing: string;
|
||||||
|
focusHistoryID: number;
|
||||||
|
inhibitingIdle: boolean;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue