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

View file

@ -70,113 +70,104 @@ export default ({
setup(self);
self
.hook(gesture,
/**
* @param {Overlay} overlay
* @param {number} realGesture
*/
(overlay, realGesture) => {
if (realGesture) {
Array.from(overlay.attribute.list())
.forEach((over) => {
over.visible = true;
});
}
else {
overlay.attribute.showTopOnly();
}
.hook(gesture, (_, realGesture) => {
if (realGesture) {
Array.from(self.attribute.list())
.forEach((over) => {
over.visible = true;
});
}
else {
self.attribute.showTopOnly();
}
// Don't allow gesture when only one player
if (overlay.attribute.list().length <= 1) {
return;
}
// Don't allow gesture when only one player
if (self.attribute.list().length <= 1) {
return;
}
overlay.attribute.dragging = true;
let offset = gesture.get_offset()[1];
const playerBox = overlay.attribute.list().at(-1);
self.attribute.dragging = true;
let offset = gesture.get_offset()[1];
const playerBox = self.attribute.list().at(-1);
if (!offset) {
return;
}
if (!offset) {
return;
}
// Slide right
if (offset >= 0) {
playerBox.setCss(`
// Slide right
if (offset >= 0) {
playerBox.setCss(`
margin-left: ${offset}px;
margin-right: -${offset}px;
${playerBox.attribute.bgStyle}
`);
}
}
// Slide left
else {
offset = Math.abs(offset);
playerBox.setCss(`
// Slide left
else {
offset = Math.abs(offset);
playerBox.setCss(`
margin-left: -${offset}px;
margin-right: ${offset}px;
${playerBox.attribute.bgStyle}
`);
}
},
'drag-update')
}
}, 'drag-update')
.hook(gesture,
/** @param {Overlay} overlay */
(overlay) => {
// Don't allow gesture when only one player
if (overlay.attribute.list().length <= 1) {
return;
}
.hook(gesture, () => {
// Don't allow gesture when only one player
if (self.attribute.list().length <= 1) {
return;
}
overlay.attribute.dragging = false;
const offset = gesture.get_offset()[1];
self.attribute.dragging = false;
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 (offset && Math.abs(offset) > MAX_OFFSET) {
// Disable inputs during animation
widget.sensitive = false;
// If crosses threshold after letting go, slide away
if (offset && Math.abs(offset) > MAX_OFFSET) {
// Disable inputs during animation
widget.sensitive = false;
// Slide away right
if (offset >= 0) {
playerBox.setCss(`
// Slide away right
if (offset >= 0) {
playerBox.setCss(`
${TRANSITION}
margin-left: ${OFFSCREEN}px;
margin-right: -${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
}
// Slide away left
else {
playerBox.setCss(`
// Slide away left
else {
playerBox.setCss(`
${TRANSITION}
margin-left: -${OFFSCREEN}px;
margin-right: ${OFFSCREEN}px;
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
playerBox.setCss(`${TRANSITION}
timeout(ANIM_DURATION, () => {
// 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}`);
}
},
'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)]);
// 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 * 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;
/** @param {Client} client */
const getFontSize = (client) => {
const valX = scale(client.size[0]) * 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;
};
/** @param {Client} client */
const IconStyle = (client) => `
min-width: ${scale(client.size[0])}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 wsName = String(client.workspace.name).replace('special:', '');
const wsId = client.workspace.id;
@ -31,42 +47,40 @@ const Client = (client, active, clients, box) => {
return Revealer({
transition: 'crossfade',
reveal_child: true,
setup: (rev) => {
rev.revealChild = true;
attribute: {
address: client.address,
to_destroy: false,
},
properties: [
['address', client.address],
['toDestroy', false],
],
child: WindowButton({
mainBox: box,
address: client.address,
onSecondaryClickRelease: () => {
on_secondary_click_release: () => {
Hyprland.sendMessage(`dispatch closewindow ${addr}`);
},
onPrimaryClickRelease: () => {
on_primary_click_release: () => {
if (wsId < 0) {
if (client.workspace.name === 'special') {
Hyprland.sendMessage('dispatch ' +
`movetoworkspacesilent special:${wsId},${addr}`)
.then(
.then(() => {
Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${wsId}`)
.then(
() => App.closeWindow('overview'),
).catch(print),
).catch(print);
.then(() => {
App.closeWindow('overview');
}).catch(print);
}).catch(print);
}
else {
Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${wsName}`).then(
() => App.closeWindow('overview'),
).catch(print);
`togglespecialworkspace ${wsName}`)
.then(() => {
App.closeWindow('overview');
}).catch(print);
}
}
else {
@ -86,14 +100,15 @@ const Client = (client, active, clients, box) => {
.catch(print);
}
Hyprland.sendMessage(`dispatch focuswindow ${addr}`).then(
() => App.closeWindow('overview'),
).catch(print);
Hyprland.sendMessage(`dispatch focuswindow ${addr}`)
.then(() => {
App.closeWindow('overview');
}).catch(print);
}
},
child: Icon({
className: `window ${active}`,
class_name: `window ${active ? 'active' : ''}`,
css: `${IconStyle(client)} font-size: 10px;`,
icon: client.class,
}),
@ -101,69 +116,79 @@ const Client = (client, active, clients, box) => {
});
};
/** @param {Box} box */
export const updateClients = (box) => {
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) => {
const fixed = workspace.getFixed();
const toRemove = fixed.get_children();
clients = clients.filter((client) => client.class);
clients.filter((client) => client.workspace.id === workspace._id)
.forEach((client) => {
let active = '';
box.attribute.workspaces.forEach(
/** @param {Revealer} workspace */
(workspace) => {
const fixed = workspace.attribute.get_fixed();
/** @type Array<Revealer> */
const toRemove = fixed.get_children();
if (client.address === Hyprland.active.client.address) {
active = 'active';
}
clients.filter((client) =>
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
client.at[1] -= 2920;
// TODO: fix multi monitor issue. this is just a temp fix
client.at[1] -= 2920;
// Special workspaces that haven't been opened yet
// return a size of 0. We need to set them to default
// values to show the workspace properly
if (client.size[0] === 0) {
client.size[0] = VARS.DEFAULT_SPECIAL.SIZE_X;
client.size[1] = VARS.DEFAULT_SPECIAL.SIZE_Y;
client.at[0] = VARS.DEFAULT_SPECIAL.POS_X;
client.at[1] = VARS.DEFAULT_SPECIAL.POS_Y;
}
// Special workspaces that haven't been opened yet
// return a size of 0. We need to set them to default
// values to show the workspace properly
if (client.size[0] === 0) {
client.size[0] = VARS.DEFAULT_SPECIAL.SIZE_X;
client.size[1] = VARS.DEFAULT_SPECIAL.SIZE_Y;
client.at[0] = VARS.DEFAULT_SPECIAL.POS_X;
client.at[1] = VARS.DEFAULT_SPECIAL.POS_Y;
}
const newClient = [
fixed.get_children()
.find((ch) => ch._address === client.address),
client.at[0] * VARS.SCALE,
client.at[1] * VARS.SCALE,
];
const newClient = [
fixed.get_children().find(
/** @param {typeof WindowButton} ch */
// @ts-expect-error
(ch) => ch.attribute.address === client.address,
),
client.at[0] * VARS.SCALE,
client.at[1] * VARS.SCALE,
];
// If it exists already
if (newClient[0]) {
toRemove.splice(toRemove.indexOf(newClient[0]), 1);
fixed.move(...newClient);
// If it exists already
if (newClient[0]) {
toRemove.splice(toRemove.indexOf(newClient[0]), 1);
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 {
newClient[0] = Client(client, active, clients, box);
fixed.put(...newClient);
ch.reveal_child = false;
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);
};

View file

@ -11,23 +11,39 @@ const DEFAULT_STYLE = `
border-radius: 10px;
`;
/**
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/widgets/revealer').default} Revealer
*/
export const Highlighter = () => Box({
vpack: 'start',
hpack: 'start',
className: 'workspace active',
class_name: 'workspace active',
css: DEFAULT_STYLE,
});
/**
* @param {Box} main
* @param {Box} highlighter
*/
export const updateCurrentWorkspace = (main, highlighter) => {
const currentId = Hyprland.active.workspace.id;
const row = Math.floor((currentId - 1) / VARS.WORKSPACE_PER_ROW);
// @ts-expect-error
const rowObject = main.children[0].children[row];
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 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)];
/**
* @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 alloc = widget.get_allocation();
const surface = new Cairo.ImageSurface(
@ -29,16 +37,19 @@ const createSurfaceFromWidget = (widget) => {
let hidden = 0;
export const WorkspaceDrop = (props) => EventBox({
/** @params {EventBoxProps} props */
export const WorkspaceDrop = ({ ...props }) => EventBox({
...props,
setup: (self) => {
self.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY);
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) {
id = self.get_parent()._name;
// @ts-expect-error
id = self.get_parent()?.attribute.name;
}
else if (id === -1) {
@ -56,11 +67,17 @@ export const WorkspaceDrop = (props) => EventBox({
},
});
/**
* @param {ButtonProps & {
* address: string
* mainBox: Box
* }} o
*/
export const WindowButton = ({
address,
mainBox,
...props
} = {}) => Button({
}) => Button({
...props,
cursor: 'pointer',
@ -78,11 +95,12 @@ export const WindowButton = ({
self.on('drag-begin', (_, context) => {
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.get_parent().destroy();
self.get_parent()?.destroy();
updateClients(mainBox);
});

View file

@ -9,21 +9,30 @@ import { Highlighter, updateCurrentWorkspace } from './current-workspace.js';
import { updateClients } from './clients.js';
// TODO: have a 'page' for each monitor, arrows on both sides to loop through
export const Overview = () => {
const highlighter = Highlighter();
const mainBox = Box({
// Do this for scss hierarchy
className: 'overview',
class_name: 'overview',
css: 'all: unset',
vertical: true,
vpack: 'center',
hpack: 'center',
attribute: {
workspaces: [],
update: () => {
getWorkspaces(mainBox);
updateWorkspaces(mainBox);
updateClients(mainBox);
updateCurrentWorkspace(mainBox, highlighter);
},
},
children: [
Box({
vertical: true,
@ -42,31 +51,24 @@ export const Overview = () => {
setup: (self) => {
self.hook(Hyprland, () => {
if (!App.getWindow('overview').visible) {
if (!App.getWindow('overview')?.visible) {
return;
}
self.update();
self?.attribute.update();
});
},
properties: [
['workspaces'],
],
});
mainBox.update = () => {
getWorkspaces(mainBox);
updateWorkspaces(mainBox);
updateClients(mainBox);
updateCurrentWorkspace(mainBox, highlighter);
};
const widget = Overlay({
overlays: [highlighter, mainBox],
attribute: {
get_child: () => mainBox,
},
child: Box({
className: 'overview',
class_name: 'overview',
css: `
min-height: ${mainBox.get_allocated_height()}px;
min-width: ${mainBox.get_allocated_width()}px;
@ -77,6 +79,7 @@ export const Overview = () => {
setup: (self) => {
self.on('get-child-position', (_, ch) => {
if (ch === mainBox) {
// @ts-expect-error
self.child.setCss(`
transition: min-height 0.2s ease, min-width 0.2s ease;
min-height: ${mainBox.get_allocated_height()}px;
@ -87,8 +90,6 @@ export const Overview = () => {
},
});
widget.getChild = () => mainBox;
return widget;
};
@ -99,7 +100,7 @@ export default () => {
close_on_unfocus: 'none',
onOpen: () => {
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;
`;
/**
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/widgets/revealer').default} Revealer
*/
/** @param {Box} box */
export const getWorkspaces = (box) => {
const children = [];
box.children.forEach((type) => {
type.children.forEach((row) => {
row.child.centerWidget.child.children.forEach((ch) => {
children.push(ch);
});
});
// @ts-expect-error
type.children.forEach(
/** @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 fixed = Fixed();
const workspace = Revealer({
transition: 'slide_right',
transitionDuration: 500,
transition_duration: 500,
attribute: {
id,
name,
get_fixed: () => fixed,
},
setup: (self) => {
if (normal) {
self.hook(Hyprland, () => {
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)
?.windows > 0 || active;
self.reveal_child =
(ws?.windows && ws.windows > 0) || active;
});
}
},
child: WorkspaceDrop({
child: Box({
className: 'workspace',
class_name: 'workspace',
css: normal ?
DEFAULT_STYLE :
@ -70,23 +97,23 @@ const Workspace = (id, name, normal = true) => {
}),
});
workspace._id = id;
workspace._name = name;
workspace.getFixed = () => fixed;
return workspace;
};
export const WorkspaceRow = (className, i) => {
/**
* @param {string} class_name
* @param {number} i
*/
export const WorkspaceRow = (class_name, i) => {
const addWorkspace = Workspace(
className === 'special' ? -1 : 1000,
className === 'special' ? 'special' : '',
class_name === 'special' ? -1 : 1000,
class_name === 'special' ? 'special' : '',
false,
);
return Revealer({
transition: 'slide_down',
hpack: className === 'special' ? '' : 'start',
hpack: class_name === 'special' ? 'fill' : 'start',
setup: (self) => {
self.hook(Hyprland, (rev) => {
@ -101,18 +128,18 @@ export const WorkspaceRow = (className, i) => {
return isInRow && (hasClients || isActive);
});
rev.revealChild = rowExists;
rev.reveal_child = rowExists;
});
},
child: CenterBox({
children: [null, EventBox({
center_widget: EventBox({
setup: (self) => {
self.hook(Hyprland, () => {
const maxId = (i + 1) * VARS.WORKSPACE_PER_ROW;
const activeId = Hyprland.active.workspace.id;
const isSpecial = className === 'special';
const isSpecial = class_name === 'special';
const nextRowExists = Hyprland.workspaces.some((ws) => {
const isInNextRow = ws.id > maxId;
const hasClients = ws.windows > 0;
@ -121,22 +148,26 @@ export const WorkspaceRow = (className, i) => {
return isInNextRow && (hasClients || isActive);
});
addWorkspace.revealChild = isSpecial || !nextRowExists;
addWorkspace.reveal_child = isSpecial || !nextRowExists;
});
},
child: Box({
className,
class_name,
children: [addWorkspace],
}),
}), null],
}),
}),
});
};
/** @param {Box} box */
export const updateWorkspaces = (box) => {
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) {
let type = 0;
@ -148,16 +179,19 @@ export const updateWorkspaces = (box) => {
}
else {
rowNo = Math.floor((ws.id - 1) / VARS.WORKSPACE_PER_ROW);
// @ts-expect-error
const wsQty = box.children[type].children.length;
if (rowNo >= wsQty) {
for (let i = wsQty; i <= rowNo; ++i) {
// @ts-expect-error
box.children[type].add(WorkspaceRow(
type ? 'special' : 'normal', i,
));
}
}
}
// @ts-expect-error
const row = box.children[type].children[rowNo]
.child.centerWidget.child;
@ -166,8 +200,15 @@ export const updateWorkspaces = (box) => {
});
// Make sure the order is correct
box._workspaces.forEach((workspace, i) => {
workspace.get_parent().reorder_child(workspace, i);
});
box.attribute.workspaces.forEach(
/**
* @param {Revealer} workspace
* @param {number} i
*/
(workspace, i) => {
// @ts-expect-error
workspace?.get_parent()?.reorder_child(workspace, i);
},
);
box.show_all();
};

View file

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

View file

@ -8,56 +8,62 @@ import CursorBox from '../misc/cursorbox.js';
const SCROLL_THRESH_H = 200;
const SCROLL_THRESH_N = 7;
/**
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('types/service/bluetooth').BluetoothDevice} BluetoothDevice
*/
const BluetoothDevice = (dev) => {
const widget = Box({
className: 'menu-item',
});
/** @param {BluetoothDevice} dev */
const BluetoothDevice = (dev) => Box({
class_name: 'menu-item',
const child = Box({
hexpand: true,
children: [
Icon({
binds: [['icon', dev, 'icon-name']],
}),
attribute: { dev },
Label({
binds: [['label', dev, 'name']],
}),
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,
children: [Revealer({
reveal_child: true,
transition: 'slide_down',
child: CursorBox({
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 = () => {
const DevList = new Map();
const topArrow = Revealer({
transition: 'slide_down',
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
className: 'scrolled-indicator',
class_name: 'scrolled-indicator',
size: 16,
css: '-gtk-icon-transform: rotate(180deg);',
}),
@ -65,15 +71,17 @@ export const BluetoothMenu = () => {
const bottomArrow = Revealer({
transition: 'slide_up',
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
className: 'scrolled-indicator',
class_name: 'scrolled-indicator',
size: 16,
}),
});
return Overlay({
pass_through: true,
overlays: [
Box({
vpack: 'start',
@ -91,7 +99,7 @@ export const BluetoothMenu = () => {
],
child: Box({
className: 'menu',
class_name: 'menu',
child: Scrollable({
hscroll: 'never',
@ -101,28 +109,36 @@ export const BluetoothMenu = () => {
self.on('edge-reached', (_, pos) => {
// Manage scroll indicators
if (pos === 2) {
topArrow.revealChild = false;
bottomArrow.revealChild = true;
topArrow.reveal_child = false;
bottomArrow.reveal_child = true;
}
else if (pos === 3) {
topArrow.revealChild = true;
bottomArrow.revealChild = false;
topArrow.reveal_child = true;
bottomArrow.reveal_child = false;
}
});
},
child: ListBox({
setup: (self) => {
self.set_sort_func((a, b) => {
return b.get_children()[0].dev.paired -
a.get_children()[0].dev.paired;
});
// @ts-expect-error
self.set_sort_func(
/**
* @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, () => {
// Get all devices
const Devices = [].concat(
Bluetooth.devices,
Bluetooth.connectedDevices,
const Devices = Bluetooth.devices.concat(
Bluetooth.connected_devices,
);
// Add missing devices
@ -130,6 +146,7 @@ export const BluetoothMenu = () => {
if (!DevList.has(dev) && dev.name) {
DevList.set(dev, BluetoothDevice(dev));
// @ts-expect-error
self.add(DevList.get(dev));
self.show_all();
}
@ -151,7 +168,7 @@ export const BluetoothMenu = () => {
}
else {
devWidget.children[0]
.revealChild = false;
.reveal_child = false;
devWidget.toDestroy = true;
}
}
@ -160,34 +177,48 @@ export const BluetoothMenu = () => {
// Start scrolling after a specified height
// is reached by the children
const height = Math.max(
self.get_parent().get_allocated_height(),
self.get_parent()?.get_allocated_height() || 0,
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) {
scroll.vscroll = 'always';
scroll.setCss(`min-height: ${height}px;`);
if (scroll) {
// @ts-expect-error
const n_child = self.get_children().length;
// Make bottom scroll indicator appear only
// when first getting overflowing children
if (!(bottomArrow.revealChild === true ||
topArrow.revealChild === true)) {
bottomArrow.revealChild = true;
if (n_child > SCROLL_THRESH_N) {
// @ts-expect-error
scroll.vscroll = 'always';
// @ts-expect-error
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
self.get_children().forEach((ch) => {
ch.changed();
});
// @ts-expect-error
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';
// @ts-expect-error
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
@ -17,16 +16,23 @@ import { BluetoothMenu } from './bluetooth.js';
const SPACING = 28;
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 {{
* command?: function
* secondary_command?: function
* onOpen?: function(Widget):void
* icon: any
* indicator?: any
* onOpen?: function(Revealer):void
* icon: string|BindTuple
* indicator?: BindTuple
* menu?: any
* }} o
*/
@ -41,10 +47,13 @@ const GridButton = ({
const Activated = Variable(false);
ButtonStates.push(Activated);
let iconWidget;
/** @type Label */
let indicatorWidget = Label();
// Allow setting icon dynamically or statically
if (typeof icon === 'string') {
icon = Icon({
iconWidget = Icon({
class_name: 'grid-label',
icon,
setup: (self) => {
@ -56,11 +65,12 @@ const GridButton = ({
},
});
}
else {
icon = Icon({
else if (Array.isArray(icon)) {
iconWidget = Icon({
class_name: 'grid-label',
setup: (self) => {
self
// @ts-expect-error
.hook(...icon)
.hook(Activated, () => {
self.setCss(`color: ${Activated.value ?
@ -72,12 +82,13 @@ const GridButton = ({
}
if (indicator) {
indicator = Label({
indicatorWidget = Label({
class_name: 'sub-label',
justification: 'left',
truncate: 'end',
maxWidthChars: 12,
max_width_chars: 12,
setup: (self) => {
// @ts-expect-error
self.hook(...indicator);
},
});
@ -87,7 +98,7 @@ const GridButton = ({
menu = Revealer({
transition: 'slide_down',
child: menu,
binds: [['revealChild', Activated, 'value']],
reveal_child: Activated.bind(),
});
}
@ -110,7 +121,7 @@ const GridButton = ({
}
},
child: icon,
child: iconWidget,
}),
CursorBox({
@ -131,10 +142,13 @@ const GridButton = ({
self.get_parent()
?.get_parent()?.get_parent()
?.get_parent()?.get_parent()
// @ts-expect-error
?.children[1];
const isSetup = rowMenu.get_children()
.find((ch) => ch === menu);
const isSetup = rowMenu.get_children().find(
/** @param {Box} ch */
(ch) => ch === menu,
);
if (!isSetup) {
rowMenu.add(menu);
@ -165,14 +179,14 @@ const GridButton = ({
],
}),
indicator,
indicatorWidget,
],
});
return widget;
};
const Row = ({ buttons } = {}) => {
const Row = ({ buttons }) => {
const widget = Box({
vertical: true,
@ -188,10 +202,13 @@ const Row = ({ buttons } = {}) => {
for (let i = 0; i < buttons.length; ++i) {
if (i === buttons.length - 1) {
// @ts-expect-error
widget.children[0].add(buttons[i]);
}
else {
// @ts-expect-error
widget.children[0].add(buttons[i]);
// @ts-expect-error
widget.children[0].add(Separator(SPACING));
}
}
@ -209,13 +226,17 @@ const FirstRow = () => Row({
// TODO: connection editor
},
icon: [Network, (icon) => {
icon.icon = Network.wifi?.iconName;
}],
icon: [Network,
/** @param {Icon} self */
(self) => {
self.icon = Network.wifi?.icon_name;
}],
indicator: [Network, (self) => {
self.label = Network.wifi?.ssid || Network.wired?.internet;
}],
indicator: [Network,
/** @param {Label} self */
(self) => {
self.label = Network.wifi?.ssid || Network.wired?.internet;
}],
menu: NetworkMenu(),
onOpen: () => Network.wifi.scan(),
@ -241,26 +262,30 @@ const FirstRow = () => Row({
// TODO: bluetooth connection editor
},
icon: [Bluetooth, (self) => {
if (Bluetooth.enabled) {
self.icon = Bluetooth.connectedDevices[0] ?
Bluetooth.connectedDevices[0].iconName :
'bluetooth-active-symbolic';
}
else {
self.icon = 'bluetooth-disabled-symbolic';
}
}],
icon: [Bluetooth,
/** @param {Icon} self */
(self) => {
if (Bluetooth.enabled) {
self.icon = Bluetooth.connected_devices[0] ?
Bluetooth.connected_devices[0].icon_name :
'bluetooth-active-symbolic';
}
else {
self.icon = 'bluetooth-disabled-symbolic';
}
}],
indicator: [Bluetooth, (self) => {
self.label = Bluetooth.connectedDevices[0] ?
`${Bluetooth.connectedDevices[0]}` :
'Disconnected';
}, 'notify::connected-devices'],
indicator: [Bluetooth,
/** @param {Label} self */
(self) => {
self.label = Bluetooth.connected_devices[0] ?
`${Bluetooth.connected_devices[0]}` :
'Disconnected';
}, 'notify::connected-devices'],
menu: BluetoothMenu(),
onOpen: (menu) => {
execAsync(`bluetoothctl scan ${menu.revealChild ?
execAsync(`bluetoothctl scan ${menu.reveal_child ?
'on' :
'off'}`).catch(print);
},
@ -282,9 +307,11 @@ const SecondRow = () => Row({
.catch(print);
},
icon: [SpeakerIcon, (self) => {
self.icon = SpeakerIcon.value;
}],
icon: [SpeakerIcon,
/** @param {Icon} self */
(self) => {
self.icon = SpeakerIcon.value;
}],
}),
GridButton({
@ -298,9 +325,11 @@ const SecondRow = () => Row({
.catch(print);
},
icon: [MicIcon, (self) => {
self.icon = MicIcon.value;
}],
icon: [MicIcon,
/** @param {Icon} self */
(self) => {
self.icon = MicIcon.value;
}],
}),
GridButton({

View file

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

View file

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

View file

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