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"],
|
||||
["spotify-adblock", "spotify", "audacious", "elisa"],
|
||||
],
|
||||
windows: {
|
||||
// Weights for fuzzysort
|
||||
title: 1,
|
||||
class: 1,
|
||||
initialTitle: 0.5,
|
||||
initialClass: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
export const notifpopups = {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
@include launcher(apps, scheme.$sapphire);
|
||||
@include launcher(files, scheme.$peach);
|
||||
@include launcher(math, scheme.$green);
|
||||
@include launcher(windows, scheme.$teal);
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: lib.s(5);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
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 type AstalApps from "gi://AstalApps";
|
||||
import AstalHyprland from "gi://AstalHyprland";
|
||||
import { launcher as config } from "../../config";
|
||||
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 { launch } from "../utils/system";
|
||||
import type { Client } from "../utils/types";
|
||||
import { MenuItem, setupCustomTooltip } from "../utils/widgets";
|
||||
import PopupWindow from "../widgets/popupwindow";
|
||||
|
||||
type Mode = "apps" | "files" | "math";
|
||||
type Mode = "apps" | "files" | "math" | "windows";
|
||||
|
||||
interface Subcommand {
|
||||
icon: string;
|
||||
|
|
@ -28,6 +29,8 @@ const getIconFromMode = (mode: Mode) => {
|
|||
return "folder";
|
||||
case "math":
|
||||
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";
|
||||
case "math":
|
||||
return "Type an expression";
|
||||
case "windows":
|
||||
return "No windows found";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -133,14 +138,16 @@ const Result = ({
|
|||
materialIcon,
|
||||
label,
|
||||
sublabel,
|
||||
tooltip,
|
||||
onClicked,
|
||||
onSecondaryClick,
|
||||
onDestroy,
|
||||
}: {
|
||||
icon?: string;
|
||||
icon?: string | Gio.Icon | null;
|
||||
materialIcon?: string;
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
tooltip?: string;
|
||||
onClicked: (self: Widget.Button) => void;
|
||||
onSecondaryClick?: (self: Widget.Button) => void;
|
||||
onDestroy?: () => void;
|
||||
|
|
@ -148,16 +155,19 @@ const Result = ({
|
|||
<button
|
||||
className="result"
|
||||
cursor="pointer"
|
||||
tooltipText={tooltip}
|
||||
onClicked={onClicked}
|
||||
onClick={(self, event) => event.button === Astal.MouseButton.SECONDARY && onSecondaryClick?.(self)}
|
||||
onDestroy={onDestroy}
|
||||
>
|
||||
<box>
|
||||
{icon && Astal.Icon.lookup_icon(icon) ? (
|
||||
<icon valign={Gtk.Align.START} className="icon" icon={icon} />
|
||||
) : (
|
||||
<label valign={Gtk.Align.START} className="icon" label={materialIcon} />
|
||||
)}
|
||||
{icon &&
|
||||
(typeof icon === "string" ? (
|
||||
Astal.Icon.lookup_icon(icon) && <icon valign={Gtk.Align.START} className="icon" icon={icon} />
|
||||
) : (
|
||||
<icon valign={Gtk.Align.START} className="icon" gicon={icon} />
|
||||
))}
|
||||
{!icon && materialIcon && <label valign={Gtk.Align.START} className="icon" label={materialIcon} />}
|
||||
{sublabel ? (
|
||||
<box vertical valign={Gtk.Align.CENTER} className="has-sublabel">
|
||||
<label hexpand truncate maxWidthChars={1} xalign={0} label={label} />
|
||||
|
|
@ -229,7 +239,7 @@ const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?:
|
|||
sublabel={math.result}
|
||||
onClicked={() => {
|
||||
if (isHistory) {
|
||||
Math.get_default().select(math);
|
||||
MathService.get_default().select(math);
|
||||
entry.set_text(math.equation);
|
||||
entry.grab_focus();
|
||||
entry.set_position(-1);
|
||||
|
|
@ -243,12 +253,159 @@ const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?:
|
|||
|
||||
const FileResult = ({ path }: { path: string }) => (
|
||||
<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()!}
|
||||
sublabel={path.startsWith(HOME) ? "~" + path.slice(HOME.length) : 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 empty = Variable(true);
|
||||
|
||||
|
|
@ -290,6 +447,12 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
|||
description: "Do math calculations",
|
||||
command: () => mode.set("math"),
|
||||
},
|
||||
windows: {
|
||||
icon: "select_window_2",
|
||||
name: "Windows",
|
||||
description: "Manage open windows",
|
||||
command: () => mode.set("windows"),
|
||||
},
|
||||
todo: {
|
||||
icon: "checklist",
|
||||
name: "Todo",
|
||||
|
|
@ -317,10 +480,12 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
|||
|
||||
const calculate = () => {
|
||||
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" />);
|
||||
}
|
||||
for (const item of Math.get_default().history)
|
||||
for (const item of MathService.get_default().history)
|
||||
self.add(<MathResult isHistory math={item} entry={entry} />);
|
||||
};
|
||||
|
||||
|
|
@ -338,18 +503,58 @@ const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> })
|
|||
})
|
||||
.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", () => {
|
||||
if (mode.get() === "math") {
|
||||
if (entry.text.startsWith("clear")) Math.get_default().clear();
|
||||
else Math.get_default().commit();
|
||||
if (entry.text.startsWith("clear")) MathService.get_default().clear();
|
||||
else MathService.get_default().commit();
|
||||
}
|
||||
self.get_children()[0]?.activate();
|
||||
});
|
||||
self.hook(entry, "changed", () => {
|
||||
if (!entry.text && mode.get() === "apps") return;
|
||||
|
||||
// Files has delay cause async so it does some stuff by itself
|
||||
const ignoreFileAsync = entry.text.startsWith(">") || mode.get() !== "files";
|
||||
// Files and windows have delay cause async so they do some stuff by themselves
|
||||
const ignoreFileAsync =
|
||||
entry.text.startsWith(">") || (mode.get() !== "files" && mode.get() !== "windows");
|
||||
if (ignoreFileAsync) self.foreach(ch => ch.destroy());
|
||||
|
||||
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() === "math") calculate();
|
||||
else if (mode.get() === "files") fileSearch();
|
||||
else if (mode.get() === "windows") listWindows();
|
||||
|
||||
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