refactor(ags): start type checking

This commit is contained in:
matt1432 2023-12-18 18:00:30 -05:00
parent ae545731e7
commit b85542091d
31 changed files with 774 additions and 639 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
*.egg-info
*.temp
*node_modules/
*types/
*package-lock.json
**/ags/style.css
result*

View file

@ -31,7 +31,6 @@
"curly": ["warn"],
"default-case-last": ["warn"],
"default-param-last": ["error"],
"dot-notation": ["warn", { "allowPattern": ".*-|_.*" }],
"eqeqeq": ["error", "smart"],
"func-names": ["warn", "never"],
"func-style": ["warn", "expression"],
@ -53,7 +52,6 @@
"ignoreDefaultValues": true
}],
"no-multi-assign": ["error"],
"no-negated-condition": ["warn"],
"no-new": ["error"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],

View file

@ -7,15 +7,24 @@ import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
import EventBox from '../misc/cursorbox.js';
/**
* @param {import('types/service/applications.js').Application} app
*/
export default (app) => {
const icon = Icon({
icon: lookUpIcon(app.icon_name) ?
app.icon_name :
app.app.get_string('Icon') === 'nix-snowflake' ?
'' :
app.app.get_string('Icon'),
size: 42,
});
const icon = Icon({ size: 42 });
const iconString = app.app.get_string('Icon');
if (app.icon_name) {
if (lookUpIcon(app.icon_name)) {
icon.icon = app.icon_name;
}
else if (iconString && iconString !== 'nix-snowflake') {
icon.icon = iconString;
}
else {
icon.icon = '';
}
}
const textBox = Box({
vertical: true,
@ -46,14 +55,13 @@ export default (app) => {
hexpand: true,
class_name: 'app',
setup: (self) => {
self.app = app;
},
attribute: { app },
onPrimaryClickRelease: () => {
onPrimaryClickRelease: (self) => {
App.closeWindow('applauncher');
Hyprland.sendMessage(`dispatch exec sh -c ${app.executable}`);
++app.frequency;
Hyprland.sendMessage(`dispatch exec sh -c
${self.attribute.app.executable}`);
++self.attribute.app.frequency;
},
child: Box({

View file

@ -14,17 +14,20 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
let fzfResults;
const list = ListBox();
/** @param {String} text */
const setSort = (text) => {
const fzf = new Fzf(Applications.list, {
selector: (app) => app.name,
tiebreakers: [(a, b) => b._frequency -
a._frequency],
tiebreakers: [(a, b) => b.frequency -
a.frequency],
});
fzfResults = fzf.find(text);
list.set_sort_func((a, b) => {
const row1 = a.get_children()[0]?.app.name;
const row2 = b.get_children()[0]?.app.name;
// @ts-expect-error
const row1 = a.get_children()[0]?.attribute.app.name;
// @ts-expect-error
const row2 = b.get_children()[0]?.attribute.app.name;
if (!row1 || !row2) {
return 0;
@ -51,9 +54,10 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
makeNewChildren();
// FIXME: always visible
const placeholder = Label({
label: " Couldn't find a match",
className: 'placeholder',
class_name: 'placeholder',
});
const entry = Entry({
@ -73,17 +77,23 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
},
on_change: ({ text }) => {
if (!text) {
return;
}
setSort(text);
let visibleApps = 0;
list.get_children().forEach((row) => {
// @ts-expect-error
row.changed();
// @ts-expect-error
const item = row.get_children()[0];
if (item?.app) {
if (item?.attribute.app) {
const isMatching = fzfResults.find((r) => {
return r.item.name === item.app.name;
return r.item.name === item.attribute.app.name;
});
row.visible = isMatching;
@ -98,7 +108,7 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
});
return Box({
className: 'applauncher',
class_name: 'applauncher',
vertical: true,
setup: (self) => {
@ -120,7 +130,7 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
children: [
Box({
className: 'header',
class_name: 'header',
children: [
Icon('preferences-system-search-symbolic'),
entry,

View file

@ -5,54 +5,49 @@ import { Label, Box, EventBox, Icon, Revealer } from 'resource:///com/github/Ayl
import { SpeakerIcon } from '../../misc/audio-icons.js';
import Separator from '../../misc/separator.js';
const SpeakerIndicator = (props) => Icon({
...props,
binds: [['icon', SpeakerIcon, 'value']],
});
const SpeakerPercentLabel = (props) => Label({
...props,
setup: (self) => {
self.hook(Audio, (label) => {
if (Audio.speaker) {
label.label = `${Math.round(Audio.speaker.volume * 100)}%`;
}
}, 'speaker-changed');
},
});
const SPACING = 5;
export default () => {
const rev = Revealer({
const icon = Icon({
icon: SpeakerIcon.bind(),
});
const hoverRevLabel = Revealer({
transition: 'slide_right',
child: Box({
children: [
Separator(SPACING),
SpeakerPercentLabel(),
Label().hook(Audio, (self) => {
if (Audio.speaker?.volume) {
self.label =
`${Math.round(Audio.speaker?.volume * 100)}%`;
}
}, 'speaker-changed'),
],
}),
});
const widget = EventBox({
onHover: () => {
rev.revealChild = true;
on_hover: () => {
hoverRevLabel.reveal_child = true;
},
onHoverLost: () => {
rev.revealChild = false;
on_hover_lost: () => {
hoverRevLabel.reveal_child = false;
},
child: Box({
className: 'audio',
children: [
SpeakerIndicator(),
rev,
child: Box({
class_name: 'audio',
children: [
icon,
hoverRevLabel,
],
}),
});
widget.rev = rev;
return widget;
};

View file

@ -5,41 +5,28 @@ import { Label, Icon, Box } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.js';
const LOW_BATT = 20;
const SPACING = 5;
const Indicator = () => Icon({
className: 'battery-indicator',
export default () => Box({
class_name: 'toggle-off battery',
binds: [['icon', Battery, 'icon-name']],
setup: (self) => {
self.hook(Battery, () => {
children: [
Icon({
class_name: 'battery-indicator',
// @ts-expect-error
icon: Battery.bind('icon_name'),
}).hook(Battery, (self) => {
self.toggleClassName('charging', Battery.charging);
self.toggleClassName('charged', Battery.charged);
self.toggleClassName('low', Battery.percent < LOW_BATT);
});
},
});
}),
const LevelLabel = (props) => Label({
...props,
className: 'label',
setup: (self) => {
self.hook(Battery, () => {
self.label = `${Battery.percent}%`;
});
},
});
const SPACING = 5;
export default () => Box({
className: 'toggle-off battery',
children: [
Indicator(),
Separator(SPACING),
LevelLabel(),
Label({
label: Battery.bind('percent')
.transform((v) => `${v}%`),
}),
],
});

View file

@ -1,68 +1,59 @@
// @ts-expect-error
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import { Label, Box, EventBox, Icon, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.js';
const Indicator = (props) => Icon({
...props,
setup: (self) => {
self.hook(Bluetooth, () => {
if (Bluetooth.enabled) {
self.icon = Bluetooth.connectedDevices[0] ?
Bluetooth.connectedDevices[0].iconName :
'bluetooth-active-symbolic';
}
else {
self.icon = 'bluetooth-disabled-symbolic';
}
});
},
});
const ConnectedLabel = (props) => Label({
...props,
setup: (self) => {
self.hook(Bluetooth, () => {
self.label = Bluetooth.connectedDevices[0] ?
`${Bluetooth.connectedDevices[0]}` :
'Disconnected';
}, 'notify::connected-devices');
},
});
const SPACING = 5;
export default () => {
const rev = Revealer({
const icon = Icon().hook(Bluetooth, (self) => {
if (Bluetooth.enabled) {
self.icon = Bluetooth.connectedDevices[0] ?
Bluetooth.connectedDevices[0].iconName :
'bluetooth-active-symbolic';
}
else {
self.icon = 'bluetooth-disabled-symbolic';
}
});
const hoverRevLabel = Revealer({
transition: 'slide_right',
child: Box({
children: [
Separator(SPACING),
ConnectedLabel(),
Label().hook(Bluetooth, (self) => {
self.label = Bluetooth.connectedDevices[0] ?
`${Bluetooth.connectedDevices[0]}` :
'Disconnected';
}, 'notify::connected-devices'),
],
}),
});
const widget = EventBox({
onHover: () => {
rev.revealChild = true;
on_hover: () => {
hoverRevLabel.reveal_child = true;
},
onHoverLost: () => {
rev.revealChild = false;
on_hover_lost: () => {
hoverRevLabel.reveal_child = false;
},
child: Box({
className: 'bluetooth',
children: [
Indicator(),
rev,
child: Box({
class_name: 'bluetooth',
children: [
icon,
hoverRevLabel,
],
}),
});
widget.rev = rev;
return widget;
};

View file

@ -6,48 +6,44 @@ import Separator from '../../misc/separator.js';
const SPACING = 5;
const Indicator = (props) => Icon({
...props,
binds: [['icon', Brightness, 'screen-icon']],
});
const BrightnessPercentLabel = (props) => Label({
...props,
setup: (self) => {
self.hook(Brightness, () => {
self.label = `${Math.round(Brightness.screen * 100)}%`;
}, 'screen');
},
});
export default () => {
const rev = Revealer({
const icon = Icon({
// @ts-expect-error
icon: Brightness.bind('screenIcon'),
});
const hoverRevLabel = Revealer({
transition: 'slide_right',
child: Box({
children: [
Separator(SPACING),
BrightnessPercentLabel(),
Label().hook(Brightness, (self) => {
self.label = `${Math.round(Brightness.screen * 100)}%`;
}, 'screen'),
],
}),
});
const widget = EventBox({
onHover: () => {
rev.revealChild = true;
on_hover: () => {
hoverRevLabel.reveal_child = true;
},
onHoverLost: () => {
rev.revealChild = false;
on_hover_lost: () => {
hoverRevLabel.reveal_child = false;
},
child: Box({
className: 'brightness',
class_name: 'brightness',
children: [
Indicator(),
rev,
icon,
hoverRevLabel,
],
}),
});
widget.rev = rev;
return widget;
};

View file

@ -2,26 +2,11 @@ import App from 'resource:///com/github/Aylur/ags/app.js';
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
import GLib from 'gi://GLib';
const { DateTime } = GLib;
const { DateTime } = imports.gi.GLib;
import EventBox from '../../misc/cursorbox.js';
const ClockModule = () => Label({
className: 'clock',
setup: (self) => {
self.poll(1000, () => {
const time = DateTime.new_now_local();
self.label = time.format('%a. ') +
time.get_day_of_month() +
time.format(' %b. %H:%M');
});
},
});
export default () => EventBox({
className: 'toggle-off',
@ -35,5 +20,12 @@ export default () => EventBox({
});
},
child: ClockModule(),
child: Label({ class_name: 'clock' })
.poll(1000, (self) => {
const time = DateTime.new_now_local();
self.label = time.format('%a. ') +
time.get_day_of_month() +
time.format(' %b. %H:%M');
}),
});

View file

@ -11,27 +11,23 @@ export default () => Box({
children: [
Separator(SPACING / 2),
Icon({
size: 30,
setup: (self) => {
self.hook(Hyprland.active.client, () => {
const app = Applications
.query(Hyprland.active.client.class)[0];
Icon({ size: 30 })
.hook(Hyprland.active.client, (self) => {
const app = Applications
.query(Hyprland.active.client.class)[0];
if (app) {
self.icon = app.iconName;
self.visible = Hyprland.active.client.title !== '';
}
});
},
}),
if (app) {
self.icon = app.icon_name;
self.visible = Hyprland.active.client.title !== '';
}
}),
Separator(SPACING),
Label({
css: 'color: #CBA6F7; font-size: 18px',
truncate: 'end',
binds: [['label', Hyprland.active.client, 'title']],
label: Hyprland.active.client.bind('title'),
}),
],
});

View file

@ -5,7 +5,7 @@ import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import EventBox from '../../misc/cursorbox.js';
import Persist from '../../misc/persist.js';
const HeartState = Variable();
const HeartState = Variable('');
Persist({
name: 'heart',
@ -22,7 +22,7 @@ export default () => EventBox({
},
child: Label({
className: 'heart-toggle',
binds: [['label', HeartState, 'value']],
class_name: 'heart-toggle',
label: HeartState.bind(),
}),
});

View file

@ -8,69 +8,75 @@ const DEFAULT_KB = 'at-translated-set-2-keyboard';
const SPACING = 4;
const Indicator = () => Label({
css: 'font-size: 20px;',
setup: (self) => {
self.hook(Hyprland, (_, _n, layout) => {
if (layout) {
if (layout === 'error') {
return;
}
/**
* @param {Label} self
* @param {string} layout
* @param {void} _
*/
const getKbdLayout = (self, _, layout) => {
if (layout) {
if (layout === 'error') {
return;
}
const shortName = layout.match(/\(([A-Za-z]+)\)/);
const shortName = layout.match(/\(([A-Za-z]+)\)/);
self.label = shortName ? shortName[1] : layout;
}
else {
// At launch, kb layout is undefined
Hyprland.sendMessage('j/devices').then((obj) => {
const kb = JSON.parse(obj).keyboards
.find((val) => val.name === DEFAULT_KB);
// @ts-expect-error
self.label = shortName ? shortName[1] : layout;
}
else {
// At launch, kb layout is undefined
Hyprland.sendMessage('j/devices').then((obj) => {
const kb = Array.from(JSON.parse(obj).keyboards)
.find((v) => v.name === DEFAULT_KB);
layout = kb['active_keymap'];
layout = kb['active_keymap'];
const shortName = layout
.match(/\(([A-Za-z]+)\)/);
const shortName = layout
.match(/\(([A-Za-z]+)\)/);
self.label = shortName ? shortName[1] : layout;
}).catch(print);
}
}, 'keyboard-layout');
},
});
// @ts-expect-error
self.label = shortName ? shortName[1] : layout;
}).catch(print);
}
};
export default () => {
const rev = Revealer({
const hoverRevLabel = Revealer({
transition: 'slide_right',
child: Box({
children: [
Separator(SPACING),
Indicator(),
Label({ css: 'font-size: 20px;' })
.hook(Hyprland, getKbdLayout, 'keyboard-layout'),
],
}),
});
const widget = EventBox({
onHover: () => {
rev.revealChild = true;
on_hover: () => {
hoverRevLabel.reveal_child = true;
},
onHoverLost: () => {
rev.revealChild = false;
on_hover_lost: () => {
hoverRevLabel.reveal_child = false;
},
child: Box({
css: 'padding: 0 10px; margin-right: -10px;',
children: [
Icon({
icon: 'input-keyboard-symbolic',
size: 20,
}),
rev,
hoverRevLabel,
],
}),
});
widget.rev = rev;
return widget;
};

View file

@ -4,76 +4,66 @@ import { Label, Box, EventBox, Icon, Revealer } from 'resource:///com/github/Ayl
import Separator from '../../misc/separator.js';
const Indicator = (props) => Icon({
...props,
setup: (self) => {
self.hook(Network, () => {
if (Network.wifi.internet === 'connected' ||
Network.wifi.internet === 'connecting') {
self.icon = Network.wifi.iconName;
}
else if (Network.wired.internet === 'connected' ||
Network.wired.internet === 'connecting') {
self.icon = Network.wired.iconName;
}
else {
self.icon = Network.wifi.iconName;
}
});
},
});
const APLabel = (props) => Label({
...props,
setup: (self) => {
self.hook(Network, () => {
if (Network.wifi.internet === 'connected' ||
Network.wifi.internet === 'connecting') {
self.label = Network.wifi.ssid;
}
else if (Network.wired.internet === 'connected' ||
Network.wired.internet === 'connecting') {
self.label = 'Connected';
}
else {
self.label = 'Disconnected';
}
});
},
});
const SPACING = 5;
export default () => {
const rev = Revealer({
const indicator = Icon().hook(Network, (self) => {
if (Network.wifi.internet === 'connected' ||
Network.wifi.internet === 'connecting') {
self.icon = Network.wifi.icon_name;
}
else if (Network.wired.internet === 'connected' ||
Network.wired.internet === 'connecting') {
self.icon = Network.wired.icon_name;
}
else {
self.icon = Network.wifi.icon_name;
}
});
const label = Label().hook(Network, (self) => {
if (Network.wifi.internet === 'connected' ||
Network.wifi.internet === 'connecting') {
self.label = Network.wifi.ssid;
}
else if (Network.wired.internet === 'connected' ||
Network.wired.internet === 'connecting') {
self.label = 'Connected';
}
else {
self.label = 'Disconnected';
}
});
const hoverRevLabel = Revealer({
transition: 'slide_right',
child: Box({
children: [
Separator(SPACING),
APLabel(),
label,
],
}),
});
const widget = EventBox({
onHover: () => {
rev.revealChild = true;
on_hover: () => {
hoverRevLabel.reveal_child = true;
},
onHoverLost: () => {
rev.revealChild = false;
on_hover_lost: () => {
hoverRevLabel.reveal_child = false;
},
child: Box({
className: 'network',
children: [
Indicator(),
rev,
child: Box({
class_name: 'network',
children: [
indicator,
hoverRevLabel,
],
}),
});
widget.rev = rev;
return widget;
};

View file

@ -13,8 +13,11 @@ export default () => EventBox({
className: 'toggle-off',
onPrimaryClickRelease: (self) => {
App.getWindow('notification-center')
.setXPos(self.get_allocation(), 'right');
// @ts-expect-error
App.getWindow('notification-center')?.setXPos(
self.get_allocation(),
'right',
);
App.toggleWindow('notification-center');
},
@ -28,31 +31,27 @@ export default () => EventBox({
},
child: CenterBox({
className: 'notif-panel',
class_name: 'notif-panel',
center_widget: Box({
children: [
Icon({
setup: (self) => {
self.hook(Notifications, () => {
if (Notifications.dnd) {
self.icon = 'notification-disabled-symbolic';
}
else if (Notifications.notifications.length > 0) {
self.icon = 'notification-new-symbolic';
}
else {
self.icon = 'notification-symbolic';
}
});
},
Icon().hook(Notifications, (self) => {
if (Notifications.dnd) {
self.icon = 'notification-disabled-symbolic';
}
else if (Notifications.notifications.length > 0) {
self.icon = 'notification-new-symbolic';
}
else {
self.icon = 'notification-symbolic';
}
}),
Separator(SPACING),
Label({
binds: [['label', Notifications, 'notifications',
(n) => String(n.length)]],
label: Notifications.bind('notifications')
.transform((n) => String(n.length)),
}),
],
}),

View file

@ -1,4 +1,4 @@
import { Box, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Tablet from '../../../services/tablet.js';
import EventBox from '../../misc/cursorbox.js';
@ -9,14 +9,12 @@ export default () => EventBox({
onPrimaryClickRelease: () => Tablet.toggleOsk(),
setup: (self) => {
self.hook(Tablet, () => {
self.toggleClassName('toggle-on', Tablet.oskState);
}, 'osk-toggled');
},
child: Box({
className: 'osk-toggle',
children: [Label(' 󰌌 ')],
child: Label({
class_name: 'osk-toggle',
xalign: 0.6,
label: '󰌌 ',
}),
});
}).hook(Tablet, (self) => {
self.toggleClassName('toggle-on', Tablet.oskState);
}, 'osk-toggled');

View file

@ -20,8 +20,11 @@ export default () => EventBox({
onHoverLost: () => { /**/ },
onPrimaryClickRelease: (self) => {
App.getWindow('notification-center')
.setXPos(self.get_allocation(), 'right');
// @ts-expect-error
App.getWindow('notification-center').setXPos(
self.get_allocation(),
'right',
);
App.toggleWindow('quick-settings');
},
@ -35,7 +38,7 @@ export default () => EventBox({
},
child: Box({
className: 'quick-settings-toggle',
class_name: 'quick-settings-toggle',
vertical: false,
children: [
Separator(SPACING),

View file

@ -9,35 +9,40 @@ const REVEAL_DURATION = 500;
const SPACING = 12;
/** @param {import('types/service/systemtray').TrayItem} item */
const SysTrayItem = (item) => {
if (item.id === 'spotify-client') {
return;
}
return MenuItem({
// @ts-expect-error
submenu: item.menu,
tooltip_markup: item.bind('tooltip_markup'),
child: Revealer({
transition: 'slide_right',
transitionDuration: REVEAL_DURATION,
transition_duration: REVEAL_DURATION,
child: Icon({
size: 24,
binds: [['icon', item, 'icon']],
icon: item.bind('icon'),
}),
}),
submenu: item.menu,
binds: [['tooltipMarkup', item, 'tooltip-markup']],
});
};
const SysTray = () => MenuBar({
setup: (self) => {
self.items = new Map();
attribute: {
items: new Map(),
},
setup: (self) => {
self
.hook(SystemTray, (_, id) => {
const item = SystemTray.getItem(id);
if (self.items.has(id) || !item) {
if (self.attribute.items.has(id) || !item) {
return;
}
@ -48,21 +53,22 @@ const SysTray = () => MenuBar({
return;
}
self.items.set(id, w);
self.add(w);
self.attribute.items.set(id, w);
self.child = w;
self.show_all();
w.child.revealChild = true;
// @ts-expect-error
w.child.reveal_child = true;
}, 'added')
.hook(SystemTray, (_, id) => {
if (!self.items.has(id)) {
if (!self.attribute.items.has(id)) {
return;
}
self.items.get(id).child.revealChild = false;
self.attribute.items.get(id).child.reveal_child = false;
timeout(REVEAL_DURATION, () => {
self.items.get(id).destroy();
self.items.delete(id);
self.attribute.items.get(id).destroy();
self.attribute.items.delete(id);
});
}, 'removed');
},
@ -74,21 +80,17 @@ export default () => {
return Revealer({
transition: 'slide_right',
setup: (self) => {
self.hook(SystemTray, () => {
self.revealChild = systray.get_children().length > 0;
});
},
child: Box({
children: [
Box({
className: 'sys-tray',
class_name: 'sys-tray',
children: [systray],
}),
Separator(SPACING),
],
}),
}).hook(SystemTray, (self) => {
self.reveal_child = systray.get_children().length > 0;
});
};

View file

@ -5,19 +5,16 @@ import EventBox from '../../misc/cursorbox.js';
export default () => EventBox({
className: 'toggle-off',
class_name: 'toggle-off',
onPrimaryClickRelease: () => Tablet.toggleMode(),
setup: (self) => {
self.hook(Tablet, () => {
self.toggleClassName('toggle-on', Tablet.tabletMode);
}, 'mode-toggled');
},
child: Box({
className: 'tablet-toggle',
class_name: 'tablet-toggle',
vertical: false,
children: [Label(' 󰦧 ')],
}),
});
}).hook(Tablet, (self) => {
self.toggleClassName('toggle-on', Tablet.tabletMode);
}, 'mode-toggled');

View file

@ -7,36 +7,52 @@ import EventBox from '../../misc/cursorbox.js';
const URGENT_DURATION = 1000;
/** @typedef {import('types/widget.js').Widget} Widget */
const Workspace = ({ i } = {}) => {
/** @property {number} id */
const Workspace = ({ id }) => {
return Revealer({
transition: 'slide_right',
properties: [['id', i]],
attribute: { id },
child: EventBox({
tooltipText: `${i}`,
tooltipText: `${id}`,
onPrimaryClickRelease: () => {
Hyprland.sendMessage(`dispatch workspace ${i}`);
Hyprland.sendMessage(`dispatch workspace ${id}`);
},
child: Box({
vpack: 'center',
className: 'button',
class_name: 'button',
setup: (self) => {
self.update = (addr) => {
const occupied = Hyprland.getWorkspace(i)?.windows > 0;
/**
* @param {Widget} _
* @param {string|undefined} addr
*/
const update = (_, addr) => {
const workspace = Hyprland.getWorkspace(id);
const occupied = workspace && workspace['windows'] > 0;
self.toggleClassName('occupied', occupied);
self.toggleClassName('empty', !occupied);
if (!addr) {
return;
}
// Deal with urgent windows
if (Hyprland.getClient(addr)?.workspace.id === i) {
const client = Hyprland.getClient(addr);
const isThisUrgent = client &&
client['workspace']['id'] === id;
if (isThisUrgent) {
self.toggleClassName('urgent', true);
// Only show for a sec when urgent is current workspace
if (Hyprland.active.workspace.id === i) {
if (Hyprland.active.workspace.id === id) {
timeout(URGENT_DURATION, () => {
self.toggleClassName('urgent', false);
});
@ -45,13 +61,13 @@ const Workspace = ({ i } = {}) => {
};
self
.hook(Hyprland, () => self.update())
.hook(Hyprland, update)
// Deal with urgent windows
.hook(Hyprland, (_, a) => {
self.update(a);
}, 'urgent-window')
.hook(Hyprland, update, 'urgent-window')
.hook(Hyprland.active.workspace, () => {
if (Hyprland.active.workspace.id === i) {
if (Hyprland.active.workspace.id === id) {
self.toggleClassName('urgent', false);
}
});
@ -65,75 +81,80 @@ export default () => {
const L_PADDING = 16;
const WS_WIDTH = 30;
/** @param {Widget} self */
const updateHighlight = (self) => {
const currentId = Hyprland.active.workspace.id;
// @ts-expect-error
const indicators = self.get_parent().get_children()[0].child.children;
const currentIndex = indicators.findIndex((w) => w._id === currentId);
const currentIndex = Array.from(indicators)
.findIndex((w) => w.attribute.id === currentId);
if (currentIndex < 0) {
return;
}
// @ts-expect-error
self.setCss(`margin-left: ${L_PADDING + (currentIndex * WS_WIDTH)}px`);
};
const highlight = Box({
vpack: 'center',
hpack: 'start',
className: 'button active',
setup: (self) => {
self.hook(Hyprland.active.workspace, updateHighlight);
},
});
class_name: 'button active',
}).hook(Hyprland.active.workspace, updateHighlight);
const widget = Overlay({
pass_through: true,
overlays: [highlight],
child: EventBox({
child: Box({
className: 'workspaces',
class_name: 'workspaces',
properties: [
['workspaces'],
attribute: { workspaces: [] },
['refresh', (self) => {
self.children.forEach((rev) => {
setup: (self) => {
const refresh = () => {
Array.from(self.children).forEach((rev) => {
// @ts-expect-error
rev.reveal_child = false;
});
self._workspaces.forEach((ws) => {
Array.from(self.attribute.workspaces).forEach((ws) => {
ws.revealChild = true;
});
}],
};
['updateWorkspaces', (self) => {
const updateWorkspaces = () => {
Hyprland.workspaces.forEach((ws) => {
const currentWs = self.children.find((ch) => {
return ch._id === ws.id;
});
const currentWs = Array.from(self.children)
// @ts-expect-error
.find((ch) => ch.attribute.id === ws['id']);
if (!currentWs && ws.id > 0) {
self.add(Workspace({ i: ws.id }));
if (!currentWs && ws['id'] > 0) {
self.add(Workspace({ id: ws['id'] }));
}
});
self.show_all();
// Make sure the order is correct
self._workspaces.forEach((workspace, i) => {
workspace.get_parent().reorder_child(workspace, i);
});
}],
],
setup: (self) => {
self.hook(Hyprland, () => {
self._workspaces = self.children.filter((ch) => {
return Hyprland.workspaces.find((ws) => {
return ws.id === ch._id;
Array.from(self.attribute.workspaces)
.forEach((workspace, i) => {
workspace.get_parent()
.reorder_child(workspace, i);
});
}).sort((a, b) => a._id - b._id);
};
self._updateWorkspaces(self);
self._refresh(self);
self.hook(Hyprland, () => {
self.attribute.workspaces =
self.children.filter((ch) => {
return Hyprland.workspaces.find((ws) => {
// @ts-expect-error
return ws['id'] === ch.attribute.id;
});
// @ts-expect-error
}).sort((a, b) => a.attribute.id - b.attribute.id);
updateWorkspaces();
refresh();
// Make sure the highlight doesn't go too far
const TEMP_TIMEOUT = 10;

View file

@ -4,6 +4,7 @@ import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, EventBox, Revealer, Window } from 'resource:///com/github/Aylur/ags/widget.js';
/** @param {import('types/variable.js').Variable} variable */
const BarCloser = (variable) => Window({
name: 'bar-closer',
visible: false,
@ -11,8 +12,9 @@ const BarCloser = (variable) => Window({
layer: 'overlay',
child: EventBox({
onHover: (self) => {
on_hover: (self) => {
variable.value = false;
// @ts-expect-error
self.get_parent().visible = false;
},
@ -22,6 +24,7 @@ const BarCloser = (variable) => Window({
}),
});
/** @param {import('types/widgets/revealer').RevealerProps} props */
export default (props) => {
const Revealed = Variable(true);
const barCloser = BarCloser(Revealed);
@ -38,7 +41,9 @@ export default (props) => {
Hyprland.active.workspace.id,
);
Revealed.value = !workspace?.hasfullscreen;
if (workspace) {
Revealed.value = !workspace['hasfullscreen'];
}
})
.hook(Hyprland, (_, fullscreen) => {
@ -50,7 +55,7 @@ export default (props) => {
Revealer({
...props,
transition: 'slide_down',
revealChild: true,
reveal_child: true,
binds: [['revealChild', Revealed, 'value']],
}),
@ -59,7 +64,7 @@ export default (props) => {
binds: [['revealChild', Revealed, 'value', (v) => !v]],
child: EventBox({
onHover: () => {
on_hover: () => {
barCloser.visible = true;
Revealed.value = true;
},

View file

@ -27,10 +27,9 @@ export default () => Window({
child: BarReveal({
child: CenterBox({
css: 'margin: 6px 5px 5px 5px',
className: 'bar',
vertical: false,
class_name: 'bar',
startWidget: Box({
start_widget: Box({
hpack: 'start',
children: [
@ -53,7 +52,7 @@ export default () => Window({
],
}),
centerWidget: Box({
center_widget: Box({
children: [
Separator(SPACING),
@ -63,7 +62,7 @@ export default () => Window({
],
}),
endWidget: Box({
end_widget: Box({
hpack: 'end',
children: [
Heart(),

View file

@ -4,7 +4,7 @@ import Gtk from 'gi://Gtk';
const Lang = imports.lang;
export default (
place,
place = 'top left',
css = 'background-color: black;',
) => Box({
hpack: place.includes('left') ? 'start' : 'end',
@ -27,6 +27,7 @@ export default (
.get_property('border-radius', Gtk.StateFlags.NORMAL);
widget.set_size_request(r, r);
// @ts-expect-error
widget.connect('draw', Lang.bind(widget, (_, cr) => {
const c = widget.get_style_context()
.get_property('background-color', Gtk.StateFlags.NORMAL);

View file

@ -1,29 +1,28 @@
import { Box, Calendar, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import GLib from 'gi://GLib';
const { DateTime } = GLib;
const { DateTime } = imports.gi.GLib;
import PopupWindow from './misc/popup.js';
const Divider = () => Box({
className: 'divider',
class_name: 'divider',
vertical: true,
});
const Time = () => Box({
className: 'timebox',
class_name: 'timebox',
vertical: true,
children: [
children: [
Box({
className: 'time-container',
class_name: 'time-container',
hpack: 'center',
vpack: 'center',
children: [
children: [
Label({
className: 'content',
class_name: 'content',
label: 'hour',
setup: (self) => {
self.poll(1000, () => {
@ -35,7 +34,7 @@ const Time = () => Box({
Divider(),
Label({
className: 'content',
class_name: 'content',
label: 'minute',
setup: (self) => {
self.poll(1000, () => {
@ -48,11 +47,13 @@ const Time = () => Box({
}),
Box({
className: 'date-container',
class_name: 'date-container',
hpack: 'center',
child: Label({
css: 'font-size: 20px',
label: 'complete date',
setup: (self) => {
self.poll(1000, () => {
const time = DateTime.new_now_local();
@ -69,23 +70,26 @@ const Time = () => Box({
});
const CalendarWidget = () => Box({
className: 'cal-box',
class_name: 'cal-box',
child: Calendar({
showDayNames: true,
showHeading: true,
className: 'cal',
show_day_names: true,
show_heading: true,
class_name: 'cal',
}),
});
const TOP_MARGIN = 6;
export default () => PopupWindow({
name: 'calendar',
anchor: ['top'],
margins: [TOP_MARGIN, 0, 0, 0],
name: 'calendar',
child: Box({
className: 'date',
class_name: 'date',
vertical: true,
children: [
Time(),
CalendarWidget(),

View file

@ -11,25 +11,49 @@ const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
export default ({
properties,
setup = () => {/**/},
props,
} = {}) => {
attribute = {},
setup = (self) => {
self;
},
...props
}) => {
const widget = EventBox();
const gesture = Gtk.GestureDrag.new(widget);
// Have empty PlayerBox to define the size of the widget
const emptyPlayer = Box({ className: 'player' });
// Set this prop to differentiate it easily
emptyPlayer.empty = true;
const emptyPlayer = Box({
class_name: 'player',
attribute: { empty: true },
});
const content = Overlay({
...props,
properties: [
...properties,
['dragging', false],
],
attribute: {
...attribute,
dragging: false,
list: () => content.get_children()
// @ts-expect-error
.filter((ch) => !ch.attribute?.empty),
includesWidget: (playerW) => {
return Array.from(content.attribute.list())
.find((w) => w === playerW);
},
showTopOnly: () => Array.from(content.attribute.list())
.forEach((over) => {
over.visible = over === content.attribute.list().at(-1);
}),
moveToTop: (player) => {
player.visible = true;
content.reorder_overlay(player, -1);
timeout(ANIM_DURATION, () => {
content.attribute.showTopOnly();
});
},
},
child: emptyPlayer,
@ -37,32 +61,36 @@ export default ({
setup(self);
self
// @ts-expect-error
.hook(gesture, (overlay, realGesture) => {
if (realGesture) {
overlay.list().forEach((over) => {
overlay.attribute.list().forEach((over) => {
over.visible = true;
});
}
else {
overlay.showTopOnly();
overlay.attribute.showTopOnly();
}
// Don't allow gesture when only one player
if (overlay.list().length <= 1) {
if (overlay.attribute.list().length <= 1) {
return;
}
overlay._dragging = true;
overlay.attribute.dragging = true;
let offset = gesture.get_offset()[1];
const playerBox = overlay.attribute.list().at(-1);
const playerBox = overlay.list().at(-1);
if (!offset) {
return;
}
// Slide right
if (offset >= 0) {
playerBox.setCss(`
margin-left: ${offset}px;
margin-right: -${offset}px;
${playerBox._bgStyle}
${playerBox.attribute.bgStyle}
`);
}
@ -72,24 +100,24 @@ export default ({
playerBox.setCss(`
margin-left: -${offset}px;
margin-right: ${offset}px;
${playerBox._bgStyle}
${playerBox.attribute.bgStyle}
`);
}
}, 'drag-update')
.hook(gesture, (overlay) => {
// Don't allow gesture when only one player
if (overlay.list().length <= 1) {
if (overlay.attribute.list().length <= 1) {
return;
}
overlay._dragging = false;
overlay.attribute.dragging = false;
const offset = gesture.get_offset()[1];
const playerBox = overlay.list().at(-1);
const playerBox = overlay.attribute.list().at(-1);
// If crosses threshold after letting go, slide away
if (Math.abs(offset) > MAX_OFFSET) {
if (offset && Math.abs(offset) > MAX_OFFSET) {
// Disable inputs during animation
widget.sensitive = false;
@ -99,7 +127,7 @@ export default ({
${TRANSITION}
margin-left: ${OFFSCREEN}px;
margin-right: -${OFFSCREEN}px;
opacity: 0.7; ${playerBox._bgStyle}
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
@ -109,7 +137,7 @@ export default ({
${TRANSITION}
margin-left: -${OFFSCREEN}px;
margin-right: ${OFFSCREEN}px;
opacity: 0.7; ${playerBox._bgStyle}
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
@ -117,16 +145,17 @@ export default ({
// Put the player in the back after anim
overlay.reorder_overlay(playerBox, 0);
// Recenter player
playerBox.setCss(playerBox._bgStyle);
playerBox.setCss(playerBox.attribute.bgStyle);
widget.sensitive = true;
overlay.showTopOnly();
overlay.attribute.showTopOnly();
});
}
else {
// Recenter with transition for animation
playerBox.setCss(`${TRANSITION} ${playerBox._bgStyle}`);
playerBox.setCss(`${TRANSITION}
${playerBox.attribute.bgStyle}`);
}
}, 'drag-end');
},
@ -134,27 +163,5 @@ export default ({
widget.add(content);
// Overlay methods
content.list = () => content.get_children()
.filter((ch) => !ch.empty);
content.includesWidget = (playerW) => {
return content.list().find((w) => w === playerW);
};
content.showTopOnly = () => content.list().forEach((over) => {
over.visible = over === content.list().at(-1);
});
content.moveToTop = (player) => {
player.visible = true;
content.reorder_overlay(player, -1);
timeout(ANIM_DURATION, () => {
content.showTopOnly();
});
};
widget.getOverlay = () => content;
return widget;
};

View file

@ -6,6 +6,12 @@ import { execAsync, lookUpIcon, readFileAsync } from 'resource:///com/github/Ayl
import Separator from '../misc/separator.js';
import EventBox from '../misc/cursorbox.js';
/**
* @typedef {import('types/service/mpris').MprisPlayer} Player
* @typedef {import('types/variable').Variable} Variable
* @typedef {import('types/widgets/overlay').default} Overlay
*/
const ICON_SIZE = 32;
const icons = {
@ -29,59 +35,66 @@ const icons = {
};
export const CoverArt = (player, props) => CenterBox({
/**
* @param {Player} player
* @param {Variable} colors
* @param {import('types/widgets/centerbox').CenterBoxProps=} props
*/
export const CoverArt = (player, colors, props) => CenterBox({
...props,
// @ts-expect-error
vertical: true,
properties: [
['bgStyle', ''],
['player', player],
],
attribute: {
bgStyle: '',
player,
},
setup: (self) => {
// Give temp cover art
readFileAsync(player.coverPath).catch(() => {
if (!player.colors.value && !player.trackCoverUrl) {
player.colors.value = {
readFileAsync(player.cover_path).catch(() => {
if (!colors.value && !player.track_cover_url) {
colors.value = {
imageAccent: '#6b4fa2',
buttonAccent: '#ecdcff',
buttonText: '#25005a',
hoverAccent: '#d4baff',
};
self._bgStyle = `
self.attribute.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${player.colors.value.imageAccent}),
${colors.value.imageAccent}),
rgb(0, 0, 0);
background-size: cover;
background-position: center;
`;
self.setCss(self._bgStyle);
self.setCss(self.attribute.bgStyle);
}
});
self.hook(player, () => {
execAsync(['bash', '-c', `[[ -f "${player.coverPath}" ]] &&
coloryou "${player.coverPath}" | grep -v Warning`])
execAsync(['bash', '-c', `[[ -f "${player.cover_path}" ]] &&
coloryou "${player.cover_path}" | grep -v Warning`])
.then((out) => {
if (!Mpris.players.find((p) => player === p)) {
return;
}
player.colors.value = JSON.parse(out);
colors.value = JSON.parse(out);
self._bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${player.colors.value.imageAccent}),
url("${player.coverPath}");
background-size: cover;
background-position: center;
`;
self.attribute.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${colors.value.imageAccent}),
url("${player.cover_path}");
background-size: cover;
background-position: center;
`;
if (!self.get_parent()._dragging) {
self.setCss(self._bgStyle);
// @ts-expect-error
if (!self?.get_parent()?.attribute.dragging) {
self.setCss(self.attribute.bgStyle);
}
}).catch((err) => {
if (err !== '') {
@ -92,40 +105,48 @@ export const CoverArt = (player, props) => CenterBox({
},
});
export const TitleLabel = (player, props) => Label({
...props,
/** @param {Player} player */
export const TitleLabel = (player) => Label({
xalign: 0,
maxWidthChars: 40,
max_width_chars: 40,
truncate: 'end',
justification: 'left',
className: 'title',
binds: [['label', player, 'track-title']],
class_name: 'title',
label: player.bind('track_title'),
});
export const ArtistLabel = (player, props) => Label({
...props,
/** @param {Player} player */
export const ArtistLabel = (player) => Label({
xalign: 0,
maxWidthChars: 40,
max_width_chars: 40,
truncate: 'end',
justification: 'left',
className: 'artist',
binds: [['label', player, 'track-artists', (a) => a.join(', ') || '']],
class_name: 'artist',
label: player.bind('track_artists')
.transform((a) => a.join(', ') || ''),
});
export const PlayerIcon = (player, overlay, props) => {
/**
* @param {Player} player
* @param {Overlay} overlay
*/
export const PlayerIcon = (player, overlay) => {
/**
* @param {Player} p
* @param {Overlay=} widget
* @param {Overlay=} over
*/
const playerIcon = (p, widget, over) => EventBox({
...props,
tooltipText: p.identity || '',
tooltip_text: p.identity || '',
onPrimaryClickRelease: () => {
widget?.moveToTop(over);
widget?.attribute.moveToTop(over);
},
child: Icon({
className: widget ? 'position-indicator' : 'player-icon',
size: widget ? '' : ICON_SIZE,
class_name: widget ? 'position-indicator' : 'player-icon',
size: widget ? 0 : ICON_SIZE,
setup: (self) => {
self.hook(p, () => {
@ -137,33 +158,34 @@ export const PlayerIcon = (player, overlay, props) => {
}),
});
return Box({
setup: (self) => {
self.hook(Mpris, () => {
const thisIndex = overlay.list()
.indexOf(self.get_parent().get_parent());
return Box().hook(Mpris, (self) => {
const thisIndex = overlay.attribute.list()
.indexOf(self?.get_parent()?.get_parent());
self.children = overlay.list().map((over, i) => {
self.children.push(Separator(2));
self.children = Array.from(overlay.attribute.list())
.map((over, i) => {
self.children.push(Separator(2));
return i === thisIndex ?
playerIcon(player) :
playerIcon(over._player, overlay, over);
}).reverse();
});
},
return i === thisIndex ?
playerIcon(player) :
playerIcon(over.attribute.player, overlay, over);
})
.reverse();
});
};
export const PositionSlider = (player, props) => Slider({
...props,
className: 'position-slider',
/**
* @param {Player} player
* @param {Variable} colors
*/
export const PositionSlider = (player, colors) => Slider({
class_name: 'position-slider',
cursor: 'pointer',
vpack: 'center',
hexpand: true,
drawValue: false,
draw_value: false,
onChange: ({ value }) => {
on_change: ({ value }) => {
player.position = player.length * value;
},
@ -180,9 +202,9 @@ export const PositionSlider = (player, props) => Slider({
self
.poll(1000, () => update())
.hook(player, () => update(), 'position')
.hook(player.colors, () => {
if (player.colors.value) {
const c = player.colors.value;
.hook(colors, () => {
if (colors.value) {
const c = colors.value;
self.setCss(`
highlight { background-color: ${c.buttonAccent}; }
@ -192,29 +214,29 @@ export const PositionSlider = (player, props) => Slider({
`);
}
})
.on('button-press-event', (s) => {
s.cursor = 'grabbing';
.on('button-press-event', () => {
self.cursor = 'grabbing';
})
.on('button-release-event', (s) => {
s.cursor = 'pointer';
.on('button-release-event', () => {
self.cursor = 'pointer';
});
},
});
const PlayerButton = ({ player, items, onClick, prop }) => EventBox({
const PlayerButton = ({ player, colors, items, onClick, prop }) => EventBox({
child: Button({
properties: [['hovered', false]],
attribute: { hovered: false },
child: Stack({ items }),
onPrimaryClickRelease: () => player[onClick](),
on_primary_click_release: () => player[onClick](),
onHover: (self) => {
self._hovered = true;
on_hover: (self) => {
self.attribute.hovered = true;
if (prop === 'playBackStatus' && player.colors.value) {
const c = player.colors.value;
if (prop === 'playBackStatus' && colors.value) {
const c = colors.value;
items.forEach((item) => {
Array.from(items).forEach((item) => {
item[1].setCss(`
background-color: ${c.hoverAccent};
color: ${c.buttonText};
@ -227,12 +249,12 @@ const PlayerButton = ({ player, items, onClick, prop }) => EventBox({
}
},
onHoverLost: (self) => {
self._hovered = false;
if (prop === 'playBackStatus' && player.colors.value) {
const c = player.colors.value;
on_hover_lost: (self) => {
self.attribute.hovered = false;
if (prop === 'playBackStatus' && colors.value) {
const c = colors.value;
items.forEach((item) => {
Array.from(items).forEach((item) => {
item[1].setCss(`
background-color: ${c.buttonAccent};
color: ${c.buttonText};
@ -246,19 +268,20 @@ const PlayerButton = ({ player, items, onClick, prop }) => EventBox({
setup: (self) => {
self
.hook(player, () => {
// @ts-expect-error
self.child.shown = `${player[prop]}`;
})
.hook(player.colors, () => {
.hook(colors, () => {
if (!Mpris.players.find((p) => player === p)) {
return;
}
if (player.colors.value) {
const c = player.colors.value;
if (colors.value) {
const c = colors.value;
if (prop === 'playBackStatus') {
if (self._hovered) {
items.forEach((item) => {
if (self.attribute.hovered) {
Array.from(items).forEach((item) => {
item[1].setCss(`
background-color: ${c.hoverAccent};
color: ${c.buttonText};
@ -270,7 +293,7 @@ const PlayerButton = ({ player, items, onClick, prop }) => EventBox({
});
}
else {
items.forEach((item) => {
Array.from(items).forEach((item) => {
item[1].setCss(`
background-color: ${c.buttonAccent};
color: ${c.buttonText};
@ -292,15 +315,20 @@ const PlayerButton = ({ player, items, onClick, prop }) => EventBox({
}),
});
export const ShuffleButton = (player) => PlayerButton({
/**
* @param {Player} player
* @param {Variable} colors
*/
export const ShuffleButton = (player, colors) => PlayerButton({
player,
colors,
items: [
['true', Label({
className: 'shuffle enabled',
class_name: 'shuffle enabled',
label: icons.mpris.shuffle.enabled,
})],
['false', Label({
className: 'shuffle disabled',
class_name: 'shuffle disabled',
label: icons.mpris.shuffle.disabled,
})],
],
@ -308,19 +336,24 @@ export const ShuffleButton = (player) => PlayerButton({
prop: 'shuffleStatus',
});
export const LoopButton = (player) => PlayerButton({
/**
* @param {Player} player
* @param {Variable} colors
*/
export const LoopButton = (player, colors) => PlayerButton({
player,
colors,
items: [
['None', Label({
className: 'loop none',
class_name: 'loop none',
label: icons.mpris.loop.none,
})],
['Track', Label({
className: 'loop track',
class_name: 'loop track',
label: icons.mpris.loop.track,
})],
['Playlist', Label({
className: 'loop playlist',
class_name: 'loop playlist',
label: icons.mpris.loop.playlist,
})],
],
@ -328,19 +361,24 @@ export const LoopButton = (player) => PlayerButton({
prop: 'loopStatus',
});
export const PlayPauseButton = (player) => PlayerButton({
/**
* @param {Player} player
* @param {Variable} colors
*/
export const PlayPauseButton = (player, colors) => PlayerButton({
player,
colors,
items: [
['Playing', Label({
className: 'pausebutton playing',
class_name: 'pausebutton playing',
label: icons.mpris.playing,
})],
['Paused', Label({
className: 'pausebutton paused',
class_name: 'pausebutton paused',
label: icons.mpris.paused,
})],
['Stopped', Label({
className: 'pausebutton stopped paused',
class_name: 'pausebutton stopped paused',
label: icons.mpris.stopped,
})],
],
@ -348,15 +386,20 @@ export const PlayPauseButton = (player) => PlayerButton({
prop: 'playBackStatus',
});
export const PreviousButton = (player) => PlayerButton({
/**
* @param {Player} player
* @param {Variable} colors
*/
export const PreviousButton = (player, colors) => PlayerButton({
player,
colors,
items: [
['true', Label({
className: 'previous',
class_name: 'previous',
label: icons.mpris.prev,
})],
['false', Label({
className: 'previous',
class_name: 'previous',
label: icons.mpris.prev,
})],
],
@ -364,15 +407,20 @@ export const PreviousButton = (player) => PlayerButton({
prop: 'canGoPrev',
});
export const NextButton = (player) => PlayerButton({
/**
* @param {Player} player
* @param {Variable} colors
*/
export const NextButton = (player, colors) => PlayerButton({
player,
colors,
items: [
['true', Label({
className: 'next',
class_name: 'next',
label: icons.mpris.next,
})],
['false', Label({
className: 'next',
class_name: 'next',
label: icons.mpris.next,
})],
],

View file

@ -8,10 +8,21 @@ import PlayerGesture from './gesture.js';
import Separator from '../misc/separator.js';
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
const SPACING = 8;
/**
* @typedef {import('types/service/mpris').MprisPlayer} Player
* @typedef {import('types/widgets/overlay').default} Overlay
* @typedef {import('types/variable').Variable} Variable
*/
/**
* @param {Player} player
* @param {Overlay} overlay
*/
const Top = (player, overlay) => Box({
className: 'top',
class_name: 'top',
hpack: 'start',
vpack: 'start',
@ -20,16 +31,21 @@ const Top = (player, overlay) => Box({
],
});
const Center = (player) => Box({
className: 'center',
/**
* @param {Player} player
* @param {Variable} colors
*/
const Center = (player, colors) => Box({
class_name: 'center',
children: [
CenterBox({
// @ts-expect-error
vertical: true,
children: [
Box({
className: 'metadata',
class_name: 'metadata',
vertical: true,
hpack: 'start',
vpack: 'center',
@ -46,11 +62,12 @@ const Center = (player) => Box({
}),
CenterBox({
// @ts-expect-error
vertical: true,
children: [
null,
mpris.PlayPauseButton(player),
mpris.PlayPauseButton(player, colors),
null,
],
}),
@ -58,41 +75,43 @@ const Center = (player) => Box({
],
});
const SPACING = 8;
const Bottom = (player) => Box({
className: 'bottom',
/**
* @param {Player} player
* @param {Variable} colors
*/
const Bottom = (player, colors) => Box({
class_name: 'bottom',
children: [
mpris.PreviousButton(player, {
vpack: 'end',
hpack: 'start',
}),
mpris.PreviousButton(player, colors),
Separator(SPACING),
mpris.PositionSlider(player),
mpris.PositionSlider(player, colors),
Separator(SPACING),
mpris.NextButton(player),
mpris.NextButton(player, colors),
Separator(SPACING),
mpris.ShuffleButton(player),
mpris.ShuffleButton(player, colors),
Separator(SPACING),
mpris.LoopButton(player),
mpris.LoopButton(player, colors),
],
});
const PlayerBox = (player, overlay) => {
const widget = mpris.CoverArt(player, {
className: `player ${player.name}`,
/**
* @param {Player} player
* @param {Variable} colors
* @param {Overlay} overlay
*/
const PlayerBox = (player, colors, overlay) => {
const widget = mpris.CoverArt(player, colors, {
class_name: `player ${player.name}`,
hexpand: true,
children: [
Top(player, overlay),
Center(player),
Bottom(player),
],
start_widget: Top(player, overlay),
center_widget: Center(player, colors),
end_widget: Bottom(player, colors),
});
widget.visible = false;
@ -102,26 +121,28 @@ const PlayerBox = (player, overlay) => {
export default () => {
const content = PlayerGesture({
properties: [
['players', new Map()],
['setup', false],
],
attribute: {
players: new Map(),
setup: false,
},
setup: (self) => {
self
.hook(Mpris, (overlay, busName) => {
if (overlay._players.has(busName)) {
.hook(Mpris, (overlay, bus_name) => {
const players = overlay.attribute.players;
if (players.has(bus_name)) {
return;
}
// Sometimes the signal doesn't give the busName
if (!busName) {
// Sometimes the signal doesn't give the bus_name
if (!bus_name) {
const player = Mpris.players.find((p) => {
return !overlay._players.has(p.busName);
return !players.has(p.bus_name);
});
if (player) {
busName = player.busName;
bus_name = player.bus_name;
}
else {
return;
@ -129,54 +150,67 @@ export default () => {
}
// Get the one on top so we can move it up later
const previousFirst = overlay.list().at(-1);
const previousFirst = overlay.attribute.list().at(-1);
// Make the new player
const player = Mpris.getPlayer(busName);
const player = Mpris.getPlayer(bus_name);
const Colors = Variable(null);
player.colors = Variable();
overlay._players.set(
busName,
PlayerBox(player, content.getOverlay()),
if (!player) {
return;
}
players.set(
bus_name,
// @ts-expect-error
PlayerBox(player, Colors, content.child),
);
overlay.overlays = Array.from(overlay._players.values())
overlay.overlays = Array.from(players.values())
.map((widget) => widget);
const includes = overlay.attribute
.includesWidget(previousFirst);
// Select favorite player at startup
if (!overlay._setup && overlay._players.has(FAVE_PLAYER)) {
overlay.moveToTop(overlay._players.get(FAVE_PLAYER));
overlay._setup = true;
if (!overlay.attribute.setup && players.has(FAVE_PLAYER)) {
overlay.attribute.moveToTop(players.get(FAVE_PLAYER));
overlay.attribute.setup = true;
}
// Move previousFirst on top again
else if (overlay.includesWidget(previousFirst)) {
overlay.moveToTop(previousFirst);
else if (includes) {
overlay.attribute.moveToTop(previousFirst);
}
}, 'player-added')
.hook(Mpris, (overlay, busName) => {
if (!busName || !overlay._players.has(busName)) {
.hook(Mpris, (overlay, bus_name) => {
const players = overlay.attribute.players;
if (!bus_name || !players.has(bus_name)) {
return;
}
// Get the one on top so we can move it up later
const previousFirst = overlay.list().at(-1);
const previousFirst = overlay.attribute.list().at(-1);
// Remake overlays without deleted one
overlay._players.delete(busName);
overlay.overlays = Array.from(overlay._players.values())
players.delete(bus_name);
overlay.overlays = Array.from(players.values())
.map((widget) => widget);
// Move previousFirst on top again
if (overlay.includesWidget(previousFirst)) {
overlay.moveToTop(previousFirst);
const includes = overlay.attribute
.includesWidget(previousFirst);
if (includes) {
overlay.attribute.moveToTop(previousFirst);
}
}, 'player-closed');
},
});
return Box({
className: 'media',
class_name: 'media',
child: content,
});
};

View file

@ -6,19 +6,39 @@ import Gtk from 'gi://Gtk';
// TODO: wrap in another EventBox for disabled cursor
/**
* @typedef {import('types/widget.js').Widget} Widget
* @typedef {Widget & Object} CursorProps
* @property {boolean=} isButton
* @property {function(Widget):void=} onPrimaryClickRelease
*
* @param {CursorProps} obj
*/
export default ({
isButton = false,
onPrimaryClickRelease = () => { /**/ },
onPrimaryClickRelease = (self) => {
self;
},
...props
}) => {
// Make this variable to know if the function should
// be executed depending on where the click is released
const CanRun = Variable(true);
const properties = {
let widgetFunc;
if (isButton) {
widgetFunc = Button;
}
else {
widgetFunc = EventBox;
}
const widget = widgetFunc({
...props,
cursor: 'pointer',
onPrimaryClickRelease: (self) => {
on_primary_click_release: (self) => {
// Every click, do a one shot connect to
// CanRun to wait for location of click
const id = CanRun.connect('changed', () => {
@ -29,19 +49,11 @@ export default ({
CanRun.disconnect(id);
});
},
};
let widget;
if (isButton) {
widget = Button(properties);
}
else {
widget = EventBox(properties);
}
});
const gesture = Gtk.GestureLongPress.new(widget);
// @ts-expect-error
widget.hook(gesture, () => {
const pointer = gesture.get_point(null);
const x = pointer[1];

View file

@ -1,6 +1,19 @@
import { execAsync, readFileAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
const { get_home_dir } = imports.gi.GLib;
/**
* @typedef {Object} Persist
* @property {string} name
* @property {typeof imports.gi.GObject} gobject
* @property {string} prop
* @property {boolean|string=} condition - if string, compare following props to this
* @property {boolean|string=} whenTrue
* @property {boolean|string=} whenFalse
* @property {string=} signal
*
* @param {Persist} props
*/
export default ({
name,
gobject,
@ -9,7 +22,7 @@ export default ({
whenTrue = condition,
whenFalse = false,
signal = 'changed',
} = {}) => {
}) => {
const cacheFile = `${get_home_dir()}/.cache/ags/.${name}`;
const stateCmd = () => ['bash', '-c',

View file

@ -1,17 +1,19 @@
{
"main": "config.js",
"dependencies": {
"@girs/dbusmenugtk3-0.4": "^0.4.0-3.2.2",
"@girs/gtk-3.0": "^3.24.39-3.2.2",
"@girs/gudev-1.0": "^1.0.0-3.2.2",
"@girs/gvc-1.0": "^1.0.0-3.2.2",
"@girs/nm-1.0": "^1.45.1-3.2.2",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"eslint": "^8.52.0",
"fzf": "^0.5.2",
"stylelint-config-standard-scss": "^11.0.0"
"fzf": "^0.5.2"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^1.4.0"
"eslint": "^8.52.0",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"stylelint-config-standard-scss": "^11.0.0",
"@stylistic/eslint-plugin": "^1.4.0",
"@girs/dbusmenugtk3-0.4": "^0.4.0-3.2.0",
"@girs/gudev-1.0": "^1.0.0-3.2.2",
"@girs/gobject-2.0": "^2.76.1-3.2.3",
"@girs/gtk-3.0": "^3.24.39-3.2.2",
"@girs/gvc-1.0": "^1.0.0-3.1.0",
"@girs/nm-1.0": "^1.43.1-3.1.0"
}
}

View file

@ -105,7 +105,7 @@
}
}
.label {
label {
font-size: 20px;
}
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022"
],
"allowJs": true,
"checkJs": true,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"typeRoots": [
"./types/ags.d.ts",
"./node_modules/@girs"
],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}