hyprpanel
This commit is contained in:
1 parent
10080a83ee
commit
5151ac24da
322 files changed
+38276
No files matched your search
@@ -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>);
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user