refactor(ags): typecheck more stuff

This commit is contained in:
matt1432 2023-12-23 01:14:21 -05:00
parent e8197651d9
commit 894fdd92f1
17 changed files with 596 additions and 404 deletions

View file

@ -1,6 +1,5 @@
import App from 'resource:///com/github/Aylur/ags/app.js'; import App from 'resource:///com/github/Aylur/ags/app.js';
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js'; import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
// TODO: find cleaner way to import this // TODO: find cleaner way to import this
import { Fzf } from '../../node_modules/fzf/dist/fzf.es.js'; import { Fzf } from '../../node_modules/fzf/dist/fzf.es.js';

View file

@ -1,4 +1,3 @@
// @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';
@ -11,8 +10,8 @@ const SPACING = 5;
export default () => { export default () => {
const icon = Icon().hook(Bluetooth, (self) => { const icon = Icon().hook(Bluetooth, (self) => {
if (Bluetooth.enabled) { if (Bluetooth.enabled) {
self.icon = Bluetooth.connectedDevices[0] ? self.icon = Bluetooth.connected_devices[0] ?
Bluetooth.connectedDevices[0].iconName : Bluetooth.connected_devices[0].icon_name :
'bluetooth-active-symbolic'; 'bluetooth-active-symbolic';
} }
else { else {
@ -29,8 +28,8 @@ export default () => {
Separator(SPACING), Separator(SPACING),
Label().hook(Bluetooth, (self) => { Label().hook(Bluetooth, (self) => {
self.label = Bluetooth.connectedDevices[0] ? self.label = Bluetooth.connected_devices[0] ?
`${Bluetooth.connectedDevices[0]}` : `${Bluetooth.connected_devices[0]}` :
'Disconnected'; 'Disconnected';
}, 'notify::connected-devices'), }, 'notify::connected-devices'),
], ],

View file

@ -70,113 +70,104 @@ export default ({
setup(self); setup(self);
self self
.hook(gesture, .hook(gesture, (_, realGesture) => {
/** if (realGesture) {
* @param {Overlay} overlay Array.from(self.attribute.list())
* @param {number} realGesture .forEach((over) => {
*/ over.visible = true;
(overlay, realGesture) => { });
if (realGesture) { }
Array.from(overlay.attribute.list()) else {
.forEach((over) => { self.attribute.showTopOnly();
over.visible = true; }
});
}
else {
overlay.attribute.showTopOnly();
}
// Don't allow gesture when only one player // Don't allow gesture when only one player
if (overlay.attribute.list().length <= 1) { if (self.attribute.list().length <= 1) {
return; return;
} }
overlay.attribute.dragging = true; self.attribute.dragging = true;
let offset = gesture.get_offset()[1]; let offset = gesture.get_offset()[1];
const playerBox = overlay.attribute.list().at(-1); const playerBox = self.attribute.list().at(-1);
if (!offset) { if (!offset) {
return; 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.attribute.bgStyle} ${playerBox.attribute.bgStyle}
`); `);
} }
// Slide left // Slide left
else { else {
offset = Math.abs(offset); offset = Math.abs(offset);
playerBox.setCss(` playerBox.setCss(`
margin-left: -${offset}px; margin-left: -${offset}px;
margin-right: ${offset}px; margin-right: ${offset}px;
${playerBox.attribute.bgStyle} ${playerBox.attribute.bgStyle}
`); `);
} }
}, }, 'drag-update')
'drag-update')
.hook(gesture, .hook(gesture, () => {
/** @param {Overlay} overlay */ // Don't allow gesture when only one player
(overlay) => { if (self.attribute.list().length <= 1) {
// Don't allow gesture when only one player return;
if (overlay.attribute.list().length <= 1) { }
return;
}
overlay.attribute.dragging = false; self.attribute.dragging = false;
const offset = gesture.get_offset()[1]; const offset = gesture.get_offset()[1];
const playerBox = overlay.attribute.list().at(-1); const playerBox = self.attribute.list().at(-1);
// If crosses threshold after letting go, slide away // If crosses threshold after letting go, slide away
if (offset && 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;
// Slide away right // Slide away right
if (offset >= 0) { if (offset >= 0) {
playerBox.setCss(` playerBox.setCss(`
${TRANSITION} ${TRANSITION}
margin-left: ${OFFSCREEN}px; margin-left: ${OFFSCREEN}px;
margin-right: -${OFFSCREEN}px; margin-right: -${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle} opacity: 0.7; ${playerBox.attribute.bgStyle}
`); `);
} }
// Slide away left // Slide away left
else { else {
playerBox.setCss(` playerBox.setCss(`
${TRANSITION} ${TRANSITION}
margin-left: -${OFFSCREEN}px; margin-left: -${OFFSCREEN}px;
margin-right: ${OFFSCREEN}px; margin-right: ${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle} opacity: 0.7; ${playerBox.attribute.bgStyle}
`); `);
}
timeout(ANIM_DURATION, () => {
// Put the player in the back after anim
overlay.reorder_overlay(playerBox, 0);
// Recenter player
playerBox.setCss(playerBox.attribute.bgStyle);
widget.sensitive = true;
overlay.attribute.showTopOnly();
});
} }
else {
// Recenter with transition for animation timeout(ANIM_DURATION, () => {
playerBox.setCss(`${TRANSITION} // Put the player in the back after anim
self.reorder_overlay(playerBox, 0);
// Recenter player
playerBox.setCss(playerBox.attribute.bgStyle);
widget.sensitive = true;
self.attribute.showTopOnly();
});
}
else {
// Recenter with transition for animation
playerBox.setCss(`${TRANSITION}
${playerBox.attribute.bgStyle}`); ${playerBox.attribute.bgStyle}`);
} }
}, }, 'drag-end');
'drag-end');
}, },
}); });

View file

@ -26,6 +26,7 @@ const OSDs = () => {
}, },
}); });
// Send reference of stack to all items
stack.items = OSDList.map((osd, i) => [`${i}`, osd(stack)]); stack.items = OSDList.map((osd, i) => [`${i}`, osd(stack)]);
// Delay popup method so it // Delay popup method so it

View file

