summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-16 16:35:37 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-16 16:35:37 +1100
commit02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38 (patch)
tree5e2a56becf6ba6961995e541ce9688224f704773 /modules
parentpopupwindow: switch to class (diff)
downloadcaelestia-shell-02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38.tar.gz
caelestia-shell-02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38.tar.bz2
caelestia-shell-02fd2e97f2c8a53bf2344e6fa8b14769cb15ee38.zip
refactor: move ts to src
Also move popupwindow to own file
Diffstat (limited to 'modules')
-rw-r--r--modules/bar.tsx500
-rw-r--r--modules/launcher.tsx390
-rw-r--r--modules/notifpopups.tsx177
-rw-r--r--modules/osds.tsx326
4 files changed, 0 insertions, 1393 deletions
diff --git a/modules/bar.tsx b/modules/bar.tsx
deleted file mode 100644
index 5ed309e..0000000
--- a/modules/bar.tsx
+++ /dev/null
@@ -1,500 +0,0 @@
-import { execAsync, GLib, register, Variable } from "astal";
-import { bind, kebabify } from "astal/binding";
-import { App, Astal, astalify, Gdk, Gtk, type ConstructProps } from "astal/gtk3";
-import AstalBluetooth from "gi://AstalBluetooth";
-import AstalHyprland from "gi://AstalHyprland";
-import AstalNetwork from "gi://AstalNetwork";
-import AstalNotifd from "gi://AstalNotifd";
-import AstalTray from "gi://AstalTray";
-import AstalWp01 from "gi://AstalWp";
-import { bar as config } from "../config";
-import type { Monitor } from "../services/monitors";
-import Players from "../services/players";
-import Updates from "../services/updates";
-import { getAppCategoryIcon } from "../utils/icons";
-import { ellipsize } from "../utils/strings";
-import { osIcon } from "../utils/system";
-import { setupCustomTooltip } from "../utils/widgets";
-
-const hyprland = AstalHyprland.get_default();
-
-const hookFocusedClientProp = (
- self: any, // Ugh why is there no base Widget type
- prop: keyof AstalHyprland.Client,
- callback: (c: AstalHyprland.Client | null) => void
-) => {
- let id: number | null = null;
- let lastClient: AstalHyprland.Client | null = null;
- self.hook(hyprland, "notify::focused-client", () => {
- if (id) lastClient?.disconnect(id);
- lastClient = hyprland.focusedClient; // Can be null
- id = lastClient?.connect(`notify::${kebabify(prop)}`, () => callback(lastClient));
- callback(lastClient);
- });
- self.connect("destroy", () => id && lastClient?.disconnect(id));
- callback(lastClient);
-};
-
-const OSIcon = () => <label className="module os-icon" label={osIcon} />;
-
-const ActiveWindow = () => (
- <box
- hasTooltip
- className="module active-window"
- setup={self => {
- const title = Variable("");
- const updateTooltip = (c: AstalHyprland.Client | null) =>
- title.set(c?.class && c?.title ? `${c.class}: ${c.title}` : "");
- hookFocusedClientProp(self, "class", updateTooltip);
- hookFocusedClientProp(self, "title", updateTooltip);
- updateTooltip(hyprland.focusedClient);
-
- const window = setupCustomTooltip(self, bind(title));
- if (window) {
- self.hook(title, (_, v) => !v && window.hide());
- self.hook(window, "map", () => !title.get() && window.hide());
- }
- }}
- >
- <label
- className="icon"
- setup={self =>
- hookFocusedClientProp(self, "class", c => {
- self.label = c?.class ? getAppCategoryIcon(c.class) : "desktop_windows";
- })
- }
- />
- <label
- setup={self =>
- hookFocusedClientProp(self, "title", c => (self.label = c?.title ? ellipsize(c.title) : "Desktop"))
- }
- />
- </box>
-);
-
-const MediaPlaying = () => {
- const players = Players.get_default();
- const getLabel = (fallback = "") =>
- players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback;
- return (
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) {
- // TODO: media panel
- } else if (event.button === Astal.MouseButton.SECONDARY) players.lastPlayer?.play_pause();
- else if (event.button === Astal.MouseButton.MIDDLE) players.lastPlayer?.raise();
- }}
- setup={self => {
- const label = Variable(getLabel());
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => label.set(getLabel()));
- setupCustomTooltip(self, bind(label));
- }}
- >
- <box className="module media-playing">
- <icon
- setup={self =>
- players.hookLastPlayer(self, "notify::identity", () => {
- const icon = `caelestia-${players.lastPlayer?.identity.toLowerCase()}-symbolic`;
- self.icon = players.lastPlayer
- ? Astal.Icon.lookup_icon(icon)
- ? icon
- : "caelestia-media-generic-symbolic"
- : "caelestia-media-none-symbolic";
- })
- }
- />
- <label
- setup={self =>
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => {
- self.label = ellipsize(getLabel("No media")); // TODO: scroll text
- })
- }
- />
- </box>
- </button>
- );
-};
-
-const Workspace = ({ idx }: { idx: number }) => {
- let wsId = Math.floor((hyprland.focusedWorkspace.id - 1) / config.wsPerGroup) * config.wsPerGroup + idx;
- return (
- <button
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- onClicked={() => hyprland.dispatch("workspace", String(wsId))}
- setup={self => {
- const update = () =>
- self.toggleClassName(
- "occupied",
- hyprland.clients.some(c => c.workspace.id === wsId)
- );
-
- self.hook(hyprland, "notify::focused-workspace", () => {
- wsId = Math.floor((hyprland.focusedWorkspace.id - 1) / config.wsPerGroup) * config.wsPerGroup + idx;
- self.toggleClassName("focused", hyprland.focusedWorkspace.id === wsId);
- update();
- });
- self.hook(hyprland, "client-added", update);
- self.hook(hyprland, "client-moved", update);
- self.hook(hyprland, "client-removed", update);
-
- self.toggleClassName("focused", hyprland.focusedWorkspace.id === wsId);
- update();
- }}
- />
- );
-};
-
-const Workspaces = () => (
- <eventbox
- onScroll={(_, event) => {
- const activeWs = hyprland.focusedClient?.workspace.name;
- if (activeWs?.startsWith("special:")) hyprland.dispatch("togglespecialworkspace", activeWs.slice(8));
- else if (event.delta_y > 0 || hyprland.focusedWorkspace.id > 1)
- hyprland.dispatch("workspace", (event.delta_y < 0 ? "-" : "+") + 1);
- }}
- >
- <box className="module workspaces">
- {Array.from({ length: config.wsPerGroup }).map((_, idx) => (
- <Workspace idx={idx + 1} /> // Start from 1
- ))}
- </box>
- </eventbox>
-);
-
-@register()
-class TrayItemMenu extends astalify(Gtk.Menu) {
- readonly item: AstalTray.TrayItem;
-
- constructor(props: ConstructProps<TrayItemMenu, Gtk.Menu.ConstructorProps> & { item: AstalTray.TrayItem }) {
- const { item, ...sProps } = props;
- super(sProps as any);
-
- this.item = item;
-
- this.hook(item, "notify::menu-model", () => this.bind_model(item.menuModel, null, true));
- this.hook(item, "notify::action-group", () => this.insert_action_group("dbusmenu", item.actionGroup));
- this.bind_model(item.menuModel, null, true);
- this.insert_action_group("dbusmenu", item.actionGroup);
- }
-
- popup_at_widget_bottom(widget: Gtk.Widget) {
- this.item.about_to_show();
- this.popup_at_widget(widget, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
- }
-}
-
-const TrayItem = (item: AstalTray.TrayItem) => {
- const menu = (<TrayItemMenu item={item} />) as TrayItemMenu;
- return (
- <button
- onClick={(self, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) {
- if (item.isMenu) menu.popup_at_widget_bottom(self);
- else item.activate(0, 0);
- } else if (event.button === Astal.MouseButton.SECONDARY) menu.popup_at_widget_bottom(self);
- }}
- onScroll={(_, event) => {
- if (event.delta_x !== 0) item.scroll(event.delta_x, "horizontal");
- if (event.delta_y !== 0) item.scroll(event.delta_y, "vertical");
- }}
- onDestroy={() => menu.destroy()}
- setup={self => setupCustomTooltip(self, bind(item, "tooltipMarkup"))}
- >
- <icon halign={Gtk.Align.CENTER} gicon={bind(item, "gicon")} />
- </button>
- );
-};
-
-const Tray = () => <box className="module tray">{bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))}</box>;
-
-const Network = () => (
- <button
- onClick={(_, event) => {
- const network = AstalNetwork.get_default();
- if (event.button === Astal.MouseButton.PRIMARY) {
- // TODO: networks panel
- } else if (event.button === Astal.MouseButton.SECONDARY) network.wifi.enabled = !network.wifi.enabled;
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("uwsm app -- gnome-control-center wifi").catch(() => {
- network.wifi.scan();
- execAsync(
- "uwsm app -- foot -T nmtui fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
- ).catch(err => {
- // Idk why but foot always throws this error when it opens
- if (
- err.message !==
- "warn: wayland.c:1619: compositor does not implement the XDG toplevel icon protocol\nwarn: terminal.c:1973: slave exited with signal 1 (Hangup)"
- )
- console.error(err);
- });
- });
- }}
- setup={self => {
- const network = AstalNetwork.get_default();
- const tooltipText = Variable("");
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`${network.wifi.ssid} | Strength: ${network.wifi.strength}/100`);
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING)
- tooltipText.set(`Connecting to ${network.wifi.ssid}`);
- else tooltipText.set("Disconnected");
- } else if (network.primary === AstalNetwork.Primary.WIRED) {
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`Speed: ${network.wired.speed}`);
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) tooltipText.set("Connecting");
- else tooltipText.set("Disconnected");
- } else {
- tooltipText.set("Unknown");
- }
- };
- self.hook(network, "notify::primary", update);
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::ssid", update);
- self.hook(network.wifi, "notify::strength", update);
- if (network.wired) {
- self.hook(network.wired, "notify::internet", update);
- self.hook(network.wired, "notify::speed", update);
- }
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalNetwork.get_default(), "primary").as(p =>
- p === AstalNetwork.Primary.WIFI ? "wifi" : "wired"
- )}
- >
- <stack
- name="wifi"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- self.shown = String(Math.ceil(network.wifi.strength / 25));
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::strength", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="signal_wifi_0_bar" name="0" />
- <label className="icon" label="network_wifi_1_bar" name="1" />
- <label className="icon" label="network_wifi_2_bar" name="2" />
- <label className="icon" label="network_wifi_3_bar" name="3" />
- <label className="icon" label="signal_wifi_4_bar" name="4" />
- </stack>
- <stack
- name="wired"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.primary !== AstalNetwork.Primary.WIRED) return;
-
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED) self.shown = "connected";
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network, "notify::primary", update);
- if (network.wired) self.hook(network.wired, "notify::internet", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="lan" name="connected" />
- </stack>
- </stack>
- </button>
-);
-
-const Bluetooth = () => (
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) {
- // TODO: bluetooth panel
- } else if (event.button === Astal.MouseButton.SECONDARY) AstalBluetooth.get_default().toggle();
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("uwsm app -- blueman-manager").catch(console.error);
- }}
- setup={self => {
- const bluetooth = AstalBluetooth.get_default();
- const tooltipText = Variable("");
- const update = () => {
- const devices = bluetooth.get_devices().filter(d => d.connected);
- tooltipText.set(
- devices.length > 0
- ? `Connected devices: ${devices.map(d => d.alias).join(", ")}`
- : "No connected devices"
- );
- };
- const hookDevice = (device: AstalBluetooth.Device) => {
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::alias", update);
- };
- bluetooth.get_devices().forEach(hookDevice);
- self.hook(bluetooth, "device-added", (_, device) => {
- hookDevice(device);
- update();
- });
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalBluetooth.get_default(), "isPowered").as(p => (p ? "enabled" : "disabled"))}
- >
- <label className="icon" label="bluetooth" name="enabled" />
- <label className="icon" label="bluetooth_disabled" name="disabled" />
- </stack>
- </button>
-);
-
-const StatusIcons = () => (
- <box className="module status-icons">
- <Network />
- <Bluetooth />
- </box>
-);
-
-const PkgUpdates = () => (
- <box
- className="module updates"
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)
- )
- }
- >
- <label className="icon" label="download" />
- <label label={bind(Updates.get_default(), "numUpdates").as(String)} />
- </box>
-);
-
-const Notifications = () => {
- const unreadCount = Variable(0);
- return (
- <box
- className="module notifications"
- setup={self =>
- setupCustomTooltip(
- self,
- bind(unreadCount).as(n => `${n} unread notification${n === 1 ? "" : "s"}`)
- )
- }
- >
- <label className="icon" label="info" />
- <label
- label="0"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- let notifsOpen = false;
- let unread = new Set<number>();
-
- self.hook(notifd, "notified", (self, id, replaced) => {
- if (!notifsOpen && !replaced) {
- unread.add(id);
- unreadCount.set(unread.size);
- self.label = String(unread.size);
- }
- });
- self.hook(notifd, "resolved", (self, id) => {
- if (unread.delete(id)) {
- unreadCount.set(unread.size);
- self.label = String(unread.size);
- }
- });
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === "notifications") {
- notifsOpen = window.visible;
- if (notifsOpen) {
- unread.clear();
- unreadCount.set(0);
- }
- }
- });
- }}
- />
- </box>
- );
-};
-
-const DateTime = () => (
- <box className="module date-time">
- <label className="icon" label="calendar_month" />
- <label
- setup={self => {
- const pollVar = Variable(null).poll(5000, () => {
- self.label =
- GLib.DateTime.new_now_local().format(config.dateTimeFormat) ?? new Date().toLocaleString();
- return null;
- });
- self.connect("destroy", () => pollVar.drop());
- }}
- />
- </box>
-);
-
-const Power = () => (
- <button
- className="module power"
- label="power_settings_new"
- onClicked={() => execAsync("fish -c 'pkill wlogout || wlogout -p layer-shell'").catch(console.error)}
- />
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-bar"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.TOP}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- >
- <centerbox className="bar" css={"min-width: " + monitor.width * 0.8 + "px;"}>
- <box>
- <OSIcon />
- <ActiveWindow />
- <MediaPlaying />
- <button
- hexpand
- onScroll={(_, event) =>
- event.delta_y > 0 ? (monitor.brightness -= 0.1) : (monitor.brightness += 0.1)
- }
- />
- </box>
- <Workspaces />
- <box>
- <button
- hexpand
- onScroll={(_, event) => {
- const speaker = AstalWp01.get_default()?.audio.defaultSpeaker;
- if (!speaker) return;
- speaker.mute = false;
- if (event.delta_y > 0) speaker.volume -= 0.1;
- else speaker.volume += 0.1;
- }}
- />
- <Tray />
- <StatusIcons />
- <PkgUpdates />
- <Notifications />
- <DateTime />
- <Power />
- </box>
- </centerbox>
- </window>
-);
diff --git a/modules/launcher.tsx b/modules/launcher.tsx
deleted file mode 100644
index 966cdfd..0000000
--- a/modules/launcher.tsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import { bind, execAsync, Gio, GLib, register, timeout, Variable } from "astal";
-import { 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 { getAppCategoryIcon } from "../utils/icons";
-import { launch } from "../utils/system";
-import { PopupWindow, setupCustomTooltip } from "../utils/widgets";
-
-type Mode = "apps" | "files" | "math";
-
-interface Subcommand {
- icon: string;
- name: string;
- description: string;
- command: (...args: string[]) => void;
-}
-
-const getIconFromMode = (mode: Mode) => {
- switch (mode) {
- case "apps":
- return "apps";
- case "files":
- return "folder";
- case "math":
- return "calculate";
- }
-};
-
-const getEmptyTextFromMode = (mode: Mode) => {
- switch (mode) {
- case "apps":
- return "No apps found";
- case "files":
- return GLib.find_program_in_path("fd") === null ? "File search requires `fd`" : "No files found";
- case "math":
- return "Type an expression";
- }
-};
-
-const close = (self: JSX.Element) => {
- const toplevel = self.get_toplevel();
- if (toplevel instanceof Widget.Window) toplevel.hide();
-};
-
-const launchAndClose = (self: JSX.Element, astalApp: AstalApps.Application) => {
- close(self);
- launch(astalApp);
-};
-
-const openFileAndClose = (self: JSX.Element, path: string) => {
- close(self);
- execAsync([
- "bash",
- "-c",
- `dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:"file://${path}" string:"" || xdg-open "${path}"`,
- ]).catch(console.error);
-};
-
-const PinnedApp = (names: string[]) => {
- let app: Gio.DesktopAppInfo | null = null;
- let astalApp: AstalApps.Application | undefined;
- for (const name of names) {
- app = Gio.DesktopAppInfo.new(`${name}.desktop`);
- if (app) {
- astalApp = Apps.get_list().find(a => a.entry === `${name}.desktop`);
- if (app.get_icon() && astalApp) break;
- else app = null; // Set app to null if no icon or matching AstalApps#Application
- }
- }
-
- if (!app) console.error(`Launcher - Unable to find app for "${names.join(", ")}"`);
-
- return app ? (
- <button
- className="pinned-app result"
- cursor="pointer"
- onClicked={self => launchAndClose(self, astalApp!)}
- setup={self => setupCustomTooltip(self, app.get_display_name())}
- >
- <icon gicon={app.get_icon()!} />
- </button>
- ) : null;
-};
-
-const PinnedApps = () => <box homogeneous>{config.pins.map(PinnedApp)}</box>;
-
-const SearchEntry = ({ entry }: { entry: Widget.Entry }) => (
- <stack
- hexpand
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={150}
- setup={self =>
- self.hook(entry, "notify::text-length", () =>
- // Timeout to avoid flickering when replacing entire text (cause it'll set len to 0 then back to > 0)
- timeout(1, () => (self.shown = entry.textLength > 0 ? "entry" : "placeholder"))
- )
- }
- >
- <label name="placeholder" className="placeholder" xalign={0} label='Type ">" for subcommands' />
- {entry}
- </stack>
-);
-
-const Result = ({
- icon,
- materialIcon,
- label,
- sublabel,
- onClicked,
-}: {
- icon?: string;
- materialIcon?: string;
- label: string;
- sublabel?: string;
- onClicked: (self: Widget.Button) => void;
-}) => (
- <button className="result" cursor="pointer" onClicked={onClicked}>
- <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} />
- )}
- {sublabel ? (
- <box vertical valign={Gtk.Align.CENTER} className="has-sublabel">
- <label hexpand truncate maxWidthChars={1} xalign={0} label={label} />
- <label hexpand truncate maxWidthChars={1} className="sublabel" xalign={0} label={sublabel} />
- </box>
- ) : (
- <label xalign={0} label={label} />
- )}
- </box>
- </button>
-);
-
-const SubcommandResult = ({
- entry,
- subcommand,
- args,
-}: {
- entry: Widget.Entry;
- subcommand: Subcommand;
- args: string[];
-}) => (
- <Result
- materialIcon={subcommand.icon}
- label={subcommand.name}
- sublabel={subcommand.description}
- onClicked={() => {
- subcommand.command(...args);
- entry.set_text("");
- }}
- />
-);
-
-const AppResult = ({ app }: { app: AstalApps.Application }) => (
- <Result
- icon={app.iconName}
- materialIcon={getAppCategoryIcon(app)}
- label={app.name}
- sublabel={app.description}
- onClicked={self => launchAndClose(self, app)}
- />
-);
-
-const MathResult = ({ math, isHistory, entry }: { math: HistoryItem; isHistory?: boolean; entry: Widget.Entry }) => (
- <Result
- materialIcon={math.icon}
- label={math.equation}
- sublabel={math.result}
- onClicked={() => {
- if (isHistory) {
- Math.get_default().select(math);
- entry.set_text(math.equation);
- entry.grab_focus();
- entry.set_position(-1);
- } else {
- execAsync(`wl-copy -- ${math.result}`).catch(console.error);
- entry.set_text("");
- }
- }}
- />
-);
-
-const FileResult = ({ path }: { path: string }) => (
- <Result
- label={path.split("/").pop()!}
- sublabel={path.startsWith(HOME) ? "~" + path.slice(HOME.length) : path}
- onClicked={self => openFileAndClose(self, path)}
- />
-);
-
-const Results = ({ entry, mode }: { entry: Widget.Entry; mode: Variable<Mode> }) => {
- const empty = Variable(true);
-
- 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>
- <box
- vertical
- name="list"
- setup={self => {
- const subcommands: Record<string, Subcommand> = {
- apps: {
- icon: "apps",
- name: "Apps",
- description: "Search for apps",
- command: () => mode.set("apps"),
- },
- files: {
- icon: "folder",
- name: "Files",
- description: "Search for files",
- command: () => mode.set("files"),
- },
- math: {
- icon: "calculate",
- name: "Math",
- description: "Do math calculations",
- command: () => mode.set("math"),
- },
- todo: {
- icon: "checklist",
- name: "Todo",
- description: "Create a todo in <INSERT_TODO_APP>",
- command: (...args) => {
- // TODO: todo service or maybe use external app
- },
- },
- };
- const subcommandList = Object.keys(subcommands);
-
- const updateEmpty = () => empty.set(self.get_children().length === 0);
-
- const appSearch = () => {
- const apps = Apps.fuzzy_query(entry.text);
- if (apps.length > config.maxResults) apps.length = config.maxResults;
- for (const app of apps) self.add(<AppResult app={app} />);
- };
-
- const calculate = () => {
- if (entry.text) {
- self.add(<MathResult math={Math.get_default().evaluate(entry.text)} entry={entry} />);
- self.add(<box className="separator" />);
- }
- for (const item of Math.get_default().history)
- self.add(<MathResult isHistory math={item} entry={entry} />);
- };
-
- const fileSearch = () =>
- execAsync(["fd", ...config.fdOpts, entry.text, HOME])
- .then(out => {
- const paths = out.split("\n").filter(path => path);
- if (paths.length > config.maxResults) paths.length = config.maxResults;
- self.foreach(ch => ch.destroy());
- for (const path of paths) self.add(<FileResult path={path} />);
- })
- .catch(e => {
- // Ignore execAsync error
- if (!(e instanceof Gio.IOErrorEnum || e instanceof GLib.SpawnError)) console.error(e);
- })
- .finally(updateEmpty);
-
- self.hook(entry, "activate", () => {
- if (mode.get() === "math") {
- if (entry.text.startsWith("clear")) Math.get_default().clear();
- else Math.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";
- if (ignoreFileAsync) self.foreach(ch => ch.destroy());
-
- if (entry.text.startsWith(">")) {
- const args = entry.text.split(" ");
- for (const { target } of fuzzysort.go(args[0].slice(1), subcommandList, { all: true }))
- self.add(
- <SubcommandResult
- entry={entry}
- subcommand={subcommands[target]}
- args={args.slice(1)}
- />
- );
- } else if (mode.get() === "apps") appSearch();
- else if (mode.get() === "math") calculate();
- else if (mode.get() === "files") fileSearch();
-
- if (ignoreFileAsync) updateEmpty();
- });
- }}
- />
- </stack>
- );
-};
-
-const LauncherContent = ({
- mode,
- showResults,
- entry,
-}: {
- mode: Variable<Mode>;
- showResults: Variable<boolean>;
- entry: Widget.Entry;
-}) => (
- <box vertical className={bind(mode).as(m => `launcher ${m}`)}>
- <box className="search-bar">
- <label className="icon" label="search" />
- <SearchEntry entry={entry} />
- <label className="icon" label={bind(mode).as(getIconFromMode)} />
- </box>
- <revealer
- revealChild={bind(showResults).as(s => !s)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- >
- <PinnedApps />
- </revealer>
- <revealer
- revealChild={bind(showResults)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
- transitionDuration={150}
- >
- <Results entry={entry} mode={mode} />
- </revealer>
- </box>
-);
-
-@register()
-export default class Launcher extends PopupWindow {
- readonly mode: Variable<Mode>;
-
- constructor() {
- const entry = (<entry name="entry" />) as Widget.Entry;
- const mode = Variable<Mode>("apps");
- const showResults = Variable.derive([bind(entry, "textLength"), mode], (t, m) => t > 0 || m !== "apps");
-
- super({
- name: "launcher",
- anchor: Astal.WindowAnchor.TOP,
- keymode: Astal.Keymode.EXCLUSIVE,
- 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: <LauncherContent mode={mode} showResults={showResults} entry={entry} />,
- });
-
- this.mode = mode;
-
- this.connect("show", () => (this.marginTop = AstalHyprland.get_default().focusedMonitor.height / 4));
-
- // Clear search on hide if not in math mode
- this.connect("hide", () => mode.get() !== "math" && entry.set_text(""));
-
- this.connect("destroy", () => showResults.drop());
- }
-
- open(mode: Mode) {
- this.mode.set(mode);
- this.show();
- }
-}
diff --git a/modules/notifpopups.tsx b/modules/notifpopups.tsx
deleted file mode 100644
index c3441a9..0000000
--- a/modules/notifpopups.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import { GLib, register, timeout } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import AstalNotifd from "gi://AstalNotifd";
-import { notifpopups as config } from "../config";
-import { desktopEntrySubs } from "../utils/icons";
-import { setupChildClickthrough } from "../utils/widgets";
-
-const urgencyToString = (urgency: AstalNotifd.Urgency) => {
- switch (urgency) {
- case AstalNotifd.Urgency.LOW:
- return "low";
- case AstalNotifd.Urgency.NORMAL:
- return "normal";
- case AstalNotifd.Urgency.CRITICAL:
- return "critical";
- }
-};
-
-const getTime = (time: number) => {
- const messageTime = GLib.DateTime.new_from_unix_local(time);
- const todayDay = GLib.DateTime.new_now_local().get_day_of_year();
- if (messageTime.get_day_of_year() === todayDay) {
- const aMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60);
- return aMinuteAgo !== null && messageTime.compare(aMinuteAgo) > 0 ? "Now" : messageTime.format("%H:%M");
- } else if (messageTime.get_day_of_year() === todayDay - 1) return "Yesterday";
- return messageTime.format("%d/%m");
-};
-
-const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: string }) => {
- // Try app icon
- let icon = Astal.Icon.lookup_icon(appIcon) && appIcon;
- // Try desktop entry
- if (!icon) {
- if (desktopEntrySubs.hasOwnProperty(desktopEntry)) icon = desktopEntrySubs[desktopEntry];
- else if (Astal.Icon.lookup_icon(desktopEntry)) icon = desktopEntry;
- }
- return icon ? <icon className="app-icon" icon={icon} /> : null;
-};
-
-const Image = ({ icon }: { icon: string }) => {
- if (GLib.file_test(icon, GLib.FileTest.EXISTS))
- return (
- <box
- valign={Gtk.Align.START}
- className="image"
- css={`
- background-image: url("${icon}");
- `}
- />
- );
- if (Astal.Icon.lookup_icon(icon)) return <icon valign={Gtk.Align.START} className="image" icon={icon} />;
- return null;
-};
-
-@register()
-class NotifPopup extends Widget.Box {
- readonly #revealer;
- #destroyed = false;
-
- constructor({ notification }: { notification: AstalNotifd.Notification }) {
- super();
-
- this.#revealer = (
- <revealer revealChild transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} transitionDuration={150}>
- <box className="wrapper">
- <box vertical className={`popup ${urgencyToString(notification.urgency)}`}>
- <box className="header">
- <AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} />
- <label className="app-name" label={notification.appName ?? "Unknown"} />
- <box hexpand />
- <label
- className="time"
- label={getTime(notification.time)!}
- setup={self =>
- timeout(60000, () => !this.#destroyed && (self.label = getTime(notification.time)!))
- }
- />
- </box>
- <box hexpand className="separator" />
- <box className="content">
- {notification.image && <Image icon={notification.image} />}
- <box vertical>
- <label className="summary" xalign={0} label={notification.summary} truncate />
- <label className="body" xalign={0} label={notification.body} wrap useMarkup />
- </box>
- </box>
- <box className="actions">
- <button hexpand cursor="pointer" onClicked={() => notification.dismiss()} label="Close" />
- {notification.actions.map(a => (
- <button hexpand cursor="pointer" onClicked={() => notification.invoke(a.id)}>
- {notification.actionIcons ? <icon icon={a.label} /> : a.label}
- </button>
- ))}
- </box>
- </box>
- </box>
- </revealer>
- ) as Widget.Revealer;
- this.add(this.#revealer);
-
- // Init animation
- const width = this.get_preferred_width()[1];
- this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
- timeout(1, () => {
- this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
- });
-
- // Close popup after timeout if transient or expire enabled in config
- if (config.expire || notification.transient)
- timeout(
- notification.expireTimeout > 0
- ? notification.expireTimeout
- : notification.urgency === AstalNotifd.Urgency.CRITICAL
- ? 10000
- : 5000,
- () => this.destroyWithAnims()
- );
- }
-
- destroyWithAnims() {
- if (this.#destroyed) return;
- this.#destroyed = true;
-
- const animTime = 120;
- const animMargin = this.get_allocated_width();
- this.css = `transition: ${animTime}ms cubic-bezier(0.85, 0, 0.15, 1);
- margin-left: ${animMargin}px; margin-right: -${animMargin}px;`;
- timeout(animTime, () => {
- this.#revealer.revealChild = false;
- timeout(this.#revealer.transitionDuration, () => this.destroy());
- });
- }
-}
-
-export default () => (
- <window
- namespace="caelestia-notifpopups"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- >
- <box
- vertical
- valign={Gtk.Align.START}
- className="notifpopups"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- const map = new Map<number, NotifPopup>();
- self.hook(notifd, "notified", (self, id) => {
- const notification = notifd.get_notification(id);
-
- const popup = (<NotifPopup notification={notification} />) as NotifPopup;
- popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
- map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, popup);
-
- self.add(
- <eventbox
- // Dismiss on middle click
- onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
- // Close on hover lost
- onHoverLost={() => popup.destroyWithAnims()}
- >
- {popup}
- </eventbox>
- );
-
- // Limit number of popups
- if (config.maxPopups > 0 && self.children.length > config.maxPopups)
- map.values().next().value?.destroyWithAnims();
- });
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
-
- // Change input region to child region so can click through empty space
- setupChildClickthrough(self);
- }}
- />
- </window>
-);
diff --git a/modules/osds.tsx b/modules/osds.tsx
deleted file mode 100644
index b6e1333..0000000
--- a/modules/osds.tsx
+++ /dev/null
@@ -1,326 +0,0 @@
-import { execAsync, register, timeout, Variable, type Time } from "astal";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import cairo from "cairo";
-import AstalWp from "gi://AstalWp";
-import Cairo from "gi://cairo";
-import Pango from "gi://Pango";
-import PangoCairo from "gi://PangoCairo";
-import { osds as config } from "../config";
-import Monitors, { type Monitor } from "../services/monitors";
-import { PopupWindow } from "../utils/widgets";
-
-const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL);
-const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number;
-
-const mix = (a: number, b: number, r: number) => a * r + b * (1 - r);
-
-const pangoWeightToStr = (weight: Pango.Weight) => {
- switch (weight) {
- case Pango.Weight.ULTRALIGHT:
- return "UltraLight";
- case Pango.Weight.LIGHT:
- return "Light";
- case Pango.Weight.BOLD:
- return "Bold";
- case Pango.Weight.ULTRABOLD:
- return "UltraBold";
- case Pango.Weight.HEAVY:
- return "Heavy";
- default:
- return "Normal";
- }
-};
-
-const SliderOsd = ({
- fillIcons,
- monitor,
- type,
- windowSetup,
- className = "",
- initValue,
- drawAreaSetup,
-}: {
- fillIcons?: boolean;
- monitor?: Monitor;
- type: "volume" | "brightness";
- windowSetup: (self: Widget.Window, show: () => void) => void;
- className?: string;
- initValue: number;
- drawAreaSetup: (self: Widget.DrawingArea, icon: Variable<string>) => void;
-}) => (
- <PopupWindow
- name={type}
- monitor={monitor?.id}
- keymode={Astal.Keymode.NONE}
- anchor={config[type].position}
- margin={config[type].margin}
- setup={self => {
- let time: Time | null = null;
- const hideAfterTimeout = () => {
- time?.cancel();
- time = timeout(config[type].hideDelay, () => self.hide());
- };
- self.connect("show", hideAfterTimeout);
- windowSetup(self, () => {
- self.show();
- hideAfterTimeout();
- });
- }}
- >
- <box className={type}>
- <drawingarea
- className={`inner ${className}`}
- css={"font-size: " + initValue + "px;"}
- setup={self => {
- const halfPi = Math.PI / 2;
- const vertical =
- config[type].position === Astal.WindowAnchor.LEFT ||
- config[type].position === Astal.WindowAnchor.RIGHT;
-
- const icon = Variable("");
- drawAreaSetup(self, icon);
- self.hook(icon, () => self.queue_draw());
-
- // Init size
- const styleContext = self.get_style_context();
- const width = getNumStyle(styleContext, "min-width");
- const height = getNumStyle(styleContext, "min-height");
- if (vertical) self.set_size_request(height, width);
- else self.set_size_request(width, height);
-
- let fontDesc: Pango.FontDescription | null = null;
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const parent = self.get_parent();
- if (!parent) return;
-
- const styleContext = self.get_style_context();
- const pContext = parent.get_style_context();
-
- let width = getNumStyle(styleContext, "min-width");
- let height = getNumStyle(styleContext, "min-height");
-
- const progressValue = getNumStyle(styleContext, "font-size");
- let radius = getNumStyle(pContext, "border-radius");
- // Flatten when near 0, do before swap cause its simpler
- radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
-
- if (vertical) [width, height] = [height, width]; // Swap if vertical
- self.set_size_request(width, height);
-
- const progressPosition = vertical
- ? height * (1 - progressValue) + radius // Top is 0, but we want it to start from the bottom
- : width * progressValue - radius;
-
- const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
-
- // Background
- if (vertical) {
- cr.arc(radius, progressPosition, radius, -Math.PI, -halfPi); // Top left
- cr.arc(width - radius, progressPosition, radius, -halfPi, 0); // Top right
- cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
- } else {
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
- cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
- }
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
-
- const fg = pContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setAntialias(cairo.Antialias.BEST);
-
- // Progress number, at top/right
- let nw = 0;
- let nh = 0;
- if (config[type].showValue) {
- const numLayout = parent.create_pango_layout(String(Math.round(progressValue * 100)));
- [nw, nh] = numLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = ((1 - progressValue) * height) / nh;
- cr.moveTo((width - nw) / 2, radius / 2);
- } else {
- diff = ((1 - progressValue) * width) / nw;
- cr.moveTo(width - nw - radius, (height - nh) / 2);
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(bg.red, fg.red, diff),
- mix(bg.green, fg.green, diff),
- mix(bg.blue, fg.blue, diff),
- mix(bg.alpha, fg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, numLayout);
- }
-
- // Progress icon, follows progress
- if (fontDesc === null) {
- const weight = pangoWeightToStr(getStyle(pContext, "font-weight") as Pango.Weight);
- const size = getNumStyle(pContext, "font-size") * 1.5;
- fontDesc = Pango.font_description_from_string(
- `Material Symbols Rounded ${weight} ${size}px`
- );
- // Ugh GTK CSS doesn't support font-variations, so you need to manually create the layout and font desc instead of using Gtk.Widget#create_pango_layout
- if (fillIcons) fontDesc.set_variations("FILL=1");
- }
-
- const iconLayout = PangoCairo.create_layout(cr);
- iconLayout.set_font_description(fontDesc);
- iconLayout.set_text(icon.get(), -1);
-
- const [iw, ih] = iconLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = (progressValue * height) / ih;
- cr.moveTo(
- (width - iw) / 2,
- Math.max(nh, Math.min(height - ih, progressPosition - ih / 2 + radius))
- );
- } else {
- diff = (progressValue * width) / iw;
- cr.moveTo(
- Math.min(
- width - nw * 1.1 - iw - radius,
- Math.max(0, progressPosition - iw / 2 - radius)
- ),
- (height - ih) / 2
- );
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(fg.red, bg.red, diff),
- mix(fg.green, bg.green, diff),
- mix(fg.blue, bg.blue, diff),
- mix(fg.alpha, bg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, iconLayout);
- });
- }}
- />
- </box>
- </PopupWindow>
-);
-
-const Volume = ({ audio }: { audio: AstalWp.Audio }) => (
- <SliderOsd
- fillIcons
- type="volume"
- windowSetup={(self, show) => {
- self.hook(audio.defaultSpeaker, "notify::volume", show);
- self.hook(audio.defaultSpeaker, "notify::mute", show);
- }}
- className={audio.defaultSpeaker.mute ? "mute" : ""}
- initValue={audio.defaultSpeaker.volume}
- drawAreaSetup={(self, icon) => {
- const updateIcon = () => {
- if (/head(phone|set)/i.test(audio.defaultSpeaker.icon)) icon.set("headphones");
- else if (audio.defaultSpeaker.mute) icon.set("no_sound");
- else if (audio.defaultSpeaker.volume === 0) icon.set("volume_mute");
- else if (audio.defaultSpeaker.volume <= 0.5) icon.set("volume_down");
- else icon.set("volume_up");
- };
- updateIcon();
- self.hook(audio.defaultSpeaker, "notify::icon", updateIcon);
- self.hook(audio.defaultSpeaker, "notify::mute", () => {
- updateIcon();
- self.toggleClassName("mute", audio.defaultSpeaker.mute);
- });
- self.hook(audio.defaultSpeaker, "notify::volume", () => {
- updateIcon();
- self.css = `font-size: ${audio.defaultSpeaker.volume}px`;
- });
- }}
- />
-);
-
-const Brightness = ({ monitor }: { monitor: Monitor }) => (
- <SliderOsd
- monitor={monitor}
- type="brightness"
- windowSetup={(self, show) => self.hook(monitor, "notify::brightness", show)}
- initValue={monitor.brightness}
- drawAreaSetup={(self, icon) => {
- const update = () => {
- if (monitor.brightness > 0.66) icon.set("brightness_high");
- else if (monitor.brightness > 0.33) icon.set("brightness_medium");
- else if (monitor.brightness > 0) icon.set("brightness_low");
- else icon.set("brightness_empty");
- self.css = `font-size: ${monitor.brightness}px`;
- };
- self.hook(monitor, "notify::brightness", update);
- update();
- }}
- />
-);
-
-@register()
-class LockOsd extends Widget.Window {
- readonly lockType: "caps" | "num";
-
- #timeout: Time | null = null;
-
- constructor({ type, icon, right }: { type: "caps" | "num"; icon: string; right?: boolean }) {
- super({
- visible: false,
- name: `lock-${type}`,
- application: App,
- namespace: `caelestia-lock-${type}`,
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- exclusivity: Astal.Exclusivity.IGNORE,
- });
-
- this.lockType = type;
- this.#update();
-
- this.add(
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}>
- <label vexpand className="icon" label={icon} />
- <label vexpand className="text" label={type.slice(0, 1).toUpperCase() + type.slice(1) + "lock"} />
- </box>
- );
-
- // Clickthrough
- this.connect("size-allocate", () => this.input_shape_combine_region(new Cairo.Region()));
-
- // Move over when other indicator opens/closes
- this.hook(App, "window-toggled", (_, window) => {
- if (window !== this && window instanceof LockOsd) {
- const child = this.get_child();
- if (!child) return;
- this[right ? "marginLeft" : "marginRight"] = window.visible
- ? child.get_preferred_width()[1] + config.lock.spacing
- : 0;
- }
- });
- }
-
- #update() {
- execAsync(`fish -c 'cat /sys/class/leds/input*::${this.lockType}lock/brightness'`)
- .then(out => (this.get_child() as Widget.Box | null)?.toggleClassName("enabled", out.includes("1")))
- .catch(console.error);
- }
-
- show() {
- super.show();
- this.#update();
- this.#timeout?.cancel();
- this.#timeout = timeout(config.lock[this.lockType].hideDelay, () => this.hide());
- }
-}
-
-export default () => {
- if (AstalWp.get_default()) <Volume audio={AstalWp.get_default()!.audio} />;
- Monitors.get_default().forEach(monitor => <Brightness monitor={monitor} />);
-
- <LockOsd type="caps" icon="keyboard_capslock" />;
- <LockOsd right type="num" icon="filter_1" />;
-
- return null;
-};