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 *.egg-info
*.temp *.temp
*node_modules/ *node_modules/
*types/
*package-lock.json *package-lock.json
**/ags/style.css **/ags/style.css
result* result*

View file

@ -31,7 +31,6 @@
"curly": ["warn"], "curly": ["warn"],
"default-case-last": ["warn"], "default-case-last": ["warn"],
"default-param-last": ["error"], "default-param-last": ["error"],
"dot-notation": ["warn", { "allowPattern": ".*-|_.*" }],
"eqeqeq": ["error", "smart"], "eqeqeq": ["error", "smart"],
"func-names": ["warn", "never"], "func-names": ["warn", "never"],
"func-style": ["warn", "expression"], "func-style": ["warn", "expression"],
@ -53,7 +52,6 @@
"ignoreDefaultValues": true "ignoreDefaultValues": true
}], }],
"no-multi-assign": ["error"], "no-multi-assign": ["error"],
"no-negated-condition": ["warn"],
"no-new": ["error"], "no-new": ["error"],
"no-new-func": ["error"], "no-new-func": ["error"],
"no-new-wrappers": ["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'; import EventBox from '../misc/cursorbox.js';
/**
* @param {import('types/service/applications.js').Application} app
*/
export default (app) => { export default (app) => {
const icon = Icon({ const icon = Icon({ size: 42 });
icon: lookUpIcon(app.icon_name) ? const iconString = app.app.get_string('Icon');
app.icon_name :
app.app.get_string('Icon') === 'nix-snowflake' ? if (app.icon_name) {
'' : if (lookUpIcon(app.icon_name)) {
app.app.get_string('Icon'), icon.icon = app.icon_name;
size: 42, }
}); else if (iconString && iconString !== 'nix-snowflake') {
icon.icon = iconString;
}
else {
icon.icon = '';
}
}
const textBox = Box({ const textBox = Box({
vertical: true, vertical: true,
@ -46,14 +55,13 @@ export default (app) => {
hexpand: true, hexpand: true,
class_name: 'app', class_name: 'app',
setup: (self) => { attribute: { app },
self.app = app;
},
onPrimaryClickRelease: () => { onPrimaryClickRelease: (self) => {
App.closeWindow('applauncher'); App.closeWindow('applauncher');
Hyprland.sendMessage(`dispatch exec sh -c ${app.executable}`); Hyprland.sendMessage(`dispatch exec sh -c
++app.frequency; ${self.attribute.app.executable}`);
++self.attribute.app.frequency;
}, },
child: Box({ child: Box({

View file

@ -14,17 +14,20 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
let fzfResults; let fzfResults;
const list = ListBox(); const list = ListBox();
/** @param {String} text */
const setSort = (text) => { const setSort = (text) => {
const fzf = new Fzf(Applications.list, { const fzf = new Fzf(Applications.list, {
selector: (app) => app.name, selector: (app) => app.name,
tiebreakers: [(a, b) => b._frequency - tiebreakers: [(a, b) => b.frequency -
a._frequency], a.frequency],
}); });
fzfResults = fzf.find(text); fzfResults = fzf.find(text);
list.set_sort_func((a, b) => { list.set_sort_func((a, b) => {
const row1 = a.get_children()[0]?.app.name; // @ts-expect-error
const row2 = b.get_children()[0]?.app.name; const row1 = a.get_children()[0]?.attribute.app.name;
// @ts-expect-error
const row2 = b.get_children()[0]?.attribute.app.name;
if (!row1 || !row2) { if (!row1 || !row2) {
return 0; return 0;
@ -51,9 +54,10 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
makeNewChildren(); makeNewChildren();
// FIXME: always visible
const placeholder = Label({ const placeholder = Label({
label: " Couldn't find a match", label: " Couldn't find a match",
className: 'placeholder', class_name: 'placeholder',
}); });
const entry = Entry({ const entry = Entry({
@ -73,17 +77,23 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
}, },
on_change: ({ text }) => { on_change: ({ text }) => {
if (!text) {
return;
}
setSort(text); setSort(text);
let visibleApps = 0; let visibleApps = 0;
list.get_children().forEach((row) => { list.get_children().forEach((row) => {
// @ts-expect-error
row.changed(); row.changed();
// @ts-expect-error
const item = row.get_children()[0]; const item = row.get_children()[0];
if (item?.app) { if (item?.attribute.app) {
const isMatching = fzfResults.find((r) => { const isMatching = fzfResults.find((r) => {
return r.item.name === item.app.name; return r.item.name === item.attribute.app.name;
}); });
row.visible = isMatching; row.visible = isMatching;
@ -98,7 +108,7 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
}); });
return Box({ return Box({
className: 'applauncher', class_name: 'applauncher',
vertical: true, vertical: true,
setup: (self) => { setup: (self) => {
@ -120,7 +130,7 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
children: [ children: [
Box({ Box({
className: 'header', class_name: 'header',
children: [ children: [
Icon('preferences-system-search-symbolic'), Icon('preferences-system-search-symbolic'),
entry, 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 { SpeakerIcon } from '../../misc/audio-icons.js';
import Separator from '../../misc/separator.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; const SPACING = 5;
export default () => { export default () => {
const rev = Revealer({ const icon = Icon({
icon: SpeakerIcon.bind(),
});
const hoverRevLabel = Revealer({
transition: 'slide_right', transition: 'slide_right',
child: Box({ child: Box({
children: [ children: [
Separator(SPACING), Separator(SPACING),
SpeakerPercentLabel(),
Label().hook(Audio, (self) => {
if (Audio.speaker?.volume) {
self.label =
`${Math.round(Audio.speaker?.volume * 100)}%`;
}
}, 'speaker-changed'),
], ],
}), }),
}); });
const widget = EventBox({ const widget = EventBox({
onHover: () => { on_hover: () => {
rev.revealChild = true; hoverRevLabel.reveal_child = true;
}, },
onHoverLost: () => { on_hover_lost: () => {
rev.revealChild = false; hoverRevLabel.reveal_child = false;
}, },
child: Box({
className: 'audio',
children: [
SpeakerIndicator(),
rev, child: Box({
class_name: 'audio',
children: [
icon,
hoverRevLabel,
], ],
}), }),
}); });
widget.rev = rev;
return widget; 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'; import Separator from '../../misc/separator.js';
const LOW_BATT = 20; const LOW_BATT = 20;
const SPACING = 5;
const Indicator = () => Icon({ export default () => Box({
className: 'battery-indicator', class_name: 'toggle-off battery',
binds: [['icon', Battery, 'icon-name']], children: [
Icon({
setup: (self) => { class_name: 'battery-indicator',
self.hook(Battery, () => { // @ts-expect-error
icon: Battery.bind('icon_name'),
}).hook(Battery, (self) => {
self.toggleClassName('charging', Battery.charging); self.toggleClassName('charging', Battery.charging);
self.toggleClassName('charged', Battery.charged); self.toggleClassName('charged', Battery.charged);
self.toggleClassName('low', Battery.percent < LOW_BATT); 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), 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 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 { Label, Box, EventBox, Icon, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.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; const SPACING = 5;
export default () => { 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', transition: 'slide_right',
child: Box({ child: Box({
children: [ children: [
Separator(SPACING), Separator(SPACING),
ConnectedLabel(),
Label().hook(Bluetooth, (self) => {
self.label = Bluetooth.connectedDevices[0] ?
`${Bluetooth.connectedDevices[0]}` :
'Disconnected';
}, 'notify::connected-devices'),
], ],
}), }),
}); });
const widget = EventBox({ const widget = EventBox({
onHover: () => { on_hover: () => {
rev.revealChild = true; hoverRevLabel.reveal_child = true;
}, },
onHoverLost: () => { on_hover_lost: () => {
rev.revealChild = false; hoverRevLabel.reveal_child = false;
}, },
child: Box({
className: 'bluetooth',
children: [
Indicator(),
rev, child: Box({
class_name: 'bluetooth',
children: [
icon,
hoverRevLabel,
], ],
}), }),
}); });
widget.rev = rev;
return widget; return widget;
}; };

View file

@ -6,48 +6,44 @@ import Separator from '../../misc/separator.js';
const SPACING = 5; 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 () => { export default () => {
const rev = Revealer({ const icon = Icon({
// @ts-expect-error
icon: Brightness.bind('screenIcon'),
});
const hoverRevLabel = Revealer({
transition: 'slide_right', transition: 'slide_right',
child: Box({ child: Box({
children: [ children: [
Separator(SPACING), Separator(SPACING),
BrightnessPercentLabel(),
Label().hook(Brightness, (self) => {
self.label = `${Math.round(Brightness.screen * 100)}%`;
}, 'screen'),
], ],
}), }),
}); });
const widget = EventBox({ const widget = EventBox({
onHover: () => { on_hover: () => {
rev.revealChild = true; hoverRevLabel.reveal_child = true;
}, },
onHoverLost: () => { on_hover_lost: () => {
rev.revealChild = false; hoverRevLabel.reveal_child = false;
}, },
child: Box({ child: Box({
className: 'brightness', class_name: 'brightness',
children: [ children: [
Indicator(), icon,
rev, hoverRevLabel,
], ],
}), }),
}); });
widget.rev = rev;
return widget; 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 { Label } from 'resource:///com/github/Aylur/ags/widget.js';
import GLib from 'gi://GLib'; const { DateTime } = imports.gi.GLib;
const { DateTime } = GLib;
import EventBox from '../../misc/cursorbox.js'; 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({ export default () => EventBox({
className: 'toggle-off', 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: [ children: [
Separator(SPACING / 2), Separator(SPACING / 2),
Icon({ Icon({ size: 30 })
size: 30, .hook(Hyprland.active.client, (self) => {
setup: (self) => { const app = Applications
self.hook(Hyprland.active.client, () => { .query(Hyprland.active.client.class)[0];
const app = Applications
.query(Hyprland.active.client.class)[0];
if (app) { if (app) {
self.icon = app.iconName; self.icon = app.icon_name;
self.visible = Hyprland.active.client.title !== ''; self.visible = Hyprland.active.client.title !== '';
} }
}); }),
},
}),
Separator(SPACING), Separator(SPACING),
Label({ Label({
css: 'color: #CBA6F7; font-size: 18px', css: 'color: #CBA6F7; font-size: 18px',
truncate: 'end', 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 EventBox from '../../misc/cursorbox.js';
import Persist from '../../misc/persist.js'; import Persist from '../../misc/persist.js';
const HeartState = Variable(); const HeartState = Variable('');
Persist({ Persist({
name: 'heart', name: 'heart',
@ -22,7 +22,7 @@ export default () => EventBox({
}, },
child: Label({ child: Label({
className: 'heart-toggle', class_name: 'heart-toggle',
binds: [['label', HeartState, 'value']], label: HeartState.bind(),
}), }),
}); });

View file

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

View file

@ -13,8 +13,11 @@ export default () => EventBox({
className: 'toggle-off', className: 'toggle-off',
onPrimaryClickRelease: (self) => { onPrimaryClickRelease: (self) => {
App.getWindow('notification-center') // @ts-expect-error
.setXPos(self.get_allocation(), 'right'); App.getWindow('notification-center')?.setXPos(
self.get_allocation(),
'right',
);
App.toggleWindow('notification-center'); App.toggleWindow('notification-center');
}, },
@ -28,31 +31,27 @@ export default () => EventBox({
}, },
child: CenterBox({ child: CenterBox({
className: 'notif-panel', class_name: 'notif-panel',
center_widget: Box({ center_widget: Box({
children: [ children: [
Icon({ Icon().hook(Notifications, (self) => {
setup: (self) => { if (Notifications.dnd) {
self.hook(Notifications, () => { self.icon = 'notification-disabled-symbolic';
if (Notifications.dnd) { }
self.icon = 'notification-disabled-symbolic'; else if (Notifications.notifications.length > 0) {
} self.icon = 'notification-new-symbolic';
else if (Notifications.notifications.length > 0) { }
self.icon = 'notification-new-symbolic'; else {
} self.icon = 'notification-symbolic';
else { }
self.icon = 'notification-symbolic';
}
});
},
}), }),
Separator(SPACING), Separator(SPACING),
Label({ Label({
binds: [['label', Notifications, 'notifications', label: Notifications.bind('notifications')
(n) => String(n.length)]], .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 Tablet from '../../../services/tablet.js';
import EventBox from '../../misc/cursorbox.js'; import EventBox from '../../misc/cursorbox.js';
@ -9,14 +9,12 @@ export default () => EventBox({
onPrimaryClickRelease: () => Tablet.toggleOsk(), onPrimaryClickRelease: () => Tablet.toggleOsk(),
setup: (self) => { child: Label({
self.hook(Tablet, () => { class_name: 'osk-toggle',
self.toggleClassName('toggle-on', Tablet.oskState); xalign: 0.6,
}, 'osk-toggled'); label: '󰌌 ',
},
child: Box({
className: 'osk-toggle',
children: [Label(' 󰌌 ')],
}), }),
});
}).hook(Tablet, (self) => {
self.toggleClassName('toggle-on', Tablet.oskState);
}, 'osk-toggled');

View file

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

View file

@ -9,35 +9,40 @@ const REVEAL_DURATION = 500;
const SPACING = 12; const SPACING = 12;
/** @param {import('types/service/systemtray').TrayItem} item */
const SysTrayItem = (item) => { const SysTrayItem = (item) => {
if (item.id === 'spotify-client') { if (item.id === 'spotify-client') {
return; return;
} }
return MenuItem({ return MenuItem({
// @ts-expect-error
submenu: item.menu,
tooltip_markup: item.bind('tooltip_markup'),
child: Revealer({ child: Revealer({
transition: 'slide_right', transition: 'slide_right',
transitionDuration: REVEAL_DURATION, transition_duration: REVEAL_DURATION,
child: Icon({ child: Icon({
size: 24, size: 24,
binds: [['icon', item, 'icon']], icon: item.bind('icon'),
}), }),
}), }),
submenu: item.menu,
binds: [['tooltipMarkup', item, 'tooltip-markup']],
}); });
}; };
const SysTray = () => MenuBar({ const SysTray = () => MenuBar({
setup: (self) => { attribute: {
self.items = new Map(); items: new Map(),
},
setup: (self) => {
self self
.hook(SystemTray, (_, id) => { .hook(SystemTray, (_, id) => {
const item = SystemTray.getItem(id); const item = SystemTray.getItem(id);
if (self.items.has(id) || !item) { if (self.attribute.items.has(id) || !item) {
return; return;
} }
@ -48,21 +53,22 @@ const SysTray = () => MenuBar({
return; return;
} }
self.items.set(id, w); self.attribute.items.set(id, w);
self.add(w); self.child = w;
self.show_all(); self.show_all();
w.child.revealChild = true; // @ts-expect-error
w.child.reveal_child = true;
}, 'added') }, 'added')
.hook(SystemTray, (_, id) => { .hook(SystemTray, (_, id) => {
if (!self.items.has(id)) { if (!self.attribute.items.has(id)) {
return; return;
} }
self.items.get(id).child.revealChild = false; self.attribute.items.get(id).child.reveal_child = false;
timeout(REVEAL_DURATION, () => { timeout(REVEAL_DURATION, () => {
self.items.get(id).destroy(); self.attribute.items.get(id).destroy();
self.items.delete(id); self.attribute.items.delete(id);
}); });
}, 'removed'); }, 'removed');
}, },
@ -74,21 +80,17 @@ export default () => {
return Revealer({ return Revealer({
transition: 'slide_right', transition: 'slide_right',
setup: (self) => {
self.hook(SystemTray, () => {
self.revealChild = systray.get_children().length > 0;
});
},
child: Box({ child: Box({
children: [ children: [
Box({ Box({
className: 'sys-tray', class_name: 'sys-tray',
children: [systray], children: [systray],
}), }),
Separator(SPACING), 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({ export default () => EventBox({
className: 'toggle-off', class_name: 'toggle-off',
onPrimaryClickRelease: () => Tablet.toggleMode(), onPrimaryClickRelease: () => Tablet.toggleMode(),
setup: (self) => {
self.hook(Tablet, () => {
self.toggleClassName('toggle-on', Tablet.tabletMode);
}, 'mode-toggled');
},
child: Box({ child: Box({
className: 'tablet-toggle', class_name: 'tablet-toggle',
vertical: false, vertical: false,
children: [Label(' 󰦧 ')], 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; const URGENT_DURATION = 1000;
/** @typedef {import('types/widget.js').Widget} Widget */
const Workspace = ({ i } = {}) => {
/** @property {number} id */
const Workspace = ({ id }) => {
return Revealer({ return Revealer({
transition: 'slide_right', transition: 'slide_right',
properties: [['id', i]], attribute: { id },
child: EventBox({ child: EventBox({
tooltipText: `${i}`, tooltipText: `${id}`,
onPrimaryClickRelease: () => { onPrimaryClickRelease: () => {
Hyprland.sendMessage(`dispatch workspace ${i}`); Hyprland.sendMessage(`dispatch workspace ${id}`);
}, },
child: Box({ child: Box({
vpack: 'center', vpack: 'center',
className: 'button', class_name: 'button',
setup: (self) => { 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('occupied', occupied);
self.toggleClassName('empty', !occupied); self.toggleClassName('empty', !occupied);
if (!addr) {
return;
}
// Deal with urgent windows // 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); self.toggleClassName('urgent', true);
// Only show for a sec when urgent is current workspace // 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, () => { timeout(URGENT_DURATION, () => {
self.toggleClassName('urgent', false); self.toggleClassName('urgent', false);
}); });
@ -45,13 +61,13 @@ const Workspace = ({ i } = {}) => {
}; };
self self
.hook(Hyprland, () => self.update()) .hook(Hyprland, update)
// Deal with urgent windows // Deal with urgent windows
.hook(Hyprland, (_, a) => { .hook(Hyprland, update, 'urgent-window')
self.update(a);
}, 'urgent-window')
.hook(Hyprland.active.workspace, () => { .hook(Hyprland.active.workspace, () => {
if (Hyprland.active.workspace.id === i) { if (Hyprland.active.workspace.id === id) {
self.toggleClassName('urgent', false); self.toggleClassName('urgent', false);
} }
}); });
@ -65,75 +81,80 @@ export default () => {
const L_PADDING = 16; const L_PADDING = 16;
const WS_WIDTH = 30; const WS_WIDTH = 30;
/** @param {Widget} self */
const updateHighlight = (self) => { const updateHighlight = (self) => {
const currentId = Hyprland.active.workspace.id; const currentId = Hyprland.active.workspace.id;
// @ts-expect-error
const indicators = self.get_parent().get_children()[0].child.children; 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) { if (currentIndex < 0) {
return; return;
} }
// @ts-expect-error
self.setCss(`margin-left: ${L_PADDING + (currentIndex * WS_WIDTH)}px`); self.setCss(`margin-left: ${L_PADDING + (currentIndex * WS_WIDTH)}px`);
}; };
const highlight = Box({ const highlight = Box({
vpack: 'center', vpack: 'center',
hpack: 'start', hpack: 'start',
className: 'button active', class_name: 'button active',
setup: (self) => { }).hook(Hyprland.active.workspace, updateHighlight);
self.hook(Hyprland.active.workspace, updateHighlight);
},
});
const widget = Overlay({ const widget = Overlay({
pass_through: true, pass_through: true,
overlays: [highlight], overlays: [highlight],
child: EventBox({ child: EventBox({
child: Box({ child: Box({
className: 'workspaces', class_name: 'workspaces',
properties: [ attribute: { workspaces: [] },
['workspaces'],
['refresh', (self) => { setup: (self) => {
self.children.forEach((rev) => { const refresh = () => {
Array.from(self.children).forEach((rev) => {
// @ts-expect-error
rev.reveal_child = false; rev.reveal_child = false;
}); });
self._workspaces.forEach((ws) => { Array.from(self.attribute.workspaces).forEach((ws) => {
ws.revealChild = true; ws.revealChild = true;
}); });
}], };
['updateWorkspaces', (self) => { const updateWorkspaces = () => {
Hyprland.workspaces.forEach((ws) => { Hyprland.workspaces.forEach((ws) => {
const currentWs = self.children.find((ch) => { const currentWs = Array.from(self.children)
return ch._id === ws.id; // @ts-expect-error
}); .find((ch) => ch.attribute.id === ws['id']);
if (!currentWs && ws.id > 0) { if (!currentWs && ws['id'] > 0) {
self.add(Workspace({ i: ws.id })); self.add(Workspace({ id: ws['id'] }));
} }
}); });
self.show_all(); self.show_all();
// Make sure the order is correct // Make sure the order is correct
self._workspaces.forEach((workspace, i) => { Array.from(self.attribute.workspaces)
workspace.get_parent().reorder_child(workspace, i); .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;
}); });
}).sort((a, b) => a._id - b._id); };
self._updateWorkspaces(self); self.hook(Hyprland, () => {
self._refresh(self); 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 // Make sure the highlight doesn't go too far
const TEMP_TIMEOUT = 10; 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'; import { Box, EventBox, Revealer, Window } from 'resource:///com/github/Aylur/ags/widget.js';
/** @param {import('types/variable.js').Variable} variable */
const BarCloser = (variable) => Window({ const BarCloser = (variable) => Window({
name: 'bar-closer', name: 'bar-closer',
visible: false, visible: false,
@ -11,8 +12,9 @@ const BarCloser = (variable) => Window({
layer: 'overlay', layer: 'overlay',
child: EventBox({ child: EventBox({
onHover: (self) => { on_hover: (self) => {
variable.value = false; variable.value = false;
// @ts-expect-error
self.get_parent().visible = false; self.get_parent().visible = false;
}, },
@ -22,6 +24,7 @@ const BarCloser = (variable) => Window({
}), }),
}); });
/** @param {import('types/widgets/revealer').RevealerProps} props */
export default (props) => { export default (props) => {
const Revealed = Variable(true); const Revealed = Variable(true);
const barCloser = BarCloser(Revealed); const barCloser = BarCloser(Revealed);
@ -38,7 +41,9 @@ export default (props) => {
Hyprland.active.workspace.id, Hyprland.active.workspace.id,
); );
Revealed.value = !workspace?.hasfullscreen; if (workspace) {
Revealed.value = !workspace['hasfullscreen'];
}
}) })
.hook(Hyprland, (_, fullscreen) => { .hook(Hyprland, (_, fullscreen) => {
@ -50,7 +55,7 @@ export default (props) => {
Revealer({ Revealer({
...props, ...props,
transition: 'slide_down', transition: 'slide_down',
revealChild: true, reveal_child: true,
binds: [['revealChild', Revealed, 'value']], binds: [['revealChild', Revealed, 'value']],
}), }),
@ -59,7 +64,7 @@ export default (props) => {
binds: [['revealChild', Revealed, 'value', (v) => !v]], binds: [['revealChild', Revealed, 'value', (v) => !v]],
child: EventBox({ child: EventBox({
onHover: () => { on_hover: () => {
barCloser.visible = true; barCloser.visible = true;
Revealed.value = true; Revealed.value = true;
}, },

View file

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

View file

@ -4,7 +4,7 @@ import Gtk from 'gi://Gtk';
const Lang = imports.lang; const Lang = imports.lang;
export default ( export default (
place, place = 'top left',
css = 'background-color: black;', css = 'background-color: black;',
) => Box({ ) => Box({
hpack: place.includes('left') ? 'start' : 'end', hpack: place.includes('left') ? 'start' : 'end',
@ -27,6 +27,7 @@ export default (
.get_property('border-radius', Gtk.StateFlags.NORMAL); .get_property('border-radius', Gtk.StateFlags.NORMAL);
widget.set_size_request(r, r); widget.set_size_request(r, r);
// @ts-expect-error
widget.connect('draw', Lang.bind(widget, (_, cr) => { widget.connect('draw', Lang.bind(widget, (_, cr) => {
const c = widget.get_style_context() const c = widget.get_style_context()
.get_property('background-color', Gtk.StateFlags.NORMAL); .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 { Box, Calendar, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import GLib from 'gi://GLib'; const { DateTime } = imports.gi.GLib;
const { DateTime } = GLib;
import PopupWindow from './misc/popup.js'; import PopupWindow from './misc/popup.js';
const Divider = () => Box({ const Divider = () => Box({
className: 'divider', class_name: 'divider',
vertical: true, vertical: true,
}); });
const Time = () => Box({ const Time = () => Box({
className: 'timebox', class_name: 'timebox',
vertical: true, vertical: true,
children: [
children: [
Box({ Box({
className: 'time-container', class_name: 'time-container',
hpack: 'center', hpack: 'center',
vpack: 'center', vpack: 'center',
children: [
children: [
Label({ Label({
className: 'content', class_name: 'content',
label: 'hour', label: 'hour',
setup: (self) => { setup: (self) => {
self.poll(1000, () => { self.poll(1000, () => {
@ -35,7 +34,7 @@ const Time = () => Box({
Divider(), Divider(),
Label({ Label({
className: 'content', class_name: 'content',
label: 'minute', label: 'minute',
setup: (self) => { setup: (self) => {
self.poll(1000, () => { self.poll(1000, () => {
@ -48,11 +47,13 @@ const Time = () => Box({
}), }),
Box({ Box({
className: 'date-container', class_name: 'date-container',
hpack: 'center', hpack: 'center',
child: Label({ child: Label({
css: 'font-size: 20px', css: 'font-size: 20px',
label: 'complete date', label: 'complete date',
setup: (self) => { setup: (self) => {
self.poll(1000, () => { self.poll(1000, () => {
const time = DateTime.new_now_local(); const time = DateTime.new_now_local();
@ -69,23 +70,26 @@ const Time = () => Box({
}); });
const CalendarWidget = () => Box({ const CalendarWidget = () => Box({
className: 'cal-box', class_name: 'cal-box',
child: Calendar({ child: Calendar({
showDayNames: true, show_day_names: true,
showHeading: true, show_heading: true,
className: 'cal', class_name: 'cal',
}), }),
}); });
const TOP_MARGIN = 6; const TOP_MARGIN = 6;
export default () => PopupWindow({ export default () => PopupWindow({
name: 'calendar',
anchor: ['top'], anchor: ['top'],
margins: [TOP_MARGIN, 0, 0, 0], margins: [TOP_MARGIN, 0, 0, 0],
name: 'calendar',
child: Box({ child: Box({
className: 'date', class_name: 'date',
vertical: true, vertical: true,
children: [ children: [
Time(), Time(),
CalendarWidget(), CalendarWidget(),

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,19 @@
import { execAsync, readFileAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js'; import { execAsync, readFileAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
const { get_home_dir } = imports.gi.GLib; 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 ({ export default ({
name, name,
gobject, gobject,
@ -9,7 +22,7 @@ export default ({
whenTrue = condition, whenTrue = condition,
whenFalse = false, whenFalse = false,
signal = 'changed', signal = 'changed',
} = {}) => { }) => {
const cacheFile = `${get_home_dir()}/.cache/ags/.${name}`; const cacheFile = `${get_home_dir()}/.cache/ags/.${name}`;
const stateCmd = () => ['bash', '-c', const stateCmd = () => ['bash', '-c',

View file

@ -1,17 +1,19 @@
{ {
"main": "config.js",
"dependencies": { "dependencies": {
"@girs/dbusmenugtk3-0.4": "^0.4.0-3.2.2", "fzf": "^0.5.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"
}, },
"devDependencies": { "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; 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
}
}