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);
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
export const substitutes = {
|
||||
'transmission-gtk': 'transmission',
|
||||
'blueberry.py': 'blueberry',
|
||||
Caprine: 'facebook-messenger',
|
||||
'com.raggesilver.BlackBox-symbolic': 'terminal-symbolic',
|
||||
'org.wezfurlong.wezterm-symbolic': 'terminal-symbolic',
|
||||
'audio-headset-bluetooth': 'audio-headphones-symbolic',
|
||||
'audio-card-analog-usb': 'audio-speakers-symbolic',
|
||||
'audio-card-analog-pci': 'audio-card-symbolic',
|
||||
'preferences-system': 'emblem-system-symbolic',
|
||||
'com.github.Aylur.ags-symbolic': 'controls-symbolic',
|
||||
'com.github.Aylur.ags': 'controls-symbolic',
|
||||
};
|
||||
|
||||
export default {
|
||||
missing: 'image-missing-symbolic',
|
||||
nix: {
|
||||
nix: 'nix-snowflake-symbolic',
|
||||
},
|
||||
app: {
|
||||
terminal: 'terminal-symbolic',
|
||||
},
|
||||
fallback: {
|
||||
executable: 'application-x-executable',
|
||||
notification: 'dialog-information-symbolic',
|
||||
video: 'video-x-generic-symbolic',
|
||||
audio: 'audio-x-generic-symbolic',
|
||||
},
|
||||
ui: {
|
||||
close: 'window-close-symbolic',
|
||||
colorpicker: 'color-select-symbolic',
|
||||
info: 'info-symbolic',
|
||||
link: 'external-link-symbolic',
|
||||
lock: 'system-lock-screen-symbolic',
|
||||
menu: 'open-menu-symbolic',
|
||||
refresh: 'view-refresh-symbolic',
|
||||
search: 'system-search-symbolic',
|
||||
settings: 'emblem-system-symbolic',
|
||||
themes: 'preferences-desktop-theme-symbolic',
|
||||
tick: 'object-select-symbolic',
|
||||
time: 'hourglass-symbolic',
|
||||
toolbars: 'toolbars-symbolic',
|
||||
warning: 'dialog-warning-symbolic',
|
||||
avatar: 'avatar-default-symbolic',
|
||||
arrow: {
|
||||
right: 'pan-end-symbolic',
|
||||
left: 'pan-start-symbolic',
|
||||
down: 'pan-down-symbolic',
|
||||
up: 'pan-up-symbolic',
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
mic: {
|
||||
muted: 'microphone-disabled-symbolic',
|
||||
low: 'microphone-sensitivity-low-symbolic',
|
||||
medium: 'microphone-sensitivity-medium-symbolic',
|
||||
high: 'microphone-sensitivity-high-symbolic',
|
||||
},
|
||||
volume: {
|
||||
muted: 'audio-volume-muted-symbolic',
|
||||
low: 'audio-volume-low-symbolic',
|
||||
medium: 'audio-volume-medium-symbolic',
|
||||
high: 'audio-volume-high-symbolic',
|
||||
overamplified: 'audio-volume-overamplified-symbolic',
|
||||
},
|
||||
type: {
|
||||
headset: 'audio-headphones-symbolic',
|
||||
speaker: 'audio-speakers-symbolic',
|
||||
card: 'audio-card-symbolic',
|
||||
},
|
||||
mixer: 'mixer-symbolic',
|
||||
},
|
||||
powerprofile: {
|
||||
balanced: 'power-profile-balanced-symbolic',
|
||||
'power-saver': 'power-profile-power-saver-symbolic',
|
||||
performance: 'power-profile-performance-symbolic',
|
||||
},
|
||||
asusctl: {
|
||||
profile: {
|
||||
Balanced: 'power-profile-balanced-symbolic',
|
||||
Quiet: 'power-profile-power-saver-symbolic',
|
||||
Performance: 'power-profile-performance-symbolic',
|
||||
},
|
||||
mode: {
|
||||
Integrated: 'processor-symbolic',
|
||||
Hybrid: 'controller-symbolic',
|
||||
},
|
||||
},
|
||||
battery: {
|
||||
charging: 'battery-flash-symbolic',
|
||||
warning: 'battery-empty-symbolic',
|
||||
},
|
||||
bluetooth: {
|
||||
enabled: 'bluetooth-active-symbolic',
|
||||
disabled: 'bluetooth-disabled-symbolic',
|
||||
},
|
||||
brightness: {
|
||||
indicator: 'display-brightness-symbolic',
|
||||
keyboard: 'keyboard-brightness-symbolic',
|
||||
screen: 'display-brightness-symbolic',
|
||||
},
|
||||
powermenu: {
|
||||
sleep: 'weather-clear-night-symbolic',
|
||||
reboot: 'system-reboot-symbolic',
|
||||
logout: 'system-log-out-symbolic',
|
||||
shutdown: 'system-shutdown-symbolic',
|
||||
},
|
||||
recorder: {
|
||||
recording: 'media-record-symbolic',
|
||||
},
|
||||
notifications: {
|
||||
noisy: 'org.gnome.Settings-notifications-symbolic',
|
||||
silent: 'notifications-disabled-symbolic',
|
||||
message: 'chat-bubbles-symbolic',
|
||||
},
|
||||
trash: {
|
||||
full: 'user-trash-full-symbolic',
|
||||
empty: 'user-trash-symbolic',
|
||||
},
|
||||
mpris: {
|
||||
shuffle: {
|
||||
enabled: 'media-playlist-shuffle-symbolic',
|
||||
disabled: 'media-playlist-consecutive-symbolic',
|
||||
},
|
||||
loop: {
|
||||
none: 'media-playlist-repeat-symbolic',
|
||||
track: 'media-playlist-repeat-song-symbolic',
|
||||
playlist: 'media-playlist-repeat-symbolic',
|
||||
},
|
||||
playing: 'media-playback-pause-symbolic',
|
||||
paused: 'media-playback-start-symbolic',
|
||||
stopped: 'media-playback-start-symbolic',
|
||||
prev: 'media-skip-backward-symbolic',
|
||||
next: 'media-skip-forward-symbolic',
|
||||
},
|
||||
system: {
|
||||
cpu: 'org.gnome.SystemMonitor-symbolic',
|
||||
ram: 'drive-harddisk-solidstate-symbolic',
|
||||
temp: 'temperature-symbolic',
|
||||
},
|
||||
color: {
|
||||
dark: 'dark-mode-symbolic',
|
||||
light: 'light-mode-symbolic',
|
||||
},
|
||||
weather: {
|
||||
warning: 'dialog-warning-symbolic',
|
||||
sunny: 'weather-clear-symbolic',
|
||||
clear: 'weather-clear-night-symbolic',
|
||||
partly_cloudy: 'weather-few-clouds-symbolic',
|
||||
partly_cloudy_night: 'weather-few-clouds-night-symbolic',
|
||||
cloudy: 'weather-overcast-symbolic',
|
||||
overcast: 'weather-overcast-symbolic',
|
||||
mist: 'weather-overcast-symbolic',
|
||||
patchy_rain_nearby: 'weather-showers-scattered-symbolic',
|
||||
patchy_rain_possible: 'weather-showers-scattered-symbolic',
|
||||
patchy_snow_possible: 'weather-snow-symbolic',
|
||||
patchy_sleet_possible: 'weather-snow-symbolic',
|
||||
patchy_freezing_drizzle_possible: 'weather-showers-scattered-symbolic',
|
||||
thundery_outbreaks_possible: 'weather-overcast-symbolic',
|
||||
blowing_snow: 'weather-snow-symbolic',
|
||||
blizzard: 'weather-snow-symbolic',
|
||||
fog: 'weather-fog-symbolic',
|
||||
freezing_fog: 'weather-fog-symbolic',
|
||||
patchy_light_drizzle: 'weather-showers-scattered-symbolic',
|
||||
light_drizzle: 'weather-showers-symbolic',
|
||||
freezing_drizzle: 'weather-showers-symbolic',
|
||||
heavy_freezing_drizzle: 'weather-showers-symbolic',
|
||||
patchy_light_rain: 'weather-showers-scattered-symbolic',
|
||||
light_rain: 'weather-showers-symbolic',
|
||||
moderate_rain_at_times: 'weather-showers-symbolic',
|
||||
moderate_rain: 'weather-showers-symbolic',
|
||||
heavy_rain_at_times: 'weather-showers-symbolic',
|
||||
heavy_rain: 'weather-showers-symbolic',
|
||||
light_freezing_rain: 'weather-showers-symbolic',
|
||||
moderate_or_heavy_freezing_rain: 'weather-showers-symbolic',
|
||||
light_sleet: 'weather-snow-symbolic',
|
||||
moderate_or_heavy_sleet: 'weather-snow-symbolic',
|
||||
patchy_light_snow: 'weather-snow-symbolic',
|
||||
light_snow: 'weather-snow-symbolic',
|
||||
patchy_moderate_snow: 'weather-snow-symbolic',
|
||||
moderate_snow: 'weather-snow-symbolic',
|
||||
patchy_heavy_snow: 'weather-snow-symbolic',
|
||||
heavy_snow: 'weather-snow-symbolic',
|
||||
ice_pellets: 'weather-showers-symbolic',
|
||||
light_rain_shower: 'weather-showers-symbolic',
|
||||
moderate_or_heavy_rain_shower: 'weather-showers-symbolic',
|
||||
torrential_rain_shower: 'weather-showers-symbolic',
|
||||
light_sleet_showers: 'weather-showers-symbolic',
|
||||
moderate_or_heavy_sleet_showers: 'weather-showers-symbolic',
|
||||
light_snow_showers: 'weather-snow-symbolic',
|
||||
moderate_or_heavy_snow_showers: 'weather-snow-symbolic',
|
||||
light_showers_of_ice_pellets: 'weather-showers-symbolic',
|
||||
moderate_or_heavy_showers_of_ice_pellets: 'weather-showers-symbolic',
|
||||
patchy_light_rain_with_thunder: 'weather-showers-scattered-symbolic',
|
||||
moderate_or_heavy_rain_with_thunder: 'weather-showers-symbolic',
|
||||
patchy_light_snow_with_thunder: 'weather-snow-symbolic',
|
||||
moderate_or_heavy_snow_with_thunder: 'weather-snow-symbolic',
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,54 @@
|
||||
export const weatherIcons = {
|
||||
warning: '',
|
||||
sunny: '',
|
||||
clear: '',
|
||||
partly_cloudy: '',
|
||||
partly_cloudy_night: '',
|
||||
cloudy: '',
|
||||
overcast: '',
|
||||
mist: '',
|
||||
patchy_rain_nearby: '',
|
||||
patchy_rain_possible: '',
|
||||
patchy_snow_possible: '',
|
||||
patchy_sleet_possible: '',
|
||||
patchy_freezing_drizzle_possible: '',
|
||||
thundery_outbreaks_possible: '',
|
||||
blowing_snow: '',
|
||||
blizzard: '',
|
||||
fog: '',
|
||||
freezing_fog: '',
|
||||
patchy_light_drizzle: '',
|
||||
light_drizzle: '',
|
||||
freezing_drizzle: '',
|
||||
heavy_freezing_drizzle: '',
|
||||
patchy_light_rain: '',
|
||||
light_rain: '',
|
||||
moderate_rain_at_times: '',
|
||||
moderate_rain: '',
|
||||
heavy_rain_at_times: '',
|
||||
heavy_rain: '',
|
||||
light_freezing_rain: '',
|
||||
moderate_or_heavy_freezing_rain: '',
|
||||
light_sleet: '',
|
||||
moderate_or_heavy_sleet: '',
|
||||
patchy_light_snow: '',
|
||||
light_snow: '',
|
||||
patchy_moderate_snow: '',
|
||||
moderate_snow: '',
|
||||
patchy_heavy_snow: '',
|
||||
heavy_snow: '',
|
||||
ice_pellets: '',
|
||||
light_rain_shower: '',
|
||||
moderate_or_heavy_rain_shower: '',
|
||||
torrential_rain_shower: '',
|
||||
light_sleet_showers: '',
|
||||
moderate_or_heavy_sleet_showers: '',
|
||||
light_snow_showers: '',
|
||||
moderate_or_heavy_snow_showers: '',
|
||||
light_showers_of_ice_pellets: '',
|
||||
moderate_or_heavy_showers_of_ice_pellets: '',
|
||||
patchy_light_rain_with_thunder: '',
|
||||
moderate_or_heavy_rain_with_thunder: '',
|
||||
patchy_light_snow_with_thunder: '',
|
||||
moderate_or_heavy_snow_with_thunder: '',
|
||||
} as const;
|
||||
@@ -0,0 +1,75 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { getIcon } from '../utils.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const renderActiveInput = (): Box<Child, Attribute>[] => {
|
||||
return [
|
||||
Widget.Box({
|
||||
class_name: 'menu-slider-container input',
|
||||
children: [
|
||||
Widget.Button({
|
||||
vexpand: false,
|
||||
vpack: 'end',
|
||||
setup: (self) => {
|
||||
const updateClass = (): void => {
|
||||
const mic = audio.microphone;
|
||||
const className = `menu-active-button input ${mic.is_muted ? 'muted' : ''}`;
|
||||
self.class_name = className;
|
||||
};
|
||||
|
||||
self.hook(audio.microphone, updateClass, 'notify::is-muted');
|
||||
},
|
||||
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
|
||||
child: Widget.Icon({
|
||||
class_name: 'menu-active-icon input',
|
||||
setup: (self) => {
|
||||
const updateIcon = (): void => {
|
||||
const isMicMuted =
|
||||
audio.microphone.is_muted !== null ? audio.microphone.is_muted : true;
|
||||
|
||||
if (audio.microphone.volume > 0) {
|
||||
self.icon = getIcon(audio.microphone.volume, isMicMuted)['mic'];
|
||||
} else {
|
||||
self.icon = getIcon(100, true)['mic'];
|
||||
}
|
||||
};
|
||||
self.hook(audio.microphone, updateIcon, 'notify::volume');
|
||||
self.hook(audio.microphone, updateIcon, 'notify::is-muted');
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-active input',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: audio.bind('microphone').as((v) => {
|
||||
return v.description === null ? 'No input device found...' : v.description;
|
||||
}),
|
||||
}),
|
||||
Widget.Slider({
|
||||
value: audio.microphone.bind('volume').as((v) => v),
|
||||
class_name: 'menu-active-slider menu-slider inputs',
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
min: 0,
|
||||
max: 1,
|
||||
onChange: ({ value }) => (audio.microphone.volume = value),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'menu-active-percentage input',
|
||||
vpack: 'end',
|
||||
label: audio.microphone.bind('volume').as((v) => `${Math.round(v * 100)}%`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export { renderActiveInput };
|
||||
@@ -0,0 +1,68 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { getIcon } from '../utils.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const renderActivePlayback = (): Box<Child, Attribute>[] => {
|
||||
return [
|
||||
Widget.Box({
|
||||
class_name: 'menu-slider-container playback',
|
||||
children: [
|
||||
Widget.Button({
|
||||
vexpand: false,
|
||||
vpack: 'end',
|
||||
setup: (self) => {
|
||||
const updateClass = (): void => {
|
||||
const spkr = audio.speaker;
|
||||
const className = `menu-active-button playback ${spkr.is_muted ? 'muted' : ''}`;
|
||||
self.class_name = className;
|
||||
};
|
||||
|
||||
self.hook(audio.speaker, updateClass, 'notify::is-muted');
|
||||
},
|
||||
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
|
||||
child: Widget.Icon({
|
||||
class_name: 'menu-active-icon playback',
|
||||
setup: (self) => {
|
||||
const updateIcon = (): void => {
|
||||
const isSpeakerMuted = audio.speaker.is_muted !== null ? audio.speaker.is_muted : true;
|
||||
self.icon = getIcon(audio.speaker.volume, isSpeakerMuted)['spkr'];
|
||||
};
|
||||
self.hook(audio.speaker, updateIcon, 'notify::volume');
|
||||
self.hook(audio.speaker, updateIcon, 'notify::is-muted');
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-active playback',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
expand: true,
|
||||
wrap: true,
|
||||
label: audio.bind('speaker').as((v) => v.description || ''),
|
||||
}),
|
||||
Widget.Slider({
|
||||
value: audio['speaker'].bind('volume'),
|
||||
class_name: 'menu-active-slider menu-slider playback',
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
min: 0,
|
||||
max: 1,
|
||||
onChange: ({ value }) => (audio.speaker.volume = value),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
vpack: 'end',
|
||||
class_name: 'menu-active-percentage playback',
|
||||
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export { renderActivePlayback };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { renderActiveInput } from './SelectedInput.js';
|
||||
import { renderActivePlayback } from './SelectedPlayback.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const activeDevices = (): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container volume',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container volume selected',
|
||||
hpack: 'fill',
|
||||
child: Widget.Label({
|
||||
class_name: 'menu-label audio volume',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Volume',
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section selected',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-active-container playback',
|
||||
vertical: true,
|
||||
children: renderActivePlayback(),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-active-container input',
|
||||
vertical: true,
|
||||
children: renderActiveInput(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { activeDevices };
|
||||
@@ -0,0 +1,66 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { InputDevices } from 'lib/types/audio';
|
||||
import { Stream } from 'types/service/audio';
|
||||
|
||||
const renderInputDevices = (inputDevices: Stream[]): InputDevices => {
|
||||
if (inputDevices.length === 0) {
|
||||
return [
|
||||
Widget.Button({
|
||||
class_name: `menu-unfound-button input`,
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'start',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-button-name input',
|
||||
label: 'No input devices found...',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
];
|
||||
}
|
||||
return inputDevices.map((device) => {
|
||||
return Widget.Button({
|
||||
on_primary_click: () => (audio.microphone = device),
|
||||
class_name: `menu-button audio input ${device}`,
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'start',
|
||||
children: [
|
||||
Widget.Label({
|
||||
wrap: true,
|
||||
class_name: audio.microphone
|
||||
.bind('description')
|
||||
.as((v) =>
|
||||
device.description === v
|
||||
? 'menu-button-icon active input txt-icon'
|
||||
: 'menu-button-icon input txt-icon',
|
||||
),
|
||||
label: '',
|
||||
}),
|
||||
Widget.Label({
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
class_name: audio.microphone
|
||||
.bind('description')
|
||||
.as((v) =>
|
||||
device.description === v
|
||||
? 'menu-button-name active input'
|
||||
: 'menu-button-name input',
|
||||
),
|
||||
label: device.description,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export { renderInputDevices };
|
||||
@@ -0,0 +1,60 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { PlaybackDevices } from 'lib/types/audio';
|
||||
import { Stream } from 'types/service/audio';
|
||||
|
||||
const renderPlaybacks = (playbackDevices: Stream[]): PlaybackDevices => {
|
||||
return playbackDevices.map((device) => {
|
||||
if (device.description === 'Dummy Output') {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-unfound-button playback',
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-button-name playback',
|
||||
label: 'No playback devices found...',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
}
|
||||
return Widget.Button({
|
||||
class_name: `menu-button audio playback ${device}`,
|
||||
on_primary_click: () => (audio.speaker = device),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'start',
|
||||
children: [
|
||||
Widget.Label({
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
class_name: audio.speaker
|
||||
.bind('description')
|
||||
.as((v) =>
|
||||
device.description === v
|
||||
? 'menu-button-icon active playback txt-icon'
|
||||
: 'menu-button-icon playback txt-icon',
|
||||
),
|
||||
label: '',
|
||||
}),
|
||||
Widget.Label({
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
class_name: audio.speaker
|
||||
.bind('description')
|
||||
.as((v) =>
|
||||
device.description === v
|
||||
? 'menu-button-name active playback'
|
||||
: 'menu-button-name playback',
|
||||
),
|
||||
label: device.description,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export { renderPlaybacks };
|
||||
@@ -0,0 +1,72 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
import { renderInputDevices } from './InputDevices.js';
|
||||
import { renderPlaybacks } from './PlaybackDevices.js';
|
||||
|
||||
const availableDevices = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-section-container playback',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container playback',
|
||||
hpack: 'fill',
|
||||
child: Widget.Label({
|
||||
class_name: 'menu-label audio playback',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Playback Devices',
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section playback',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-container playback',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: audio.bind('speakers').as((v) => renderPlaybacks(v)),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container input',
|
||||
hpack: 'fill',
|
||||
child: Widget.Label({
|
||||
class_name: 'menu-label audio input',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Input Devices',
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section input',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-container input',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: audio.bind('microphones').as((v) => renderInputDevices(v)),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { availableDevices };
|
||||
@@ -0,0 +1,25 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { activeDevices } from './active/index.js';
|
||||
import { availableDevices } from './available/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'audiomenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items audio',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container audio',
|
||||
children: [activeDevices(), availableDevices()],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const speakerIcons = {
|
||||
101: 'audio-volume-overamplified-symbolic',
|
||||
66: 'audio-volume-high-symbolic',
|
||||
34: 'audio-volume-medium-symbolic',
|
||||
1: 'audio-volume-low-symbolic',
|
||||
0: 'audio-volume-muted-symbolic',
|
||||
} as const;
|
||||
|
||||
const inputIcons = {
|
||||
101: 'microphone-sensitivity-high-symbolic',
|
||||
66: 'microphone-sensitivity-high-symbolic',
|
||||
34: 'microphone-sensitivity-medium-symbolic',
|
||||
1: 'microphone-sensitivity-low-symbolic',
|
||||
0: 'microphone-disabled-symbolic',
|
||||
};
|
||||
|
||||
type IconVolumes = keyof typeof speakerIcons;
|
||||
|
||||
const getIcon = (audioVol: number, isMuted: boolean): Record<string, string> => {
|
||||
const thresholds: IconVolumes[] = [101, 66, 34, 1, 0];
|
||||
const icon = isMuted ? 0 : thresholds.find((threshold) => threshold <= audioVol * 100) || 0;
|
||||
|
||||
return {
|
||||
spkr: speakerIcons[icon],
|
||||
mic: inputIcons[icon],
|
||||
};
|
||||
};
|
||||
|
||||
export { getIcon };
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { BluetoothDevice } from 'types/service/bluetooth';
|
||||
|
||||
const connectedControls = (dev: BluetoothDevice, connectedDevices: BluetoothDevice[]): BoxWidget => {
|
||||
if (!connectedDevices.includes(dev.address)) {
|
||||
return Widget.Box({});
|
||||
}
|
||||
|
||||
return Widget.Box({
|
||||
vpack: 'start',
|
||||
class_name: 'bluetooth-controls',
|
||||
children: [
|
||||
Widget.Button({
|
||||
class_name: 'menu-icon-button unpair bluetooth',
|
||||
child: Widget.Label({
|
||||
tooltip_text: dev.paired ? 'Unpair' : 'Pair',
|
||||
class_name: 'menu-icon-button-label unpair bluetooth txt-icon',
|
||||
label: dev.paired ? '' : '',
|
||||
}),
|
||||
on_primary_click: () =>
|
||||
Utils.execAsync([
|
||||
'bash',
|
||||
'-c',
|
||||
`bluetoothctl ${dev.paired ? 'unpair' : 'pair'} ${dev.address}`,
|
||||
]).catch((err) =>
|
||||
console.error(`bluetoothctl ${dev.paired ? 'unpair' : 'pair'} ${dev.address}`, err),
|
||||
),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'menu-icon-button disconnect bluetooth',
|
||||
child: Widget.Label({
|
||||
tooltip_text: dev.connected ? 'Disconnect' : 'Connect',
|
||||
class_name: 'menu-icon-button-label disconnect bluetooth txt-icon',
|
||||
label: dev.connected ? '' : '',
|
||||
}),
|
||||
on_primary_click: () => dev.setConnection(!dev.connected),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'menu-icon-button untrust bluetooth',
|
||||
child: Widget.Label({
|
||||
tooltip_text: dev.trusted ? 'Untrust' : 'Trust',
|
||||
class_name: 'menu-icon-button-label untrust bluetooth txt-icon',
|
||||
label: dev.trusted ? '' : '',
|
||||
}),
|
||||
on_primary_click: () =>
|
||||
Utils.execAsync([
|
||||
'bash',
|
||||
'-c',
|
||||
`bluetoothctl ${dev.trusted ? 'untrust' : 'trust'} ${dev.address}`,
|
||||
]).catch((err) =>
|
||||
console.error(`bluetoothctl ${dev.trusted ? 'untrust' : 'trust'} ${dev.address}`, err),
|
||||
),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'menu-icon-button delete bluetooth',
|
||||
child: Widget.Label({
|
||||
tooltip_text: 'Forget',
|
||||
class_name: 'menu-icon-button-label delete bluetooth txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
on_primary_click: () => {
|
||||
Utils.execAsync(['bash', '-c', `bluetoothctl remove ${dev.address}`]).catch((err) =>
|
||||
console.error('Bluetooth Remove', err),
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { connectedControls };
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Bluetooth } from 'types/service/bluetooth.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { connectedControls } from './connectedControls.js';
|
||||
import { getBluetoothIcon } from '../utils.js';
|
||||
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const devices = (bluetooth: Bluetooth, self: Box<Gtk.Widget, unknown>): Box<Child, Attribute> => {
|
||||
return self.hook(bluetooth, () => {
|
||||
if (!bluetooth.enabled) {
|
||||
return (self.child = Widget.Box({
|
||||
class_name: 'bluetooth-items',
|
||||
vertical: true,
|
||||
expand: true,
|
||||
vpack: 'center',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'bluetooth-disabled dim',
|
||||
hexpand: true,
|
||||
label: 'Bluetooth is disabled',
|
||||
}),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
const availableDevices = bluetooth.devices
|
||||
.filter((btDev) => btDev.name !== null)
|
||||
.sort((a, b) => {
|
||||
if (a.connected || a.paired) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.connected || b.paired) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return b.name - a.name;
|
||||
});
|
||||
|
||||
const conDevNames = availableDevices.filter((d) => d.connected || d.paired).map((d) => d.address);
|
||||
|
||||
if (!availableDevices.length) {
|
||||
return (self.child = Widget.Box({
|
||||
class_name: 'bluetooth-items',
|
||||
vertical: true,
|
||||
expand: true,
|
||||
vpack: 'center',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'no-bluetooth-devices dim',
|
||||
hexpand: true,
|
||||
label: 'No devices currently found',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'search-bluetooth-label dim',
|
||||
hexpand: true,
|
||||
label: "Press '' to search",
|
||||
}),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
return (self.child = Widget.Box({
|
||||
vertical: true,
|
||||
children: availableDevices.map((device) => {
|
||||
return Widget.Box({
|
||||
children: [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: `bluetooth-element-item ${device}`,
|
||||
on_primary_click: () => {
|
||||
if (!conDevNames.includes(device.address)) device.setConnection(true);
|
||||
},
|
||||
child: Widget.Box({
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
class_name: 'menu-button-container',
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'start',
|
||||
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? 'active' : ''} txt-icon`,
|
||||
label: getBluetoothIcon(`${device['icon_name']}-symbolic`),
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
vpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'center',
|
||||
hpack: 'start',
|
||||
class_name: 'menu-button-name bluetooth',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: device.alias,
|
||||
}),
|
||||
Widget.Revealer({
|
||||
hpack: 'start',
|
||||
reveal_child: device.connected || device.paired,
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
class_name: 'connection-status dim',
|
||||
label: device.connected ? 'Connected' : 'Paired',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'end',
|
||||
children: device.connecting
|
||||
? [
|
||||
Widget.Spinner({
|
||||
vpack: 'start',
|
||||
class_name: 'spinner bluetooth',
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
connectedControls(device, conDevNames),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
export { devices };
|
||||
@@ -0,0 +1,26 @@
|
||||
const bluetooth = await Service.import('bluetooth');
|
||||
import { label } from './label.js';
|
||||
import { devices } from './devicelist.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
const Devices = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
label(bluetooth),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section',
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-content',
|
||||
vertical: true,
|
||||
setup: (self) => {
|
||||
devices(bluetooth, self);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Devices };
|
||||
@@ -0,0 +1,65 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { Bluetooth } from 'types/service/bluetooth';
|
||||
|
||||
const label = (bluetooth: Bluetooth): BoxWidget => {
|
||||
const searchInProgress = Variable(false);
|
||||
|
||||
const startRotation = (): void => {
|
||||
searchInProgress.value = true;
|
||||
setTimeout(() => {
|
||||
searchInProgress.value = false;
|
||||
}, 10 * 1000);
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'menu-label-container',
|
||||
hpack: 'fill',
|
||||
vpack: 'start',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-label',
|
||||
vpack: 'center',
|
||||
hpack: 'start',
|
||||
label: 'Bluetooth',
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'controls-container',
|
||||
vpack: 'start',
|
||||
children: [
|
||||
Widget.Switch({
|
||||
class_name: 'menu-switch bluetooth',
|
||||
hexpand: true,
|
||||
hpack: 'end',
|
||||
active: bluetooth.bind('enabled'),
|
||||
on_activate: ({ active }) => {
|
||||
searchInProgress.value = false;
|
||||
Utils.execAsync(['bash', '-c', `bluetoothctl power ${active ? 'on' : 'off'}`]).catch(
|
||||
(err) => console.error(`bluetoothctl power ${active ? 'on' : 'off'}`, err),
|
||||
);
|
||||
},
|
||||
}),
|
||||
Widget.Separator({
|
||||
class_name: 'menu-separator bluetooth',
|
||||
}),
|
||||
Widget.Button({
|
||||
vpack: 'center',
|
||||
class_name: 'menu-icon-button search',
|
||||
on_primary_click: () => {
|
||||
startRotation();
|
||||
Utils.execAsync(['bash', '-c', 'bluetoothctl --timeout 120 scan on']).catch((err) => {
|
||||
searchInProgress.value = false;
|
||||
console.error('bluetoothctl --timeout 120 scan on', err);
|
||||
});
|
||||
},
|
||||
child: Widget.Icon({
|
||||
class_name: searchInProgress.bind('value').as((v) => (v ? 'spinning' : '')),
|
||||
icon: 'view-refresh-symbolic',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { label };
|
||||
@@ -0,0 +1,24 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { Devices } from './devices/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'bluetoothmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items bluetooth',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container bluetooth',
|
||||
child: Devices(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const getBluetoothIcon = (iconName: string): string => {
|
||||
const deviceIconMap = [
|
||||
['^audio-card*', ''],
|
||||
['^audio-headphones*', ''],
|
||||
['^audio-headset*', ''],
|
||||
['^audio-input*', ''],
|
||||
['^audio-speakers*', ''],
|
||||
['^bluetooth*', ''],
|
||||
['^camera*', ''],
|
||||
['^computer*', ''],
|
||||
['^input-gaming*', ''],
|
||||
['^input-keyboard*', ''],
|
||||
['^input-mouse*', ''],
|
||||
['^input-tablet*', ''],
|
||||
['^media*', ''],
|
||||
['^modem*', ''],
|
||||
['^network*', ''],
|
||||
['^phone*', ''],
|
||||
['^printer*', ''],
|
||||
['^scanner*', ''],
|
||||
['^video-camera*', ''],
|
||||
];
|
||||
|
||||
const foundMatch = deviceIconMap.find((icon) => RegExp(icon[0]).test(iconName.toLowerCase()));
|
||||
|
||||
return foundMatch ? foundMatch[1] : '';
|
||||
};
|
||||
|
||||
export { getBluetoothIcon };
|
||||
@@ -0,0 +1,24 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
const CalendarWidget = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'calendar-menu-item-container calendar',
|
||||
hpack: 'fill',
|
||||
vpack: 'fill',
|
||||
expand: true,
|
||||
child: Widget.Box({
|
||||
class_name: 'calendar-container-box',
|
||||
child: Widget.Calendar({
|
||||
expand: true,
|
||||
hpack: 'fill',
|
||||
vpack: 'fill',
|
||||
class_name: 'calendar-menu-widget',
|
||||
showDayNames: true,
|
||||
showDetails: false,
|
||||
showHeading: true,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { CalendarWidget };
|
||||
@@ -0,0 +1,36 @@
|
||||
import DropdownMenu from 'modules/menus/shared/dropdown/index';
|
||||
import { TimeWidget } from './time/index';
|
||||
import { CalendarWidget } from './calendar';
|
||||
import { WeatherWidget } from './weather/index';
|
||||
import options from 'options';
|
||||
import Window from 'types/widgets/window';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
|
||||
const { enabled: weatherEnabled } = options.menus.clock.weather;
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'calendarmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'calendar-menu-content',
|
||||
css: 'padding: 1px; margin: -1px;',
|
||||
vexpand: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'calendar-content-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'calendar-content-items',
|
||||
vertical: true,
|
||||
children: weatherEnabled.bind('value').as((isWeatherEnabled) => {
|
||||
return [TimeWidget(), CalendarWidget(), ...(isWeatherEnabled ? [WeatherWidget()] : [])];
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import options from 'options';
|
||||
|
||||
const { military, hideSeconds } = options.menus.clock.time;
|
||||
|
||||
const time = Variable('', {
|
||||
poll: [1000, 'date "+%I:%M:%S"'],
|
||||
});
|
||||
|
||||
const period = Variable('', {
|
||||
poll: [1000, 'date "+%p"'],
|
||||
});
|
||||
|
||||
const militaryTime = Variable('', {
|
||||
poll: [1000, 'date "+%H:%M:%S"'],
|
||||
});
|
||||
|
||||
const TimeWidget = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'calendar-menu-item-container clock',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
hpack: 'fill',
|
||||
child: Widget.Box({
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
hpack: 'center',
|
||||
class_name: 'clock-content-items',
|
||||
children: Utils.merge(
|
||||
[military.bind('value'), hideSeconds.bind('value')],
|
||||
(is24hr: boolean, hideSeconds: boolean) => {
|
||||
if (!is24hr) {
|
||||
return [
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'clock-content-time',
|
||||
label: hideSeconds ? time.bind().as((str) => str.slice(0, -3)) : time.bind(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'end',
|
||||
class_name: 'clock-content-period',
|
||||
label: period.bind(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'clock-content-time',
|
||||
label: hideSeconds
|
||||
? militaryTime.bind().as((str) => str.slice(0, -3))
|
||||
: militaryTime.bind(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
},
|
||||
),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { TimeWidget };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Weather, WeatherIconTitle } from 'lib/types/weather.js';
|
||||
import { Variable } from 'types/variable.js';
|
||||
import { weatherIcons } from 'modules/icons/weather.js';
|
||||
import { isValidWeatherIconTitle } from 'globals/weather';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { getNextEpoch } from '../utils';
|
||||
|
||||
export const HourlyIcon = (theWeather: Variable<Weather>, hoursFromNow: number): BoxWidget => {
|
||||
const getIconQuery = (wthr: Weather): WeatherIconTitle => {
|
||||
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
|
||||
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
|
||||
|
||||
if (weatherAtEpoch === undefined) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
let iconQuery = weatherAtEpoch.condition.text.trim().toLowerCase().replaceAll(' ', '_');
|
||||
|
||||
if (!weatherAtEpoch?.is_day && iconQuery === 'partly_cloudy') {
|
||||
iconQuery = 'partly_cloudy_night';
|
||||
}
|
||||
|
||||
if (isValidWeatherIconTitle(iconQuery)) {
|
||||
return iconQuery;
|
||||
} else {
|
||||
return 'warning';
|
||||
}
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
hpack: 'center',
|
||||
child: theWeather.bind('value').as((w) => {
|
||||
const iconQuery = getIconQuery(w);
|
||||
const weatherIcn = weatherIcons[iconQuery] || weatherIcons['warning'];
|
||||
|
||||
return Widget.Label({
|
||||
hpack: 'center',
|
||||
class_name: 'hourly-weather-icon txt-icon',
|
||||
label: weatherIcn,
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import { HourlyIcon } from './icon/index.js';
|
||||
import { HourlyTemp } from './temperature/index.js';
|
||||
import { HourlyTime } from './time/index.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
export const Hourly = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
vertical: false,
|
||||
hexpand: true,
|
||||
hpack: 'fill',
|
||||
class_name: 'hourly-weather-container',
|
||||
children: [1, 2, 3, 4].map((hoursFromNow) => {
|
||||
return Widget.Box({
|
||||
class_name: 'hourly-weather-item',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
HourlyTime(theWeather, hoursFromNow),
|
||||
HourlyIcon(theWeather, hoursFromNow),
|
||||
HourlyTemp(theWeather, hoursFromNow),
|
||||
],
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import options from 'options';
|
||||
import Label from 'types/widgets/label';
|
||||
import { Child } from 'lib/types/widget';
|
||||
import { getNextEpoch } from '../utils';
|
||||
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const HourlyTemp = (theWeather: Variable<Weather>, hoursFromNow: number): Label<Child> => {
|
||||
return Widget.Label({
|
||||
class_name: 'hourly-weather-temp',
|
||||
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
|
||||
if (!Object.keys(wthr).length) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
|
||||
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
|
||||
|
||||
if (unt === 'imperial') {
|
||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : '-'}° F`;
|
||||
}
|
||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : '-'}° C`;
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Child } from 'lib/types/widget';
|
||||
import { Variable } from 'types/variable';
|
||||
import Label from 'types/widgets/label';
|
||||
import { getNextEpoch } from '../utils';
|
||||
|
||||
export const HourlyTime = (theWeather: Variable<Weather>, hoursFromNow: number): Label<Child> => {
|
||||
return Widget.Label({
|
||||
class_name: 'hourly-weather-time',
|
||||
label: theWeather.bind('value').as((w) => {
|
||||
if (!Object.keys(w).length) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const nextEpoch = getNextEpoch(w, hoursFromNow);
|
||||
const dateAtEpoch = new Date(nextEpoch * 1000);
|
||||
let hours = dateAtEpoch.getHours();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12 || 12;
|
||||
|
||||
return `${hours}${ampm}`;
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
|
||||
export const getNextEpoch = (wthr: Weather, hoursFromNow: number): number => {
|
||||
const currentEpoch = wthr.location.localtime_epoch;
|
||||
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
|
||||
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
|
||||
|
||||
const curHour = new Date(currentEpoch * 1000).getHours();
|
||||
|
||||
/*
|
||||
* NOTE: Since the API is only capable of showing the current day; if
|
||||
* the hours left in the day are less than 4 (aka spilling into the next day),
|
||||
* then rewind to contain the prediction within the current day.
|
||||
*/
|
||||
if (curHour > 19) {
|
||||
const hoursToRewind = curHour - 19;
|
||||
nextEpoch = 3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600;
|
||||
}
|
||||
return nextEpoch;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import { getWeatherStatusTextIcon } from 'globals/weather.js';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
export const TodayIcon = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
vpack: 'center',
|
||||
hpack: 'start',
|
||||
class_name: 'calendar-menu-weather today icon container',
|
||||
child: Widget.Label({
|
||||
class_name: 'calendar-menu-weather today icon txt-icon',
|
||||
label: theWeather.bind('value').as((w) => {
|
||||
return getWeatherStatusTextIcon(w);
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TodayIcon } from './icon/index.js';
|
||||
import { TodayStats } from './stats/index.js';
|
||||
import { TodayTemperature } from './temperature/index.js';
|
||||
import { Hourly } from './hourly/index.js';
|
||||
import { globalWeatherVar } from 'globals/weather.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
const WeatherWidget = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'calendar-menu-item-container weather',
|
||||
child: Widget.Box({
|
||||
class_name: 'weather-container-box',
|
||||
setup: (self) => {
|
||||
return (self.child = Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'calendar-menu-weather today',
|
||||
hexpand: true,
|
||||
children: [
|
||||
TodayIcon(globalWeatherVar),
|
||||
TodayTemperature(globalWeatherVar),
|
||||
TodayStats(globalWeatherVar),
|
||||
],
|
||||
}),
|
||||
Widget.Separator({
|
||||
class_name: 'menu-separator weather',
|
||||
}),
|
||||
Hourly(globalWeatherVar),
|
||||
],
|
||||
}));
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { WeatherWidget };
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import options from 'options';
|
||||
import { Unit } from 'lib/types/options';
|
||||
import { getRainChance, getWindConditions } from 'globals/weather';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const TodayStats = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'calendar-menu-weather today stats container',
|
||||
hpack: 'end',
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'weather wind',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'weather wind icon txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'weather wind label',
|
||||
label: Utils.merge(
|
||||
[theWeather.bind('value'), unit.bind('value')],
|
||||
(wthr: Weather, unt: Unit) => {
|
||||
return getWindConditions(wthr, unt);
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'weather precip',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'weather precip icon txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'weather precip label',
|
||||
label: theWeather.bind('value').as((v) => getRainChance(v)),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import options from 'options';
|
||||
import { getTemperature, getWeatherIcon } from 'globals/weather';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const TodayTemperature = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
class_name: 'calendar-menu-weather today temp container',
|
||||
vertical: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'calendar-menu-weather today temp label',
|
||||
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
|
||||
return getTemperature(wthr, unt);
|
||||
}),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: theWeather
|
||||
.bind('value')
|
||||
.as(
|
||||
(v) =>
|
||||
`calendar-menu-weather today temp label icon txt-icon ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
|
||||
),
|
||||
label: theWeather
|
||||
.bind('value')
|
||||
.as((v) => getWeatherIcon(Math.ceil(v.current.temp_f)).icon),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
child: Widget.Label({
|
||||
max_width_chars: 17,
|
||||
truncate: 'end',
|
||||
lines: 2,
|
||||
class_name: theWeather
|
||||
.bind('value')
|
||||
.as(
|
||||
(v) =>
|
||||
`calendar-menu-weather today condition label ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
|
||||
),
|
||||
label: theWeather.bind('value').as((v) => v.current.condition.text),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
const network = await Service.import('network');
|
||||
const bluetooth = await Service.import('bluetooth');
|
||||
const notifications = await Service.import('notifications');
|
||||
const audio = await Service.import('audio');
|
||||
|
||||
const Controls = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'dashboard-card controls-container',
|
||||
hpack: 'fill',
|
||||
vpack: 'fill',
|
||||
expand: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
tooltip_text: 'Toggle Wifi',
|
||||
expand: true,
|
||||
setup: (self) => {
|
||||
self.hook(network, () => {
|
||||
return (self.class_name = `dashboard-button wifi ${!network.wifi.enabled ? 'disabled' : ''}`);
|
||||
});
|
||||
},
|
||||
on_primary_click: () => network.toggleWifi(),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
setup: (self) => {
|
||||
self.hook(network, () => {
|
||||
return (self.label = network.wifi.enabled ? '' : '');
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
tooltip_text: 'Toggle Bluetooth',
|
||||
expand: true,
|
||||
class_name: bluetooth
|
||||
.bind('enabled')
|
||||
.as((btOn) => `dashboard-button bluetooth ${!btOn ? 'disabled' : ''}`),
|
||||
on_primary_click: () => bluetooth.toggle(),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
label: bluetooth.bind('enabled').as((btOn) => (btOn ? '' : '')),
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
tooltip_text: 'Toggle Notifications',
|
||||
expand: true,
|
||||
class_name: notifications
|
||||
.bind('dnd')
|
||||
.as((dnd) => `dashboard-button notifications ${dnd ? 'disabled' : ''}`),
|
||||
on_primary_click: () => (notifications.dnd = !notifications.dnd),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
label: notifications.bind('dnd').as((dnd) => (dnd ? '' : '')),
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
tooltip_text: 'Toggle Mute (Playback)',
|
||||
expand: true,
|
||||
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
return (self.class_name = `dashboard-button playback ${audio.speaker.is_muted ? 'disabled' : ''}`);
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
return (self.label = audio.speaker.is_muted ? '' : '');
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
tooltip_text: 'Toggle Mute (Microphone)',
|
||||
expand: true,
|
||||
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
return (self.class_name = `dashboard-button input ${audio.microphone.is_muted ? 'disabled' : ''}`);
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
return (self.label = audio.microphone.is_muted ? '' : '');
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Controls };
|
||||
@@ -0,0 +1,121 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import options from 'options';
|
||||
|
||||
const { left, right } = options.menus.dashboard.directories;
|
||||
|
||||
const Directories = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'dashboard-card directories-container',
|
||||
vpack: 'fill',
|
||||
hpack: 'fill',
|
||||
expand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
expand: true,
|
||||
class_name: 'section right',
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'start',
|
||||
expand: true,
|
||||
class_name: 'directory-link left top',
|
||||
on_primary_click: left.directory1.command.bind('value').as((cmd) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(cmd);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
label: left.directory1.label.bind('value'),
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
expand: true,
|
||||
hpack: 'start',
|
||||
class_name: 'directory-link left middle',
|
||||
on_primary_click: left.directory2.command.bind('value').as((cmd) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(cmd);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
label: left.directory2.label.bind('value'),
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
expand: true,
|
||||
hpack: 'start',
|
||||
class_name: 'directory-link left bottom',
|
||||
on_primary_click: left.directory3.command.bind('value').as((cmd) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(cmd);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
label: left.directory3.label.bind('value'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
expand: true,
|
||||
class_name: 'section left',
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'start',
|
||||
expand: true,
|
||||
class_name: 'directory-link right top',
|
||||
on_primary_click: right.directory1.command.bind('value').as((cmd) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(cmd);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
label: right.directory1.label.bind('value'),
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
expand: true,
|
||||
hpack: 'start',
|
||||
class_name: 'directory-link right middle',
|
||||
on_primary_click: right.directory2.command.bind('value').as((cmd) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(cmd);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
label: right.directory2.label.bind('value'),
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
expand: true,
|
||||
hpack: 'start',
|
||||
class_name: 'directory-link right bottom',
|
||||
on_primary_click: right.directory3.command.bind('value').as((cmd) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(cmd);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
label: right.directory3.label.bind('value'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Directories };
|
||||
@@ -0,0 +1,52 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { Profile } from './profile/index.js';
|
||||
import { Shortcuts } from './shortcuts/index.js';
|
||||
import { Controls } from './controls/index.js';
|
||||
import { Stats } from './stats/index.js';
|
||||
import { Directories } from './directories/index.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
const { controls, shortcuts, stats, directories } = options.menus.dashboard;
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'dashboardmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'dashboard-menu-content',
|
||||
css: 'padding: 1px; margin: -1px;',
|
||||
vexpand: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'dashboard-content-container',
|
||||
vertical: true,
|
||||
children: Utils.merge(
|
||||
[
|
||||
controls.enabled.bind('value'),
|
||||
shortcuts.enabled.bind('value'),
|
||||
stats.enabled.bind('value'),
|
||||
directories.enabled.bind('value'),
|
||||
],
|
||||
(isControlsEnabled, isShortcutsEnabled, isStatsEnabled, isDirectoriesEnabled) => {
|
||||
return [
|
||||
Widget.Box({
|
||||
class_name: 'dashboard-content-items',
|
||||
vertical: true,
|
||||
children: [
|
||||
Profile(),
|
||||
...(isShortcutsEnabled ? [Shortcuts()] : []),
|
||||
...(isControlsEnabled ? [Controls()] : []),
|
||||
...(isDirectoriesEnabled ? [Directories()] : []),
|
||||
...(isStatsEnabled ? [Stats()] : []),
|
||||
],
|
||||
}),
|
||||
];
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import powermenu from '../../power/helpers/actions.js';
|
||||
import { PowerOptions } from 'lib/types/options.js';
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
|
||||
import options from 'options';
|
||||
import { BoxWidget, Child } from 'lib/types/widget.js';
|
||||
import Label from 'types/widgets/label.js';
|
||||
const { image, name } = options.menus.dashboard.powermenu.avatar;
|
||||
const { confirmation, shutdown, logout, sleep, reboot } = options.menus.dashboard.powermenu;
|
||||
|
||||
const Profile = (): BoxWidget => {
|
||||
const handleClick = (action: PowerOptions): void => {
|
||||
const actions = {
|
||||
shutdown: shutdown.value,
|
||||
reboot: reboot.value,
|
||||
logout: logout.value,
|
||||
sleep: sleep.value,
|
||||
};
|
||||
App.closeWindow('dashboardmenu');
|
||||
|
||||
if (!confirmation.value) {
|
||||
Utils.execAsync(actions[action]).catch((err) =>
|
||||
console.error(`Failed to execute ${action} command. Error: ${err}`),
|
||||
);
|
||||
} else {
|
||||
powermenu.action(action);
|
||||
}
|
||||
};
|
||||
|
||||
const getIconForButton = (txtIcon: string): Label<Child> => {
|
||||
return Widget.Label({
|
||||
className: 'txt-icon',
|
||||
label: txtIcon,
|
||||
});
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'profiles-container',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'profile-picture-container dashboard-card',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
class_name: 'profile-picture',
|
||||
css: image.bind('value').as((i) => {
|
||||
try {
|
||||
GdkPixbuf.Pixbuf.new_from_file(i);
|
||||
return `background-image: url("${i}")`;
|
||||
} catch {
|
||||
return `background-image: url("${App.configDir}/assets/hyprpanel.png")`;
|
||||
}
|
||||
}),
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'center',
|
||||
class_name: 'profile-name',
|
||||
label: name.bind('value').as((v) => {
|
||||
if (v === 'system') {
|
||||
return Utils.exec('bash -c whoami');
|
||||
}
|
||||
return v;
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'power-menu-container dashboard-card',
|
||||
vertical: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button shutdown',
|
||||
on_clicked: () => handleClick('shutdown'),
|
||||
tooltip_text: 'Shut Down',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button restart',
|
||||
on_clicked: () => handleClick('reboot'),
|
||||
tooltip_text: 'Restart',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button lock',
|
||||
on_clicked: () => handleClick('logout'),
|
||||
tooltip_text: 'Log Out',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button sleep',
|
||||
on_clicked: () => handleClick('sleep'),
|
||||
tooltip_text: 'Sleep',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Profile };
|
||||
@@ -0,0 +1,317 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
import { Attribute, BoxWidget, Child } from 'lib/types/widget';
|
||||
import options from 'options';
|
||||
import { Variable as VarType } from 'types/variable';
|
||||
import Box from 'types/widgets/box';
|
||||
import Button from 'types/widgets/button';
|
||||
import Label from 'types/widgets/label';
|
||||
|
||||
const { left, right } = options.menus.dashboard.shortcuts;
|
||||
|
||||
const Shortcuts = (): BoxWidget => {
|
||||
const isRecording = Variable(false, {
|
||||
poll: [
|
||||
1000,
|
||||
`${App.configDir}/services/screen_record.sh status`,
|
||||
(out): boolean => {
|
||||
if (out === 'recording') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
],
|
||||
});
|
||||
const handleClick = (action: string, tOut: number = 250): void => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
|
||||
setTimeout(() => {
|
||||
Utils.execAsync(action)
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => err);
|
||||
}, tOut);
|
||||
};
|
||||
|
||||
const recordingDropdown = Widget.Menu({
|
||||
class_name: 'dropdown recording',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
setup: (self) => {
|
||||
const renderMonitorList = (): void => {
|
||||
const displays = hyprland.monitors.map((mon) => {
|
||||
return Widget.MenuItem({
|
||||
label: `Display ${mon.name}`,
|
||||
on_activate: () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`${App.configDir}/services/screen_record.sh start ${mon.name}`).catch(
|
||||
(err) => console.error(err),
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: This is disabled since window recording isn't available on wayland
|
||||
// const apps = hyprland.clients.map((clt) => {
|
||||
// return Widget.MenuItem({
|
||||
// label: `${clt.class.charAt(0).toUpperCase() + clt.class.slice(1)} (Workspace ${clt.workspace.name})`,
|
||||
// on_activate: () => {
|
||||
// App.closeWindow('dashboardmenu');
|
||||
// Utils.execAsync(
|
||||
// `${App.configDir}/services/screen_record.sh start ${clt.focusHistoryID}`,
|
||||
// ).catch((err) => console.error(err));
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
self.children = [
|
||||
...displays,
|
||||
// Disabled since window recording isn't available on wayland
|
||||
// ...apps
|
||||
];
|
||||
};
|
||||
self.hook(hyprland, renderMonitorList, 'monitor-added');
|
||||
self.hook(hyprland, renderMonitorList, 'monitor-removed');
|
||||
},
|
||||
});
|
||||
|
||||
type ShortcutFixed = {
|
||||
tooltip: string;
|
||||
command: string;
|
||||
icon: string;
|
||||
configurable: false;
|
||||
};
|
||||
|
||||
type ShortcutVariable = {
|
||||
tooltip: VarType<string>;
|
||||
command: VarType<string>;
|
||||
icon: VarType<string>;
|
||||
configurable?: true;
|
||||
};
|
||||
|
||||
type Shortcut = ShortcutFixed | ShortcutVariable;
|
||||
|
||||
const cmdLn = (sCut: ShortcutVariable): boolean => {
|
||||
return sCut.command.value.length > 0;
|
||||
};
|
||||
|
||||
const leftCardHidden = Variable(
|
||||
!(cmdLn(left.shortcut1) || cmdLn(left.shortcut2) || cmdLn(left.shortcut3) || cmdLn(left.shortcut4)),
|
||||
);
|
||||
|
||||
const createButton = (shortcut: Shortcut, className: string): Button<Label<Attribute>, Attribute> => {
|
||||
if (shortcut.configurable !== false) {
|
||||
return Widget.Button({
|
||||
vexpand: true,
|
||||
tooltip_text: shortcut.tooltip.value,
|
||||
class_name: className,
|
||||
on_primary_click: () => handleClick(shortcut.command.value),
|
||||
child: Widget.Label({
|
||||
class_name: 'button-label txt-icon',
|
||||
label: shortcut.icon.value,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// handle non-configurable shortcut
|
||||
return Widget.Button({
|
||||
vexpand: true,
|
||||
tooltip_text: shortcut.tooltip,
|
||||
class_name: className,
|
||||
on_primary_click: (_, event) => {
|
||||
if (shortcut.command === 'settings-dialog') {
|
||||
App.closeWindow('dashboardmenu');
|
||||
App.toggleWindow('settings-dialog');
|
||||
} else if (shortcut.command === 'record') {
|
||||
if (isRecording.value === true) {
|
||||
App.closeWindow('dashboardmenu');
|
||||
return Utils.execAsync(`${App.configDir}/services/screen_record.sh stop`).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
} else {
|
||||
recordingDropdown.popup_at_pointer(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'button-label txt-icon',
|
||||
label: shortcut.icon,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createButtonIfCommandExists = (
|
||||
shortcut: Shortcut,
|
||||
className: string,
|
||||
command: string,
|
||||
): Button<Label<Attribute>, Attribute> | Box<Child, Attribute> => {
|
||||
if (command.length > 0) {
|
||||
return createButton(shortcut, className);
|
||||
}
|
||||
return Widget.Box();
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'shortcuts-container',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
child: Utils.merge(
|
||||
[
|
||||
left.shortcut1.command.bind('value'),
|
||||
left.shortcut2.command.bind('value'),
|
||||
left.shortcut1.tooltip.bind('value'),
|
||||
left.shortcut2.tooltip.bind('value'),
|
||||
left.shortcut1.icon.bind('value'),
|
||||
left.shortcut2.icon.bind('value'),
|
||||
left.shortcut3.command.bind('value'),
|
||||
left.shortcut4.command.bind('value'),
|
||||
left.shortcut3.tooltip.bind('value'),
|
||||
left.shortcut4.tooltip.bind('value'),
|
||||
left.shortcut3.icon.bind('value'),
|
||||
left.shortcut4.icon.bind('value'),
|
||||
],
|
||||
() => {
|
||||
const isVisibleLeft = cmdLn(left.shortcut1) || cmdLn(left.shortcut2);
|
||||
const isVisibleRight = cmdLn(left.shortcut3) || cmdLn(left.shortcut4);
|
||||
|
||||
if (!isVisibleLeft && !isVisibleRight) {
|
||||
leftCardHidden.value = true;
|
||||
return Widget.Box();
|
||||
}
|
||||
|
||||
leftCardHidden.value = false;
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'container most-used dashboard-card',
|
||||
children: [
|
||||
Widget.Box({
|
||||
className: `card-button-section-container ${isVisibleRight && isVisibleLeft ? 'visible' : ''}`,
|
||||
child: isVisibleLeft
|
||||
? Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut1,
|
||||
`dashboard-button top-button ${cmdLn(left.shortcut2) ? 'paired' : ''}`,
|
||||
left.shortcut1.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut2,
|
||||
'dashboard-button',
|
||||
left.shortcut2.command.value,
|
||||
),
|
||||
],
|
||||
})
|
||||
: Widget.Box({
|
||||
children: [],
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
className: 'card-button-section-container',
|
||||
child: isVisibleRight
|
||||
? Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut3,
|
||||
`dashboard-button top-button ${cmdLn(left.shortcut4) ? 'paired' : ''}`,
|
||||
left.shortcut3.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut4,
|
||||
'dashboard-button',
|
||||
left.shortcut4.command.value,
|
||||
),
|
||||
],
|
||||
})
|
||||
: Widget.Box({
|
||||
children: [],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
),
|
||||
}),
|
||||
Widget.Box({
|
||||
child: Utils.merge(
|
||||
[
|
||||
right.shortcut1.command.bind('value'),
|
||||
right.shortcut1.tooltip.bind('value'),
|
||||
right.shortcut1.icon.bind('value'),
|
||||
right.shortcut3.command.bind('value'),
|
||||
right.shortcut3.tooltip.bind('value'),
|
||||
right.shortcut3.icon.bind('value'),
|
||||
leftCardHidden.bind('value'),
|
||||
isRecording.bind('value'),
|
||||
],
|
||||
() => {
|
||||
return Widget.Box({
|
||||
class_name: `container utilities dashboard-card ${!leftCardHidden.value ? 'paired' : ''}`,
|
||||
children: [
|
||||
Widget.Box({
|
||||
className: `card-button-section-container visible`,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
right.shortcut1,
|
||||
'dashboard-button top-button paired',
|
||||
right.shortcut1.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
{
|
||||
tooltip: 'HyprPanel Configuration',
|
||||
command: 'settings-dialog',
|
||||
icon: '',
|
||||
configurable: false,
|
||||
},
|
||||
'dashboard-button',
|
||||
'settings-dialog',
|
||||
),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
className: 'card-button-section-container',
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
right.shortcut3,
|
||||
'dashboard-button top-button paired',
|
||||
right.shortcut3.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
{
|
||||
tooltip: 'Record Screen',
|
||||
command: 'record',
|
||||
icon: '',
|
||||
configurable: false,
|
||||
},
|
||||
`dashboard-button record ${isRecording.value ? 'active' : ''}`,
|
||||
'record',
|
||||
),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Shortcuts };
|
||||
@@ -0,0 +1,362 @@
|
||||
import options from 'options';
|
||||
import { GPU_Stat } from 'lib/types/gpustat';
|
||||
import { dependencies } from 'lib/utils';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { GenericResourceMetrics } from 'lib/types/customModules/generic';
|
||||
|
||||
const { terminal } = options;
|
||||
const { enable_gpu } = options.menus.dashboard.stats;
|
||||
|
||||
const Stats = (): BoxWidget => {
|
||||
const divide = ([total, free]: number[]): number => free / total;
|
||||
|
||||
const formatSizeInGB = (sizeInKB: number): number => Number((sizeInKB / 1024 ** 2).toFixed(2));
|
||||
|
||||
const cpu = Variable(0, {
|
||||
poll: [
|
||||
2000,
|
||||
'top -b -n 1',
|
||||
(out): number => {
|
||||
if (typeof out !== 'string') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cpuOut = out.split('\n').find((line) => line.includes('Cpu(s)'));
|
||||
|
||||
if (cpuOut === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const freeCpu = parseFloat(cpuOut.split(/\s+/)[1].replace(',', '.'));
|
||||
return divide([100, freeCpu]);
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const ram = Variable(
|
||||
{ total: 0, used: 0, percentage: 0 },
|
||||
{
|
||||
poll: [
|
||||
2000,
|
||||
'free',
|
||||
(out): GenericResourceMetrics => {
|
||||
if (typeof out !== 'string') {
|
||||
return { total: 0, used: 0, percentage: 0 };
|
||||
}
|
||||
|
||||
const ramOut = out.split('\n').find((line) => line.includes('Mem:'));
|
||||
|
||||
if (ramOut === undefined) {
|
||||
return { total: 0, used: 0, percentage: 0 };
|
||||
}
|
||||
|
||||
const [totalRam, usedRam] = ramOut.split(/\s+/).splice(1, 2).map(Number);
|
||||
|
||||
return {
|
||||
percentage: divide([totalRam, usedRam]),
|
||||
total: formatSizeInGB(totalRam),
|
||||
used: formatSizeInGB(usedRam),
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const gpu = Variable(0);
|
||||
|
||||
const GPUStat = Widget.Box({
|
||||
child: enable_gpu.bind('value').as((gpStat) => {
|
||||
if (!gpStat || !dependencies('gpustat')) {
|
||||
return Widget.Box();
|
||||
}
|
||||
|
||||
return Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'stat gpu',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
setup: (self) => {
|
||||
const getGpuUsage = (): void => {
|
||||
if (!enable_gpu.value) {
|
||||
gpu.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.execAsync('gpustat --json')
|
||||
.then((out) => {
|
||||
if (typeof out !== 'string') {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(out);
|
||||
|
||||
const totalGpu = 100;
|
||||
const usedGpu =
|
||||
data.gpus.reduce((acc: number, gpu: GPU_Stat) => {
|
||||
return acc + gpu['utilization.gpu'];
|
||||
}, 0) / data.gpus.length;
|
||||
|
||||
gpu.value = divide([totalGpu, usedGpu]);
|
||||
} catch (e) {
|
||||
console.error('Error getting GPU stats:', e);
|
||||
gpu.value = 0;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`An error occurred while fetching GPU stats: ${err}`);
|
||||
});
|
||||
};
|
||||
|
||||
self.poll(2000, getGpuUsage);
|
||||
|
||||
Utils.merge([gpu.bind('value'), enable_gpu.bind('value')], (gpu, enableGpu) => {
|
||||
if (!enableGpu) {
|
||||
return (self.children = []);
|
||||
}
|
||||
|
||||
return (self.children = [
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return (): void => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return (): void => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.LevelBar({
|
||||
class_name: 'stats-bar',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
value: gpu,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
},
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'end',
|
||||
children: Utils.merge([gpu.bind('value'), enable_gpu.bind('value')], (gpuUsed, enableGpu) => {
|
||||
if (!enableGpu) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
Widget.Label({
|
||||
class_name: 'stat-value gpu',
|
||||
label: `${Math.floor(gpuUsed * 100)}%`,
|
||||
}),
|
||||
];
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
const storage = Variable(
|
||||
{ total: 0, used: 0, percentage: 0 },
|
||||
{
|
||||
poll: [
|
||||
2000,
|
||||
'df -B1 /',
|
||||
(out): GenericResourceMetrics => {
|
||||
if (typeof out !== 'string') {
|
||||
return { total: 0, used: 0, percentage: 0 };
|
||||
}
|
||||
|
||||
const dfOut = out.split('\n').find((line) => line.startsWith('/'));
|
||||
|
||||
if (dfOut === undefined) {
|
||||
return { total: 0, used: 0, percentage: 0 };
|
||||
}
|
||||
|
||||
const parts = dfOut.split(/\s+/);
|
||||
const size = parseInt(parts[1], 10);
|
||||
const used = parseInt(parts[2], 10);
|
||||
|
||||
const sizeInGB = formatSizeInGB(size);
|
||||
const usedInGB = formatSizeInGB(used);
|
||||
|
||||
return {
|
||||
total: Math.floor(sizeInGB / 1000),
|
||||
used: Math.floor(usedInGB / 1000),
|
||||
percentage: divide([size, used]),
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'dashboard-card stats-container',
|
||||
vertical: true,
|
||||
vpack: 'fill',
|
||||
hpack: 'fill',
|
||||
expand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'stat cpu',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
children: [
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.LevelBar({
|
||||
class_name: 'stats-bar',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
bar_mode: 'continuous',
|
||||
max_value: 1,
|
||||
value: cpu.bind('value'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'end',
|
||||
class_name: 'stat-value cpu',
|
||||
label: cpu.bind('value').as((v) => `${Math.floor(v * 100)}%`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'stat ram',
|
||||
vpack: 'center',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.LevelBar({
|
||||
class_name: 'stats-bar',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
value: ram.bind('value').as((v) => v.percentage),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'end',
|
||||
class_name: 'stat-value ram',
|
||||
label: ram.bind('value').as((v) => `${v.used}/${v.total} GB`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
GPUStat,
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'stat storage',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
children: [
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
on_primary_click: terminal.bind('value').as((term) => {
|
||||
return () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`bash -c "${term} -e btop"`).catch(
|
||||
(err) => `Failed to open btop: ${err}`,
|
||||
);
|
||||
};
|
||||
}),
|
||||
child: Widget.LevelBar({
|
||||
class_name: 'stats-bar',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
value: storage.bind('value').as((v) => v.percentage),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'end',
|
||||
class_name: 'stat-value storage',
|
||||
label: storage.bind('value').as((v) => `${v.used}/${v.total} GB`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Stats };
|
||||
@@ -0,0 +1,58 @@
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
import brightness from '../../../../services/Brightness.js';
|
||||
import icons from '../../../icons/index.js';
|
||||
|
||||
const Brightness = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container brightness',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container',
|
||||
hpack: 'fill',
|
||||
child: Widget.Label({
|
||||
class_name: 'menu-label',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Brightness',
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section',
|
||||
vpack: 'fill',
|
||||
vexpand: true,
|
||||
vertical: true,
|
||||
child: Widget.Box({
|
||||
class_name: 'brightness-container',
|
||||
children: [
|
||||
Widget.Icon({
|
||||
vexpand: true,
|
||||
vpack: 'center',
|
||||
class_name: 'brightness-slider-icon',
|
||||
icon: icons.brightness.screen,
|
||||
}),
|
||||
Widget.Slider({
|
||||
vpack: 'center',
|
||||
vexpand: true,
|
||||
value: brightness.bind('screen'),
|
||||
class_name: 'menu-active-slider menu-slider brightness',
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
min: 0,
|
||||
max: 1,
|
||||
onChange: ({ value }) => (brightness.screen = value),
|
||||
}),
|
||||
Widget.Label({
|
||||
vpack: 'center',
|
||||
vexpand: true,
|
||||
class_name: 'brightness-slider-label',
|
||||
label: brightness.bind('screen').as((b) => `${Math.round(b * 100)}%`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Brightness };
|
||||
@@ -0,0 +1,25 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { EnergyProfiles } from './profiles/index.js';
|
||||
import { Brightness } from './brightness/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'energymenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items energy',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container energy',
|
||||
children: [Brightness(), EnergyProfiles()],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
const powerProfiles = await Service.import('powerprofiles');
|
||||
import { PowerProfile, PowerProfileObject, PowerProfiles } from 'lib/types/powerprofiles.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
import icons from '../../../icons/index.js';
|
||||
import { uptime } from 'lib/variables.js';
|
||||
|
||||
const EnergyProfiles = (): BoxWidget => {
|
||||
const isValidProfile = (profile: string): profile is PowerProfile =>
|
||||
profile === 'power-saver' || profile === 'balanced' || profile === 'performance';
|
||||
|
||||
function renderUptime(curUptime: number): string {
|
||||
const days = Math.floor(curUptime / (60 * 24));
|
||||
const hours = Math.floor((curUptime % (60 * 24)) / 60);
|
||||
const minutes = Math.floor(curUptime % 60);
|
||||
return ` : ${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container energy',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container',
|
||||
hpack: 'fill',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-label',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Power Profile',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'menu-label uptime',
|
||||
label: uptime.bind().as(renderUptime),
|
||||
tooltipText: 'Uptime',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section',
|
||||
vpack: 'fill',
|
||||
vexpand: true,
|
||||
vertical: true,
|
||||
children: powerProfiles.bind('profiles').as((profiles: PowerProfiles) => {
|
||||
return profiles.map((prof: PowerProfileObject) => {
|
||||
const profileLabels = {
|
||||
'power-saver': 'Power Saver',
|
||||
balanced: 'Balanced',
|
||||
performance: 'Performance',
|
||||
};
|
||||
|
||||
const profileType = prof.Profile;
|
||||
|
||||
if (!isValidProfile(profileType)) {
|
||||
return profileLabels.balanced;
|
||||
}
|
||||
|
||||
return Widget.Button({
|
||||
on_primary_click: () => {
|
||||
powerProfiles.active_profile = prof.Profile;
|
||||
},
|
||||
class_name: powerProfiles.bind('active_profile').as((active) => {
|
||||
return `power-profile-item ${active === prof.Profile ? 'active' : ''}`;
|
||||
}),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: 'power-profile-icon',
|
||||
icon: icons.powerprofile[profileType],
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'power-profile-label',
|
||||
label: profileLabels[profileType],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { EnergyProfiles };
|
||||
@@ -0,0 +1,25 @@
|
||||
import PowerMenu from './power/index.js';
|
||||
import Verification from './power/verification.js';
|
||||
import AudioMenu from './audio/index.js';
|
||||
import NetworkMenu from './network/index.js';
|
||||
import BluetoothMenu from './bluetooth/index.js';
|
||||
import MediaMenu from './media/index.js';
|
||||
import NotificationsMenu from './notifications/index.js';
|
||||
import CalendarMenu from './calendar/index.js';
|
||||
import EnergyMenu from './energy/index.js';
|
||||
import DashboardMenu from './dashboard/index.js';
|
||||
import PowerDropdown from './powerDropdown/index.js';
|
||||
|
||||
export default [
|
||||
PowerMenu(),
|
||||
Verification(),
|
||||
AudioMenu(),
|
||||
NetworkMenu(),
|
||||
BluetoothMenu(),
|
||||
MediaMenu(),
|
||||
NotificationsMenu(),
|
||||
CalendarMenu(),
|
||||
EnergyMenu(),
|
||||
DashboardMenu(),
|
||||
PowerDropdown(),
|
||||
];
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { Mpris, MprisPlayer } from 'types/service/mpris';
|
||||
|
||||
const media = await Service.import('mpris');
|
||||
|
||||
const Bar = (getPlayerInfo: (media: Mpris) => MprisPlayer): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'media-indicator-current-progress-bar',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
child: Widget.Slider({
|
||||
hexpand: true,
|
||||
tooltip_text: '--',
|
||||
class_name: 'menu-slider media progress',
|
||||
draw_value: false,
|
||||
on_change: ({ value }) => {
|
||||
const foundPlayer = getPlayerInfo(media);
|
||||
if (foundPlayer === undefined) {
|
||||
return;
|
||||
}
|
||||
return (foundPlayer.position = value * foundPlayer.length);
|
||||
},
|
||||
setup: (self) => {
|
||||
const update = (): void => {
|
||||
const foundPlayer = getPlayerInfo(media);
|
||||
if (foundPlayer !== undefined) {
|
||||
const value = foundPlayer.length ? foundPlayer.position / foundPlayer.length : 0;
|
||||
self.value = value > 0 ? value : 0;
|
||||
} else {
|
||||
self.value = 0;
|
||||
}
|
||||
};
|
||||
self.hook(media, update);
|
||||
self.poll(1000, update);
|
||||
|
||||
const updateTooltip = (): void => {
|
||||
const foundPlayer = getPlayerInfo(media);
|
||||
if (foundPlayer === undefined) {
|
||||
self.tooltip_text = '00:00';
|
||||
return;
|
||||
}
|
||||
const curHour = Math.floor(foundPlayer.position / 3600);
|
||||
const curMin = Math.floor((foundPlayer.position % 3600) / 60);
|
||||
const curSec = Math.floor(foundPlayer.position % 60);
|
||||
|
||||
if (typeof foundPlayer.position === 'number' && foundPlayer.position >= 0) {
|
||||
const formatTime = (time: number): string => {
|
||||
return time.toString().padStart(2, '0');
|
||||
};
|
||||
|
||||
const formatHour = (hour: number): string => {
|
||||
return hour > 0 ? formatTime(hour) + ':' : '';
|
||||
};
|
||||
|
||||
self.tooltip_text = `${formatHour(curHour)}${formatTime(curMin)}:${formatTime(curSec)}`;
|
||||
} else {
|
||||
self.tooltip_text = `00:00`;
|
||||
}
|
||||
};
|
||||
self.poll(1000, updateTooltip);
|
||||
self.hook(media, updateTooltip);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Bar };
|
||||
@@ -0,0 +1,198 @@
|
||||
import { MprisPlayer } from 'types/service/mpris.js';
|
||||
import icons from '../../../icons/index.js';
|
||||
import { LoopStatus, PlaybackStatus } from 'lib/types/mpris.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
const media = await Service.import('mpris');
|
||||
|
||||
const Controls = (getPlayerInfo: () => MprisPlayer): BoxWidget => {
|
||||
const isValidLoopStatus = (status: string): status is LoopStatus => ['none', 'track', 'playlist'].includes(status);
|
||||
|
||||
const isValidPlaybackStatus = (status: string): status is PlaybackStatus =>
|
||||
['playing', 'paused', 'stopped'].includes(status);
|
||||
|
||||
const isLoopActive = (player: MprisPlayer): string => {
|
||||
return player['loop_status'] !== null && ['track', 'playlist'].includes(player['loop_status'].toLowerCase())
|
||||
? 'active'
|
||||
: '';
|
||||
};
|
||||
|
||||
const isShuffleActive = (player: MprisPlayer): string => {
|
||||
return player['shuffle_status'] !== null && player['shuffle_status'] ? 'active' : '';
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'media-indicator-current-player-controls',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-current-controls',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-control shuffle',
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'center',
|
||||
hasTooltip: true,
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const foundPlayer = getPlayerInfo();
|
||||
if (foundPlayer === undefined) {
|
||||
self.tooltip_text = 'Unavailable';
|
||||
self.class_name = 'media-indicator-control-button shuffle disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
self.tooltip_text =
|
||||
foundPlayer.shuffle_status !== null
|
||||
? foundPlayer.shuffle_status
|
||||
? 'Shuffling'
|
||||
: 'Not Shuffling'
|
||||
: null;
|
||||
self.on_primary_click = (): void => {
|
||||
foundPlayer.shuffle();
|
||||
};
|
||||
self.class_name = `media-indicator-control-button shuffle ${isShuffleActive(foundPlayer)} ${foundPlayer.shuffle_status !== null ? 'enabled' : 'disabled'}`;
|
||||
});
|
||||
},
|
||||
child: Widget.Icon(icons.mpris.shuffle['enabled']),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'center',
|
||||
child: Widget.Icon(icons.mpris.prev),
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const foundPlayer = getPlayerInfo();
|
||||
if (foundPlayer === undefined) {
|
||||
self.class_name = 'media-indicator-control-button prev disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
self.on_primary_click = (): void => {
|
||||
foundPlayer.previous();
|
||||
};
|
||||
self.class_name = `media-indicator-control-button prev ${foundPlayer.can_go_prev !== null && foundPlayer.can_go_prev ? 'enabled' : 'disabled'}`;
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'center',
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const foundPlayer = getPlayerInfo();
|
||||
if (foundPlayer === undefined) {
|
||||
self.class_name = 'media-indicator-control-button play disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
self.on_primary_click = (): void => {
|
||||
foundPlayer.playPause();
|
||||
};
|
||||
self.class_name = `media-indicator-control-button play ${foundPlayer.can_play !== null ? 'enabled' : 'disabled'}`;
|
||||
});
|
||||
},
|
||||
child: Widget.Icon({
|
||||
icon: Utils.watch(icons.mpris.paused, media, 'changed', () => {
|
||||
const foundPlayer: MprisPlayer = getPlayerInfo();
|
||||
if (foundPlayer === undefined) {
|
||||
return icons.mpris['paused'];
|
||||
}
|
||||
const playbackStatus = foundPlayer.play_back_status?.toLowerCase();
|
||||
|
||||
if (playbackStatus && isValidPlaybackStatus(playbackStatus)) {
|
||||
return icons.mpris[playbackStatus];
|
||||
} else {
|
||||
return icons.mpris['paused'];
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: `media-indicator-control next`,
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'center',
|
||||
child: Widget.Icon(icons.mpris.next),
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const foundPlayer = getPlayerInfo();
|
||||
if (foundPlayer === undefined) {
|
||||
self.class_name = 'media-indicator-control-button next disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
self.on_primary_click = (): void => {
|
||||
foundPlayer.next();
|
||||
};
|
||||
self.class_name = `media-indicator-control-button next ${foundPlayer.can_go_next !== null && foundPlayer.can_go_next ? 'enabled' : 'disabled'}`;
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-control loop',
|
||||
children: [
|
||||
Widget.Button({
|
||||
hpack: 'center',
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const foundPlayer = getPlayerInfo();
|
||||
if (foundPlayer === undefined) {
|
||||
self.tooltip_text = 'Unavailable';
|
||||
self.class_name = 'media-indicator-control-button shuffle disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
self.tooltip_text =
|
||||
foundPlayer.loop_status !== null
|
||||
? foundPlayer.loop_status
|
||||
? 'Shuffling'
|
||||
: 'Not Shuffling'
|
||||
: null;
|
||||
self.on_primary_click = (): void => {
|
||||
foundPlayer.loop();
|
||||
};
|
||||
self.class_name = `media-indicator-control-button loop ${isLoopActive(foundPlayer)} ${foundPlayer.loop_status !== null ? 'enabled' : 'disabled'}`;
|
||||
});
|
||||
},
|
||||
child: Widget.Icon({
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const foundPlayer: MprisPlayer = getPlayerInfo();
|
||||
|
||||
if (foundPlayer === undefined) {
|
||||
self.icon = icons.mpris.loop['none'];
|
||||
return;
|
||||
}
|
||||
|
||||
const loopStatus = foundPlayer.loop_status?.toLowerCase();
|
||||
|
||||
if (loopStatus && isValidLoopStatus(loopStatus)) {
|
||||
self.icon = icons.mpris.loop[loopStatus];
|
||||
} else {
|
||||
self.icon = icons.mpris.loop['none'];
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Controls };
|
||||
@@ -0,0 +1,89 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { MprisPlayer } from 'types/service/mpris';
|
||||
|
||||
const media = await Service.import('mpris');
|
||||
|
||||
const MediaInfo = (getPlayerInfo: () => MprisPlayer): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'media-indicator-current-media-info',
|
||||
hpack: 'center',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-current-song-name',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
truncate: 'end',
|
||||
max_width_chars: 31,
|
||||
wrap: true,
|
||||
class_name: 'media-indicator-current-song-name-label',
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const curPlayer = getPlayerInfo();
|
||||
return (self.label =
|
||||
curPlayer !== undefined && curPlayer['track_title'].length
|
||||
? curPlayer['track_title']
|
||||
: 'No Media Currently Playing');
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-current-song-author',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
max_width_chars: 35,
|
||||
class_name: 'media-indicator-current-song-author-label',
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const curPlayer = getPlayerInfo();
|
||||
|
||||
const makeArtistList = (trackArtists: string[]): string => {
|
||||
if (trackArtists.length === 1 && !trackArtists[0].length) {
|
||||
return '-----';
|
||||
}
|
||||
|
||||
return trackArtists.join(', ');
|
||||
};
|
||||
|
||||
return (self.label =
|
||||
curPlayer !== undefined && curPlayer['track_artists'].length
|
||||
? makeArtistList(curPlayer['track_artists'])
|
||||
: '-----');
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-current-song-album',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
max_width_chars: 40,
|
||||
class_name: 'media-indicator-current-song-album-label',
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const curPlayer = getPlayerInfo();
|
||||
return (self.label =
|
||||
curPlayer !== undefined && curPlayer['track_album'].length
|
||||
? curPlayer['track_album']
|
||||
: '---');
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { MediaInfo };
|
||||
@@ -0,0 +1,23 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { Media } from './media.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'mediamenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items media',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items-container media',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Media(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
const media = await Service.import('mpris');
|
||||
import { MediaInfo } from './components/mediainfo.js';
|
||||
import { Controls } from './components/controls.js';
|
||||
import { Bar } from './components/bar.js';
|
||||
import { MprisPlayer } from 'types/service/mpris.js';
|
||||
import options from 'options.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
const { tint, color } = options.theme.bar.menus.menu.media.card;
|
||||
|
||||
const generateAlbumArt = (imageUrl: string): string => {
|
||||
const userTint = tint.value;
|
||||
const userHexColor = color.value;
|
||||
|
||||
const r = parseInt(userHexColor.slice(1, 3), 16);
|
||||
const g = parseInt(userHexColor.slice(3, 5), 16);
|
||||
const b = parseInt(userHexColor.slice(5, 7), 16);
|
||||
|
||||
const alpha = userTint / 100;
|
||||
|
||||
const css = `background-image: linear-gradient(
|
||||
rgba(${r}, ${g}, ${b}, ${alpha}),
|
||||
rgba(${r}, ${g}, ${b}, ${alpha}),
|
||||
${userHexColor} 65em
|
||||
), url("${imageUrl}");`;
|
||||
|
||||
return css;
|
||||
};
|
||||
const Media = (): BoxWidget => {
|
||||
const curPlayer = Variable('');
|
||||
|
||||
media.connect('changed', () => {
|
||||
const statusOrder = {
|
||||
Playing: 1,
|
||||
Paused: 2,
|
||||
Stopped: 3,
|
||||
};
|
||||
|
||||
const isPlaying = media.players.find((p) => p['play_back_status'] === 'Playing');
|
||||
|
||||
const playerStillExists = media.players.some((p) => curPlayer.value === p['bus_name']);
|
||||
|
||||
const nextPlayerUp = media.players.sort(
|
||||
(a, b) => statusOrder[a['play_back_status']] - statusOrder[b['play_back_status']],
|
||||
)[0].bus_name;
|
||||
|
||||
if (isPlaying || !playerStillExists) {
|
||||
curPlayer.value = nextPlayerUp;
|
||||
}
|
||||
});
|
||||
|
||||
const getPlayerInfo = (): MprisPlayer => {
|
||||
return media.players.find((p) => p.bus_name === curPlayer.value) || media.players[0];
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container',
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section',
|
||||
vertical: false,
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-content',
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'media-content',
|
||||
child: Widget.Box({
|
||||
class_name: 'media-indicator-right-section',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [MediaInfo(getPlayerInfo), Controls(getPlayerInfo), Bar(getPlayerInfo)],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
setup: (self) => {
|
||||
self.hook(media, () => {
|
||||
const curPlayer = getPlayerInfo();
|
||||
if (curPlayer !== undefined) {
|
||||
self.css = generateAlbumArt(curPlayer.track_cover_url);
|
||||
}
|
||||
});
|
||||
|
||||
Utils.merge([color.bind('value'), tint.bind('value')], () => {
|
||||
const curPlayer = getPlayerInfo();
|
||||
if (curPlayer !== undefined) {
|
||||
self.css = generateAlbumArt(curPlayer.track_cover_url);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Media };
|
||||
@@ -0,0 +1,69 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
const network = await Service.import('network');
|
||||
|
||||
const Ethernet = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container ethernet',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container',
|
||||
hpack: 'fill',
|
||||
child: Widget.Label({
|
||||
class_name: 'menu-label',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Ethernet',
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section',
|
||||
vertical: true,
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-content',
|
||||
vertical: true,
|
||||
setup: (self) => {
|
||||
self.hook(network, () => {
|
||||
return (self.child = Widget.Box({
|
||||
class_name: 'network-element-item',
|
||||
child: Widget.Box({
|
||||
hpack: 'start',
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: `network-icon ethernet ${network.wired.state === 'activated' ? 'active' : ''}`,
|
||||
tooltip_text: network.wired.internet,
|
||||
icon: `${network.wired['icon_name']}`,
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'connection-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'active-connection',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: `Ethernet Connection ${network.wired.state !== 'unknown' && typeof network.wired?.speed === 'number' ? `(${network.wired?.speed / 1000} Gbps)` : ''}`,
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'start',
|
||||
class_name: 'connection-status dim',
|
||||
label:
|
||||
network.wired.internet.charAt(0).toUpperCase() +
|
||||
network.wired.internet.slice(1),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}));
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Ethernet };
|
||||
@@ -0,0 +1,22 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { Ethernet } from './ethernet/index.js';
|
||||
import { Wifi } from './wifi/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'networkmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items network',
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container network',
|
||||
children: [Ethernet(), Wifi()],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { WifiIcon } from 'lib/types/network';
|
||||
|
||||
const getWifiIcon = (iconName: string): WifiIcon => {
|
||||
const deviceIconMap: [string, WifiIcon][] = [
|
||||
['network-wireless-acquiring', ''],
|
||||
['network-wireless-connected', ''],
|
||||
['network-wireless-encrypted', ''],
|
||||
['network-wireless-hotspot', ''],
|
||||
['network-wireless-no-route', ''],
|
||||
['network-wireless-offline', ''],
|
||||
['network-wireless-signal-excellent', ''],
|
||||
['network-wireless-signal-good', ''],
|
||||
['network-wireless-signal-ok', ''],
|
||||
['network-wireless-signal-weak', ''],
|
||||
['network-wireless-signal-none', ''],
|
||||
];
|
||||
|
||||
const foundMatch = deviceIconMap.find((icon) => RegExp(icon[0]).test(iconName.toLowerCase()));
|
||||
|
||||
return foundMatch ? foundMatch[1] : '';
|
||||
};
|
||||
|
||||
export { getWifiIcon };
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Network } from 'types/service/network';
|
||||
import { Variable } from 'types/variable';
|
||||
import { AccessPoint } from 'lib/types/network';
|
||||
import Box from 'types/widgets/box';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
|
||||
const renderWapStaging = (
|
||||
self: Box<Child, Attribute>,
|
||||
network: Network,
|
||||
staging: Variable<AccessPoint>,
|
||||
connecting: Variable<string>,
|
||||
): void => {
|
||||
Utils.merge([network.bind('wifi'), staging.bind('value')], () => {
|
||||
if (!Object.keys(staging.value).length) {
|
||||
return (self.child = Widget.Box());
|
||||
}
|
||||
|
||||
return (self.child = Widget.Box({
|
||||
class_name: 'network-element-item staging',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: `network-icon wifi`,
|
||||
icon: `${staging.value.iconName}`,
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'connection-container',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'active-connection',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: staging.value.ssid,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Revealer({
|
||||
hpack: 'end',
|
||||
reveal_child: connecting.bind('value').as((c) => staging.value.bssid === c),
|
||||
child: Widget.Spinner({
|
||||
class_name: 'spinner wap',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'network-password-input-container',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Entry({
|
||||
hpack: 'start',
|
||||
hexpand: true,
|
||||
visibility: false,
|
||||
class_name: 'network-password-input',
|
||||
placeholder_text: 'enter password',
|
||||
onAccept: (selfInp) => {
|
||||
connecting.value = staging.value.bssid || '';
|
||||
Utils.execAsync(
|
||||
`nmcli dev wifi connect ${staging.value.bssid} password ${selfInp.text}`,
|
||||
)
|
||||
.catch((err) => {
|
||||
connecting.value = '';
|
||||
console.error(`Failed to connect to wifi: ${staging.value.ssid}... ${err}`);
|
||||
Utils.notify({
|
||||
summary: 'Network',
|
||||
body: err,
|
||||
timeout: 5000,
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
connecting.value = '';
|
||||
staging.value = {} as AccessPoint;
|
||||
});
|
||||
selfInp.text = '';
|
||||
},
|
||||
}),
|
||||
Widget.Button({
|
||||
hpack: 'end',
|
||||
class_name: 'close-network-password-input-button',
|
||||
on_primary_click: () => {
|
||||
connecting.value = '';
|
||||
staging.value = {} as AccessPoint;
|
||||
},
|
||||
child: Widget.Icon({
|
||||
class_name: 'close-network-password-input-icon',
|
||||
icon: 'window-close-symbolic',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
export { renderWapStaging };
|
||||
@@ -0,0 +1,219 @@
|
||||
import { Network } from 'types/service/network.js';
|
||||
import { AccessPoint, WifiStatus } from 'lib/types/network.js';
|
||||
import { Variable } from 'types/variable.js';
|
||||
import { getWifiIcon } from '../utils.js';
|
||||
import { WIFI_STATUS_MAP } from 'globals/network.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
const renderWAPs = (
|
||||
self: Box<Child, Attribute>,
|
||||
network: Network,
|
||||
staging: Variable<AccessPoint>,
|
||||
connecting: Variable<string>,
|
||||
): void => {
|
||||
const getIdBySsid = (ssid: string, nmcliOutput: string): string | undefined => {
|
||||
const lines = nmcliOutput.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
const columns = line.trim().split(/\s{2,}/);
|
||||
if (columns[0].includes(ssid)) {
|
||||
return columns[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isValidWifiStatus = (status: string): status is WifiStatus => {
|
||||
return status in WIFI_STATUS_MAP;
|
||||
};
|
||||
|
||||
const getWifiStatus = (): string => {
|
||||
const wifiState = network.wifi.state?.toLowerCase();
|
||||
|
||||
if (wifiState && isValidWifiStatus(wifiState)) {
|
||||
return WIFI_STATUS_MAP[wifiState];
|
||||
}
|
||||
return WIFI_STATUS_MAP['unknown'];
|
||||
};
|
||||
|
||||
self.hook(network, () => {
|
||||
Utils.merge([staging.bind('value'), connecting.bind('value')], () => {
|
||||
// NOTE: Sometimes the network service will yield a "this._device is undefined" when
|
||||
// trying to access the "access_points" property. So we must validate that
|
||||
// it's not 'undefined'
|
||||
// --
|
||||
// Also this is an AGS bug that needs to be fixed
|
||||
|
||||
// TODO: Remove @ts-ignore once AGS bug is fixed
|
||||
// @ts-expect-error to fix AGS bug
|
||||
let WAPs = network.wifi._device !== undefined ? network.wifi['access_points'] : [];
|
||||
|
||||
const dedupeWAPs = (): AccessPoint[] => {
|
||||
const dedupMap: Record<string, AccessPoint> = {};
|
||||
WAPs.forEach((item: AccessPoint) => {
|
||||
if (item.ssid !== null && !Object.hasOwnProperty.call(dedupMap, item.ssid)) {
|
||||
dedupMap[item.ssid] = item;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(dedupMap).map((itm) => dedupMap[itm]);
|
||||
};
|
||||
|
||||
WAPs = dedupeWAPs();
|
||||
|
||||
const isInStaging = (wap: AccessPoint): boolean => {
|
||||
if (Object.keys(staging.value).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return wap.bssid === staging.value.bssid;
|
||||
};
|
||||
|
||||
const isDisconnecting = (wap: AccessPoint): boolean => {
|
||||
if (wap.ssid === network.wifi.ssid) {
|
||||
return network.wifi.state.toLowerCase() === 'deactivating';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const filteredWAPs = WAPs.filter((ap: AccessPoint) => {
|
||||
return ap.ssid !== 'Unknown' && !isInStaging(ap);
|
||||
}).sort((a: AccessPoint, b: AccessPoint) => {
|
||||
if (network.wifi.ssid === a.ssid) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (network.wifi.ssid === b.ssid) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return b.strength - a.strength;
|
||||
});
|
||||
|
||||
if (filteredWAPs.length <= 0 && Object.keys(staging.value).length === 0) {
|
||||
return (self.child = Widget.Label({
|
||||
class_name: 'waps-not-found dim',
|
||||
expand: true,
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
label: 'No Wi-Fi Networks Found',
|
||||
}));
|
||||
}
|
||||
return (self.children = filteredWAPs.map((ap: AccessPoint) => {
|
||||
return Widget.Box({
|
||||
children: [
|
||||
Widget.Button({
|
||||
on_primary_click: () => {
|
||||
if (ap.bssid === connecting.value || ap.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
connecting.value = ap.bssid || '';
|
||||
Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`)
|
||||
.then(() => {
|
||||
connecting.value = '';
|
||||
staging.value = {} as AccessPoint;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.toLowerCase().includes('secrets were required, but not provided')) {
|
||||
staging.value = ap;
|
||||
} else {
|
||||
Utils.notify({
|
||||
summary: 'Network',
|
||||
body: err,
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
connecting.value = '';
|
||||
});
|
||||
},
|
||||
class_name: 'network-element-item',
|
||||
child: Widget.Box({
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'start',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'start',
|
||||
class_name: `network-icon wifi ${ap.ssid === network.wifi.ssid ? 'active' : ''} txt-icon`,
|
||||
label: getWifiIcon(`${ap['iconName']}`),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'connection-container',
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'center',
|
||||
class_name: 'active-connection',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: ap.ssid,
|
||||
}),
|
||||
Widget.Revealer({
|
||||
revealChild: ap.ssid === network.wifi.ssid,
|
||||
child: Widget.Label({
|
||||
hpack: 'start',
|
||||
class_name: 'connection-status dim',
|
||||
label: getWifiStatus(),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Revealer({
|
||||
hpack: 'end',
|
||||
vpack: 'start',
|
||||
reveal_child: ap.bssid === connecting.value || isDisconnecting(ap),
|
||||
child: Widget.Spinner({
|
||||
vpack: 'start',
|
||||
class_name: 'spinner wap',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
Widget.Revealer({
|
||||
vpack: 'start',
|
||||
reveal_child: ap.bssid !== connecting.value && ap.active,
|
||||
child: Widget.Button({
|
||||
tooltip_text: 'Delete/Forget Network',
|
||||
class_name: 'menu-icon-button network disconnect',
|
||||
on_primary_click: () => {
|
||||
connecting.value = ap.bssid || '';
|
||||
Utils.execAsync('nmcli connection show --active').then(() => {
|
||||
Utils.execAsync('nmcli connection show --active').then((res) => {
|
||||
const connectionId = getIdBySsid(ap.ssid || '', res);
|
||||
|
||||
if (connectionId === undefined) {
|
||||
console.error(
|
||||
`Error while forgetting "${ap.ssid}": Connection ID not found`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.execAsync(`nmcli connection delete ${connectionId} "${ap.ssid}"`)
|
||||
.then(() => (connecting.value = ''))
|
||||
.catch((err) => {
|
||||
connecting.value = '';
|
||||
console.error(`Error while forgetting "${ap.ssid}": ${err}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon delete-network',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export { renderWAPs };
|
||||
@@ -0,0 +1,81 @@
|
||||
const network = await Service.import('network');
|
||||
import { renderWAPs } from './WirelessAPs.js';
|
||||
import { renderWapStaging } from './APStaging.js';
|
||||
import { AccessPoint } from 'lib/types/network.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
const Staging = Variable({} as AccessPoint);
|
||||
const Connecting = Variable('');
|
||||
|
||||
const searchInProgress = Variable(false);
|
||||
|
||||
const startRotation = (): void => {
|
||||
searchInProgress.value = true;
|
||||
setTimeout(() => {
|
||||
searchInProgress.value = false;
|
||||
}, 5 * 1000);
|
||||
};
|
||||
|
||||
const Wifi = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container wifi',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container',
|
||||
hpack: 'fill',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-label',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Wi-Fi',
|
||||
}),
|
||||
Widget.Switch({
|
||||
class_name: 'menu-switch network',
|
||||
vpack: 'center',
|
||||
tooltip_text: 'Toggle Wifi',
|
||||
active: network.wifi.enabled,
|
||||
on_activate: () => {
|
||||
network.toggleWifi();
|
||||
},
|
||||
}),
|
||||
Widget.Button({
|
||||
vpack: 'center',
|
||||
hpack: 'end',
|
||||
class_name: 'menu-icon-button search network',
|
||||
on_primary_click: () => {
|
||||
startRotation();
|
||||
network.wifi.scan();
|
||||
},
|
||||
child: Widget.Icon({
|
||||
class_name: searchInProgress.bind('value').as((v) => (v ? 'spinning' : '')),
|
||||
icon: 'view-refresh-symbolic',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'wap-staging',
|
||||
setup: (self) => {
|
||||
renderWapStaging(self, network, Staging, Connecting);
|
||||
},
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'available-waps',
|
||||
vertical: true,
|
||||
setup: (self) => {
|
||||
renderWAPs(self, network, Staging, Connecting);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Wifi };
|
||||
@@ -0,0 +1,76 @@
|
||||
import { closeNotifications } from 'globals/notification';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { Notifications } from 'types/service/notifications';
|
||||
import options from 'options';
|
||||
|
||||
const { clearDelay } = options.notifications;
|
||||
|
||||
const Controls = (notifs: Notifications): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-menu-controls',
|
||||
expand: false,
|
||||
vertical: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container notifications',
|
||||
hpack: 'start',
|
||||
vpack: 'center',
|
||||
expand: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-label notifications',
|
||||
label: 'Notifications',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'end',
|
||||
vpack: 'center',
|
||||
expand: false,
|
||||
children: [
|
||||
Widget.Switch({
|
||||
class_name: 'menu-switch notifications',
|
||||
vpack: 'center',
|
||||
active: notifs.bind('dnd').as((dnd: boolean) => !dnd),
|
||||
on_activate: ({ active }) => {
|
||||
notifs.dnd = !active;
|
||||
},
|
||||
}),
|
||||
Widget.Box({
|
||||
children: [
|
||||
Widget.Separator({
|
||||
hpack: 'center',
|
||||
vexpand: true,
|
||||
vertical: true,
|
||||
class_name: 'menu-separator notification-controls',
|
||||
}),
|
||||
Widget.Button({
|
||||
className: 'clear-notifications-button',
|
||||
tooltip_text: 'Clear Notifications',
|
||||
on_primary_click: clearDelay.bind('value').as((delay) => {
|
||||
return () => {
|
||||
if (removingNotifications.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
return closeNotifications(notifs.notifications, delay);
|
||||
};
|
||||
}),
|
||||
child: Widget.Label({
|
||||
class_name: removingNotifications.bind('value').as((removing: boolean) => {
|
||||
return removing
|
||||
? 'clear-notifications-label txt-icon removing'
|
||||
: 'clear-notifications-label txt-icon';
|
||||
}),
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Controls };
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Notification } from 'types/service/notifications.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
const notifs = await Service.import('notifications');
|
||||
import { Controls } from './controls/index.js';
|
||||
import { NotificationCard } from './notification/index.js';
|
||||
import { NotificationPager } from './pager/index.js';
|
||||
|
||||
import options from 'options';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const { displayedTotal } = options.notifications;
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
const curPage = Variable(1);
|
||||
|
||||
Utils.merge(
|
||||
[curPage.bind('value'), displayedTotal.bind('value'), notifs.bind('notifications')],
|
||||
(currentPage: number, dispTotal: number, notifications: Notification[]) => {
|
||||
// If the page doesn't have enough notifications to display, go back
|
||||
// to the previous page.
|
||||
if (notifications.length <= (currentPage - 1) * dispTotal) {
|
||||
curPage.value = currentPage <= 1 ? 1 : currentPage - 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return DropdownMenu({
|
||||
name: 'notificationsmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'notification-menu-content',
|
||||
css: 'padding: 1px; margin: -1px;',
|
||||
hexpand: true,
|
||||
vexpand: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-container menu',
|
||||
vertical: true,
|
||||
hexpand: false,
|
||||
vexpand: false,
|
||||
children: [Controls(notifs), NotificationCard(notifs, curPage), NotificationPager(curPage)],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { Notification, Notifications } from 'types/service/notifications';
|
||||
const Actions = (notif: Notification, notifs: Notifications): BoxWidget => {
|
||||
if (notif.actions !== undefined && notif.actions.length > 0) {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-card-actions menu',
|
||||
hexpand: true,
|
||||
vpack: 'end',
|
||||
children: notif.actions.map((action) => {
|
||||
return Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: 'notification-action-buttons menu',
|
||||
on_primary_click: () => {
|
||||
if (action.id.includes('scriptAction:-')) {
|
||||
App.closeWindow('notificationsmenu');
|
||||
Utils.execAsync(`${action.id.replace('scriptAction:-', '')}`).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
notifs.CloseNotification(notif.id);
|
||||
} else {
|
||||
App.closeWindow('notificationsmenu');
|
||||
notif.invoke(action.id);
|
||||
}
|
||||
},
|
||||
child: Widget.Box({
|
||||
hpack: 'center',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'notification-action-buttons-label menu',
|
||||
hexpand: true,
|
||||
max_width_chars: 15,
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: action.label,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'spacer',
|
||||
});
|
||||
};
|
||||
|
||||
export { Actions };
|
||||
@@ -0,0 +1,25 @@
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
import { notifHasImg } from '../../utils.js';
|
||||
import { Notification } from 'types/service/notifications';
|
||||
|
||||
export const Body = (notif: Notification): BoxWidget => {
|
||||
return Widget.Box({
|
||||
vpack: 'start',
|
||||
hexpand: true,
|
||||
class_name: 'notification-card-body menu',
|
||||
children: [
|
||||
Widget.Label({
|
||||
hexpand: true,
|
||||
use_markup: true,
|
||||
xalign: 0,
|
||||
justification: 'left',
|
||||
truncate: 'end',
|
||||
lines: 2,
|
||||
max_width_chars: !notifHasImg(notif) ? 35 : 28,
|
||||
wrap: true,
|
||||
class_name: 'notification-card-body-label menu',
|
||||
label: notif['body'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Attribute } from 'lib/types/widget';
|
||||
import { Notification, Notifications } from 'types/service/notifications';
|
||||
import Button from 'types/widgets/button';
|
||||
import Label from 'types/widgets/label';
|
||||
export const CloseButton = (notif: Notification, notifs: Notifications): Button<Label<Attribute>, Attribute> => {
|
||||
return Widget.Button({
|
||||
class_name: 'close-notification-button menu',
|
||||
on_primary_click: () => {
|
||||
notifs.CloseNotification(notif.id);
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon notif-close',
|
||||
label: '',
|
||||
hpack: 'center',
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Notification } from 'types/service/notifications.js';
|
||||
import { NotificationIcon } from 'lib/types/notification.js';
|
||||
import { getNotificationIcon } from 'globals/notification';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
const NotificationIcon = ({ app_entry = '', app_icon = '', app_name = '' }: Partial<Notification>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
css: `
|
||||
min-width: 2rem;
|
||||
min-height: 2rem;
|
||||
`,
|
||||
child: Widget.Icon({
|
||||
class_name: 'notification-icon menu',
|
||||
icon: getNotificationIcon(app_name, app_icon, app_entry),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { NotificationIcon };
|
||||
@@ -0,0 +1,54 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import { Notification } from 'types/service/notifications';
|
||||
import { NotificationIcon } from './icon.js';
|
||||
import { notifHasImg } from '../../utils.js';
|
||||
import options from 'options.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
const { military } = options.menus.clock.time;
|
||||
|
||||
export const Header = (notif: Notification): BoxWidget => {
|
||||
const time = (time: number, format = '%I:%M %p'): string => {
|
||||
return GLib.DateTime.new_from_unix_local(time).format(military.value ? '%H:%M' : format) || '--:--';
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
vertical: false,
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-header menu',
|
||||
hpack: 'start',
|
||||
children: [NotificationIcon(notif)],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-header menu',
|
||||
hexpand: true,
|
||||
vpack: 'start',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'notification-card-header-label menu',
|
||||
hpack: 'start',
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
max_width_chars: !notifHasImg(notif) ? 34 : 22,
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: notif['summary'],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-header menu',
|
||||
hpack: 'end',
|
||||
vpack: 'start',
|
||||
hexpand: true,
|
||||
child: Widget.Label({
|
||||
vexpand: true,
|
||||
class_name: 'notification-time',
|
||||
label: time(notif.time),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Notification } from 'types/service/notifications';
|
||||
import { notifHasImg } from '../../utils.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
const Image = (notif: Notification): BoxWidget => {
|
||||
if (notifHasImg(notif)) {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-card-image-container menu',
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
vexpand: false,
|
||||
child: Widget.Box({
|
||||
hpack: 'center',
|
||||
vexpand: false,
|
||||
class_name: 'notification-card-image menu',
|
||||
css: `background-image: url("${notif.image}")`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return Widget.Box();
|
||||
};
|
||||
|
||||
export { Image };
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Notifications, Notification } from 'types/service/notifications';
|
||||
import { notifHasImg } from '../utils.js';
|
||||
import { Header } from './header/index.js';
|
||||
import { Actions } from './actions/index.js';
|
||||
import { Image } from './image/index.js';
|
||||
import { Placeholder } from './placeholder/index.js';
|
||||
import { Body } from './body/index.js';
|
||||
import { CloseButton } from './close/index.js';
|
||||
import options from 'options.js';
|
||||
import { Variable } from 'types/variable.js';
|
||||
import { filterNotifications } from 'lib/shared/notifications.js';
|
||||
import Scrollable from 'types/widgets/scrollable.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const { displayedTotal, ignore } = options.notifications;
|
||||
|
||||
const NotificationCard = (notifs: Notifications, curPage: Variable<number>): Scrollable<Child, Attribute> => {
|
||||
return Widget.Scrollable({
|
||||
vscroll: 'automatic',
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-content-container notifications',
|
||||
hpack: 'center',
|
||||
vexpand: true,
|
||||
spacing: 0,
|
||||
vertical: true,
|
||||
setup: (self) => {
|
||||
Utils.merge(
|
||||
[
|
||||
notifs.bind('notifications'),
|
||||
curPage.bind('value'),
|
||||
displayedTotal.bind('value'),
|
||||
ignore.bind('value'),
|
||||
],
|
||||
(notifications, currentPage, dispTotal, ignoredNotifs) => {
|
||||
const filteredNotifications = filterNotifications(notifications, ignoredNotifs);
|
||||
|
||||
const sortedNotifications = filteredNotifications.sort((a, b) => b.time - a.time);
|
||||
|
||||
if (filteredNotifications.length <= 0) {
|
||||
return (self.children = [Placeholder(notifs)]);
|
||||
}
|
||||
|
||||
const pageStart = (currentPage - 1) * dispTotal;
|
||||
const pageEnd = currentPage * dispTotal;
|
||||
return (self.children = sortedNotifications
|
||||
.slice(pageStart, pageEnd)
|
||||
.map((notif: Notification) => {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-card-content-container',
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'notification-card menu',
|
||||
vpack: 'start',
|
||||
hexpand: true,
|
||||
vexpand: false,
|
||||
children: [
|
||||
Image(notif),
|
||||
Widget.Box({
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
class_name: `notification-card-content ${!notifHasImg(notif) ? 'noimg' : ' menu'}`,
|
||||
children: [Header(notif), Body(notif), Actions(notif, notifs)],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
CloseButton(notif, notifs),
|
||||
],
|
||||
});
|
||||
}));
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { NotificationCard };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { Notifications } from 'types/service/notifications';
|
||||
|
||||
const Placeholder = (notifs: Notifications): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-label-container',
|
||||
vpack: 'fill',
|
||||
hpack: 'center',
|
||||
expand: true,
|
||||
child: Widget.Box({
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
expand: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'center',
|
||||
class_name: 'placeholder-label dim bell txt-icon',
|
||||
label: notifs.bind('dnd').as((dnd) => (dnd ? '' : '')),
|
||||
}),
|
||||
Widget.Label({
|
||||
vpack: 'start',
|
||||
class_name: 'placehold-label dim message',
|
||||
label: "You're all caught up :)",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { Placeholder };
|
||||
@@ -0,0 +1,88 @@
|
||||
const notifs = await Service.import('notifications');
|
||||
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import options from 'options';
|
||||
import { Notification } from 'types/service/notifications';
|
||||
import { Variable } from 'types/variable';
|
||||
|
||||
const { displayedTotal } = options.notifications;
|
||||
const { show: showPager } = options.theme.bar.menus.menu.notifications.pager;
|
||||
|
||||
export const NotificationPager = (curPage: Variable<number>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-menu-pager',
|
||||
hexpand: true,
|
||||
vexpand: false,
|
||||
children: Utils.merge(
|
||||
[
|
||||
curPage.bind('value'),
|
||||
displayedTotal.bind('value'),
|
||||
notifs.bind('notifications'),
|
||||
showPager.bind('value'),
|
||||
],
|
||||
(currentPage: number, dispTotal: number, _: Notification[], showPgr: boolean) => {
|
||||
if (showPgr === false || (currentPage === 1 && notifs.notifications.length <= dispTotal)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
class_name: `pager-button left ${currentPage <= 1 ? 'disabled' : ''}`,
|
||||
onPrimaryClick: () => {
|
||||
curPage.value = 1;
|
||||
},
|
||||
child: Widget.Label({
|
||||
className: 'pager-button-label',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
class_name: `pager-button left ${currentPage <= 1 ? 'disabled' : ''}`,
|
||||
onPrimaryClick: () => {
|
||||
curPage.value = currentPage <= 1 ? 1 : currentPage - 1;
|
||||
},
|
||||
child: Widget.Label({
|
||||
className: 'pager-button-label',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Label({
|
||||
hexpand: true,
|
||||
hpack: 'center',
|
||||
class_name: 'pager-label',
|
||||
label: `${currentPage} / ${Math.ceil(notifs.notifications.length / dispTotal) || 1}`,
|
||||
}),
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
hpack: 'end',
|
||||
class_name: `pager-button right ${currentPage >= Math.ceil(notifs.notifications.length / dispTotal) ? 'disabled' : ''}`,
|
||||
onPrimaryClick: () => {
|
||||
const maxPage = Math.ceil(notifs.notifications.length / displayedTotal.value);
|
||||
curPage.value = currentPage >= maxPage ? currentPage : currentPage + 1;
|
||||
},
|
||||
child: Widget.Label({
|
||||
className: 'pager-button-label',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
hpack: 'end',
|
||||
class_name: `pager-button right ${currentPage >= Math.ceil(notifs.notifications.length / dispTotal) ? 'disabled' : ''}`,
|
||||
onPrimaryClick: () => {
|
||||
const maxPage = Math.ceil(notifs.notifications.length / displayedTotal.value);
|
||||
curPage.value = maxPage;
|
||||
},
|
||||
child: Widget.Label({
|
||||
className: 'pager-button-label',
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
];
|
||||
},
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Notification } from 'types/service/notifications';
|
||||
|
||||
const notifHasImg = (notif: Notification): boolean => {
|
||||
return notif.image !== undefined && notif.image.length ? true : false;
|
||||
};
|
||||
|
||||
export { notifHasImg };
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Action } from 'lib/types/power';
|
||||
import options from 'options';
|
||||
const { sleep, reboot, logout, shutdown } = options.menus.dashboard.powermenu;
|
||||
|
||||
class PowerMenu extends Service {
|
||||
static {
|
||||
Service.register(
|
||||
this,
|
||||
{},
|
||||
{
|
||||
title: ['string'],
|
||||
cmd: ['string'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#title = '';
|
||||
#cmd = '';
|
||||
|
||||
get title(): string {
|
||||
return this.#title;
|
||||
}
|
||||
|
||||
action(action: Action): void {
|
||||
[this.#cmd, this.#title] = {
|
||||
sleep: [sleep.value, 'Sleep'],
|
||||
reboot: [reboot.value, 'Reboot'],
|
||||
logout: [logout.value, 'Log Out'],
|
||||
shutdown: [shutdown.value, 'Shutdown'],
|
||||
}[action];
|
||||
|
||||
this.notify('cmd');
|
||||
this.notify('title');
|
||||
this.emit('changed');
|
||||
App.closeWindow('powermenu');
|
||||
App.openWindow('verification');
|
||||
}
|
||||
|
||||
customAction(action: Action, cmnd: string): void {
|
||||
[this.#cmd, this.#title] = [cmnd, action];
|
||||
|
||||
this.notify('cmd');
|
||||
this.notify('title');
|
||||
this.emit('changed');
|
||||
App.closeWindow('powermenu');
|
||||
App.openWindow('verification');
|
||||
}
|
||||
|
||||
shutdown = (): void => {
|
||||
this.action('shutdown');
|
||||
};
|
||||
|
||||
exec = (): void => {
|
||||
App.closeWindow('verification');
|
||||
Utils.execAsync(this.#cmd);
|
||||
};
|
||||
}
|
||||
|
||||
const powermenu = new PowerMenu();
|
||||
Object.assign(globalThis, { powermenu });
|
||||
export default powermenu;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Action } from 'lib/types/power.js';
|
||||
import PopupWindow from '../shared/popup/index.js';
|
||||
import powermenu from './helpers/actions.js';
|
||||
import icons from '../../icons/index.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import { Attribute, Child, GButton } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
const SysButton = (action: Action, label: string): GButton =>
|
||||
Widget.Button({
|
||||
class_name: `widget-button powermenu-button-${action}`,
|
||||
on_clicked: () => powermenu.action(action),
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
class_name: 'system-button widget-box',
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: `system-button_icon ${action}`,
|
||||
icon: icons.powermenu[action],
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: `system-button_label ${action}`,
|
||||
label,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
export default (): Window<Child, Attribute> =>
|
||||
PopupWindow({
|
||||
name: 'powermenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'powermenu horizontal',
|
||||
children: [
|
||||
SysButton('shutdown', 'SHUTDOWN'),
|
||||
SysButton('logout', 'LOG OUT'),
|
||||
SysButton('reboot', 'REBOOT'),
|
||||
SysButton('sleep', 'SLEEP'),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import PopupWindow from '../shared/popup/index.js';
|
||||
import powermenu from './helpers/actions.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
export default (): Window<Child, Attribute> =>
|
||||
PopupWindow({
|
||||
name: 'verification',
|
||||
transition: 'crossfade',
|
||||
child: Widget.Box({
|
||||
class_name: 'verification',
|
||||
child: Widget.Box({
|
||||
class_name: 'verification-content',
|
||||
expand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'text-box',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'title',
|
||||
label: powermenu.bind('title').as((t) => t.toUpperCase()),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'desc',
|
||||
label: powermenu
|
||||
.bind('title')
|
||||
.as((p) => `Are you sure you want to ${p.toLowerCase()}?`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'buttons horizontal',
|
||||
vexpand: true,
|
||||
vpack: 'end',
|
||||
homogeneous: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
class_name: 'verification-button bar-verification_yes',
|
||||
child: Widget.Label('Yes'),
|
||||
on_clicked: powermenu.exec,
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'verification-button bar-verification_no',
|
||||
child: Widget.Label('No'),
|
||||
on_clicked: () => App.toggleWindow('verification'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PowerOptions } from 'lib/types/options';
|
||||
import options from 'options';
|
||||
import powermenu from '../power/helpers/actions';
|
||||
import { GButton } from 'lib/types/widget';
|
||||
|
||||
const { confirmation, shutdown, logout, sleep, reboot, showLabel } = options.menus.power;
|
||||
|
||||
export const PowerButton = (action: PowerOptions): GButton => {
|
||||
const handleClick = (action: PowerOptions): void => {
|
||||
const actions = {
|
||||
shutdown: shutdown.value,
|
||||
reboot: reboot.value,
|
||||
logout: logout.value,
|
||||
sleep: sleep.value,
|
||||
};
|
||||
App.closeWindow('powerdropdownmenu');
|
||||
|
||||
if (!confirmation.value) {
|
||||
Utils.execAsync(actions[action]).catch((err) =>
|
||||
console.error(`Failed to execute ${action} command. Error: ${err}`),
|
||||
);
|
||||
} else {
|
||||
powermenu.customAction(action, actions[action]);
|
||||
}
|
||||
};
|
||||
|
||||
const powerIconMap = {
|
||||
shutdown: '',
|
||||
reboot: '',
|
||||
logout: '',
|
||||
sleep: '',
|
||||
};
|
||||
|
||||
return Widget.Button({
|
||||
className: showLabel.bind('value').as((shwLbl) => {
|
||||
return `power-menu-button ${action} ${!shwLbl ? 'no-label' : ''}`;
|
||||
}),
|
||||
on_clicked: () => handleClick(action),
|
||||
child: Widget.Box({
|
||||
vertical: false,
|
||||
children: showLabel.bind('value').as((shwLbl) => {
|
||||
if (shwLbl) {
|
||||
return [
|
||||
Widget.Label({
|
||||
label: powerIconMap[action],
|
||||
className: `power-button-icon ${action}-icon txt-icon`,
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'center',
|
||||
hexpand: true,
|
||||
label: action.charAt(0).toUpperCase() + action.slice(1),
|
||||
className: `power-button-label ${action}-label show-label`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [
|
||||
Widget.Label({
|
||||
label: powerIconMap[action],
|
||||
className: `power-button-icon ${action}-icon no-label txt-icon`,
|
||||
}),
|
||||
];
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { PowerButton } from './button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'powerdropdownmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items power-dropdown',
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container power-dropdown',
|
||||
children: [PowerButton('shutdown'), PowerButton('reboot'), PowerButton('logout'), PowerButton('sleep')],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Attribute, BoxWidget } from 'lib/types/widget';
|
||||
import EventBox from 'types/widgets/eventbox';
|
||||
import { BarLocation } from 'lib/types/options';
|
||||
|
||||
const createEventBox = (className: string, windowName: string): EventBox<BoxWidget, Attribute> => {
|
||||
return Widget.EventBox({
|
||||
class_name: className,
|
||||
hexpand: true,
|
||||
vexpand: false,
|
||||
can_focus: false,
|
||||
child: Widget.Box(),
|
||||
setup: (w) => {
|
||||
w.on('button-press-event', () => App.toggleWindow(windowName));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const barEventMargins = (
|
||||
windowName: string,
|
||||
location: BarLocation = 'top',
|
||||
): [EventBox<BoxWidget, Attribute>, EventBox<BoxWidget, Attribute>] => {
|
||||
if (location === 'top') {
|
||||
return [
|
||||
createEventBox('mid-eb event-top-padding-static', windowName),
|
||||
createEventBox('mid-eb event-top-padding', windowName),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
createEventBox('mid-eb event-bottom-padding', windowName),
|
||||
createEventBox('mid-eb event-bottom-padding-static', windowName),
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import options from 'options';
|
||||
import { DropdownMenuProps } from 'lib/types/dropdownmenu';
|
||||
import { Attribute, Child, Exclusivity } from 'lib/types/widget';
|
||||
import Window from 'types/widgets/window';
|
||||
import { barEventMargins } from './eventBoxes/index';
|
||||
import { globalEventBoxes } from 'globals/dropdown';
|
||||
|
||||
const { location } = options.theme.bar;
|
||||
|
||||
// NOTE: We make the window visible for 2 seconds (on startup) so the child
|
||||
// elements can allocat their proper dimensions.
|
||||
// Otherwise the width that we rely on for menu positioning is set improperly
|
||||
// for the first time we open a menu of each type.
|
||||
const initRender = Variable(true);
|
||||
|
||||
setTimeout(() => {
|
||||
initRender.value = false;
|
||||
}, 2000);
|
||||
|
||||
export default ({
|
||||
name,
|
||||
child,
|
||||
transition,
|
||||
exclusivity = 'ignore' as Exclusivity,
|
||||
...props
|
||||
}: DropdownMenuProps): Window<Child, Attribute> =>
|
||||
Widget.Window({
|
||||
name,
|
||||
class_names: [name, 'dropdown-menu'],
|
||||
setup: (w) => w.keybind('Escape', () => App.closeWindow(name)),
|
||||
visible: initRender.bind('value'),
|
||||
keymode: 'on-demand',
|
||||
exclusivity,
|
||||
layer: 'top',
|
||||
anchor: location.bind('value').as((ln) => [ln, 'left']),
|
||||
child: Widget.EventBox({
|
||||
class_name: 'parent-event',
|
||||
on_primary_click: () => App.closeWindow(name),
|
||||
on_secondary_click: () => App.closeWindow(name),
|
||||
child: Widget.Box({
|
||||
class_name: 'top-eb',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
className: 'event-box-container',
|
||||
children: location.bind('value').as((lcn) => {
|
||||
if (lcn === 'top') {
|
||||
return barEventMargins(name);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
}),
|
||||
Widget.EventBox({
|
||||
class_name: 'in-eb menu-event-box',
|
||||
on_primary_click: () => {
|
||||
return true;
|
||||
},
|
||||
on_secondary_click: () => {
|
||||
return true;
|
||||
},
|
||||
setup: (self) => {
|
||||
globalEventBoxes.value[name] = self;
|
||||
},
|
||||
child: Widget.Box({
|
||||
class_name: 'dropdown-menu-container',
|
||||
css: 'padding: 1px; margin: -1px;',
|
||||
child: Widget.Revealer({
|
||||
revealChild: false,
|
||||
setup: (self) =>
|
||||
self.hook(App, (_, wname, visible) => {
|
||||
if (wname === name) self.reveal_child = visible;
|
||||
}),
|
||||
transition,
|
||||
transitionDuration: options.menus.transitionTime.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'dropdown-menu-container',
|
||||
can_focus: true,
|
||||
children: [child],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
className: 'event-box-container',
|
||||
children: location.bind('value').as((lcn) => {
|
||||
if (lcn === 'bottom') {
|
||||
return barEventMargins(name);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
...props,
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
|
||||
import options from 'options';
|
||||
import { bash } from 'lib/utils';
|
||||
import { Widget as TWidget } from 'types/@girs/gtk-3.0/gtk-3.0.cjs';
|
||||
import { Monitor } from 'types/service/hyprland';
|
||||
import Box from 'types/widgets/box';
|
||||
import EventBox from 'types/widgets/eventbox';
|
||||
import Revealer from 'types/widgets/revealer';
|
||||
import { globalEventBoxes } from 'globals/dropdown';
|
||||
|
||||
type NestedRevealer = Revealer<Box<TWidget, unknown>, unknown>;
|
||||
type NestedBox = Box<NestedRevealer, unknown>;
|
||||
type NestedEventBox = EventBox<NestedBox, unknown>;
|
||||
|
||||
const { location } = options.theme.bar;
|
||||
const { scalingPriority } = options;
|
||||
|
||||
export const calculateMenuPosition = async (pos: number[], windowName: string): Promise<void> => {
|
||||
const self = globalEventBoxes.value[windowName] as NestedEventBox;
|
||||
const curHyprlandMonitor = hyprland.monitors.find((m) => m.id === hyprland.active.monitor.id);
|
||||
const dropdownWidth = self.child.get_allocation().width;
|
||||
const dropdownHeight = self.child.get_allocation().height;
|
||||
|
||||
let hyprScaling = 1;
|
||||
try {
|
||||
const monitorInfo = await bash('hyprctl monitors -j');
|
||||
const parsedMonitorInfo = JSON.parse(monitorInfo);
|
||||
|
||||
const foundMonitor = parsedMonitorInfo.find((monitor: Monitor) => monitor.id === hyprland.active.monitor.id);
|
||||
hyprScaling = foundMonitor?.scale || 1;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing hyprland monitors: ${error}`);
|
||||
}
|
||||
|
||||
let monWidth = curHyprlandMonitor?.width;
|
||||
let monHeight = curHyprlandMonitor?.height;
|
||||
|
||||
if (monWidth === undefined || monHeight === undefined || hyprScaling === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If GDK Scaling is applied, then get divide width by scaling
|
||||
// to get the proper coordinates.
|
||||
// Ex: On a 2860px wide monitor... if scaling is set to 2, then the right
|
||||
// end of the monitor is the 1430th pixel.
|
||||
const gdkScale = Utils.exec('bash -c "echo $GDK_SCALE"');
|
||||
|
||||
if (scalingPriority.value === 'both') {
|
||||
const scale = parseFloat(gdkScale);
|
||||
monWidth = monWidth / scale;
|
||||
monHeight = monHeight / scale;
|
||||
|
||||
monWidth = monWidth / hyprScaling;
|
||||
monHeight = monHeight / hyprScaling;
|
||||
} else if (/^\d+(.\d+)?$/.test(gdkScale) && scalingPriority.value === 'gdk') {
|
||||
const scale = parseFloat(gdkScale);
|
||||
monWidth = monWidth / scale;
|
||||
monHeight = monHeight / scale;
|
||||
} else {
|
||||
monWidth = monWidth / hyprScaling;
|
||||
monHeight = monHeight / hyprScaling;
|
||||
}
|
||||
|
||||
// If monitor is vertical (transform = 1 || 3) swap height and width
|
||||
const isVertical = curHyprlandMonitor?.transform !== undefined ? curHyprlandMonitor.transform % 2 !== 0 : false;
|
||||
|
||||
if (isVertical) {
|
||||
[monWidth, monHeight] = [monHeight, monWidth];
|
||||
}
|
||||
|
||||
let marginRight = monWidth - dropdownWidth / 2;
|
||||
marginRight = marginRight - pos[0];
|
||||
let marginLeft = monWidth - dropdownWidth - marginRight;
|
||||
|
||||
const minimumMargin = 0;
|
||||
|
||||
if (marginRight < minimumMargin) {
|
||||
marginRight = minimumMargin;
|
||||
marginLeft = monWidth - dropdownWidth - minimumMargin;
|
||||
}
|
||||
|
||||
if (marginLeft < minimumMargin) {
|
||||
marginLeft = minimumMargin;
|
||||
marginRight = monWidth - dropdownWidth - minimumMargin;
|
||||
}
|
||||
|
||||
self.set_margin_left(marginLeft);
|
||||
self.set_margin_right(marginRight);
|
||||
|
||||
if (location.value === 'top') {
|
||||
self.set_margin_top(0);
|
||||
self.set_margin_bottom(monHeight);
|
||||
} else {
|
||||
self.set_margin_bottom(0);
|
||||
self.set_margin_top(monHeight - dropdownHeight);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
import { WINDOW_LAYOUTS } from 'globals/window';
|
||||
import { LayoutFunction, Layouts, PopupWindowProps } from 'lib/types/popupwindow';
|
||||
import { Attribute, Child, Exclusivity, GtkWidget, Transition } from 'lib/types/widget';
|
||||
import Box from 'types/widgets/box';
|
||||
import EventBox from 'types/widgets/eventbox';
|
||||
import Window from 'types/widgets/window';
|
||||
|
||||
type Opts = {
|
||||
className: string;
|
||||
vexpand: boolean;
|
||||
};
|
||||
|
||||
export const Padding = (name: string, opts: Opts): EventBox<Box<GtkWidget, Attribute>, unknown> =>
|
||||
Widget.EventBox({
|
||||
class_name: opts?.className || '',
|
||||
hexpand: true,
|
||||
vexpand: typeof opts?.vexpand === 'boolean' ? opts.vexpand : true,
|
||||
can_focus: false,
|
||||
child: Widget.Box(),
|
||||
setup: (w) => w.on('button-press-event', () => App.toggleWindow(name)),
|
||||
});
|
||||
|
||||
const PopupRevealer = (
|
||||
name: string,
|
||||
child: GtkWidget,
|
||||
transition = 'slide_down' as Transition,
|
||||
): Box<Child, Attribute> =>
|
||||
Widget.Box(
|
||||
{ css: 'padding: 1px;' },
|
||||
Widget.Revealer({
|
||||
transition,
|
||||
child: Widget.Box({
|
||||
class_name: `window-content ${name}-window`,
|
||||
child,
|
||||
}),
|
||||
transitionDuration: 200,
|
||||
setup: (self) =>
|
||||
self.hook(App, (_, wname, visible) => {
|
||||
if (wname === name) self.reveal_child = visible;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const Layout: LayoutFunction = (name: string, child: GtkWidget, transition: Transition) => ({
|
||||
center: () =>
|
||||
Widget.CenterBox(
|
||||
{},
|
||||
Padding(name, {} as Opts),
|
||||
Widget.CenterBox(
|
||||
{ vertical: true },
|
||||
Padding(name, {} as Opts),
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
top: () =>
|
||||
Widget.CenterBox(
|
||||
{},
|
||||
Padding(name, {} as Opts),
|
||||
Widget.Box({ vertical: true }, PopupRevealer(name, child, transition), Padding(name, {} as Opts)),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
'top-right': () =>
|
||||
Widget.Box(
|
||||
{},
|
||||
Padding(name, {} as Opts),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name, {
|
||||
vexpand: false,
|
||||
className: 'event-top-padding',
|
||||
}),
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
),
|
||||
'top-center': () =>
|
||||
Widget.Box(
|
||||
{},
|
||||
Padding(name, {} as Opts),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name, {
|
||||
vexpand: false,
|
||||
className: 'event-top-padding',
|
||||
}),
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
'top-left': () =>
|
||||
Widget.Box(
|
||||
{},
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name, {
|
||||
vexpand: false,
|
||||
className: 'event-top-padding',
|
||||
}),
|
||||
PopupRevealer(name, child, transition),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
'bottom-left': () =>
|
||||
Widget.Box(
|
||||
{},
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name, {} as Opts),
|
||||
PopupRevealer(name, child, transition),
|
||||
),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
'bottom-center': () =>
|
||||
Widget.Box(
|
||||
{},
|
||||
Padding(name, {} as Opts),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name, {} as Opts),
|
||||
PopupRevealer(name, child, transition),
|
||||
),
|
||||
Padding(name, {} as Opts),
|
||||
),
|
||||
'bottom-right': () =>
|
||||
Widget.Box(
|
||||
{},
|
||||
Padding(name, {} as Opts),
|
||||
Widget.Box(
|
||||
{
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
},
|
||||
Padding(name, {} as Opts),
|
||||
PopupRevealer(name, child, transition),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const isValidLayout = (layout: string): layout is Layouts => {
|
||||
return WINDOW_LAYOUTS.includes(layout);
|
||||
};
|
||||
|
||||
export default ({
|
||||
name,
|
||||
child,
|
||||
layout = 'center',
|
||||
transition = 'none',
|
||||
exclusivity = 'ignore' as Exclusivity,
|
||||
...props
|
||||
}: PopupWindowProps): Window<Child, Attribute> => {
|
||||
const layoutFn = isValidLayout(layout) ? layout : 'center';
|
||||
|
||||
const layoutWidget = Layout(name, child, transition)[layoutFn]();
|
||||
|
||||
return Widget.Window({
|
||||
name,
|
||||
class_names: [name, 'popup-window'],
|
||||
setup: (w) => w.keybind('Escape', () => App.closeWindow(name)),
|
||||
visible: false,
|
||||
keymode: 'on-demand',
|
||||
exclusivity,
|
||||
layer: 'top',
|
||||
anchor: ['top', 'bottom', 'right', 'left'],
|
||||
child: layoutWidget,
|
||||
...props,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { Notification, Notifications } from 'types/service/notifications';
|
||||
import Box from 'types/widgets/box';
|
||||
|
||||
const Action = (notif: Notification, notifs: Notifications): Box<Child, Attribute> => {
|
||||
if (notif.actions !== undefined && notif.actions.length > 0) {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-card-actions',
|
||||
hexpand: true,
|
||||
vpack: 'end',
|
||||
children: notif.actions.map((action) => {
|
||||
return Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: 'notification-action-buttons',
|
||||
on_primary_click: () => {
|
||||
if (action.id.includes('scriptAction:-')) {
|
||||
Utils.execAsync(`${action.id.replace('scriptAction:-', '')}`).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
notifs.CloseNotification(notif.id);
|
||||
} else {
|
||||
notif.invoke(action.id);
|
||||
}
|
||||
},
|
||||
child: Widget.Box({
|
||||
hpack: 'center',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'notification-action-buttons-label',
|
||||
hexpand: true,
|
||||
label: action.label,
|
||||
max_width_chars: 15,
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return Widget.Box();
|
||||
};
|
||||
|
||||
export { Action };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Notification } from 'types/service/notifications';
|
||||
import { notifHasImg } from '../../menus/notifications/utils.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
export const Body = (notif: Notification): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
vpack: 'start',
|
||||
hexpand: true,
|
||||
class_name: 'notification-card-body',
|
||||
children: [
|
||||
Widget.Label({
|
||||
hexpand: true,
|
||||
use_markup: true,
|
||||
xalign: 0,
|
||||
justification: 'left',
|
||||
truncate: 'end',
|
||||
lines: 2,
|
||||
max_width_chars: !notifHasImg(notif) ? 35 : 28,
|
||||
wrap: true,
|
||||
class_name: 'notification-card-body-label',
|
||||
label: notif['body'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { Notification, Notifications } from 'types/service/notifications';
|
||||
import Button from 'types/widgets/button';
|
||||
import Label from 'types/widgets/label';
|
||||
|
||||
export const CloseButton = (notif: Notification, notifs: Notifications): Button<Label<Child>, Attribute> => {
|
||||
return Widget.Button({
|
||||
class_name: 'close-notification-button',
|
||||
on_primary_click: () => {
|
||||
notifs.CloseNotification(notif.id);
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'txt-icon notif-close',
|
||||
label: '',
|
||||
hpack: 'center',
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Notification } from 'types/service/notifications.js';
|
||||
import { getNotificationIcon } from 'globals/notification.js';
|
||||
import Box from 'types/widgets/box';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
|
||||
const NotificationIcon = ({
|
||||
app_entry = '',
|
||||
app_icon = '',
|
||||
app_name = '',
|
||||
}: Partial<Notification>): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
css: `
|
||||
min-width: 2rem;
|
||||
min-height: 2rem;
|
||||
`,
|
||||
child: Widget.Icon({
|
||||
class_name: 'notification-icon',
|
||||
icon: getNotificationIcon(app_name, app_icon, app_entry),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export { NotificationIcon };
|
||||
@@ -0,0 +1,56 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import { notifHasImg } from '../../menus/notifications/utils.js';
|
||||
import { NotificationIcon } from './icon.js';
|
||||
import { Notification } from 'types/service/notifications';
|
||||
import options from 'options.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const { military } = options.menus.clock.time;
|
||||
|
||||
export const Header = (notif: Notification): Box<Child, Attribute> => {
|
||||
const time = (time: number, format = '%I:%M %p'): string => {
|
||||
return GLib.DateTime.new_from_unix_local(time).format(military.value ? '%H:%M' : format) || '--';
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
vertical: false,
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-header',
|
||||
hpack: 'start',
|
||||
children: [NotificationIcon(notif)],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-header',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
vpack: 'start',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'notification-card-header-label',
|
||||
hpack: 'start',
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
max_width_chars: !notifHasImg(notif) ? 30 : 19,
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: notif['summary'],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'notification-card-header menu',
|
||||
hpack: 'end',
|
||||
vpack: 'start',
|
||||
hexpand: true,
|
||||
child: Widget.Label({
|
||||
vexpand: true,
|
||||
class_name: 'notification-time',
|
||||
label: time(notif.time),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Notification } from 'types/service/notifications';
|
||||
import { notifHasImg } from '../../menus/notifications/utils.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const Image = (notif: Notification): Box<Child, Attribute> => {
|
||||
if (notifHasImg(notif)) {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-card-image-container',
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
vexpand: false,
|
||||
child: Widget.Box({
|
||||
hpack: 'center',
|
||||
vexpand: false,
|
||||
class_name: 'notification-card-image',
|
||||
css: `background-image: url("${notif.image}")`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return Widget.Box();
|
||||
};
|
||||
|
||||
export { Image };
|
||||
@@ -0,0 +1,80 @@
|
||||
const notifs = await Service.import('notifications');
|
||||
import options from 'options';
|
||||
import { notifHasImg } from '../menus/notifications/utils.js';
|
||||
import { Image } from './image/index.js';
|
||||
import { Action } from './actions/index.js';
|
||||
import { Header } from './header/index.js';
|
||||
import { Body } from './body/index.js';
|
||||
import { CloseButton } from './close/index.js';
|
||||
import { getPosition } from 'lib/utils.js';
|
||||
import { filterNotifications } from 'lib/shared/notifications.js';
|
||||
import { Notification } from 'types/service/notifications.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
const hyprland = await Service.import('hyprland');
|
||||
|
||||
const { position, timeout, cache_actions, monitor, active_monitor, displayedTotal, ignore } = options.notifications;
|
||||
|
||||
const curMonitor = Variable(monitor.value);
|
||||
|
||||
hyprland.active.connect('changed', () => {
|
||||
curMonitor.value = hyprland.active.monitor.id;
|
||||
});
|
||||
|
||||
export default (): Window<Box<Child, Attribute>, unknown> => {
|
||||
Utils.merge([timeout.bind('value'), cache_actions.bind('value')], (timeout, doCaching) => {
|
||||
notifs.popupTimeout = timeout;
|
||||
notifs.cacheActions = doCaching;
|
||||
});
|
||||
|
||||
return Widget.Window({
|
||||
name: 'notifications-window',
|
||||
class_name: 'notifications-window',
|
||||
monitor: Utils.merge(
|
||||
[curMonitor.bind('value'), monitor.bind('value'), active_monitor.bind('value')],
|
||||
(curMon, mon, activeMonitor) => {
|
||||
if (activeMonitor === true) {
|
||||
return curMon;
|
||||
}
|
||||
|
||||
return mon;
|
||||
},
|
||||
),
|
||||
layer: options.tear.bind('value').as((tear) => (tear ? 'top' : 'overlay')),
|
||||
anchor: position.bind('value').as((v) => getPosition(v)),
|
||||
exclusivity: 'normal',
|
||||
child: Widget.Box({
|
||||
class_name: 'notification-card-container',
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
setup: (self) => {
|
||||
Utils.merge(
|
||||
[notifs.bind('popups'), ignore.bind('value')],
|
||||
(notifications: Notification[], ignoredNotifs: string[]) => {
|
||||
const filteredNotifications = filterNotifications(notifications, ignoredNotifs);
|
||||
|
||||
return (self.children = filteredNotifications.slice(0, displayedTotal.value).map((notif) => {
|
||||
return Widget.Box({
|
||||
class_name: 'notification-card',
|
||||
vpack: 'start',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Image(notif),
|
||||
Widget.Box({
|
||||
vpack: 'start',
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
class_name: `notification-card-content ${!notifHasImg(notif) ? 'noimg' : ''}`,
|
||||
children: [Header(notif), Body(notif), Action(notif, notifs)],
|
||||
}),
|
||||
CloseButton(notif, notifs),
|
||||
],
|
||||
});
|
||||
}));
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { OSDOrientation } from 'lib/types/options';
|
||||
import brightness from 'services/Brightness';
|
||||
import options from 'options';
|
||||
import Box from 'types/widgets/box';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
const audio = await Service.import('audio');
|
||||
|
||||
export const OSDBar = (ort: OSDOrientation): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
class_name: 'osd-bar-container',
|
||||
children: [
|
||||
Widget.LevelBar({
|
||||
class_name: 'osd-bar',
|
||||
vertical: ort === 'vertical',
|
||||
inverted: ort === 'vertical',
|
||||
bar_mode: 'continuous',
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
self.class_names = self.class_names.filter((c) => c !== 'overflow');
|
||||
self.value = brightness.screen;
|
||||
},
|
||||
'notify::screen',
|
||||
);
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
self.class_names = self.class_names.filter((c) => c !== 'overflow');
|
||||
self.value = brightness.kbd;
|
||||
},
|
||||
'notify::kbd',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
self.toggleClassName('overflow', audio.microphone.volume > 1);
|
||||
self.value =
|
||||
audio.microphone.volume <= 1 ? audio.microphone.volume : audio.microphone.volume - 1;
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audio.microphone.volume > 1 &&
|
||||
(!options.theme.osd.muted_zero.value || audio.microphone.is_muted === false),
|
||||
);
|
||||
self.value =
|
||||
options.theme.osd.muted_zero.value && audio.microphone.is_muted !== false
|
||||
? 0
|
||||
: audio.microphone.volume <= 1
|
||||
? audio.microphone.volume
|
||||
: audio.microphone.volume - 1;
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
self.toggleClassName('overflow', audio.speaker.volume > 1);
|
||||
self.value = audio.speaker.volume <= 1 ? audio.speaker.volume : audio.speaker.volume - 1;
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audio.speaker.volume > 1 &&
|
||||
(!options.theme.osd.muted_zero.value || audio.speaker.is_muted === false),
|
||||
);
|
||||
self.value =
|
||||
options.theme.osd.muted_zero.value && audio.speaker.is_muted !== false
|
||||
? 0
|
||||
: audio.speaker.volume <= 1
|
||||
? audio.speaker.volume
|
||||
: audio.speaker.volume - 1;
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import brightness from 'services/Brightness';
|
||||
import Box from 'types/widgets/box';
|
||||
const audio = await Service.import('audio');
|
||||
|
||||
export const OSDIcon = (): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
class_name: 'osd-icon-container',
|
||||
hexpand: true,
|
||||
child: Widget.Label({
|
||||
class_name: 'osd-icon txt-icon',
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
self.label = '';
|
||||
},
|
||||
'notify::screen',
|
||||
);
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
self.label = '';
|
||||
},
|
||||
'notify::kbd',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
self.label = audio.microphone.is_muted ? '' : '';
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
self.label = audio.microphone.is_muted ? '' : '';
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
self.label = audio.speaker.is_muted ? '' : '';
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
self.label = audio.speaker.is_muted ? '' : '';
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
import options from 'options';
|
||||
import brightness from 'services/Brightness';
|
||||
import { OSDLabel } from './label/index';
|
||||
import { OSDBar } from './bar/index';
|
||||
import { OSDIcon } from './icon/index';
|
||||
import { getPosition } from 'lib/utils';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { Revealer } from 'resource:///com/github/Aylur/ags/widgets/revealer.js';
|
||||
import { Window } from 'resource:///com/github/Aylur/ags/widgets/window.js';
|
||||
const hyprland = await Service.import('hyprland');
|
||||
const audio = await Service.import('audio');
|
||||
|
||||
const { enable, duration, orientation, location, active_monitor, monitor } = options.theme.osd;
|
||||
|
||||
const curMonitor = Variable(monitor.value);
|
||||
|
||||
hyprland.active.connect('changed', () => {
|
||||
curMonitor.value = hyprland.active.monitor.id;
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
const handleRevealRevealer = (self: Revealer<Child, Attribute>, property: 'reveal_child' | 'visible'): void => {
|
||||
if (!enable.value || property !== 'reveal_child') {
|
||||
return;
|
||||
}
|
||||
|
||||
self.reveal_child = true;
|
||||
|
||||
count++;
|
||||
Utils.timeout(duration.value, () => {
|
||||
count--;
|
||||
|
||||
if (count === 0) {
|
||||
self.reveal_child = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRevealWindow = (self: Window<Child, Attribute>, property: 'reveal_child' | 'visible'): void => {
|
||||
if (!enable.value || property !== 'visible') {
|
||||
return;
|
||||
}
|
||||
|
||||
self.visible = true;
|
||||
|
||||
count++;
|
||||
Utils.timeout(duration.value, () => {
|
||||
count--;
|
||||
|
||||
if (count === 0) {
|
||||
self.visible = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleReveal = (
|
||||
self: Revealer<Child, Attribute> | Window<Child, Attribute>,
|
||||
property: 'reveal_child' | 'visible',
|
||||
): void => {
|
||||
if (self instanceof Revealer) {
|
||||
handleRevealRevealer(self, property);
|
||||
} else if (self instanceof Window) {
|
||||
handleRevealWindow(self, property);
|
||||
}
|
||||
};
|
||||
|
||||
const renderOSD = (): Revealer<Child, Attribute> => {
|
||||
return Widget.Revealer({
|
||||
transition: 'crossfade',
|
||||
reveal_child: false,
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
handleReveal(self, 'reveal_child');
|
||||
},
|
||||
'notify::screen',
|
||||
);
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
handleReveal(self, 'reveal_child');
|
||||
},
|
||||
'notify::kbd',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
handleReveal(self, 'reveal_child');
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
handleReveal(self, 'reveal_child');
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
handleReveal(self, 'reveal_child');
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
handleReveal(self, 'reveal_child');
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
child: Widget.Box({
|
||||
class_name: 'osd-container',
|
||||
vertical: orientation.bind('value').as((ort) => ort === 'vertical'),
|
||||
children: orientation.bind('value').as((ort) => {
|
||||
if (ort === 'vertical') {
|
||||
return [OSDLabel(), OSDBar(ort), OSDIcon()];
|
||||
}
|
||||
|
||||
return [OSDIcon(), OSDBar(ort), OSDLabel()];
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default (): Window<Child, Attribute> =>
|
||||
Widget.Window({
|
||||
monitor: Utils.merge(
|
||||
[curMonitor.bind('value'), monitor.bind('value'), active_monitor.bind('value')],
|
||||
(curMon, mon, activeMonitor) => {
|
||||
if (activeMonitor === true) {
|
||||
return curMon;
|
||||
}
|
||||
|
||||
return mon;
|
||||
},
|
||||
),
|
||||
name: `indicator`,
|
||||
class_name: 'indicator',
|
||||
layer: options.tear.bind('value').as((tear) => (tear ? 'top' : 'overlay')),
|
||||
anchor: location.bind('value').as((v) => getPosition(v)),
|
||||
click_through: true,
|
||||
child: Widget.Box({
|
||||
css: 'padding: 1px;',
|
||||
expand: true,
|
||||
child: renderOSD(),
|
||||
}),
|
||||
setup: (self) => {
|
||||
self.hook(enable, () => {
|
||||
handleReveal(self, 'visible');
|
||||
});
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
handleReveal(self, 'visible');
|
||||
},
|
||||
'notify::screen',
|
||||
);
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
handleReveal(self, 'visible');
|
||||
},
|
||||
'notify::kbd',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
handleReveal(self, 'visible');
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
handleReveal(self, 'visible');
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
handleReveal(self, 'visible');
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
handleReveal(self, 'visible');
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import brightness from 'services/Brightness';
|
||||
import options from 'options';
|
||||
import Box from 'types/widgets/box';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
const audio = await Service.import('audio');
|
||||
|
||||
export const OSDLabel = (): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
class_name: 'osd-label-container',
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
child: Widget.Label({
|
||||
class_name: 'osd-label',
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
setup: (self) => {
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
self.class_names = self.class_names.filter((c) => c !== 'overflow');
|
||||
self.label = `${Math.round(brightness.screen * 100)}`;
|
||||
},
|
||||
'notify::screen',
|
||||
);
|
||||
self.hook(
|
||||
brightness,
|
||||
() => {
|
||||
self.class_names = self.class_names.filter((c) => c !== 'overflow');
|
||||
self.label = `${Math.round(brightness.kbd * 100)}`;
|
||||
},
|
||||
'notify::kbd',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
self.toggleClassName('overflow', audio.microphone.volume > 1);
|
||||
self.label = `${Math.round(audio.microphone.volume * 100)}`;
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.microphone,
|
||||
() => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audio.microphone.volume > 1 &&
|
||||
(!options.theme.osd.muted_zero.value || audio.microphone.is_muted === false),
|
||||
);
|
||||
const inputVolume =
|
||||
options.theme.osd.muted_zero.value && audio.microphone.is_muted !== false
|
||||
? 0
|
||||
: Math.round(audio.microphone.volume * 100);
|
||||
self.label = `${inputVolume}`;
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
self.toggleClassName('overflow', audio.speaker.volume > 1);
|
||||
self.label = `${Math.round(audio.speaker.volume * 100)}`;
|
||||
},
|
||||
'notify::volume',
|
||||
);
|
||||
self.hook(
|
||||
audio.speaker,
|
||||
() => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audio.speaker.volume > 1 &&
|
||||
(!options.theme.osd.muted_zero.value || audio.speaker.is_muted === false),
|
||||
);
|
||||
const speakerVolume =
|
||||
options.theme.osd.muted_zero.value && audio.speaker.is_muted !== false
|
||||
? 0
|
||||
: Math.round(audio.speaker.volume * 100);
|
||||
self.label = `${speakerVolume}`;
|
||||
},
|
||||
'notify::is-muted',
|
||||
);
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
Loaded 100 of 322 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user