567 lines
18 KiB
TypeScript
567 lines
18 KiB
TypeScript
const hyprland = await Service.import("hyprland");
|
|
const battery = await Service.import("battery");
|
|
const systemtray = await Service.import("systemtray");
|
|
const audio = await Service.import("audio");
|
|
const network = await Service.import("network");
|
|
const { GLib, Gio } = imports.gi;
|
|
const decoder = new TextDecoder();
|
|
import { OpenSettings } from "apps/settings/main.ts";
|
|
import { enableClickThrough } from "./misc/clickthrough.js";
|
|
import { RoundedCorner } from "./misc/cairo_roundedcorner.js";
|
|
import { Client, Workspace } from "types/service/hyprland.js";
|
|
import Button from "types/widgets/button.js";
|
|
import Icon from "types/widgets/icon.js";
|
|
import { FileEnumerator, FileInfo } from "types/@girs/gio-2.0/gio-2.0.cjs";
|
|
const mpris = await Service.import("mpris");
|
|
const bluetooth = await Service.import("bluetooth");
|
|
import Gtk from "gi://Gtk?version=3.0";
|
|
import { MaterialIcon } from "icons.js";
|
|
import config from "services/configuration.ts";
|
|
import { toggleAppsWindow, toggleMediaWindow } from "./sideleft/main.js";
|
|
import { Variable as VariableType } from "types/variable";
|
|
|
|
const keyboard_layout = Variable("none");
|
|
hyprland.connect("keyboard-layout", (hyprland, keyboardname, layoutname) => {
|
|
keyboard_layout.setValue(layoutname.trim().toLowerCase().substr(0, 2));
|
|
});
|
|
|
|
const MATERIAL_SYMBOL_SIGNAL_STRENGTH = {
|
|
"network-wireless-signal-excellent-symbolic": "signal_wifi_4_bar",
|
|
"network-wireless-signal-good-symbolic": "network_wifi_3_bar",
|
|
"network-wireless-signal-ok-symbolic": "network_wifi_2_bar",
|
|
"network-wireless-signal-weak-symbolic": "network_wifi_1_bar",
|
|
"network-wireless-signal-none-symbolic": "signal_wifi_0_bar"
|
|
};
|
|
|
|
const time = Variable("");
|
|
const date = Variable("");
|
|
|
|
function getCurrentDateAndTime() {
|
|
const _date = new Date();
|
|
|
|
const year = _date.getFullYear();
|
|
const month = String(_date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(_date.getDate()).padStart(2, "0");
|
|
|
|
const hours = String(_date.getHours()).padStart(2, "0");
|
|
const minutes = String(_date.getMinutes()).padStart(2, "0");
|
|
|
|
const formattedDate = `${year}-${month}-${day}`;
|
|
const formattedTime = `${hours}:${minutes}`;
|
|
|
|
if (time.value != formattedTime) time.setValue(formattedTime);
|
|
if (date.value != formattedDate) date.setValue(formattedDate);
|
|
}
|
|
Utils.interval(1000, getCurrentDateAndTime);
|
|
|
|
function getIconNameFromClass(windowClass: string) {
|
|
let formattedClass = windowClass.replace(/\s+/g, "-").toLowerCase();
|
|
let homeDir = GLib.get_home_dir();
|
|
let systemDataDirs = GLib.get_system_data_dirs().map((dir) => dir + "/applications");
|
|
let dataDirs = systemDataDirs.concat([homeDir + "/.local/share/applications"]);
|
|
let icon: string | undefined;
|
|
|
|
for (let dir of dataDirs) {
|
|
let applicationsGFile = Gio.File.new_for_path(dir);
|
|
|
|
let enumerator: FileEnumerator;
|
|
try {
|
|
enumerator = applicationsGFile.enumerate_children(
|
|
"standard::name,standard::type",
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
null
|
|
);
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
|
|
let fileInfo: FileInfo | null;
|
|
while ((fileInfo = enumerator.next_file(null)) !== null) {
|
|
let desktopFile = fileInfo.get_name();
|
|
if (desktopFile.endsWith(".desktop")) {
|
|
let fileContents = GLib.file_get_contents(dir + "/" + desktopFile);
|
|
let matches = /Icon=(\S+)/.exec(decoder.decode(fileContents[1]));
|
|
if (matches && matches[1]) {
|
|
if (desktopFile.toLowerCase().includes(formattedClass)) {
|
|
icon = matches[1];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enumerator.close(null);
|
|
if (icon) break;
|
|
}
|
|
return Utils.lookUpIcon(icon) ? icon : "image-missing";
|
|
}
|
|
|
|
const dispatch = (ws: string) => hyprland.messageAsync(`dispatch workspace ${ws}`).catch(print);
|
|
|
|
function Workspaces() {
|
|
let workspaceButtons = new Map<Number, any>();
|
|
const workspaceButtonsArray: VariableType<Button<any, any>[] | any> = Variable([]);
|
|
|
|
function createWorkspaceButton(id: Number) {
|
|
return Widget.Button({
|
|
on_clicked: () => dispatch(`${id}`),
|
|
child: Widget.Label(`${id}`),
|
|
attribute: { id: id },
|
|
class_name: "workspace"
|
|
});
|
|
}
|
|
|
|
function initializeWorkspaceButtons() {
|
|
for (let i = 1; i <= 10; i++) {
|
|
workspaceButtons.set(i, createWorkspaceButton(i));
|
|
}
|
|
workspaceButtonsArray.setValue(Array.from(workspaceButtons.values()));
|
|
}
|
|
|
|
function update() {
|
|
workspaceButtons.forEach((workspace) => {
|
|
const existingWorkspace = hyprland.workspaces.some((element) => element.id === workspace.attribute.id);
|
|
workspace.toggleClassName("exists", existingWorkspace);
|
|
});
|
|
}
|
|
|
|
function activeWorkspace() {
|
|
workspaceButtons.forEach((workspace, key) => {
|
|
workspace.toggleClassName("active", workspace.attribute.id == hyprland.active.workspace.id);
|
|
});
|
|
}
|
|
|
|
initializeWorkspaceButtons();
|
|
activeWorkspace();
|
|
update();
|
|
hyprland.connect("notify::workspaces", () => {
|
|
activeWorkspace();
|
|
update();
|
|
});
|
|
hyprland.connect("notify::active", () => {
|
|
activeWorkspace();
|
|
});
|
|
|
|
return Widget.EventBox({
|
|
onScrollUp: () => dispatch("+1"),
|
|
onScrollDown: () => dispatch("-1"),
|
|
hpack: "center",
|
|
child: Widget.Box({
|
|
children: workspaceButtonsArray.bind(),
|
|
class_name: "workspaces"
|
|
})
|
|
});
|
|
}
|
|
|
|
function Clock() {
|
|
return Widget.Button({
|
|
class_name: "clock",
|
|
child: Widget.Box({
|
|
orientation: Gtk.Orientation.VERTICAL,
|
|
children: [
|
|
Widget.Label({
|
|
class_name: "time",
|
|
label: time.bind()
|
|
}),
|
|
Widget.Label({
|
|
class_name: "date",
|
|
label: date.bind()
|
|
})
|
|
]
|
|
}),
|
|
on_clicked: (self) => {
|
|
App.toggleWindow("calendar");
|
|
}
|
|
});
|
|
}
|
|
|
|
const battery_icons = {
|
|
charging: {
|
|
100: "battery_charging_full",
|
|
90: "battery_charging_90",
|
|
80: "battery_charging_80",
|
|
70: "battery_charging_80",
|
|
60: "battery_charging_60",
|
|
50: "battery_charging_50",
|
|
40: "battery_charging_30",
|
|
30: "battery_charging_30",
|
|
20: "battery_charging_20",
|
|
10: "battery_charging_20",
|
|
0: "battery_charging_20"
|
|
},
|
|
100: "battery_full",
|
|
90: "battery_6_bar",
|
|
80: "battery_5_bar",
|
|
70: "battery_5_bar",
|
|
60: "battery_4_bar",
|
|
50: "battery_3_bar",
|
|
40: "battery_2_bar",
|
|
30: "battery_2_bar",
|
|
20: "battery_1_bar",
|
|
10: "battery_1_bar",
|
|
0: "battery_alert"
|
|
};
|
|
|
|
function getClosestBatteryLevel(level: number, charging: boolean = false) {
|
|
const array = !charging ? battery_icons : battery_icons.charging;
|
|
const levels = Object.keys(array)
|
|
.map(Number)
|
|
.sort((a, b) => b - a);
|
|
for (let i = 0; i < levels.length; i++) {
|
|
if (level >= levels[i]) {
|
|
return array[levels[i]];
|
|
}
|
|
}
|
|
return array[levels[levels.length - 1]];
|
|
}
|
|
|
|
function BatteryLabel() {
|
|
return Widget.Box({
|
|
class_name: "battery",
|
|
visible: false,
|
|
children: [
|
|
MaterialIcon(getClosestBatteryLevel(battery.percent, battery.charging), "16px"),
|
|
Widget.Label({
|
|
label: battery.bind("percent").as((p) => `${p > 0 ? p : 0}%`),
|
|
visible: config.bind("config").as((config) => config.show_battery_percent)
|
|
})
|
|
],
|
|
tooltip_text: battery.bind("percent").as((p) => `Battery: ${p > 0 ? p : 0}%`),
|
|
setup: (self) => {
|
|
self.hook(battery, () => {
|
|
self.children[0].label = getClosestBatteryLevel(battery.percent, battery.charging);
|
|
self.visible = (battery.percent < 100 && battery.available) || config.config.always_show_battery;
|
|
self.toggleClassName("critical", battery.percent < 15);
|
|
});
|
|
self.hook(config, () => {
|
|
if (config.config.always_show_battery) {
|
|
self.visible = true;
|
|
} else {
|
|
self.visible = battery.percent < 100 && battery.available;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function SysTray() {
|
|
return Widget.Box({
|
|
class_name: "tray",
|
|
spacing: 5,
|
|
setup: (self) => {
|
|
self.hook(systemtray, () => {
|
|
const items = systemtray.items;
|
|
// @ts-expect-error
|
|
self.children = items.map((item) => {
|
|
if (!item.id) return undefined;
|
|
if (item.id.trim() != "nm-applet" && item.id.trim() != "blueman") {
|
|
return Widget.Button({
|
|
child: Widget.Icon({ icon: item.bind("icon") }),
|
|
on_primary_click_release: (_, event) => item.activate(event),
|
|
on_secondary_click_release: (_, event) => item.openMenu(event),
|
|
tooltip_markup: item.bind("tooltip_markup")
|
|
});
|
|
} else {
|
|
return undefined;
|
|
}
|
|
});
|
|
if (self.children.length > 0) self.visible = true;
|
|
else self.visible = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function AppLauncher() {
|
|
const button = Widget.Button({
|
|
class_name: "filled_tonal_button",
|
|
on_clicked: () => {
|
|
toggleAppsWindow();
|
|
},
|
|
child: MaterialIcon("search")
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
function OpenSideLeft() {
|
|
const button = Widget.Button({
|
|
class_name: "filled_tonal_button",
|
|
on_clicked: () => {
|
|
App.toggleWindow("sideleft");
|
|
},
|
|
child: MaterialIcon("dock_to_right")
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
function Wifi() {
|
|
return Widget.Button({
|
|
class_name: "bar_wifi",
|
|
on_primary_click_release: () => {
|
|
OpenSettings("network");
|
|
},
|
|
on_secondary_click_release: (_, event) => {
|
|
const nm_applet = systemtray.items.find((item) => item.id == "nm-applet");
|
|
if (nm_applet) {
|
|
nm_applet.openMenu(event);
|
|
} else {
|
|
Utils.execAsync("nm-connection-editor").catch(print);
|
|
}
|
|
},
|
|
child: MaterialIcon("signal_wifi_off", "16px"),
|
|
tooltip_text: "Disabled"
|
|
}).hook(network, (self) => {
|
|
if (network.wifi.enabled) {
|
|
self.tooltip_text = network.wifi.ssid || "Unknown";
|
|
self.child.label = MATERIAL_SYMBOL_SIGNAL_STRENGTH[network.wifi.icon_name] || "signal_wifi_off";
|
|
} else {
|
|
self.tooltip_text = "Disabled";
|
|
self.child.label = "signal_wifi_off";
|
|
}
|
|
});
|
|
}
|
|
|
|
function Bluetooth() {
|
|
return Widget.Button({
|
|
class_name: "bar_bluetooth",
|
|
on_primary_click_release: () => {
|
|
OpenSettings("bluetooth");
|
|
},
|
|
on_secondary_click_release: (_, event) => {
|
|
const blueman = systemtray.items.find((item) => item.id == "blueman");
|
|
if (blueman) {
|
|
blueman.openMenu(event);
|
|
} else {
|
|
Utils.execAsync("blueman-manager").catch(print);
|
|
}
|
|
},
|
|
child: MaterialIcon("bluetooth_disabled", "16px")
|
|
}).hook(bluetooth, (self) => {
|
|
if (bluetooth.enabled) {
|
|
self.child.label = "bluetooth";
|
|
} else {
|
|
self.child.label = "bluetooth_disabled";
|
|
}
|
|
});
|
|
}
|
|
|
|
function MediaPlayer() {
|
|
let metadata = mpris.players[0]?.metadata;
|
|
const button = Widget.Button({
|
|
class_name: "filled_tonal_button",
|
|
on_primary_click_release: () => {
|
|
toggleMediaWindow();
|
|
},
|
|
on_secondary_click_release: () => {
|
|
Utils.execAsync(["playerctl", "play-pause"]).catch(print);
|
|
},
|
|
child: MaterialIcon("play_circle"),
|
|
visible: false,
|
|
tooltip_text: "Unknown"
|
|
}).hook(mpris, (self) => {
|
|
if (mpris.players.length > 0) {
|
|
self.visible = true;
|
|
metadata = mpris.players[0]?.metadata;
|
|
if (metadata) self.tooltip_text = `${metadata["xesam:artist"]} - ${metadata["xesam:title"]}`;
|
|
} else {
|
|
self.visible = false;
|
|
self.tooltip_text = "Unknown";
|
|
}
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
function KeyboardLayout() {
|
|
const widget = Widget.Label({
|
|
class_name: "keyboard",
|
|
visible: keyboard_layout.bind().as((c) => c != "none"),
|
|
label: keyboard_layout.bind()
|
|
});
|
|
return widget;
|
|
}
|
|
|
|
function OpenSideBar() {
|
|
const button = Widget.Button({
|
|
class_name: "filled_tonal_button",
|
|
on_primary_click_release: () => {
|
|
App.toggleWindow("sidebar");
|
|
},
|
|
on_secondary_click_release: () => {
|
|
OpenSettings();
|
|
},
|
|
child: MaterialIcon("dock_to_left")
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
const focus = ({ address }) => Utils.execAsync(`hyprctl dispatch focuswindow address:${address}`).catch(print);
|
|
|
|
function TaskBar() {
|
|
if (!config.config.show_taskbar) {
|
|
return undefined;
|
|
}
|
|
let globalWidgets: Button<Icon<any>, any>[] = [];
|
|
|
|
function Clients(clients: Client[]) {
|
|
const currentClientIds = clients.map((client) => client.pid);
|
|
globalWidgets = globalWidgets.filter((widget) => currentClientIds.includes(widget.attribute.pid));
|
|
|
|
clients.forEach((client) => {
|
|
if (client.class === "kitty") return;
|
|
|
|
let widget = globalWidgets.find((w) => w.attribute.pid === client.pid);
|
|
if (widget) {
|
|
widget.tooltip_markup = client.title;
|
|
} else {
|
|
let icon: string | undefined;
|
|
if (client.class === "com.github.Aylur.ags") {
|
|
icon =
|
|
client.initialTitle === "Settings"
|
|
? "emblem-system-symbolic"
|
|
: client.initialTitle === "Emoji Picker"
|
|
? "face-smile-symbolic"
|
|
: undefined;
|
|
} else {
|
|
icon = getIconNameFromClass(client.class);
|
|
}
|
|
|
|
widget = Widget.Button({
|
|
attribute: { pid: client.pid },
|
|
child: Widget.Icon({ icon }),
|
|
tooltip_markup: client.title,
|
|
on_clicked: () => focus(client)
|
|
});
|
|
globalWidgets.push(widget);
|
|
}
|
|
});
|
|
return globalWidgets;
|
|
}
|
|
|
|
return Widget.Box({
|
|
class_name: "tray",
|
|
spacing: 5,
|
|
setup: (self) => {
|
|
self.hook(hyprland, () => {
|
|
self.children = Clients(hyprland.clients);
|
|
self.visible = self.children.length > 0;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function volumeIndicator() {
|
|
return Widget.EventBox({
|
|
onScrollUp: () => (audio.speaker.volume += 0.01),
|
|
onScrollDown: () => (audio.speaker.volume -= 0.01),
|
|
class_name: "volume_box",
|
|
child: Widget.Button({
|
|
on_primary_click_release: () => App.toggleWindow("audio"),
|
|
on_middle_click_release: () => Utils.execAsync("pavucontrol").catch(print),
|
|
on_secondary_click_release: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
|
|
child: MaterialIcon("volume_off", "16px").hook(audio.speaker, (self) => {
|
|
const vol = audio.speaker.volume * 100;
|
|
const icon = [
|
|
[101, "sound_detection_loud_sound"],
|
|
[67, "volume_up"],
|
|
[34, "volume_down"],
|
|
[1, "volume_mute"],
|
|
[0, "volume_off"]
|
|
].find(([threshold]) => Number(threshold) <= vol)?.[1];
|
|
if (audio.speaker.is_muted) self.label = "volume_off";
|
|
else self.label = String(icon!);
|
|
self.tooltip_text = `Volume ${Math.floor(vol)}%`;
|
|
})
|
|
})
|
|
});
|
|
}
|
|
|
|
const Applets = () =>
|
|
Widget.Box({
|
|
class_name: "bar_applets",
|
|
spacing: 5,
|
|
children: [volumeIndicator(), Bluetooth(), Wifi()]
|
|
});
|
|
|
|
const Dot = () =>
|
|
Widget.Label({
|
|
class_name: "dot",
|
|
use_markup: true,
|
|
label: "·",
|
|
css: "font-weight: 900;"
|
|
});
|
|
|
|
function Left() {
|
|
// @ts-expect-error
|
|
return Widget.Box({
|
|
// margin_left: 15,
|
|
class_name: "modules-left",
|
|
hpack: "start",
|
|
spacing: 8,
|
|
children: [AppLauncher(), OpenSideLeft(), MediaPlayer(), TaskBar()]
|
|
});
|
|
}
|
|
|
|
function Center() {
|
|
return Widget.Box({
|
|
class_name: "modules-center",
|
|
hpack: "center",
|
|
spacing: 8,
|
|
children: [Workspaces()]
|
|
});
|
|
}
|
|
|
|
function Right() {
|
|
return Widget.Box({
|
|
// margin_right: 15,
|
|
class_name: "modules-right",
|
|
hpack: "end",
|
|
spacing: 8,
|
|
children: [KeyboardLayout(), BatteryLabel(), SysTray(), Applets(), Clock(), OpenSideBar()]
|
|
});
|
|
}
|
|
|
|
export const Bar = async (monitor = 0) => {
|
|
return Widget.Window({
|
|
name: `bar-${monitor}`,
|
|
class_name: "bar",
|
|
monitor,
|
|
anchor: ["top", "left", "right"],
|
|
exclusivity: "exclusive",
|
|
child: Widget.CenterBox({
|
|
start_widget: Left(),
|
|
center_widget: Center(),
|
|
end_widget: Right()
|
|
})
|
|
});
|
|
};
|
|
|
|
export const BarCornerTopLeft = (monitor = 0) =>
|
|
Widget.Window({
|
|
monitor,
|
|
name: `bar_corner_tl${monitor}`,
|
|
class_name: "transparent",
|
|
layer: "top",
|
|
anchor: ["top", "left"],
|
|
exclusivity: "normal",
|
|
visible: true,
|
|
child: RoundedCorner("top_left", { className: "corner" }),
|
|
setup: enableClickThrough
|
|
});
|
|
|
|
export const BarCornerTopRight = (monitor = 0) =>
|
|
Widget.Window({
|
|
monitor,
|
|
name: `bar_corner_tr${monitor}`,
|
|
class_name: "transparent",
|
|
layer: "top",
|
|
anchor: ["top", "right"],
|
|
exclusivity: "normal",
|
|
visible: true,
|
|
child: RoundedCorner("top_right", { className: "corner" }),
|
|
setup: enableClickThrough
|
|
});
|