@ -7,7 +7,16 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { WindowButton } from './dragndrop.js'; import { WindowButton } from './dragndrop.js';
import * as VARS from './variables.js'; import * as VARS from './variables.js';
/**
* @typedef {import('types/service/hyprland.js').Client} Client
* @typedef {import('types/widgets/revealer').default} Revealer
* @typedef {import('types/widgets/box').default} Box
*/
/** @param {number} size */
const scale = (size) => (size * VARS.SCALE) - VARS.MARGIN; const scale = (size) => (size * VARS.SCALE) - VARS.MARGIN;
/** @param {Client} client */
const getFontSize = (client) => { const getFontSize = (client) => {
const valX = scale(client.size[0]) * VARS.ICON_SCALE; const valX = scale(client.size[0]) * VARS.ICON_SCALE;
const valY = scale(client.size[1]) * VARS.ICON_SCALE; const valY = scale(client.size[1]) * VARS.ICON_SCALE;
@ -17,6 +26,7 @@ const getFontSize = (client) => {
return size <= 0 ? 0.1 : size; return size <= 0 ? 0.1 : size;
}; };
/** @param {Client} client */
const IconStyle = (client) => ` const IconStyle = (client) => `
min-width: ${scale(client.size[0])}px; min-width: ${scale(client.size[0])}px;
min-height: ${scale(client.size[1])}px; min-height: ${scale(client.size[1])}px;
@ -24,6 +34,12 @@ const IconStyle = (client) => `
`; `;
/**
* @param {Client} client
* @param {boolean} active
* @param {Array<Client>} clients
* @param {Box} box
*/
const Client = (client, active, clients, box) => { const Client = (client, active, clients, box) => {
const wsName = String(client.workspace.name).replace('special:', ''); const wsName = String(client.workspace.name).replace('special:', '');
const wsId = client.workspace.id; const wsId = client.workspace.id;
@ -31,42 +47,40 @@ const Client = (client, active, clients, box) => {
return Revealer({ return Revealer({
transition: 'crossfade', transition: 'crossfade',
reveal_child: true,
setup: (rev) => { attribute: {
rev.revealChild = true; address: client.address,
to_destroy: false,
}, },
properties: [
['address', client.address],
['toDestroy', false],
],
child: WindowButton({ child: WindowButton({
mainBox: box, mainBox: box,
address: client.address, address: client.address,
onSecondaryClickRelease: () => { on_secondary_click_release: () => {
Hyprland.sendMessage(`dispatch closewindow ${addr}`); Hyprland.sendMessage(`dispatch closewindow ${addr}`);
}, },
onPrimaryClickRelease: () => { on_primary_click_release: () => {
if (wsId < 0) { if (wsId < 0) {
if (client.workspace.name === 'special') { if (client.workspace.name === 'special') {
Hyprland.sendMessage('dispatch ' + Hyprland.sendMessage('dispatch ' +
`movetoworkspacesilent special:${wsId},${addr}`) `movetoworkspacesilent special:${wsId},${addr}`)
.then( .then(() => {
Hyprland.sendMessage('dispatch ' + Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${wsId}`) `togglespecialworkspace ${wsId}`)
.then( .then(() => {
() => App.closeWindow('overview'), App.closeWindow('overview');
).catch(print), }).catch(print);
).catch(print); }).catch(print);
} }
else { else {
Hyprland.sendMessage('dispatch ' + Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${wsName}`).then( `togglespecialworkspace ${wsName}`)
() => App.closeWindow('overview'), .then(() => {
).catch(print); App.closeWindow('overview');
}).catch(print);
} }
} }
else { else {
@ -86,14 +100,15 @@ const Client = (client, active, clients, box) => {
.catch(print); .catch(print);
} }
Hyprland.sendMessage(`dispatch focuswindow ${addr}`).then( Hyprland.sendMessage(`dispatch focuswindow ${addr}`)
() => App.closeWindow('overview'), .then(() => {
).catch(print); App.closeWindow('overview');
}).catch(print);
} }
}, },
child: Icon({ child: Icon({
className: `window ${active}`, class_name: `window ${active ? 'active' : ''}`,
css: `${IconStyle(client)} font-size: 10px;`, css: `${IconStyle(client)} font-size: 10px;`,
icon: client.class, icon: client.class,
}), }),
@ -101,69 +116,79 @@ const Client = (client, active, clients, box) => {
}); });
}; };
/** @param {Box} box */
export const updateClients = (box) => { export const updateClients = (box) => {
Hyprland.sendMessage('j/clients').then((out) => { Hyprland.sendMessage('j/clients').then((out) => {
const clients = JSON.parse(out).filter((client) => client.class); /** @type Array<Client> */
let clients = JSON.parse(out);
box._workspaces.forEach((workspace) => { clients = clients.filter((client) => client.class);
const fixed = workspace.getFixed();
const toRemove = fixed.get_children();
clients.filter((client) => client.workspace.id === workspace._id) box.attribute.workspaces.forEach(
.forEach((client) => { /** @param {Revealer} workspace */
let active = ''; (workspace) => {
const fixed = workspace.attribute.get_fixed();
/** @type Array<Revealer> */
const toRemove = fixed.get_children();
if (client.address === Hyprland.active.client.address) { clients.filter((client) =>
active = 'active'; client.workspace.id === workspace.attribute.id)
} .forEach((client) => {
const active =
client.address === Hyprland.active.client.address;
// TODO: fix multi monitor issue. this is just a temp fix // TODO: fix multi monitor issue. this is just a temp fix
client.at[1] -= 2920; client.at[1] -= 2920;
// Special workspaces that haven't been opened yet // Special workspaces that haven't been opened yet
// return a size of 0. We need to set them to default // return a size of 0. We need to set them to default
// values to show the workspace properly // values to show the workspace properly
if (client.size[0] === 0) { if (client.size[0] === 0) {
client.size[0] = VARS.DEFAULT_SPECIAL.SIZE_X; client.size[0] = VARS.DEFAULT_SPECIAL.SIZE_X;
client.size[1] = VARS.DEFAULT_SPECIAL.SIZE_Y; client.size[1] = VARS.DEFAULT_SPECIAL.SIZE_Y;
client.at[0] = VARS.DEFAULT_SPECIAL.POS_X; client.at[0] = VARS.DEFAULT_SPECIAL.POS_X;
client.at[1] = VARS.DEFAULT_SPECIAL.POS_Y; client.at[1] = VARS.DEFAULT_SPECIAL.POS_Y;
} }
const newClient = [ const newClient = [
fixed.get_children() fixed.get_children().find(
.find((ch) => ch._address === client.address), /** @param {typeof WindowButton} ch */
client.at[0] * VARS.SCALE, // @ts-expect-error
client.at[1] * VARS.SCALE, (ch) => ch.attribute.address === client.address,
]; ),
client.at[0] * VARS.SCALE,
client.at[1] * VARS.SCALE,
];
// If it exists already // If it exists already
if (newClient[0]) { if (newClient[0]) {
toRemove.splice(toRemove.indexOf(newClient[0]), 1); toRemove.splice(toRemove.indexOf(newClient[0]), 1);
fixed.move(...newClient); fixed.move(...newClient);
}
else {
newClient[0] = Client(client, active, clients, box);
fixed.put(...newClient);
}
// Set a timeout here to have an animation when the icon first appears
timeout(1, () => {
newClient[0].child.child.className =
`window ${active}`;
newClient[0].child.child.setCss(IconStyle(client));
});
});
fixed.show_all();
toRemove.forEach((ch) => {
if (ch.attribute.to_destroy) {
ch.destroy();
} }
else { else {
newClient[0] = Client(client, active, clients, box); ch.reveal_child = false;
fixed.put(...newClient); ch.attribute.to_destroy = true;
} }
// Set a timeout here to have an animation when the icon first appears
timeout(1, () => {
newClient[0].child.child.className = `window ${active}`;
newClient[0].child.child.setCss(IconStyle(client));
});
}); });
},
fixed.show_all(); );
toRemove.forEach((ch) => {
if (ch._toDestroy) {
ch.destroy();
}
else {
ch.revealChild = false;
ch._toDestroy = true;
}
});
});
}).catch(print); }).catch(print);
}; };

View file

@ -11,23 +11,39 @@ const DEFAULT_STYLE = `
border-radius: 10px; border-radius: 10px;
`; `;
/**
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/widgets/revealer').default} Revealer
*/
export const Highlighter = () => Box({ export const Highlighter = () => Box({
vpack: 'start', vpack: 'start',
hpack: 'start', hpack: 'start',
className: 'workspace active', class_name: 'workspace active',
css: DEFAULT_STYLE, css: DEFAULT_STYLE,
}); });
/**
* @param {Box} main
* @param {Box} highlighter
*/
export const updateCurrentWorkspace = (main, highlighter) => { export const updateCurrentWorkspace = (main, highlighter) => {
const currentId = Hyprland.active.workspace.id; const currentId = Hyprland.active.workspace.id;
const row = Math.floor((currentId - 1) / VARS.WORKSPACE_PER_ROW); const row = Math.floor((currentId - 1) / VARS.WORKSPACE_PER_ROW);
// @ts-expect-error
const rowObject = main.children[0].children[row]; const rowObject = main.children[0].children[row];
const workspaces = rowObject.child.centerWidget.child const workspaces = rowObject.child.centerWidget.child
.get_children().filter((w) => w.revealChild); .get_children().filter(
/** @param {Revealer} w */
(w) => w.reveal_child,
);
const currentIndex = workspaces.findIndex((w) => w._id === currentId); const currentIndex = workspaces.findIndex(
/** @param {Revealer} w */
(w) => w.attribute.id === currentId,
);
const left = currentIndex * ((VARS.SCREEN.X * VARS.SCALE) + PADDING); const left = currentIndex * ((VARS.SCREEN.X * VARS.SCALE) + PADDING);
const height = row * ((VARS.SCREEN.Y * VARS.SCALE) + (PADDING / 2)); const height = row * ((VARS.SCREEN.Y * VARS.SCALE) + (PADDING / 2));

View file

@ -9,7 +9,15 @@ import { updateClients } from './clients.js';
const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)]; const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
/**
* @typedef {import('types/widgets/button').default} Button
* @typedef {import('types/widgets/button').ButtonProps} ButtonProps
* @typedef {import('types/widgets/eventbox').EventBoxProps=} EventBoxProps
* @typedef {import('types/widgets/box').default} Box
*/
/** @param {Button} widget */
const createSurfaceFromWidget = (widget) => { const createSurfaceFromWidget = (widget) => {
const alloc = widget.get_allocation(); const alloc = widget.get_allocation();
const surface = new Cairo.ImageSurface( const surface = new Cairo.ImageSurface(
@ -29,16 +37,19 @@ const createSurfaceFromWidget = (widget) => {
let hidden = 0; let hidden = 0;
export const WorkspaceDrop = (props) => EventBox({ /** @params {EventBoxProps} props */
export const WorkspaceDrop = ({ ...props }) => EventBox({
...props, ...props,
setup: (self) => { setup: (self) => {
self.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY); self.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY);
self.on('drag-data-received', (_, _c, _x, _y, data) => { self.on('drag-data-received', (_, _c, _x, _y, data) => {
let id = self.get_parent()._id; // @ts-expect-error
let id = self.get_parent()?.attribute.id;
if (id < -1) { if (id < -1) {
id = self.get_parent()._name; // @ts-expect-error
id = self.get_parent()?.attribute.name;
} }
else if (id === -1) { else if (id === -1) {
@ -56,11 +67,17 @@ export const WorkspaceDrop = (props) => EventBox({
}, },
}); });
/**
* @param {ButtonProps & {
* address: string
* mainBox: Box
* }} o
*/
export const WindowButton = ({ export const WindowButton = ({
address, address,
mainBox, mainBox,
...props ...props
} = {}) => Button({ }) => Button({
...props, ...props,
cursor: 'pointer', cursor: 'pointer',
@ -78,11 +95,12 @@ export const WindowButton = ({
self.on('drag-begin', (_, context) => { self.on('drag-begin', (_, context) => {
Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(self)); Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(self));
self.get_parent().revealChild = false; // @ts-expect-error
self.get_parent()?.set_reveal_child(false);
}); });
self.on('drag-end', () => { self.on('drag-end', () => {
self.get_parent().destroy(); self.get_parent()?.destroy();
updateClients(mainBox); updateClients(mainBox);
}); });

View file

@ -9,21 +9,30 @@ import { Highlighter, updateCurrentWorkspace } from './current-workspace.js';
import { updateClients } from './clients.js'; import { updateClients } from './clients.js';
// TODO: have a 'page' for each monitor, arrows on both sides to loop through // TODO: have a 'page' for each monitor, arrows on both sides to loop through
export const Overview = () => { export const Overview = () => {
const highlighter = Highlighter(); const highlighter = Highlighter();
const mainBox = Box({ const mainBox = Box({
// Do this for scss hierarchy // Do this for scss hierarchy
className: 'overview', class_name: 'overview',
css: 'all: unset', css: 'all: unset',
vertical: true, vertical: true,
vpack: 'center', vpack: 'center',
hpack: 'center', hpack: 'center',
attribute: {
workspaces: [],
update: () => {
getWorkspaces(mainBox);
updateWorkspaces(mainBox);
updateClients(mainBox);
updateCurrentWorkspace(mainBox, highlighter);
},
},
children: [ children: [
Box({ Box({
vertical: true, vertical: true,
@ -42,31 +51,24 @@ export const Overview = () => {
setup: (self) => { setup: (self) => {
self.hook(Hyprland, () => { self.hook(Hyprland, () => {
if (!App.getWindow('overview').visible) { if (!App.getWindow('overview')?.visible) {
return; return;
} }
self.update(); self?.attribute.update();
}); });
}, },
properties: [
['workspaces'],
],
}); });
mainBox.update = () => {
getWorkspaces(mainBox);
updateWorkspaces(mainBox);
updateClients(mainBox);
updateCurrentWorkspace(mainBox, highlighter);
};
const widget = Overlay({ const widget = Overlay({
overlays: [highlighter, mainBox], overlays: [highlighter, mainBox],
attribute: {
get_child: () => mainBox,
},
child: Box({ child: Box({
className: 'overview', class_name: 'overview',
css: ` css: `
min-height: ${mainBox.get_allocated_height()}px; min-height: ${mainBox.get_allocated_height()}px;
min-width: ${mainBox.get_allocated_width()}px; min-width: ${mainBox.get_allocated_width()}px;
@ -77,6 +79,7 @@ export const Overview = () => {
setup: (self) => { setup: (self) => {
self.on('get-child-position', (_, ch) => { self.on('get-child-position', (_, ch) => {
if (ch === mainBox) { if (ch === mainBox) {
// @ts-expect-error
self.child.setCss(` self.child.setCss(`
transition: min-height 0.2s ease, min-width 0.2s ease; transition: min-height 0.2s ease, min-width 0.2s ease;
min-height: ${mainBox.get_allocated_height()}px; min-height: ${mainBox.get_allocated_height()}px;
@ -87,8 +90,6 @@ export const Overview = () => {
}, },
}); });
widget.getChild = () => mainBox;
return widget; return widget;
}; };
@ -99,7 +100,7 @@ export default () => {
close_on_unfocus: 'none', close_on_unfocus: 'none',
onOpen: () => { onOpen: () => {
win.attribute.set_child(Overview()); win.attribute.set_child(Overview());
win.attribute.get_child().getChild().update(); win.attribute.get_child().attribute.get_child().attribute.update();
}, },
}); });

View file

@ -10,42 +10,69 @@ const DEFAULT_STYLE = `
min-height: ${VARS.SCREEN.Y * VARS.SCALE}px; min-height: ${VARS.SCREEN.Y * VARS.SCALE}px;
`; `;
/**
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/widgets/revealer').default} Revealer
*/
/** @param {Box} box */
export const getWorkspaces = (box) => { export const getWorkspaces = (box) => {
const children = []; const children = [];
box.children.forEach((type) => { box.children.forEach((type) => {
type.children.forEach((row) => { // @ts-expect-error
row.child.centerWidget.child.children.forEach((ch) => { type.children.forEach(
children.push(ch); /** @param {Revealer} row */
}); (row) => {
}); // @ts-expect-error
row.child.centerWidget.child.children.forEach(
/** @param {Revealer} workspace */
(workspace) => {
children.push(workspace);
},
);
},
);
}); });
box._workspaces = children.sort((a, b) => a._id - b._id); box.attribute.workspaces = children.sort((a, b) =>
a.attribute.id - b.attribute.id);
}; };
/**
* @param {number} id
* @param {string} name
* @param {boolean} normal
*/
const Workspace = (id, name, normal = true) => { const Workspace = (id, name, normal = true) => {
const fixed = Fixed(); const fixed = Fixed();
const workspace = Revealer({ const workspace = Revealer({
transition: 'slide_right', transition: 'slide_right',
transitionDuration: 500, transition_duration: 500,
attribute: {
id,
name,
get_fixed: () => fixed,
},
setup: (self) => { setup: (self) => {
if (normal) { if (normal) {
self.hook(Hyprland, () => { self.hook(Hyprland, () => {
const activeId = Hyprland.active.workspace.id; const activeId = Hyprland.active.workspace.id;
const active = activeId === self._id; const active = activeId === self.attribute.id;
const ws = Hyprland.getWorkspace(self.attribute.id);
self.revealChild = Hyprland.getWorkspace(self._id) self.reveal_child =
?.windows > 0 || active; (ws?.windows && ws.windows > 0) || active;
}); });
} }
}, },
child: WorkspaceDrop({ child: WorkspaceDrop({
child: Box({ child: Box({
className: 'workspace', class_name: 'workspace',
css: normal ? css: normal ?
DEFAULT_STYLE : DEFAULT_STYLE :
@ -70,23 +97,23 @@ const Workspace = (id, name, normal = true) => {
}), }),
}); });
workspace._id = id;
workspace._name = name;
workspace.getFixed = () => fixed;
return workspace; return workspace;
}; };
export const WorkspaceRow = (className, i) => { /**
* @param {string} class_name
* @param {number} i
*/
export const WorkspaceRow = (class_name, i) => {
const addWorkspace = Workspace( const addWorkspace = Workspace(
className === 'special' ? -1 : 1000, class_name === 'special' ? -1 : 1000,
className === 'special' ? 'special' : '', class_name === 'special' ? 'special' : '',
false, false,
); );
return Revealer({ return Revealer({
transition: 'slide_down', transition: 'slide_down',
hpack: className === 'special' ? '' : 'start', hpack: class_name === 'special' ? 'fill' : 'start',
setup: (self) => { setup: (self) => {
self.hook(Hyprland, (rev) => { self.hook(Hyprland, (rev) => {
@ -101,18 +128,18 @@ export const WorkspaceRow = (className, i) => {
return isInRow && (hasClients || isActive); return isInRow && (hasClients || isActive);
}); });
rev.revealChild = rowExists; rev.reveal_child = rowExists;
}); });
}, },
child: CenterBox({ child: CenterBox({
children: [null, EventBox({ center_widget: EventBox({
setup: (self) => { setup: (self) => {
self.hook(Hyprland, () => { self.hook(Hyprland, () => {
const maxId = (i + 1) * VARS.WORKSPACE_PER_ROW; const maxId = (i + 1) * VARS.WORKSPACE_PER_ROW;
const activeId = Hyprland.active.workspace.id; const activeId = Hyprland.active.workspace.id;
const isSpecial = className === 'special'; const isSpecial = class_name === 'special';
const nextRowExists = Hyprland.workspaces.some((ws) => { const nextRowExists = Hyprland.workspaces.some((ws) => {
const isInNextRow = ws.id > maxId; const isInNextRow = ws.id > maxId;
const hasClients = ws.windows > 0; const hasClients = ws.windows > 0;
@ -121,22 +148,26 @@ export const WorkspaceRow = (className, i) => {
return isInNextRow && (hasClients || isActive); return isInNextRow && (hasClients || isActive);
}); });
addWorkspace.revealChild = isSpecial || !nextRowExists; addWorkspace.reveal_child = isSpecial || !nextRowExists;
}); });
}, },
child: Box({ child: Box({
className, class_name,
children: [addWorkspace], children: [addWorkspace],
}), }),
}), null], }),
}), }),
}); });
}; };
/** @param {Box} box */
export const updateWorkspaces = (box) => { export const updateWorkspaces = (box) => {
Hyprland.workspaces.forEach((ws) => { Hyprland.workspaces.forEach((ws) => {
const currentWs = box._workspaces.find((ch) => ch._id === ws.id); const currentWs = box.attribute.workspaces.find(
/** @param {Revealer} ch */
(ch) => ch.attribute.id === ws.id,
);
if (!currentWs) { if (!currentWs) {
let type = 0; let type = 0;
@ -148,16 +179,19 @@ export const updateWorkspaces = (box) => {
} }
else { else {
rowNo = Math.floor((ws.id - 1) / VARS.WORKSPACE_PER_ROW); rowNo = Math.floor((ws.id - 1) / VARS.WORKSPACE_PER_ROW);
// @ts-expect-error
const wsQty = box.children[type].children.length; const wsQty = box.children[type].children.length;
if (rowNo >= wsQty) { if (rowNo >= wsQty) {
for (let i = wsQty; i <= rowNo; ++i) { for (let i = wsQty; i <= rowNo; ++i) {
// @ts-expect-error
box.children[type].add(WorkspaceRow( box.children[type].add(WorkspaceRow(
type ? 'special' : 'normal', i, type ? 'special' : 'normal', i,
)); ));
} }
} }
} }
// @ts-expect-error
const row = box.children[type].children[rowNo] const row = box.children[type].children[rowNo]
.child.centerWidget.child; .child.centerWidget.child;
@ -166,8 +200,15 @@ export const updateWorkspaces = (box) => {
}); });
// Make sure the order is correct // Make sure the order is correct
box._workspaces.forEach((workspace, i) => { box.attribute.workspaces.forEach(
workspace.get_parent().reorder_child(workspace, i); /**
}); * @param {Revealer} workspace
* @param {number} i
*/
(workspace, i) => {
// @ts-expect-error
workspace?.get_parent()?.reorder_child(workspace, i);
},
);
box.show_all(); box.show_all();
}; };

View file

@ -13,7 +13,7 @@ const PowermenuWidget = () => CenterBox({
// @ts-expect-error // @ts-expect-error
vertical: false, vertical: false,
startWidget: CursorBox({ start_widget: CursorBox({
class_name: 'shutdown', class_name: 'shutdown',
on_primary_click_release: () => execAsync(['systemctl', 'poweroff']) on_primary_click_release: () => execAsync(['systemctl', 'poweroff'])
.catch(print), .catch(print),
@ -23,7 +23,7 @@ const PowermenuWidget = () => CenterBox({
}), }),
}), }),
centerWidget: CursorBox({ center_widget: CursorBox({
class_name: 'reboot', class_name: 'reboot',
on_primary_click_release: () => execAsync(['systemctl', 'reboot']) on_primary_click_release: () => execAsync(['systemctl', 'reboot'])
.catch(print), .catch(print),
@ -33,7 +33,7 @@ const PowermenuWidget = () => CenterBox({
}), }),
}), }),
endWidget: CursorBox({ end_widget: CursorBox({
class_name: 'logout', class_name: 'logout',
on_primary_click_release: () => Hyprland.sendMessage('dispatch exit') on_primary_click_release: () => Hyprland.sendMessage('dispatch exit')
.catch(print), .catch(print),

View file

@ -8,56 +8,62 @@ import CursorBox from '../misc/cursorbox.js';
const SCROLL_THRESH_H = 200; const SCROLL_THRESH_H = 200;
const SCROLL_THRESH_N = 7; const SCROLL_THRESH_N = 7;
/**
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/service/bluetooth').BluetoothDevice} BluetoothDevice
*/
const BluetoothDevice = (dev) => { /** @param {BluetoothDevice} dev */
const widget = Box({ const BluetoothDevice = (dev) => Box({
className: 'menu-item', class_name: 'menu-item',
});
const child = Box({ attribute: { dev },
hexpand: true,
children: [
Icon({
binds: [['icon', dev, 'icon-name']],
}),
Label({ children: [Revealer({
binds: [['label', dev, 'name']], reveal_child: true,
}),
Icon({
icon: 'object-select-symbolic',
hexpand: true,
hpack: 'end',
setup: (self) => {
self.hook(dev, () => {
self.setCss(`opacity: ${dev.paired ? '1' : '0'};`);
});
},
}),
],
});
widget.dev = dev;
widget.add(Revealer({
revealChild: true,
transition: 'slide_down', transition: 'slide_down',
child: CursorBox({ child: CursorBox({
on_primary_click_release: () => dev.setConnection(true), on_primary_click_release: () => dev.setConnection(true),
child,
}),
}));
return widget; child: Box({
}; hexpand: true,
children: [
Icon({
icon: dev.bind('icon_name'),
}),
Label({
label: dev.bind('name'),
}),
Icon({
icon: 'object-select-symbolic',
hexpand: true,
hpack: 'end',
}).hook(dev, (self) => {
self.setCss(`opacity: ${dev.paired ?
'1' :
'0'};
`);
}),
],
}),
}),
})],
});
export const BluetoothMenu = () => { export const BluetoothMenu = () => {
const DevList = new Map(); const DevList = new Map();
const topArrow = Revealer({ const topArrow = Revealer({
transition: 'slide_down', transition: 'slide_down',
child: Icon({ child: Icon({
icon: `${App.configDir }/icons/down-large.svg`, icon: `${App.configDir }/icons/down-large.svg`,
className: 'scrolled-indicator', class_name: 'scrolled-indicator',
size: 16, size: 16,
css: '-gtk-icon-transform: rotate(180deg);', css: '-gtk-icon-transform: rotate(180deg);',
}), }),
@ -65,15 +71,17 @@ export const BluetoothMenu = () => {
const bottomArrow = Revealer({ const bottomArrow = Revealer({
transition: 'slide_up', transition: 'slide_up',
child: Icon({ child: Icon({
icon: `${App.configDir }/icons/down-large.svg`, icon: `${App.configDir }/icons/down-large.svg`,
className: 'scrolled-indicator', class_name: 'scrolled-indicator',
size: 16, size: 16,
}), }),
}); });
return Overlay({ return Overlay({
pass_through: true, pass_through: true,
overlays: [ overlays: [
Box({ Box({
vpack: 'start', vpack: 'start',
@ -91,7 +99,7 @@ export const BluetoothMenu = () => {
], ],
child: Box({ child: Box({
className: 'menu', class_name: 'menu',
child: Scrollable({ child: Scrollable({
hscroll: 'never', hscroll: 'never',
@ -101,28 +109,36 @@ export const BluetoothMenu = () => {
self.on('edge-reached', (_, pos) => { self.on('edge-reached', (_, pos) => {
// Manage scroll indicators // Manage scroll indicators
if (pos === 2) { if (pos === 2) {
topArrow.revealChild = false; topArrow.reveal_child = false;
bottomArrow.revealChild = true; bottomArrow.reveal_child = true;
} }
else if (pos === 3) { else if (pos === 3) {
topArrow.revealChild = true; topArrow.reveal_child = true;
bottomArrow.revealChild = false; bottomArrow.reveal_child = false;
} }
}); });
}, },
child: ListBox({ child: ListBox({
setup: (self) => { setup: (self) => {
self.set_sort_func((a, b) => { // @ts-expect-error
return b.get_children()[0].dev.paired - self.set_sort_func(
a.get_children()[0].dev.paired; /**
}); * @param {Box} a
* @param {Box} b
*/
(a, b) => {
// @ts-expect-error
return b.get_children()[0].attribute.dev.paired - // eslint-disable-line
// @ts-expect-error
a.get_children()[0].attribute.dev.paired;
},
);
self.hook(Bluetooth, () => { self.hook(Bluetooth, () => {
// Get all devices // Get all devices
const Devices = [].concat( const Devices = Bluetooth.devices.concat(
Bluetooth.devices, Bluetooth.connected_devices,
Bluetooth.connectedDevices,
); );
// Add missing devices // Add missing devices
@ -130,6 +146,7 @@ export const BluetoothMenu = () => {
if (!DevList.has(dev) && dev.name) { if (!DevList.has(dev) && dev.name) {
DevList.set(dev, BluetoothDevice(dev)); DevList.set(dev, BluetoothDevice(dev));
// @ts-expect-error
self.add(DevList.get(dev)); self.add(DevList.get(dev));
self.show_all(); self.show_all();
} }
@ -151,7 +168,7 @@ export const BluetoothMenu = () => {
} }
else { else {
devWidget.children[0] devWidget.children[0]
.revealChild = false; .reveal_child = false;
devWidget.toDestroy = true; devWidget.toDestroy = true;
} }
} }
@ -160,34 +177,48 @@ export const BluetoothMenu = () => {
// Start scrolling after a specified height // Start scrolling after a specified height
// is reached by the children // is reached by the children
const height = Math.max( const height = Math.max(
self.get_parent().get_allocated_height(), self.get_parent()?.get_allocated_height() || 0,
SCROLL_THRESH_H, SCROLL_THRESH_H,
); );
const scroll = self.get_parent().get_parent(); const scroll = self.get_parent()?.get_parent();
if (self.get_children().length > SCROLL_THRESH_N) { if (scroll) {
scroll.vscroll = 'always'; // @ts-expect-error
scroll.setCss(`min-height: ${height}px;`); const n_child = self.get_children().length;
// Make bottom scroll indicator appear only if (n_child > SCROLL_THRESH_N) {
// when first getting overflowing children // @ts-expect-error
if (!(bottomArrow.revealChild === true || scroll.vscroll = 'always';
topArrow.revealChild === true)) { // @ts-expect-error
bottomArrow.revealChild = true; scroll.setCss(`min-height: ${height}px;`);
// Make bottom scroll indicator appear only
// when first getting overflowing children
if (!(bottomArrow.reveal_child === true ||
topArrow.reveal_child === true)) {
bottomArrow.reveal_child = true;
}
}
else {
// @ts-expect-error
scroll.vscroll = 'never';
// @ts-expect-error
scroll.setCss('');
topArrow.reveal_child = false;
bottomArrow.reveal_child = false;
} }
}
else {
scroll.vscroll = 'never';
scroll.setCss('');
topArrow.revealChild = false;
bottomArrow.revealChild = false;
} }
// Trigger sort_func // Trigger sort_func
self.get_children().forEach((ch) => { // @ts-expect-error
ch.changed(); self.get_children().forEach(
}); /** @param {Box} ListBoxRow */
(ListBoxRow) => {
// @ts-expect-error
ListBoxRow.changed();
},
);
}); });
}, },
}), }),

View file

@ -1,5 +1,4 @@
import App from 'resource:///com/github/Aylur/ags/app.js'; import App from 'resource:///com/github/Aylur/ags/app.js';
// @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 Network from 'resource:///com/github/Aylur/ags/service/network.js'; import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js'; import Variable from 'resource:///com/github/Aylur/ags/variable.js';
@ -17,16 +16,23 @@ import { BluetoothMenu } from './bluetooth.js';
const SPACING = 28; const SPACING = 28;
const ButtonStates = []; const ButtonStates = [];
/** @typedef {import('types/widgets/widget').default} Widget */ /**
* @typedef {import('types/widgets/widget').default} Widget
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/widgets/icon').default} Icon
* @typedef {import('types/widgets/label').default} Label
* @typedef {import('types/widgets/revealer').default} Revealer
* @typedef {[any, function, (string|undefined)?]} BindTuple
*/
/** /**
* @param {{ * @param {{
* command?: function * command?: function
* secondary_command?: function * secondary_command?: function
* onOpen?: function(Widget):void * onOpen?: function(Revealer):void
* icon: any * icon: string|BindTuple
* indicator?: any * indicator?: BindTuple
* menu?: any * menu?: any
* }} o * }} o
*/ */
@ -41,10 +47,13 @@ const GridButton = ({
const Activated = Variable(false); const Activated = Variable(false);
ButtonStates.push(Activated); ButtonStates.push(Activated);
let iconWidget;
/** @type Label */
let indicatorWidget = Label();
// Allow setting icon dynamically or statically // Allow setting icon dynamically or statically
if (typeof icon === 'string') { if (typeof icon === 'string') {
icon = Icon({ iconWidget = Icon({
class_name: 'grid-label', class_name: 'grid-label',
icon, icon,
setup: (self) => { setup: (self) => {
@ -56,11 +65,12 @@ const GridButton = ({
}, },
}); });
} }
else { else if (Array.isArray(icon)) {
icon = Icon({ iconWidget = Icon({
class_name: 'grid-label', class_name: 'grid-label',
setup: (self) => { setup: (self) => {
self self
// @ts-expect-error
.hook(...icon) .hook(...icon)
.hook(Activated, () => { .hook(Activated, () => {
self.setCss(`color: ${Activated.value ? self.setCss(`color: ${Activated.value ?
@ -72,12 +82,13 @@ const GridButton = ({
} }
if (indicator) { if (indicator) {
indicator = Label({ indicatorWidget = Label({
class_name: 'sub-label', class_name: 'sub-label',
justification: 'left', justification: 'left',
truncate: 'end', truncate: 'end',
maxWidthChars: 12, max_width_chars: 12,
setup: (self) => { setup: (self) => {
// @ts-expect-error
self.hook(...indicator); self.hook(...indicator);
}, },
}); });
@ -87,7 +98,7 @@ const GridButton = ({
menu = Revealer({ menu = Revealer({
transition: 'slide_down', transition: 'slide_down',
child: menu, child: menu,
binds: [['revealChild', Activated, 'value']], reveal_child: Activated.bind(),
}); });
} }
@ -110,7 +121,7 @@ const GridButton = ({
} }
}, },
child: icon, child: iconWidget,
}), }),
CursorBox({ CursorBox({
@ -131,10 +142,13 @@ const GridButton = ({
self.get_parent() self.get_parent()
?.get_parent()?.get_parent() ?.get_parent()?.get_parent()
?.get_parent()?.get_parent() ?.get_parent()?.get_parent()
// @ts-expect-error
?.children[1]; ?.children[1];
const isSetup = rowMenu.get_children() const isSetup = rowMenu.get_children().find(
.find((ch) => ch === menu); /** @param {Box} ch */
(ch) => ch === menu,
);
if (!isSetup) { if (!isSetup) {
rowMenu.add(menu); rowMenu.add(menu);
@ -165,14 +179,14 @@ const GridButton = ({
], ],
}), }),
indicator, indicatorWidget,
], ],
}); });
return widget; return widget;
}; };
const Row = ({ buttons } = {}) => { const Row = ({ buttons }) => {
const widget = Box({ const widget = Box({
vertical: true, vertical: true,
@ -188,10 +202,13 @@ const Row = ({ buttons } = {}) => {
for (let i = 0; i < buttons.length; ++i) { for (let i = 0; i < buttons.length; ++i) {
if (i === buttons.length - 1) { if (i === buttons.length - 1) {
// @ts-expect-error
widget.children[0].add(buttons[i]); widget.children[0].add(buttons[i]);
} }
else { else {
// @ts-expect-error
widget.children[0].add(buttons[i]); widget.children[0].add(buttons[i]);
// @ts-expect-error
widget.children[0].add(Separator(SPACING)); widget.children[0].add(Separator(SPACING));
} }
} }
@ -209,13 +226,17 @@ const FirstRow = () => Row({
// TODO: connection editor // TODO: connection editor
}, },
icon: [Network, (icon) => { icon: [Network,
icon.icon = Network.wifi?.iconName; /** @param {Icon} self */
}], (self) => {
self.icon = Network.wifi?.icon_name;
}],
indicator: [Network, (self) => { indicator: [Network,
self.label = Network.wifi?.ssid || Network.wired?.internet; /** @param {Label} self */
}], (self) => {
self.label = Network.wifi?.ssid || Network.wired?.internet;
}],
menu: NetworkMenu(), menu: NetworkMenu(),
onOpen: () => Network.wifi.scan(), onOpen: () => Network.wifi.scan(),
@ -241,26 +262,30 @@ const FirstRow = () => Row({
// TODO: bluetooth connection editor // TODO: bluetooth connection editor
}, },
icon: [Bluetooth, (self) => { icon: [Bluetooth,
if (Bluetooth.enabled) { /** @param {Icon} self */
self.icon = Bluetooth.connectedDevices[0] ? (self) => {
Bluetooth.connectedDevices[0].iconName : if (Bluetooth.enabled) {
'bluetooth-active-symbolic'; self.icon = Bluetooth.connected_devices[0] ?
} Bluetooth.connected_devices[0].icon_name :
else { 'bluetooth-active-symbolic';
self.icon = 'bluetooth-disabled-symbolic'; }
} else {
}], self.icon = 'bluetooth-disabled-symbolic';
}
}],
indicator: [Bluetooth, (self) => { indicator: [Bluetooth,
self.label = Bluetooth.connectedDevices[0] ? /** @param {Label} self */
`${Bluetooth.connectedDevices[0]}` : (self) => {
'Disconnected'; self.label = Bluetooth.connected_devices[0] ?
}, 'notify::connected-devices'], `${Bluetooth.connected_devices[0]}` :
'Disconnected';
}, 'notify::connected-devices'],
menu: BluetoothMenu(), menu: BluetoothMenu(),
onOpen: (menu) => { onOpen: (menu) => {
execAsync(`bluetoothctl scan ${menu.revealChild ? execAsync(`bluetoothctl scan ${menu.reveal_child ?
'on' : 'on' :
'off'}`).catch(print); 'off'}`).catch(print);
}, },
@ -282,9 +307,11 @@ const SecondRow = () => Row({
.catch(print); .catch(print);
}, },
icon: [SpeakerIcon, (self) => { icon: [SpeakerIcon,
self.icon = SpeakerIcon.value; /** @param {Icon} self */
}], (self) => {
self.icon = SpeakerIcon.value;
}],
}), }),
GridButton({ GridButton({
@ -298,9 +325,11 @@ const SecondRow = () => Row({
.catch(print); .catch(print);
}, },
icon: [MicIcon, (self) => { icon: [MicIcon,
self.icon = MicIcon.value; /** @param {Icon} self */
}], (self) => {
self.icon = MicIcon.value;
}],
}), }),
GridButton({ GridButton({

View file

@ -14,18 +14,18 @@ const QuickSettingsWidget = () => {
}); });
return Box({ return Box({
className: 'qs-container', class_name: 'qs-container',
vertical: true, vertical: true,
children: [ children: [
Box({ Box({
className: 'quick-settings', class_name: 'quick-settings',
vertical: true, vertical: true,
children: [ children: [
Label({ Label({
label: 'Control Center', label: 'Control Center',
className: 'title', class_name: 'title',
hpack: 'start', hpack: 'start',
css: ` css: `
margin-left: 20px; margin-left: 20px;

View file

@ -10,42 +10,41 @@ import CursorBox from '../misc/cursorbox.js';
const SCROLL_THRESH_H = 200; const SCROLL_THRESH_H = 200;
const SCROLL_THRESH_N = 7; const SCROLL_THRESH_N = 7;
/** @typedef {import('types/widgets/box').default} Box */
/** @param {any} ap */
const AccessPoint = (ap) => { const AccessPoint = (ap) => {
const widget = Box({ const widget = Box({
className: 'menu-item', class_name: 'menu-item',
attribute: {
ap: Variable(ap),
},
}); });
widget.ap = Variable(ap);
const child = Box({ const child = Box({
hexpand: true, hexpand: true,
children: [ children: [
Icon({ Icon().hook(widget.attribute.ap, (self) => {
setup: (self) => { self.icon = widget.attribute.ap.value.iconName;
self.hook(widget.ap, () => {
self.icon = widget.ap.value.iconName;
});
},
}), }),
Label({ Label().hook(widget.attribute.ap, (self) => {
setup: (self) => { self.label = widget.attribute.ap.value.ssid || '';
self.hook(widget.ap, () => {
self.label = widget.ap.value.ssid || '';
});
},
}), }),
Icon({ Icon({
icon: 'object-select-symbolic', icon: 'object-select-symbolic',
hexpand: true, hexpand: true,
hpack: 'end', hpack: 'end',
setup: (self) => { setup: (self) => {
self.hook(Network, () => { self.hook(Network, () => {
self.setCss( self.setCss(
`opacity: ${ `opacity: ${
widget.ap.value.ssid === Network.wifi.ssid ? widget.attribute.ap.value.ssid ===
Network.wifi.ssid ?
'1' : '1' :
'0' '0'
}; };
@ -58,12 +57,13 @@ const AccessPoint = (ap) => {
}); });
widget.add(Revealer({ widget.add(Revealer({
revealChild: true, reveal_child: true,
transition: 'slide_down', transition: 'slide_down',
child: CursorBox({ child: CursorBox({
on_primary_click_release: () => { on_primary_click_release: () => {
execAsync(`nmcli device wifi connect execAsync(`nmcli device wifi connect
${widget.ap.value.bssid}`).catch(print); ${widget.attribute.ap.value.bssid}`).catch(print);
}, },
child, child,
}), }),
@ -74,11 +74,13 @@ const AccessPoint = (ap) => {
export const NetworkMenu = () => { export const NetworkMenu = () => {
const APList = new Map(); const APList = new Map();
const topArrow = Revealer({ const topArrow = Revealer({
transition: 'slide_down', transition: 'slide_down',
child: Icon({ child: Icon({
icon: `${App.configDir }/icons/down-large.svg`, icon: `${App.configDir }/icons/down-large.svg`,
className: 'scrolled-indicator', class_name: 'scrolled-indicator',
size: 16, size: 16,
css: '-gtk-icon-transform: rotate(180deg);', css: '-gtk-icon-transform: rotate(180deg);',
}), }),
@ -86,15 +88,17 @@ export const NetworkMenu = () => {
const bottomArrow = Revealer({ const bottomArrow = Revealer({
transition: 'slide_up', transition: 'slide_up',
child: Icon({ child: Icon({
icon: `${App.configDir }/icons/down-large.svg`, icon: `${App.configDir }/icons/down-large.svg`,
className: 'scrolled-indicator', class_name: 'scrolled-indicator',
size: 16, size: 16,
}), }),
}); });
return Overlay({ return Overlay({
pass_through: true, pass_through: true,
overlays: [ overlays: [
Box({ Box({
vpack: 'start', vpack: 'start',
@ -112,7 +116,7 @@ export const NetworkMenu = () => {
], ],
child: Box({ child: Box({
className: 'menu', class_name: 'menu',
child: Scrollable({ child: Scrollable({
hscroll: 'never', hscroll: 'never',
@ -122,22 +126,32 @@ export const NetworkMenu = () => {
self.on('edge-reached', (_, pos) => { self.on('edge-reached', (_, pos) => {
// Manage scroll indicators // Manage scroll indicators
if (pos === 2) { if (pos === 2) {
topArrow.revealChild = false; topArrow.reveal_child = false;
bottomArrow.revealChild = true; bottomArrow.reveal_child = true;
} }
else if (pos === 3) { else if (pos === 3) {
topArrow.revealChild = true; topArrow.reveal_child = true;
bottomArrow.revealChild = false; bottomArrow.reveal_child = false;
} }
}); });
}, },
child: ListBox({ child: ListBox({
setup: (self) => { setup: (self) => {
self.set_sort_func((a, b) => { // @ts-expect-error
return b.get_children()[0].ap.value.strength - self.set_sort_func(
a.get_children()[0].ap.value.strength; /**
}); * @param {Box} a
* @param {Box} b
*/
(a, b) => {
return b.get_children()[0]
// @ts-expect-error
.attribute.ap.value.strength -
// @ts-expect-error
a.get_children()[0].attribute.ap.value.strength;
},
);
self.hook(Network, () => { self.hook(Network, () => {
// Add missing APs // Add missing APs
@ -145,15 +159,17 @@ export const NetworkMenu = () => {
if (ap.ssid !== 'Unknown') { if (ap.ssid !== 'Unknown') {
if (APList.has(ap.ssid)) { if (APList.has(ap.ssid)) {
const accesPoint = APList.get(ap.ssid) const accesPoint = APList.get(ap.ssid)
.ap.value; .attribute.ap.value;
if (accesPoint.strength < ap.strength) { if (accesPoint.strength < ap.strength) {
APList.get(ap.ssid).ap.value = ap; APList.get(ap.ssid).attribute
.ap.value = ap;
} }
} }
else { else {
APList.set(ap.ssid, AccessPoint(ap)); APList.set(ap.ssid, AccessPoint(ap));
// @ts-expect-error
self.add(APList.get(ap.ssid)); self.add(APList.get(ap.ssid));
self.show_all(); self.show_all();
} }
@ -176,7 +192,7 @@ export const NetworkMenu = () => {
} }
else { else {
apWidget.children[0] apWidget.children[0]
.revealChild = false; .reveal_child = false;
apWidget.toDestroy = true; apWidget.toDestroy = true;
} }
} }
@ -185,34 +201,48 @@ export const NetworkMenu = () => {
// Start scrolling after a specified height // Start scrolling after a specified height
// is reached by the children // is reached by the children
const height = Math.max( const height = Math.max(
self.get_parent().get_allocated_height(), self.get_parent()?.get_allocated_height() || 0,
SCROLL_THRESH_H, SCROLL_THRESH_H,
); );
const scroll = self.get_parent().get_parent(); const scroll = self.get_parent()?.get_parent();
if (self.get_children().length > SCROLL_THRESH_N) { if (scroll) {
scroll.vscroll = 'always'; // @ts-expect-error
scroll.setCss(`min-height: ${height}px;`); const n_child = self.get_children().length;
// Make bottom scroll indicator appear only if (n_child > SCROLL_THRESH_N) {
// when first getting overflowing children // @ts-expect-error
if (!(bottomArrow.revealChild === true || scroll.vscroll = 'always';
topArrow.revealChild === true)) { // @ts-expect-error
bottomArrow.revealChild = true; scroll.setCss(`min-height: ${height}px;`);
// Make bottom scroll indicator appear only
// when first getting overflowing children
if (!(bottomArrow.reveal_child === true ||
topArrow.reveal_child === true)) {
bottomArrow.reveal_child = true;
}
}
else {
// @ts-expect-error
scroll.vscroll = 'never';
// @ts-expect-error
scroll.setCss('');
topArrow.reveal_child = false;
bottomArrow.reveal_child = false;
} }
}
else {
scroll.vscroll = 'never';
scroll.setCss('');
topArrow.revealChild = false;
bottomArrow.revealChild = false;
} }
// Trigger sort_func // Trigger sort_func
self.get_children().forEach((ch) => { // @ts-expect-error
ch.changed(); self.get_children().forEach(
}); /** @param {Box} ListBoxRow */
(ListBoxRow) => {
// @ts-expect-error
ListBoxRow.changed();
},
);
}); });
}, },
}), }),

View file

@ -7,20 +7,20 @@ import { SpeakerIcon } from '../misc/audio-icons.js';
export default () => Box({ export default () => Box({
className: 'slider-box', class_name: 'slider-box',
vertical: true, vertical: true,
hpack: 'center', hpack: 'center',
children: [ children: [
Box({ Box({
className: 'slider', class_name: 'slider',
vpack: 'start', vpack: 'start',
hpack: 'center', hpack: 'center',
children: [ children: [
Icon({ Icon({
size: 26, size: 26,
className: 'slider-label', class_name: 'slider-label',
binds: [['icon', SpeakerIcon, 'value']], binds: [['icon', SpeakerIcon, 'value']],
}), }),
@ -30,14 +30,16 @@ export default () => Box({
max: 0.999, max: 0.999,
draw_value: false, draw_value: false,
onChange: ({ value }) => { on_change: ({ value }) => {
Audio.speaker.volume = value; if (Audio.speaker) {
Audio.speaker.volume = value;
}
}, },
setup: (self) => { setup: (self) => {
self self
.hook(Audio, () => { .hook(Audio, () => {
self.value = Audio.speaker?.volume; self.value = Audio.speaker?.volume || 0;
}, 'speaker-changed') }, 'speaker-changed')
.on('button-press-event', () => { .on('button-press-event', () => {
@ -53,13 +55,13 @@ export default () => Box({
}), }),
Box({ Box({
className: 'slider', class_name: 'slider',
vpack: 'start', vpack: 'start',
hpack: 'center', hpack: 'center',
children: [ children: [
Icon({ Icon({
className: 'slider-label', class_name: 'slider-label',
binds: [['icon', Brightness, 'screen-icon']], binds: [['icon', Brightness, 'screen-icon']],
}), }),
@ -68,7 +70,7 @@ export default () => Box({
vpack: 'center', vpack: 'center',
draw_value: false, draw_value: false,
onChange: ({ value }) => { on_change: ({ value }) => {
Brightness.screen = value; Brightness.screen = value;
}, },

View file

@ -4,6 +4,7 @@ import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
import { CenterBox, Icon, ToggleButton } from 'resource:///com/github/Aylur/ags/widget.js'; import { CenterBox, Icon, ToggleButton } from 'resource:///com/github/Aylur/ags/widget.js';
/** @param {import('types/widgets/revealer').default} rev */
export default (rev) => CenterBox({ export default (rev) => CenterBox({
center_widget: ToggleButton({ center_widget: ToggleButton({
cursor: 'pointer', cursor: 'pointer',
@ -11,30 +12,32 @@ export default (rev) => CenterBox({
setup: (self) => { setup: (self) => {
// Open at startup if there are players // Open at startup if there are players
const id = Mpris.connect('changed', () => { const id = Mpris.connect('changed', () => {
// @ts-expect-error
self.set_active(Mpris.players.length > 0); self.set_active(Mpris.players.length > 0);
Mpris.disconnect(id); Mpris.disconnect(id);
}); });
self.on('toggled', () => { self.on('toggled', () => {
// @ts-expect-error
if (self.get_active()) { if (self.get_active()) {
self.get_children()[0] self.child
.setCss('-gtk-icon-transform: rotate(0deg);'); // @ts-expect-error
rev.revealChild = true; ?.setCss('-gtk-icon-transform: rotate(0deg);');
rev.reveal_child = true;
} }
else { else {
self.get_children()[0] self.child
.setCss('-gtk-icon-transform: rotate(180deg);'); // @ts-expect-error
rev.revealChild = false; ?.setCss('-gtk-icon-transform: rotate(180deg);');
rev.reveal_child = false;
} }
}); });
}, },
child: Icon({ child: Icon({
icon: `${App.configDir }/icons/down-large.svg`, icon: `${App.configDir }/icons/down-large.svg`,
className: 'arrow', class_name: 'arrow',
css: '-gtk-icon-transform: rotate(180deg);', css: '-gtk-icon-transform: rotate(180deg);',
}), }),
}), }),
start_widget: null,
end_widget: null,
}); });

View file

@ -114,12 +114,18 @@ class Brightness extends Service {
#monitorKbdState() { #monitorKbdState() {
Variable(0, { Variable(0, {
poll: [INTERVAL, `brightnessctl -d ${KBD} g`, (out) => { poll: [
if (out !== this.#kbd) { INTERVAL,
this.#kbd = out; `brightnessctl -d ${KBD} g`,
this.emit('kbd', this.#kbd); (out) => {
} if (parseInt(out) !== this.#kbd) {
}], this.#kbd = parseInt(out);
this.emit('kbd', this.#kbd);
return this.#kbd;
}
},
],
}); });
} }
} }