hyprpanel

This commit is contained in:
wt committed 2024-10-23 21:12:15 +07:00
1 parent 10080a83ee
commit 5151ac24da
322 files changed
+38276

No files matched your search

+315
View File
@@ -0,0 +1,315 @@
const hyprland = await Service.import('hyprland');
import {
Menu,
Workspaces,
ClientTitle,
Media,
Notifications,
Volume,
Network,
Bluetooth,
BatteryLabel,
Clock,
SysTray,
// Custom Modules
Ram,
Cpu,
Storage,
Netstat,
KbInput,
Updates,
Submap,
Weather,
Power,
} from './Exports';
import { BarItemBox as WidgetContainer } from '../shared/barItemBox.js';
import options from 'options';
import Gdk from 'gi://Gdk?version=3.0';
import Button from 'types/widgets/button.js';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
import './SideEffects';
import { WindowLayer } from 'lib/types/options.js';
import { Attribute, Child } from 'lib/types/widget.js';
import Window from 'types/widgets/window.js';
const { layouts } = options.bar;
const { location } = options.theme.bar;
export type BarWidget = keyof typeof widget;
type Section =
| 'battery'
| 'dashboard'
| 'workspaces'
| 'windowtitle'
| 'media'
| 'notifications'
| 'volume'
| 'network'
| 'bluetooth'
| 'clock'
| 'ram'
| 'cpu'
| 'storage'
| 'netstat'
| 'kbinput'
| 'updates'
| 'submap'
| 'weather'
| 'power'
| 'systray';
type Layout = {
left: Section[];
middle: Section[];
right: Section[];
};
type BarLayout = {
[key: string]: Layout;
};
const getLayoutForMonitor = (monitor: number, layouts: BarLayout): Layout => {
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
const wildcard = Object.keys(layouts).find((key) => key === '*');
if (matchingKey) {
return layouts[matchingKey];
}
if (wildcard) {
return layouts[wildcard];
}
return {
left: ['dashboard', 'workspaces', 'windowtitle'],
middle: ['media'],
right: ['volume', 'network', 'bluetooth', 'battery', 'systray', 'clock', 'notifications'],
};
};
const widget = {
battery: (): Button<Child, Attribute> => WidgetContainer(BatteryLabel()),
dashboard: (): Button<Child, Attribute> => WidgetContainer(Menu()),
workspaces: (monitor: number): Button<Child, Attribute> => WidgetContainer(Workspaces(monitor)),
windowtitle: (): Button<Child, Attribute> => WidgetContainer(ClientTitle()),
media: (): Button<Child, Attribute> => WidgetContainer(Media()),
notifications: (): Button<Child, Attribute> => WidgetContainer(Notifications()),
volume: (): Button<Child, Attribute> => WidgetContainer(Volume()),
network: (): Button<Child, Attribute> => WidgetContainer(Network()),
bluetooth: (): Button<Child, Attribute> => WidgetContainer(Bluetooth()),
clock: (): Button<Child, Attribute> => WidgetContainer(Clock()),
systray: (): Button<Child, Attribute> => WidgetContainer(SysTray()),
ram: (): Button<Child, Attribute> => WidgetContainer(Ram()),
cpu: (): Button<Child, Attribute> => WidgetContainer(Cpu()),
storage: (): Button<Child, Attribute> => WidgetContainer(Storage()),
netstat: (): Button<Child, Attribute> => WidgetContainer(Netstat()),
kbinput: (): Button<Child, Attribute> => WidgetContainer(KbInput()),
updates: (): Button<Child, Attribute> => WidgetContainer(Updates()),
submap: (): Button<Child, Attribute> => WidgetContainer(Submap()),
weather: (): Button<Child, Attribute> => WidgetContainer(Weather()),
power: (): Button<Child, Attribute> => WidgetContainer(Power()),
};
type GdkMonitors = {
[key: string]: {
key: string;
model: string;
used: boolean;
};
};
function getGdkMonitors(): GdkMonitors {
const display = Gdk.Display.get_default();
if (display === null) {
console.error('Failed to get Gdk display.');
return {};
}
const numGdkMonitors = display.get_n_monitors();
const gdkMonitors: GdkMonitors = {};
for (let i = 0; i < numGdkMonitors; i++) {
const curMonitor = display.get_monitor(i);
if (curMonitor === null) {
console.warn(`Monitor at index ${i} is null.`);
continue;
}
const model = curMonitor.get_model() || '';
const geometry = curMonitor.get_geometry();
const scaleFactor = curMonitor.get_scale_factor();
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
gdkMonitors[i] = { key, model, used: false };
}
return gdkMonitors;
}
/**
* NOTE: Some more funky stuff being done by GDK.
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
*
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
* the id 0 will ALWAYS be DP-1.
*
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
* is at id 2.
*
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
* is being determined as by Hyprland so the bars show up on the right monitors.
*
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
* in the case that there are multiple models in the same resolution with the same scale. We find the
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
*
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
*
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
* an existing one or shutting it off.
*
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
*
* Fun stuff really... :facepalm:
*/
const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set<number>): number => {
const gdkMonitors = getGdkMonitors();
if (Object.keys(gdkMonitors).length === 0) {
console.error('No GDK monitors were found.');
return monitor;
}
// Get the GDK monitor for the given monitor index
const gdkMonitor = gdkMonitors[monitor];
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
const directMatch = hyprland.monitors.find((hypMon) => {
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
});
if (directMatch) {
usedHyprlandMonitors.add(directMatch.id);
return directMatch.id;
}
// Second pass: Relaxed matching without considering the monitor index
const hyprlandMonitor = hyprland.monitors.find((hypMon) => {
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id);
});
if (hyprlandMonitor) {
usedHyprlandMonitors.add(hyprlandMonitor.id);
return hyprlandMonitor.id;
}
// Fallback: Find the first available monitor ID that hasn't been used
const fallbackMonitor = hyprland.monitors.find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
if (fallbackMonitor) {
usedHyprlandMonitors.add(fallbackMonitor.id);
return fallbackMonitor.id;
}
// Ensure we return a valid monitor ID that actually exists
for (let i = 0; i < hyprland.monitors.length; i++) {
if (!usedHyprlandMonitors.has(i)) {
usedHyprlandMonitors.add(i);
return i;
}
}
// As a last resort, return the original monitor index if no unique monitor can be found
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
return monitor;
};
export const Bar = (() => {
const usedHyprlandMonitors = new Set<number>();
return (monitor: number): Window<Child, Attribute> => {
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
return Widget.Window({
name: `bar-${hyprlandMonitor}`,
class_name: 'bar',
monitor,
visible: true,
anchor: location.bind('value').as((ln) => [ln, 'left', 'right']),
exclusivity: 'exclusive',
layer: Utils.merge(
[options.theme.bar.layer.bind('value'), options.tear.bind('value')],
(barLayer: WindowLayer, tear: boolean) => {
if (tear && barLayer === 'overlay') {
return 'top';
}
return barLayer;
},
),
child: Widget.Box({
class_name: 'bar-panel-container',
child: Widget.CenterBox({
class_name: 'bar-panel',
css: 'padding: 1px',
startWidget: Widget.Box({
class_name: 'box-left',
hexpand: true,
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value as BarLayout);
self.children = foundLayout.left
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
});
},
}),
centerWidget: Widget.Box({
class_name: 'box-center',
hpack: 'center',
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value as BarLayout);
self.children = foundLayout.middle
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
});
},
}),
endWidget: Widget.Box({
class_name: 'box-right',
hpack: 'end',
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value as BarLayout);
self.children = foundLayout.right
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
});
},
}),
}),
}),
});
};
})();
+47
View File
@@ -0,0 +1,47 @@
import { Menu } from './menu/index';
import { Workspaces } from './workspaces/index';
import { ClientTitle } from './window_title/index';
import { Media } from './media/index';
import { Notifications } from './notifications/index';
import { Volume } from './volume/index';
import { Network } from './network/index';
import { Bluetooth } from './bluetooth/index';
import { BatteryLabel } from './battery/index';
import { Clock } from './clock/index';
import { SysTray } from './systray/index';
// Custom Modules
import { Ram } from '../../customModules/ram/index';
import { Cpu } from '../../customModules/cpu/index';
import { Storage } from 'customModules/storage/index';
import { Netstat } from 'customModules/netstat/index';
import { KbInput } from 'customModules/kblayout/index';
import { Updates } from 'customModules/updates/index';
import { Submap } from 'customModules/submap/index';
import { Weather } from 'customModules/weather/index';
import { Power } from 'customModules/power/index';
export {
Menu,
Workspaces,
ClientTitle,
Media,
Notifications,
Volume,
Network,
Bluetooth,
BatteryLabel,
Clock,
SysTray,
// Custom Modules
Ram,
Cpu,
Storage,
Netstat,
KbInput,
Updates,
Submap,
Weather,
Power,
};
+29
View File
@@ -0,0 +1,29 @@
import options from 'options';
const { showIcon, showTime } = options.bar.clock;
showIcon.connect('changed', () => {
if (!showTime.value && !showIcon.value) {
showTime.value = true;
}
});
showTime.connect('changed', () => {
if (!showTime.value && !showIcon.value) {
showIcon.value = true;
}
});
const { label, icon } = options.bar.windowtitle;
label.connect('changed', () => {
if (!label.value && !icon.value) {
icon.value = true;
}
});
icon.connect('changed', () => {
if (!label.value && !icon.value) {
label.value = true;
}
});
+133
View File
@@ -0,0 +1,133 @@
const battery = await Service.import('battery');
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { label: show_label, rightClick, middleClick, scrollUp, scrollDown, hideLabelWhenFull } = options.bar.battery;
const BatteryLabel = (): BarBoxChild => {
const isVis = Variable(battery.available);
const batIcon = Utils.merge(
[battery.bind('percent'), battery.bind('charging'), battery.bind('charged')],
(batPercent: number, batCharging, batCharged) => {
if (batCharged) return `battery-level-100-charged-symbolic`;
else return `battery-level-${Math.floor(batPercent / 10) * 10}${batCharging ? '-charging' : ''}-symbolic`;
},
);
battery.connect('changed', ({ available }) => {
isVis.value = available;
});
const formatTime = (seconds: number): Record<string, number> => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return { hours, minutes };
};
const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean): string => {
if (isCharged) {
return 'Fully Charged!!!';
}
const { hours, minutes } = formatTime(timeSeconds);
if (isCharging) {
return `${hours} hours ${minutes} minutes until full`;
} else {
return `${hours} hours ${minutes} minutes left`;
}
};
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `battery-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
visible: battery.bind('available'),
tooltip_text: battery.bind('time_remaining').as((t) => t.toString()),
children: Utils.merge(
[
battery.bind('available'),
show_label.bind('value'),
battery.bind('charged'),
hideLabelWhenFull.bind('value'),
],
(batAvail, showLabel, isCharged, hideWhenFull) => {
if (batAvail && showLabel) {
return [
Widget.Icon({
class_name: 'bar-button-icon battery',
icon: batIcon,
}),
...(hideWhenFull && isCharged
? []
: [
Widget.Label({
class_name: 'bar-button-label battery',
label: battery.bind('percent').as((p) => `${Math.floor(p)}%`),
}),
]),
];
} else if (batAvail && !showLabel) {
return [
Widget.Icon({
class_name: 'bar-button-icon battery',
icon: batIcon,
}),
];
} else {
return [];
}
},
),
setup: (self) => {
self.hook(battery, () => {
if (battery.available) {
self.tooltip_text = generateTooltip(battery.time_remaining, battery.charging, battery.charged);
}
});
},
}),
isVis,
boxClass: 'battery',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
onPrimaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'energymenu');
},
},
};
};
export { BatteryLabel };
@@ -0,0 +1,74 @@
const bluetooth = await Service.import('bluetooth');
import Gdk from 'gi://Gdk?version=3.0';
import options from 'options';
import { openMenu } from '../utils.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { label, rightClick, middleClick, scrollDown, scrollUp } = options.bar.bluetooth;
const Bluetooth = (): BarBoxChild => {
const btIcon = Widget.Label({
label: bluetooth.bind('enabled').as((v) => (v ? '󰂯' : '󰂲')),
class_name: 'bar-button-icon bluetooth txt-icon bar',
});
const btText = Widget.Label({
label: Utils.merge([bluetooth.bind('enabled'), bluetooth.bind('connected_devices')], (btEnabled, btDevices) => {
return btEnabled && btDevices.length ? ` Connected (${btDevices.length})` : btEnabled ? 'On' : 'Off';
}),
class_name: 'bar-button-label bluetooth',
});
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `bluetooth-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: options.bar.bluetooth.label.bind('value').as((showLabel) => {
if (showLabel) {
return [btIcon, btText];
}
return [btIcon];
}),
}),
isVisible: true,
boxClass: 'bluetooth',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'bluetoothmenu');
},
},
};
};
export { Bluetooth };
+83
View File
@@ -0,0 +1,83 @@
import Gdk from 'gi://Gdk?version=3.0';
import GLib from 'gi://GLib';
import { openMenu } from '../utils.js';
import options from 'options';
import { DateTime } from 'types/@girs/glib-2.0/glib-2.0.cjs';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { format, icon, showIcon, showTime, rightClick, middleClick, scrollUp, scrollDown } = options.bar.clock;
const { style } = options.theme.bar.buttons;
const date = Variable(GLib.DateTime.new_now_local(), {
poll: [1000, (): DateTime => GLib.DateTime.new_now_local()],
});
const time = Utils.derive([date, format], (c, f) => c.format(f) || '');
const Clock = (): BarBoxChild => {
const clockTime = Widget.Label({
class_name: 'bar-button-label clock bar',
label: time.bind(),
});
const clockIcon = Widget.Label({
label: icon.bind('value'),
class_name: 'bar-button-icon clock txt-icon bar',
});
return {
component: Widget.Box({
className: Utils.merge(
[style.bind('value'), showIcon.bind('value'), showTime.bind('value')],
(btnStyle, shwIcn, shwLbl) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `clock-container ${styleMap[btnStyle]} ${!shwLbl ? 'no-label' : ''} ${!shwIcn ? 'no-icon' : ''}`;
},
),
children: Utils.merge([showIcon.bind('value'), showTime.bind('value')], (shIcn, shTm) => {
if (shIcn && !shTm) {
return [clockIcon];
} else if (shTm && !shIcn) {
return [clockTime];
}
return [clockIcon, clockTime];
}),
}),
isVisible: true,
boxClass: 'clock',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'calendarmenu');
},
},
};
};
export { Clock };
+113
View File
@@ -0,0 +1,113 @@
import Gdk from 'gi://Gdk?version=3.0';
const mpris = await Service.import('mpris');
import { openMenu } from '../utils.js';
import options from 'options';
import { getCurrentPlayer } from 'lib/shared/media.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand } from 'customModules/utils.js';
const { show_artist, truncation, truncation_size, show_label, show_active_only, rightClick, middleClick } =
options.bar.media;
const Media = (): BarBoxChild => {
const activePlayer = Variable(mpris.players[0]);
const isVis = Variable(!show_active_only.value);
show_active_only.connect('changed', () => {
isVis.value = !show_active_only.value || mpris.players.length > 0;
});
mpris.connect('changed', () => {
const curPlayer = getCurrentPlayer(activePlayer.value);
activePlayer.value = curPlayer;
isVis.value = !show_active_only.value || mpris.players.length > 0;
});
const getIconForPlayer = (playerName: string): string => {
const windowTitleMap = [
['Firefox', '󰈹'],
['Microsoft Edge', '󰇩'],
['Discord', ''],
['Plex', '󰚺'],
['Spotify', '󰓇'],
['(.*)', '󰝚'],
];
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0], 'i').test(playerName));
return foundMatch ? foundMatch[1] : '󰝚';
};
const songIcon = Variable('');
const mediaLabel = Utils.watch('Media', [mpris, show_artist, truncation, truncation_size, show_label], () => {
if (activePlayer.value && show_label.value) {
const { track_title, identity, track_artists } = activePlayer.value;
songIcon.value = getIconForPlayer(identity);
const trackArtist = show_artist.value ? ` - ${track_artists.join(', ')}` : ``;
const truncatedLabel = truncation.value
? `${track_title + trackArtist}`.substring(0, truncation_size.value)
: `${track_title + trackArtist}`;
return track_title.length === 0
? `No media playing...`
: truncatedLabel.length < truncation_size.value || !truncation.value
? `${truncatedLabel}`
: `${truncatedLabel.substring(0, truncatedLabel.length - 3)}...`;
} else {
songIcon.value = getIconForPlayer(activePlayer.value?.identity || '');
return `Media`;
}
});
return {
component: Widget.Box({
visible: false,
child: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
(style) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `media-container ${styleMap[style]}`;
},
),
child: Widget.Box({
children: [
Widget.Label({
class_name: 'bar-button-icon media txt-icon bar',
label: songIcon.bind('value').as((v) => v || '󰝚'),
}),
Widget.Label({
class_name: 'bar-button-label media',
label: mediaLabel,
}),
],
}),
}),
}),
isVis,
boxClass: 'media',
props: {
on_scroll_up: () => activePlayer.value?.next(),
on_scroll_down: () => activePlayer.value?.previous(),
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'mediamenu');
},
onSecondaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
},
onMiddleClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
},
},
};
};
export { Media };
+56
View File
@@ -0,0 +1,56 @@
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.launcher;
const Menu = (): BarBoxChild => {
return {
component: Widget.Box({
className: Utils.merge([options.theme.bar.buttons.style.bind('value')], (style) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `dashboard ${styleMap[style]}`;
}),
child: Widget.Label({
class_name: 'bar-menu_label bar-button_icon txt-icon bar',
label: options.bar.launcher.icon.bind('value'),
}),
}),
isVisible: true,
boxClass: 'dashboard',
props: {
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'dashboardmenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Menu };
+106
View File
@@ -0,0 +1,106 @@
import Gdk from 'gi://Gdk?version=3.0';
const network = await Service.import('network');
import options from 'options';
import { openMenu } from '../utils.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const {
label: networkLabel,
truncation,
truncation_size,
rightClick,
middleClick,
scrollDown,
scrollUp,
} = options.bar.network;
const Network = (): BarBoxChild => {
return {
component: Widget.Box({
vpack: 'fill',
vexpand: true,
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), networkLabel.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `network-container ${styleMap[style]}${!showLabel ? ' no-label' : ''}`;
},
),
children: [
Widget.Icon({
class_name: 'bar-button-icon network-icon',
icon: Utils.merge(
[network.bind('primary'), network.bind('wifi'), network.bind('wired')],
(pmry, wfi, wrd) => {
if (pmry === 'wired') {
return wrd.icon_name;
}
return wfi.icon_name;
},
),
}),
Widget.Box({
child: Utils.merge(
[
network.bind('primary'),
network.bind('wifi'),
networkLabel.bind('value'),
truncation.bind('value'),
truncation_size.bind('value'),
],
(pmry, wfi, showLbl, trunc, tSize) => {
if (!showLbl) {
return Widget.Box();
}
if (pmry === 'wired') {
return Widget.Label({
class_name: 'bar-button-label network-label',
label: 'Wired'.substring(0, tSize),
});
}
return Widget.Label({
class_name: 'bar-button-label network-label',
label: wfi.ssid ? `${trunc ? wfi.ssid.substring(0, tSize) : wfi.ssid}` : '--',
});
},
),
}),
],
}),
isVisible: true,
boxClass: 'network',
props: {
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'networkmenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Network };
@@ -0,0 +1,94 @@
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { filterNotifications } from 'lib/shared/notifications.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { show_total, rightClick, middleClick, scrollUp, scrollDown, hideCountWhenZero } = options.bar.notifications;
const { ignore } = options.notifications;
const notifs = await Service.import('notifications');
export const Notifications = (): BarBoxChild => {
return {
component: Widget.Box({
hpack: 'start',
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_total.bind('value')],
(style, showTotal) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `notifications-container ${styleMap[style]} ${!showTotal ? 'no-label' : ''}`;
},
),
child: Widget.Box({
hpack: 'start',
class_name: 'bar-notifications',
children: Utils.merge(
[
notifs.bind('notifications'),
notifs.bind('dnd'),
show_total.bind('value'),
ignore.bind('value'),
hideCountWhenZero.bind('value'),
],
(notif, dnd, showTotal, ignoredNotifs, hideCountForZero) => {
const filteredNotifications = filterNotifications(notif, ignoredNotifs);
const notifIcon = Widget.Label({
hpack: 'center',
class_name: 'bar-button-icon notifications txt-icon bar',
label: dnd ? '󰂛' : filteredNotifications.length > 0 ? '󱅫' : '󰂚',
});
const notifLabel = Widget.Label({
hpack: 'center',
class_name: 'bar-button-label notifications',
label: filteredNotifications.length.toString(),
});
if (showTotal) {
if (hideCountForZero && filteredNotifications.length === 0) {
return [notifIcon];
}
return [notifIcon, notifLabel];
}
return [notifIcon];
},
),
}),
}),
isVisible: true,
boxClass: 'notifications',
props: {
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'notificationsmenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
@@ -0,0 +1,44 @@
import Gdk from 'gi://Gdk?version=3.0';
import { BarBoxChild, SelfButton } from 'lib/types/bar';
import { Notify } from 'lib/utils';
const systemtray = await Service.import('systemtray');
import options from 'options';
const { ignore } = options.bar.systray;
const SysTray = (): BarBoxChild => {
const isVis = Variable(false);
const items = Utils.merge([systemtray.bind('items'), ignore.bind('value')], (items, ignored) => {
const filteredTray = items.filter(({ id }) => !ignored.includes(id));
isVis.value = filteredTray.length > 0;
return filteredTray.map((item) => {
return Widget.Button({
cursor: 'pointer',
child: Widget.Icon({
class_name: 'systray-icon',
icon: item.bind('icon'),
}),
on_primary_click: (_: SelfButton, event: Gdk.Event) => item.activate(event),
on_secondary_click: (_, event) => item.openMenu(event),
onMiddleClick: () => Notify({ summary: 'App Name', body: item.id }),
tooltip_markup: item.bind('tooltip_markup'),
});
});
});
return {
component: Widget.Box({
class_name: 'systray-container',
children: items,
}),
isVisible: true,
boxClass: 'systray',
isVis,
props: {},
};
};
export { SysTray };
+56
View File
@@ -0,0 +1,56 @@
import Gdk from 'gi://Gdk?version=3.0';
import { Attribute, Child } from 'lib/types/widget';
import { calculateMenuPosition } from 'modules/menus/shared/dropdown/locationHandler/index';
import Button from 'types/widgets/button';
export const closeAllMenus = (): void => {
const menuWindows = App.windows
.filter((w) => {
if (w.name) {
return /.*menu/.test(w.name);
}
return false;
})
.map((w) => w.name);
menuWindows.forEach((w) => {
if (w) {
App.closeWindow(w);
}
});
};
export const openMenu = async (clicked: Button<Child, Attribute>, event: Gdk.Event, window: string): Promise<void> => {
/*
* NOTE: We have to make some adjustments so the menu pops up relatively
* to the center of the button clicked. We don't want the menu to spawn
* offcenter dependending on which edge of the button you click on.
* -------------
* To fix this, we take the x coordinate of the click within the button's bounds.
* If you click the left edge of a 100 width button, then the x axis will be 0
* and if you click the right edge then the x axis will be 100.
* -------------
* Then we divide the width of the button by 2 to get the center of the button and then get
* the offset by subtracting the clicked x coordinate. Then we can apply that offset
* to the x coordinate of the click relative to the screen to get the center of the
* icon click.
*/
const middleOfButton = Math.floor(clicked.get_allocated_width() / 2);
const xAxisOfButtonClick = clicked.get_pointer()[0];
const middleOffset = middleOfButton - xAxisOfButtonClick;
const clickPos = event.get_root_coords();
const adjustedXCoord = clickPos[1] + middleOffset;
const coords = [adjustedXCoord, clickPos[2]];
try {
await calculateMenuPosition(coords, window);
} catch (error) {
console.error(`Error calculating menu position: ${error}`);
}
closeAllMenus();
App.toggleWindow(window);
};
+106
View File
@@ -0,0 +1,106 @@
import Gdk from 'gi://Gdk?version=3.0';
const audio = await Service.import('audio');
import { openMenu } from '../utils.js';
import options from 'options';
import { Binding } from 'lib/utils.js';
import { VolumeIcons } from 'lib/types/volume.js';
import { BarBoxChild } from 'lib/types/bar.js';
import { Bind } from 'lib/types/variable.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.volume;
const Volume = (): BarBoxChild => {
const icons: VolumeIcons = {
101: '󰕾',
66: '󰕾',
34: '󰖀',
1: '󰕿',
0: '󰝟',
};
const getIcon = (): Bind => {
const icon: Binding<number> = Utils.merge(
[audio.speaker.bind('is_muted'), audio.speaker.bind('volume')],
(isMuted, vol) => {
if (isMuted) return 0;
const foundVol = [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
if (foundVol !== undefined) {
return foundVol;
}
return 101;
},
);
return icon.as((i: number) => (i !== undefined ? icons[i] : icons[101]));
};
const volIcn = Widget.Label({
hexpand: true,
label: getIcon(),
class_name: 'bar-button-icon volume txt-icon bar',
});
const volPct = Widget.Label({
hexpand: true,
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
class_name: 'bar-button-label volume',
});
return {
component: Widget.Box({
hexpand: true,
vexpand: true,
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), options.bar.volume.label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `volume-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: options.bar.volume.label.bind('value').as((showLabel) => {
if (showLabel) {
return [volIcn, volPct];
}
return [volIcn];
}),
}),
isVisible: true,
boxClass: 'volume',
props: {
onPrimaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'audiomenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Volume };
@@ -0,0 +1,241 @@
const hyprland = await Service.import('hyprland');
import { BarBoxChild } from 'lib/types/bar';
import options from 'options';
import { Attribute, Child } from 'lib/types/widget';
import { ActiveClient } from 'types/service/hyprland';
import Label from 'types/widgets/label';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
const { leftClick, rightClick, middleClick, scrollDown, scrollUp } = options.bar.windowtitle;
const filterTitle = (windowtitle: ActiveClient): Record<string, string> => {
const windowTitleMap = [
// user provided values
...options.bar.windowtitle.title_map.value,
// Original Entries
['kitty', '󰄛', 'Kitty Terminal'],
['firefox', '󰈹', 'Firefox'],
['microsoft-edge', '󰇩', 'Edge'],
['discord', '', 'Discord'],
['vesktop', '', 'Vesktop'],
['org.kde.dolphin', '', 'Dolphin'],
['plex', '󰚺', 'Plex'],
['steam', '', 'Steam'],
['spotify', '󰓇', 'Spotify'],
['ristretto', '󰋩', 'Ristretto'],
['obsidian', '󱓧', 'Obsidian'],
// Browsers
['google-chrome', '', 'Google Chrome'],
['brave-browser', '󰖟', 'Brave Browser'],
['chromium', '', 'Chromium'],
['opera', '', 'Opera'],
['vivaldi', '󰖟', 'Vivaldi'],
['waterfox', '󰖟', 'Waterfox'],
['thorium', '󰖟', 'Waterfox'],
['tor-browser', '', 'Tor Browser'],
['floorp', '󰈹', 'Floorp'],
// Terminals
['gnome-terminal', '', 'GNOME Terminal'],
['konsole', '', 'Konsole'],
['alacritty', '', 'Alacritty'],
['wezterm', '', 'Wezterm'],
['foot', '󰽒', 'Foot Terminal'],
['tilix', '', 'Tilix'],
['xterm', '', 'XTerm'],
['urxvt', '', 'URxvt'],
['st', '', 'st Terminal'],
// Development Tools
['code', '󰨞', 'Visual Studio Code'],
['vscode', '󰨞', 'VS Code'],
['sublime-text', '', 'Sublime Text'],
['atom', '', 'Atom'],
['android-studio', '󰀴', 'Android Studio'],
['intellij-idea', '', 'IntelliJ IDEA'],
['pycharm', '󱃖', 'PyCharm'],
['webstorm', '󱃖', 'WebStorm'],
['phpstorm', '󱃖', 'PhpStorm'],
['eclipse', '', 'Eclipse'],
['netbeans', '', 'NetBeans'],
['docker', '', 'Docker'],
['vim', '', 'Vim'],
['neovim', '', 'Neovim'],
['neovide', '', 'Neovide'],
['emacs', '', 'Emacs'],
// Communication Tools
['slack', '󰒱', 'Slack'],
['telegram-desktop', '', 'Telegram'],
['org.telegram.desktop', '', 'Telegram'],
['whatsapp', '󰖣', 'WhatsApp'],
['teams', '󰊻', 'Microsoft Teams'],
['skype', '󰒯', 'Skype'],
['thunderbird', '', 'Thunderbird'],
// File Managers
['nautilus', '󰝰', 'Files (Nautilus)'],
['thunar', '󰝰', 'Thunar'],
['pcmanfm', '󰝰', 'PCManFM'],
['nemo', '󰝰', 'Nemo'],
['ranger', '󰝰', 'Ranger'],
['doublecmd', '󰝰', 'Double Commander'],
['krusader', '󰝰', 'Krusader'],
// Media Players
['vlc', '󰕼', 'VLC Media Player'],
['mpv', '', 'MPV'],
['rhythmbox', '󰓃', 'Rhythmbox'],
// Graphics Tools
['gimp', '', 'GIMP'],
['inkscape', '', 'Inkscape'],
['krita', '', 'Krita'],
['blender', '󰂫', 'Blender'],
// Video Editing
['kdenlive', '', 'Kdenlive'],
// Games and Gaming Platforms
['lutris', '󰺵', 'Lutris'],
['heroic', '󰺵', 'Heroic Games Launcher'],
['minecraft', '󰍳', 'Minecraft'],
['csgo', '󰺵', 'CS:GO'],
['dota2', '󰺵', 'Dota 2'],
// Office and Productivity
['evernote', '', 'Evernote'],
['sioyek', '', 'Sioyek'],
// Cloud Services and Sync
['dropbox', '󰇣', 'Dropbox'],
// Desktop
['^$', '󰇄', 'Desktop'],
// Fallback icon
['(.+)', '󰣆', `${windowtitle.class.charAt(0).toUpperCase() + windowtitle.class.slice(1)}`],
];
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(windowtitle.class.toLowerCase()));
// return the default icon if no match is found or
// if the array element matched is not of size 3
if (!foundMatch || foundMatch.length !== 3) {
return {
icon: windowTitleMap[windowTitleMap.length - 1][1],
label: windowTitleMap[windowTitleMap.length - 1][2],
};
}
return {
icon: foundMatch[1],
label: foundMatch[2],
};
};
const getTitle = (client: ActiveClient, useCustomTitle: boolean, useClassName: boolean): string => {
if (useCustomTitle) return filterTitle(client).label;
if (useClassName) return client.class;
const title = client.title;
// If the title is empty or only filled with spaces, fallback to the class name
if (title.length === 0 || title.match(/^ *$/)) {
return client.class;
}
return title;
};
const truncateTitle = (title: string, max_size: number): string => {
if (max_size > 0 && title.length > max_size) {
return title.substring(0, max_size).trim() + '...';
}
return title;
};
const ClientTitle = (): BarBoxChild => {
const { custom_title, class_name, label, icon, truncation, truncation_size } = options.bar.windowtitle;
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `windowtitle-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: Utils.merge(
[
hyprland.active.bind('client'),
custom_title.bind('value'),
class_name.bind('value'),
label.bind('value'),
icon.bind('value'),
truncation.bind('value'),
truncation_size.bind('value'),
],
(client, useCustomTitle, useClassName, showLabel, showIcon, truncate, truncationSize) => {
const children: Label<Child>[] = [];
if (showIcon) {
children.push(
Widget.Label({
class_name: 'bar-button-icon windowtitle txt-icon bar',
label: filterTitle(client).icon,
}),
);
}
if (showLabel) {
children.push(
Widget.Label({
class_name: `bar-button-label windowtitle ${showIcon ? '' : 'no-icon'}`,
label: truncateTitle(
getTitle(client, useCustomTitle, useClassName),
truncate ? truncationSize : -1,
),
}),
);
}
return children;
},
),
}),
isVisible: true,
boxClass: 'windowtitle',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_primary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(leftClick.value, { clicked, event });
};
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { ClientTitle };
@@ -0,0 +1,162 @@
const hyprland = await Service.import('hyprland');
import { MonitorMap, WorkspaceMap, WorkspaceRule } from 'lib/types/workspace';
import options from 'options';
import { Variable } from 'types/variable';
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
export const getWorkspacesForMonitor = (curWs: number, wsRules: WorkspaceMap, monitor: number): boolean => {
if (!wsRules || !Object.keys(wsRules).length) {
return true;
}
const monitorMap: MonitorMap = {};
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({ id: m.monitorID, name: m.monitor }));
const monitors = [
...new Map([...workspaceMonitorList, ...hyprland.monitors].map((item) => [item.id, item])).values(),
];
monitors.forEach((m) => (monitorMap[m.id] = m.name));
const currentMonitorName = monitorMap[monitor];
const monitorWSRules = wsRules[currentMonitorName];
if (monitorWSRules === undefined) {
return true;
}
return monitorWSRules.includes(curWs);
};
export const getWorkspaceRules = (): WorkspaceMap => {
try {
const rules = Utils.exec('hyprctl workspacerules -j');
const workspaceRules: WorkspaceMap = {};
JSON.parse(rules).forEach((rule: WorkspaceRule) => {
const workspaceNum = parseInt(rule.workspaceString, 10);
if (isNaN(workspaceNum)) {
return;
}
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
workspaceRules[rule.monitor].push(workspaceNum);
} else {
workspaceRules[rule.monitor] = [workspaceNum];
}
});
return workspaceRules;
} catch (err) {
console.error(err);
return {};
}
};
export const getCurrentMonitorWorkspaces = (monitor: number): number[] => {
if (hyprland.monitors.length === 1) {
return Array.from({ length: workspaces.value }, (_, i) => i + 1);
}
const monitorWorkspaces = getWorkspaceRules();
const monitorMap: MonitorMap = {};
hyprland.monitors.forEach((m) => (monitorMap[m.id] = m.name));
const currentMonitorName = monitorMap[monitor];
return monitorWorkspaces[currentMonitorName];
};
type ThrottledScrollHandlers = {
throttledScrollUp: () => void;
throttledScrollDown: () => void;
};
export const isWorkspaceIgnored = (ignoredWorkspaces: Variable<string>, workspaceNumber: number): boolean => {
if (ignoredWorkspaces.value === '') return false;
const ignoredWsRegex = new RegExp(ignoredWorkspaces.value);
return ignoredWsRegex.test(workspaceNumber.toString());
};
const navigateWorkspace = (
direction: 'next' | 'prev',
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable<string>,
): void => {
const workspacesList = activeWorkspaces
? hyprland.workspaces.filter((ws) => hyprland.active.monitor.id === ws.monitorID).map((ws) => ws.id)
: currentMonitorWorkspaces.value || Array.from({ length: workspaces.value }, (_, i) => i + 1);
if (workspacesList.length === 0) return;
const currentIndex = workspacesList.indexOf(hyprland.active.workspace.id);
const step = direction === 'next' ? 1 : -1;
let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length;
let attempts = 0;
while (attempts < workspacesList.length) {
const targetWS = workspacesList[newIndex];
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) {
hyprland.messageAsync(`dispatch workspace ${targetWS}`);
return;
}
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length;
attempts++;
}
};
export const goToNextWS = (
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable<string>,
): void => {
navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
};
export const goToPrevWS = (
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable<string>,
): void => {
navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
};
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
let inThrottle: boolean;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
} as T;
}
export const createThrottledScrollHandlers = (
scrollSpeed: number,
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean = false,
): ThrottledScrollHandlers => {
const throttledScrollUp = throttle(() => {
if (reverse_scroll.value) {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
} else {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
}
}, 200 / scrollSpeed);
const throttledScrollDown = throttle(() => {
if (reverse_scroll.value) {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
} else {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
}
}, 200 / scrollSpeed);
return { throttledScrollUp, throttledScrollDown };
};
@@ -0,0 +1,44 @@
import options from 'options';
import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers';
import { BarBoxChild, SelfButton } from 'lib/types/bar';
import { occupiedWses } from './variants/occupied';
import { defaultWses } from './variants/default';
const { workspaces, scroll_speed } = options.bar.workspaces;
const Workspaces = (monitor = -1): BarBoxChild => {
const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor));
workspaces.connect('changed', () => {
currentMonitorWorkspaces.value = getCurrentMonitorWorkspaces(monitor);
});
return {
component: Widget.Box({
class_name: 'workspaces-box-container',
child: options.bar.workspaces.hideUnoccupied.bind('value').as((hideUnoccupied) => {
return hideUnoccupied ? occupiedWses(monitor) : defaultWses(monitor);
}),
}),
isVisible: true,
boxClass: 'workspaces',
props: {
setup: (self: SelfButton): void => {
Utils.merge(
[scroll_speed.bind('value'), options.bar.workspaces.hideUnoccupied.bind('value')],
(scroll_speed, hideUnoccupied) => {
const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers(
scroll_speed,
currentMonitorWorkspaces,
hideUnoccupied,
);
self.on_scroll_up = throttledScrollUp;
self.on_scroll_down = throttledScrollDown;
},
);
},
},
};
};
export { Workspaces };
@@ -0,0 +1,130 @@
import { WorkspaceIconMap } from 'lib/types/workspace';
import { isValidGjsColor } from 'lib/utils';
import options from 'options';
import { Monitor } from 'types/service/hyprland';
const hyprland = await Service.import('hyprland');
const { monochrome, background } = options.theme.bar.buttons;
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
const { showWsIcons, showAllActive, numbered_active_indicator: activeIndicator } = options.bar.workspaces;
const isWorkspaceActiveOnMonitor = (monitor: number, monitors: Monitor[], i: number): boolean => {
return showAllActive.value && monitors[monitor]?.activeWorkspace?.id === i;
};
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
const iconEntry = wsIconMap[i];
if (!iconEntry) {
return `${i}`;
}
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
if (typeof iconEntry === 'string' && iconEntry !== '') {
return iconEntry;
}
if (hasIcon) {
return iconEntry.icon;
}
return `${i}`;
};
export const getWsColor = (
wsIconMap: WorkspaceIconMap,
i: number,
smartHighlight: boolean,
monitor: number,
monitors: Monitor[],
): string => {
const iconEntry = wsIconMap[i];
const hasColor = typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
if (!iconEntry) {
return '';
}
if (
showWsIcons.value &&
smartHighlight &&
activeIndicator.value === 'highlight' &&
(hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i))
) {
const iconColor = monochrome.value ? background : wsBackground;
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.value;
const colorCss = `color: ${iconColor};`;
const backgroundCss = `background: ${iconBackground};`;
return colorCss + backgroundCss;
}
if (hasColor && isValidGjsColor(iconEntry.color)) {
return `color: ${iconEntry.color}; border-bottom-color: ${iconEntry.color};`;
}
return '';
};
export const renderClassnames = (
showIcons: boolean,
showNumbered: boolean,
numberedActiveIndicator: string,
showWsIcons: boolean,
smartHighlight: boolean,
monitor: number,
monitors: Monitor[],
i: number,
): string => {
if (showIcons) {
return 'workspace-icon txt-icon bar';
}
if (showNumbered || showWsIcons) {
const numActiveInd =
hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)
? numberedActiveIndicator
: '';
const wsIconClass = showWsIcons ? 'txt-icon' : '';
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass}`;
return className.trim();
}
return 'default';
};
export const renderLabel = (
showIcons: boolean,
available: string,
active: string,
occupied: string,
workspaceMask: boolean,
showWsIcons: boolean,
wsIconMap: WorkspaceIconMap,
i: number,
index: number,
monitor: number,
monitors: Monitor[],
): string => {
if (showIcons) {
if (hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)) {
return active;
}
if ((hyprland.getWorkspace(i)?.windows || 0) > 0) {
return occupied;
}
if (monitor !== -1) {
return available;
}
}
if (showWsIcons) {
return getWsIcon(wsIconMap, i);
}
return workspaceMask ? `${index + 1}` : `${i}`;
};
@@ -0,0 +1,141 @@
const hyprland = await Service.import('hyprland');
import options from 'options';
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
import { range } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import { getWsColor, renderClassnames, renderLabel } from '../utils';
import { WorkspaceIconMap } from 'lib/types/workspace';
import { Monitor } from 'types/service/hyprland';
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored } = options.bar.workspaces;
export const defaultWses = (monitor: number): BoxWidget => {
return Widget.Box({
children: Utils.merge(
[workspaces.bind('value'), monitorSpecific.bind('value'), ignored.bind('value')],
(workspaces: number, monitorSpecific: boolean) => {
return range(workspaces || 8)
.filter((workspaceNumber) => {
if (!monitorSpecific) {
return true;
}
const workspaceRules = getWorkspaceRules();
return (
getWorkspacesForMonitor(workspaceNumber, workspaceRules, monitor) &&
!isWorkspaceIgnored(ignored, workspaceNumber)
);
})
.sort((a, b) => {
return a - b;
})
.map((i, index) => {
return Widget.Button({
class_name: 'workspace-button',
on_primary_click: () => {
hyprland.messageAsync(`dispatch workspace ${i}`);
},
child: Widget.Label({
attribute: i,
vpack: 'center',
css: Utils.merge(
[
spacing.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.theme.matugen.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
],
(
sp: number,
showWsIcons: boolean,
workspaceIconMap: WorkspaceIconMap,
matugen: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
return (
`margin: 0rem ${0.375 * sp}rem;` +
`${showWsIcons && !matugen ? getWsColor(workspaceIconMap, i, smartHighlight, monitor, monitors) : ''}`
);
},
),
class_name: Utils.merge(
[
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.show_numbered.bind('value'),
options.bar.workspaces.numbered_active_indicator.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
],
(
showIcons: boolean,
showNumbered: boolean,
numberedActiveIndicator: string,
showWsIcons: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
return renderClassnames(
showIcons,
showNumbered,
numberedActiveIndicator,
showWsIcons,
smartHighlight,
monitor,
monitors,
i,
);
},
),
label: Utils.merge(
[
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
options.bar.workspaces.icons.occupied.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
workspaceMask.bind('value'),
hyprland.bind('monitors'),
],
(
showIcons: boolean,
available: string,
active: string,
occupied: string,
wsIconMap: WorkspaceIconMap,
showWsIcons: boolean,
workspaceMask: boolean,
monitors: Monitor[],
) => {
return renderLabel(
showIcons,
available,
active,
occupied,
workspaceMask,
showWsIcons,
wsIconMap,
i,
index,
monitor,
monitors,
);
},
),
setup: (self) => {
self.hook(hyprland, () => {
self.toggleClassName('active', hyprland.active.workspace.id === i);
self.toggleClassName('occupied', (hyprland.getWorkspace(i)?.windows || 0) > 0);
});
},
}),
});
});
},
),
});
};
@@ -0,0 +1,145 @@
const hyprland = await Service.import('hyprland');
import options from 'options';
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
import { Monitor, Workspace } from 'types/service/hyprland';
import { getWsColor, renderClassnames, renderLabel } from '../utils';
import { range } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import { WorkspaceIconMap } from 'lib/types/workspace';
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored, showAllActive } = options.bar.workspaces;
export const occupiedWses = (monitor: number): BoxWidget => {
return Widget.Box({
children: Utils.merge(
[
monitorSpecific.bind('value'),
hyprland.bind('workspaces'),
workspaceMask.bind('value'),
workspaces.bind('value'),
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
options.bar.workspaces.icons.occupied.bind('value'),
options.bar.workspaces.show_numbered.bind('value'),
options.bar.workspaces.numbered_active_indicator.bind('value'),
spacing.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.theme.matugen.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
ignored.bind('value'),
showAllActive.bind('value'),
],
(
monitorSpecific: boolean,
wkSpaces: Workspace[],
workspaceMask: boolean,
totalWkspcs: number,
showIcons: boolean,
available: string,
active: string,
occupied: string,
showNumbered: boolean,
numberedActiveIndicator: string,
spacing: number,
wsIconMap: WorkspaceIconMap,
showWsIcons: boolean,
matugen: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
const activeId = hyprland.active.workspace.id;
let allWkspcs = range(totalWkspcs || 8);
const activeWorkspaces = wkSpaces.map((w) => w.id);
const workspaceRules = getWorkspaceRules();
// Sometimes hyprland doesn't have all the monitors in the list
// so we complement it with monitors from the workspace list
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({
id: m.monitorID,
name: m.monitor,
}));
const curMonitor =
hyprland.monitors.find((m) => m.id === monitor) ||
workspaceMonitorList.find((m) => m.id === monitor);
const workspacesWithRules = Object.keys(workspaceRules).reduce((acc: number[], k: string) => {
return [...acc, ...workspaceRules[k]];
}, [] as number[]);
const activesForMonitor = activeWorkspaces.filter((w) => {
if (
curMonitor &&
Object.hasOwnProperty.call(workspaceRules, curMonitor.name) &&
workspacesWithRules.includes(w)
) {
return workspaceRules[curMonitor.name].includes(w);
}
return true;
});
if (monitorSpecific) {
const wrkspcsInRange = range(totalWkspcs).filter((w) => {
return getWorkspacesForMonitor(w, workspaceRules, monitor);
});
allWkspcs = [...new Set([...activesForMonitor, ...wrkspcsInRange])];
} else {
allWkspcs = [...new Set([...allWkspcs, ...activeWorkspaces])];
}
return allWkspcs
.sort((a, b) => {
return a - b;
})
.map((i, index) => {
if (isWorkspaceIgnored(ignored, i)) {
return Widget.Box();
}
return Widget.Button({
class_name: 'workspace-button',
on_primary_click: () => {
hyprland.messageAsync(`dispatch workspace ${i}`);
},
child: Widget.Label({
attribute: i,
vpack: 'center',
css:
`margin: 0rem ${0.375 * spacing}rem;` +
`${showWsIcons && !matugen ? getWsColor(wsIconMap, i, smartHighlight, monitor, monitors) : ''}`,
class_name: renderClassnames(
showIcons,
showNumbered,
numberedActiveIndicator,
showWsIcons,
smartHighlight,
monitor,
monitors,
i,
),
label: renderLabel(
showIcons,
available,
active,
occupied,
workspaceMask,
showWsIcons,
wsIconMap,
i,
index,
monitor,
monitors,
),
setup: (self) => {
self.toggleClassName('active', activeId === i);
self.toggleClassName('occupied', (hyprland.getWorkspace(i)?.windows || 0) > 0);
},
}),
});
});
},
),
});
};