parent
39f98b657b
commit
a0014161ce
108 changed files with 123 additions and 4164 deletions
modules/ags/config/ts
applauncher
bar
binto.tsfullscreen.ts
hovers
items
battery.tscal-opener.tsclock.tscurrent-window.tsheart.tsnotif-button.tsosk-toggle.tsquick-settings.tssystray.tstablet-toggle.tsworkspaces.ts
wim.tscorners
date.tsmedia-player
misc
notifications
on-screen-keyboard
osd
overview
powermenu.tsquick-settings
setup.ts
70
modules/ags/config/ts/applauncher/app-item.ts
Normal file
70
modules/ags/config/ts/applauncher/app-item.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
|
||||
import { Box, Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
// Types
|
||||
import { Application } from 'types/service/applications.ts';
|
||||
|
||||
|
||||
export default (app: Application) => {
|
||||
const icon = Icon({ size: 42 });
|
||||
const iconString = app.app.get_string('Icon');
|
||||
|
||||
if (app.icon_name) {
|
||||
if (lookUpIcon(app.icon_name)) {
|
||||
icon.icon = app.icon_name;
|
||||
}
|
||||
else if (iconString && iconString !== 'nix-snowflake') {
|
||||
icon.icon = iconString;
|
||||
}
|
||||
else {
|
||||
icon.icon = '';
|
||||
}
|
||||
}
|
||||
|
||||
const textBox = Box({
|
||||
vertical: true,
|
||||
vpack: 'start',
|
||||
|
||||
children: [
|
||||
Label({
|
||||
class_name: 'title',
|
||||
label: app.name,
|
||||
xalign: 0,
|
||||
truncate: 'end',
|
||||
}),
|
||||
|
||||
Label({
|
||||
class_name: 'description',
|
||||
label: app.description || '',
|
||||
wrap: true,
|
||||
xalign: 0,
|
||||
justification: 'left',
|
||||
}),
|
||||
|
||||
Label(),
|
||||
],
|
||||
});
|
||||
|
||||
return CursorBox({
|
||||
hexpand: true,
|
||||
class_name: 'app',
|
||||
|
||||
attribute: { app },
|
||||
|
||||
on_primary_click_release: () => {
|
||||
App.closeWindow('applauncher');
|
||||
app.launch();
|
||||
},
|
||||
|
||||
child: Box({
|
||||
children: [
|
||||
icon,
|
||||
textBox,
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
162
modules/ags/config/ts/applauncher/main.ts
Normal file
162
modules/ags/config/ts/applauncher/main.ts
Normal file
|
@ -0,0 +1,162 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
|
||||
// FIXME: find cleaner way to import this
|
||||
// @ts-expect-error
|
||||
import { Fzf } from 'file:///home/matt/.nix/modules/ags/config/node_modules/fzf/dist/fzf.es.js';
|
||||
|
||||
import { Box, Entry, Icon, Label, ListBox, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import PopupWindow from '../misc/popup.ts';
|
||||
import AppItem from './app-item.ts';
|
||||
|
||||
// Types
|
||||
import { Application } from 'types/service/applications.ts';
|
||||
type ListBoxRow = typeof imports.gi.Gtk.ListBoxRow;
|
||||
|
||||
|
||||
const Applauncher = (window_name = 'applauncher') => {
|
||||
let fzfResults: Array<any>;
|
||||
const list = ListBox({});
|
||||
|
||||
const setSort = (text: string) => {
|
||||
const fzf = new Fzf(Applications.list, {
|
||||
selector: (app: Application) => {
|
||||
return app.name + app.executable;
|
||||
},
|
||||
tiebreakers: [
|
||||
(a: Application, b: Application) => b.frequency - a.frequency,
|
||||
],
|
||||
});
|
||||
|
||||
fzfResults = fzf.find(text);
|
||||
list.set_sort_func(
|
||||
(a: ListBoxRow, b: ListBoxRow) => {
|
||||
const row1 = a.get_children()[0]?.attribute.app.name;
|
||||
const row2 = b.get_children()[0]?.attribute.app.name;
|
||||
|
||||
if (!row1 || !row2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fzfResults.indexOf(row1) -
|
||||
fzfResults.indexOf(row1) || 0;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const makeNewChildren = () => {
|
||||
const rows = list.get_children() as Array<ListBoxRow>;
|
||||
|
||||
rows.forEach((ch) => {
|
||||
ch.destroy();
|
||||
});
|
||||
|
||||
const children = Applications.query('')
|
||||
.flatMap((app) => AppItem(app));
|
||||
|
||||
children.forEach((ch) => {
|
||||
list.add(ch);
|
||||
});
|
||||
list.show_all();
|
||||
};
|
||||
|
||||
makeNewChildren();
|
||||
|
||||
const placeholder = Revealer({
|
||||
child: Label({
|
||||
label: " Couldn't find a match",
|
||||
class_name: 'placeholder',
|
||||
}),
|
||||
});
|
||||
|
||||
const entry = Entry({
|
||||
// Set some text so on-change works the first time
|
||||
text: '-',
|
||||
hexpand: true,
|
||||
|
||||
on_accept: ({ text }) => {
|
||||
const appList = Applications.query(text || '');
|
||||
|
||||
if (appList[0]) {
|
||||
App.closeWindow(window_name);
|
||||
appList[0].launch();
|
||||
}
|
||||
},
|
||||
|
||||
on_change: ({ text }) => {
|
||||
if (text === null) {
|
||||
return;
|
||||
}
|
||||
setSort(text);
|
||||
let visibleApps = 0;
|
||||
|
||||
const rows = list.get_children() as Array<ListBoxRow>;
|
||||
|
||||
rows.forEach((row) => {
|
||||
row.changed();
|
||||
|
||||
const item = row.get_children()[0];
|
||||
|
||||
if (item?.attribute.app) {
|
||||
const isMatching = fzfResults.find((r) => {
|
||||
return r.item.name === item.attribute.app.name;
|
||||
});
|
||||
|
||||
row.visible = isMatching;
|
||||
|
||||
if (isMatching) {
|
||||
++visibleApps;
|
||||
}
|
||||
}
|
||||
});
|
||||
placeholder.reveal_child = visibleApps <= 0;
|
||||
},
|
||||
});
|
||||
|
||||
return Box({
|
||||
class_name: 'applauncher',
|
||||
vertical: true,
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(App, (_, name, visible) => {
|
||||
if (name !== window_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.text = '';
|
||||
|
||||
if (visible) {
|
||||
entry.grab_focus();
|
||||
}
|
||||
else {
|
||||
makeNewChildren();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
children: [
|
||||
Box({
|
||||
class_name: 'header',
|
||||
children: [
|
||||
Icon('preferences-system-search-symbolic'),
|
||||
entry,
|
||||
],
|
||||
}),
|
||||
|
||||
Scrollable({
|
||||
hscroll: 'never',
|
||||
vscroll: 'automatic',
|
||||
child: Box({
|
||||
vertical: true,
|
||||
children: [list, placeholder],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: 'applauncher',
|
||||
focusable: true,
|
||||
child: Applauncher(),
|
||||
});
|
50
modules/ags/config/ts/bar/binto.ts
Normal file
50
modules/ags/config/ts/bar/binto.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Box, CenterBox, Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import SysTray from './items/systray.ts';
|
||||
import Separator from '../misc/separator.ts';
|
||||
import NotifButton from './items/notif-button.ts';
|
||||
import Clock from './items/clock.ts';
|
||||
|
||||
const PADDING = 20;
|
||||
|
||||
|
||||
export default () => Window({
|
||||
name: 'bar',
|
||||
layer: 'overlay',
|
||||
exclusivity: 'exclusive',
|
||||
anchor: ['bottom', 'left', 'right'],
|
||||
monitor: 1,
|
||||
|
||||
child: Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
CenterBox({
|
||||
class_name: 'bar',
|
||||
start_widget: Box({
|
||||
hpack: 'start',
|
||||
children: [
|
||||
Separator(PADDING),
|
||||
|
||||
SysTray(),
|
||||
],
|
||||
}),
|
||||
|
||||
center_widget: Box({
|
||||
children: [],
|
||||
}),
|
||||
|
||||
end_widget: Box({
|
||||
hpack: 'end',
|
||||
children: [
|
||||
NotifButton(),
|
||||
Separator(PADDING / 2),
|
||||
Clock(),
|
||||
|
||||
Separator(PADDING),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
Separator(PADDING, { vertical: true }),
|
||||
],
|
||||
}),
|
||||
});
|
87
modules/ags/config/ts/bar/fullscreen.ts
Normal file
87
modules/ags/config/ts/bar/fullscreen.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { Box, EventBox, Revealer, Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
// Types
|
||||
import { Variable as Var } from 'types/variable';
|
||||
import AgsBox from 'types/widgets/box';
|
||||
import { RevealerProps } from 'types/widgets/revealer';
|
||||
|
||||
const BarCloser = (variable: Var<boolean>) => Window({
|
||||
name: 'bar-closer',
|
||||
visible: false,
|
||||
anchor: ['top', 'bottom', 'left', 'right'],
|
||||
layer: 'overlay',
|
||||
|
||||
child: EventBox({
|
||||
on_hover: (self) => {
|
||||
variable.value = false;
|
||||
const parent = self.get_parent();
|
||||
|
||||
if (parent) {
|
||||
parent.visible = false;
|
||||
}
|
||||
},
|
||||
|
||||
child: Box({
|
||||
css: 'padding: 1px',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export default (props: RevealerProps) => {
|
||||
const Revealed = Variable(true);
|
||||
const barCloser = BarCloser(Revealed);
|
||||
|
||||
return Box({
|
||||
css: 'min-height: 1px',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
|
||||
setup: (self) => {
|
||||
const checkCurrentWsFsState = () => {
|
||||
const workspace = Hyprland.getWorkspace(
|
||||
Hyprland.active.workspace.id,
|
||||
);
|
||||
|
||||
if (workspace) {
|
||||
Revealed.value = !workspace['hasfullscreen'];
|
||||
}
|
||||
};
|
||||
|
||||
const checkGlobalFsState = (_: AgsBox, fullscreen: boolean) => {
|
||||
Revealed.value = !fullscreen;
|
||||
};
|
||||
|
||||
self
|
||||
.hook(Hyprland.active, checkCurrentWsFsState)
|
||||
.hook(Hyprland, checkGlobalFsState, 'fullscreen');
|
||||
},
|
||||
|
||||
children: [
|
||||
Revealer({
|
||||
...props,
|
||||
transition: 'slide_down',
|
||||
reveal_child: true,
|
||||
|
||||
}).bind('reveal_child', Revealed),
|
||||
|
||||
Revealer({
|
||||
reveal_child: Revealed.bind()
|
||||
.transform((v) => !v),
|
||||
|
||||
child: EventBox({
|
||||
on_hover: () => {
|
||||
barCloser.visible = true;
|
||||
Revealed.value = true;
|
||||
},
|
||||
|
||||
child: Box({
|
||||
css: 'min-height: 5px;',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
22
modules/ags/config/ts/bar/hovers/audio.ts
Normal file
22
modules/ags/config/ts/bar/hovers/audio.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
|
||||
|
||||
import { Label, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import { SpeakerIcon } from '../../misc/audio-icons.ts';
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'audio',
|
||||
|
||||
icon: Icon({
|
||||
icon: SpeakerIcon.bind(),
|
||||
}),
|
||||
|
||||
label: Label().hook(Audio, (self) => {
|
||||
if (Audio.speaker?.volume) {
|
||||
self.label =
|
||||
`${Math.round(Audio.speaker?.volume * 100)}%`;
|
||||
}
|
||||
}, 'speaker-changed'),
|
||||
});
|
27
modules/ags/config/ts/bar/hovers/bluetooth.ts
Normal file
27
modules/ags/config/ts/bar/hovers/bluetooth.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
|
||||
|
||||
import { Label, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'bluetooth',
|
||||
|
||||
icon: Icon().hook(Bluetooth, (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';
|
||||
}
|
||||
}),
|
||||
|
||||
label: Label().hook(Bluetooth, (self) => {
|
||||
self.label = Bluetooth.connected_devices[0] ?
|
||||
`${Bluetooth.connected_devices[0]}` :
|
||||
'Disconnected';
|
||||
}, 'notify::connected-devices'),
|
||||
});
|
17
modules/ags/config/ts/bar/hovers/brightness.ts
Normal file
17
modules/ags/config/ts/bar/hovers/brightness.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Brightness from '../../../services/brightness.ts';
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'brightness',
|
||||
|
||||
icon: Icon({
|
||||
icon: Brightness.bind('screenIcon'),
|
||||
}),
|
||||
|
||||
label: Label().hook(Brightness, (self) => {
|
||||
self.label = `${Math.round(Brightness.screen * 100)}%`;
|
||||
}, 'screen'),
|
||||
});
|
42
modules/ags/config/ts/bar/hovers/hover-revealer.ts
Normal file
42
modules/ags/config/ts/bar/hovers/hover-revealer.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Box, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Separator from '../../misc/separator.ts';
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
|
||||
|
||||
export default ({
|
||||
class_name,
|
||||
icon,
|
||||
label,
|
||||
spacing = 5,
|
||||
}) => {
|
||||
const hoverRevLabel = Revealer({
|
||||
transition: 'slide_right',
|
||||
|
||||
child: Box({
|
||||
|
||||
children: [
|
||||
Separator(spacing),
|
||||
|
||||
label,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const widget = CursorBox({
|
||||
on_hover: () => {
|
||||
hoverRevLabel.reveal_child = true;
|
||||
},
|
||||
|
||||
child: Box({
|
||||
class_name,
|
||||
|
||||
children: [
|
||||
icon,
|
||||
hoverRevLabel,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
66
modules/ags/config/ts/bar/hovers/keyboard-layout.ts
Normal file
66
modules/ags/config/ts/bar/hovers/keyboard-layout.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
const DEFAULT_KB = 'at-translated-set-2-keyboard';
|
||||
|
||||
import AgsLabel from 'types/widgets/label.ts';
|
||||
type Keyboard = {
|
||||
address: string;
|
||||
name: string;
|
||||
rules: string;
|
||||
model: string;
|
||||
layout: string;
|
||||
variant: string;
|
||||
options: string;
|
||||
active_keymap: string;
|
||||
main: boolean;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const getKbdLayout = (self: AgsLabel, _: string, layout: string) => {
|
||||
if (layout) {
|
||||
if (layout === 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortName = layout.match(/\(([A-Za-z]+)\)/);
|
||||
|
||||
self.label = shortName ? shortName[1] : layout;
|
||||
}
|
||||
else {
|
||||
// At launch, kb layout is undefined
|
||||
Hyprland.sendMessage('j/devices').then((obj) => {
|
||||
const keyboards = Array.from(JSON.parse(obj)
|
||||
.keyboards) as Array<Keyboard>;
|
||||
const kb = keyboards.find((v) => v.name === DEFAULT_KB);
|
||||
|
||||
if (kb) {
|
||||
layout = kb.active_keymap;
|
||||
|
||||
const shortName = layout
|
||||
.match(/\(([A-Za-z]+)\)/);
|
||||
|
||||
self.label = shortName ? shortName[1] : layout;
|
||||
}
|
||||
else {
|
||||
self.label = 'None';
|
||||
}
|
||||
}).catch(print);
|
||||
}
|
||||
};
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'keyboard',
|
||||
spacing: 4,
|
||||
|
||||
icon: Icon({
|
||||
icon: 'input-keyboard-symbolic',
|
||||
size: 20,
|
||||
}),
|
||||
label: Label({ css: 'font-size: 20px;' })
|
||||
.hook(Hyprland, getKbdLayout, 'keyboard-layout'),
|
||||
});
|
38
modules/ags/config/ts/bar/hovers/network.ts
Normal file
38
modules/ags/config/ts/bar/hovers/network.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
|
||||
|
||||
import { Label, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'network',
|
||||
|
||||
icon: Icon().hook(Network, (self) => {
|
||||
if (Network.wifi.internet === 'connected' ||
|
||||
Network.wifi.internet === 'connecting') {
|
||||
self.icon = Network.wifi.icon_name;
|
||||
}
|
||||
else if (Network.wired.internet === 'connected' ||
|
||||
Network.wired.internet === 'connecting') {
|
||||
self.icon = Network.wired.icon_name;
|
||||
}
|
||||
else {
|
||||
self.icon = Network.wifi.icon_name;
|
||||
}
|
||||
}),
|
||||
|
||||
label: Label().hook(Network, (self) => {
|
||||
if (Network.wifi.internet === 'connected' ||
|
||||
Network.wifi.internet === 'connecting') {
|
||||
self.label = Network.wifi.ssid || 'Unknown';
|
||||
}
|
||||
else if (Network.wired.internet === 'connected' ||
|
||||
Network.wired.internet === 'connecting') {
|
||||
self.label = 'Connected';
|
||||
}
|
||||
else {
|
||||
self.label = 'Disconnected';
|
||||
}
|
||||
}),
|
||||
});
|
31
modules/ags/config/ts/bar/items/battery.ts
Normal file
31
modules/ags/config/ts/bar/items/battery.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
|
||||
|
||||
import { Label, Icon, Box } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Separator from '../../misc/separator.ts';
|
||||
|
||||
const LOW_BATT = 20;
|
||||
const SPACING = 5;
|
||||
|
||||
|
||||
export default () => Box({
|
||||
class_name: 'toggle-off battery',
|
||||
|
||||
children: [
|
||||
Icon({
|
||||
class_name: 'battery-indicator',
|
||||
icon: Battery.bind('icon_name'),
|
||||
}).hook(Battery, (self) => {
|
||||
self.toggleClassName('charging', Battery.charging);
|
||||
self.toggleClassName('charged', Battery.charged);
|
||||
self.toggleClassName('low', Battery.percent < LOW_BATT);
|
||||
}),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
Label({
|
||||
label: Battery.bind('percent')
|
||||
.transform((v) => `${v}%`),
|
||||
}),
|
||||
],
|
||||
});
|
21
modules/ags/config/ts/bar/items/cal-opener.ts
Normal file
21
modules/ags/config/ts/bar/items/cal-opener.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
import Clock from './clock';
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
class_name: 'toggle-off',
|
||||
|
||||
on_primary_click_release: () => App.toggleWindow('calendar'),
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(App, (_, windowName, visible) => {
|
||||
if (windowName === 'calendar') {
|
||||
self.toggleClassName('toggle-on', visible);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
child: Clock(),
|
||||
});
|
12
modules/ags/config/ts/bar/items/clock.ts
Normal file
12
modules/ags/config/ts/bar/items/clock.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
|
||||
export default () => Label({ class_name: 'clock' })
|
||||
.poll(1000, (self) => {
|
||||
const time = imports.gi.GLib
|
||||
.DateTime.new_now_local();
|
||||
|
||||
self.label = time.format('%a. ') +
|
||||
time.get_day_of_month() +
|
||||
time.format(' %b. %H:%M');
|
||||
});
|
33
modules/ags/config/ts/bar/items/current-window.ts
Normal file
33
modules/ags/config/ts/bar/items/current-window.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Box, Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import Separator from '../../misc/separator.ts';
|
||||
|
||||
const SPACING = 8;
|
||||
|
||||
|
||||
export default () => Box({
|
||||
children: [
|
||||
Separator(SPACING / 2),
|
||||
|
||||
Icon({ size: 30 })
|
||||
.hook(Hyprland.active.client, (self) => {
|
||||
const app = Applications
|
||||
.query(Hyprland.active.client.class)[0];
|
||||
|
||||
if (app) {
|
||||
self.icon = app.icon_name || '';
|
||||
self.visible = Hyprland.active.client.title !== '';
|
||||
}
|
||||
}),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
Label({
|
||||
css: 'color: #CBA6F7; font-size: 18px',
|
||||
truncate: 'end',
|
||||
label: Hyprland.active.client.bind('title'),
|
||||
}),
|
||||
],
|
||||
});
|
28
modules/ags/config/ts/bar/items/heart.ts
Normal file
28
modules/ags/config/ts/bar/items/heart.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
import Persist from '../../misc/persist.ts';
|
||||
|
||||
const HeartState = Variable('');
|
||||
|
||||
Persist({
|
||||
name: 'heart',
|
||||
gobject: HeartState,
|
||||
prop: 'value',
|
||||
condition: '',
|
||||
whenFalse: '',
|
||||
});
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
on_primary_click_release: () => {
|
||||
HeartState.value = HeartState.value === '' ? '' : '';
|
||||
},
|
||||
|
||||
child: Label({
|
||||
class_name: 'heart-toggle',
|
||||
label: HeartState.bind(),
|
||||
}),
|
||||
});
|
62
modules/ags/config/ts/bar/items/notif-button.ts
Normal file
62
modules/ags/config/ts/bar/items/notif-button.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
|
||||
|
||||
import { Box, CenterBox, Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
import Separator from '../../misc/separator.ts';
|
||||
|
||||
const SPACING = 4;
|
||||
|
||||
// Types
|
||||
import AgsWindow from 'types/widgets/window.ts';
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
class_name: 'toggle-off',
|
||||
|
||||
on_primary_click_release: (self) => {
|
||||
(App.getWindow('notification-center') as AgsWindow)
|
||||
?.attribute.set_x_pos(
|
||||
self.get_allocation(),
|
||||
'right',
|
||||
);
|
||||
|
||||
App.toggleWindow('notification-center');
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(App, (_, windowName, visible) => {
|
||||
if (windowName === 'notification-center') {
|
||||
self.toggleClassName('toggle-on', visible);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
child: CenterBox({
|
||||
class_name: 'notif-panel',
|
||||
|
||||
center_widget: Box({
|
||||
children: [
|
||||
Icon().hook(Notifications, (self) => {
|
||||
if (Notifications.dnd) {
|
||||
self.icon = 'notification-disabled-symbolic';
|
||||
}
|
||||
else if (Notifications.notifications.length > 0) {
|
||||
self.icon = 'notification-new-symbolic';
|
||||
}
|
||||
else {
|
||||
self.icon = 'notification-symbolic';
|
||||
}
|
||||
}),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
Label({
|
||||
label: Notifications.bind('notifications')
|
||||
.transform((n) => String(n.length)),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
23
modules/ags/config/ts/bar/items/osk-toggle.ts
Normal file
23
modules/ags/config/ts/bar/items/osk-toggle.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Tablet from '../../../services/tablet.ts';
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
class_name: 'toggle-off',
|
||||
|
||||
on_primary_click_release: () => Tablet.toggleOsk(),
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(Tablet, () => {
|
||||
self.toggleClassName('toggle-on', Tablet.oskState);
|
||||
}, 'osk-toggled');
|
||||
},
|
||||
|
||||
child: Label({
|
||||
class_name: 'osk-toggle',
|
||||
xalign: 0.6,
|
||||
label: ' ',
|
||||
}),
|
||||
});
|
85
modules/ags/config/ts/bar/items/quick-settings.ts
Normal file
85
modules/ags/config/ts/bar/items/quick-settings.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
|
||||
import { Box, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Audio from '../hovers/audio.ts';
|
||||
import Bluetooth from '../hovers/bluetooth.ts';
|
||||
import Brightness from '../hovers/brightness.ts';
|
||||
import KeyboardLayout from '../hovers/keyboard-layout.ts';
|
||||
import Network from '../hovers/network.ts';
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
import Separator from '../../misc/separator.ts';
|
||||
|
||||
const SPACING = 4;
|
||||
|
||||
// Types
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsWindow from 'types/widgets/window.ts';
|
||||
|
||||
|
||||
export default () => {
|
||||
const hoverRevealers = [
|
||||
KeyboardLayout(),
|
||||
|
||||
Brightness(),
|
||||
|
||||
Audio(),
|
||||
|
||||
Bluetooth(),
|
||||
|
||||
Network(),
|
||||
];
|
||||
|
||||
return CursorBox({
|
||||
class_name: 'toggle-off',
|
||||
|
||||
on_primary_click_release: (self) => {
|
||||
(App.getWindow('quick-settings') as AgsWindow)
|
||||
?.attribute.set_x_pos(
|
||||
self.get_allocation(),
|
||||
'right',
|
||||
);
|
||||
|
||||
App.toggleWindow('quick-settings');
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(App, (_, windowName, visible) => {
|
||||
if (windowName === 'quick-settings') {
|
||||
self.toggleClassName('toggle-on', visible);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
attribute: {
|
||||
hoverRevealers: hoverRevealers.map((rev) => {
|
||||
const box = rev.child as AgsBox;
|
||||
|
||||
return box.children[1];
|
||||
}),
|
||||
},
|
||||
on_hover_lost: (self) => {
|
||||
self.attribute.hoverRevealers.forEach(
|
||||
(rev: AgsRevealer) => {
|
||||
rev.reveal_child = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
child: Box({
|
||||
class_name: 'quick-settings-toggle',
|
||||
vertical: false,
|
||||
children: [
|
||||
Separator(SPACING),
|
||||
|
||||
...hoverRevealers,
|
||||
|
||||
Label(' '),
|
||||
|
||||
Separator(SPACING),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
94
modules/ags/config/ts/bar/items/systray.ts
Normal file
94
modules/ags/config/ts/bar/items/systray.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
|
||||
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import { Box, Icon, MenuItem, MenuBar, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Separator from '../../misc/separator.ts';
|
||||
|
||||
const REVEAL_DURATION = 500;
|
||||
const SPACING = 12;
|
||||
|
||||
// Types
|
||||
import { TrayItem } from 'types/service/systemtray.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
type Menu = typeof imports.gi.Gtk.Menu;
|
||||
|
||||
|
||||
const SysTrayItem = (item: TrayItem) => {
|
||||
if (item.id === 'spotify-client') {
|
||||
return;
|
||||
}
|
||||
|
||||
return MenuItem({
|
||||
submenu: <Menu> item.menu,
|
||||
tooltip_markup: item.bind('tooltip_markup'),
|
||||
|
||||
child: Revealer({
|
||||
transition: 'slide_right',
|
||||
transition_duration: REVEAL_DURATION,
|
||||
|
||||
child: Icon({ size: 24 }).bind('icon', item, 'icon'),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const SysTray = () => MenuBar({
|
||||
attribute: { items: new Map() },
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(SystemTray, (_, id) => {
|
||||
const item = SystemTray.getItem(id);
|
||||
|
||||
if (self.attribute.items.has(id) || !item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const w = SysTrayItem(item);
|
||||
|
||||
// Early return if item is in blocklist
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.items.set(id, w);
|
||||
self.child = w;
|
||||
self.show_all();
|
||||
|
||||
(<AgsRevealer> w.child).reveal_child = true;
|
||||
}, 'added')
|
||||
|
||||
.hook(SystemTray, (_, id) => {
|
||||
if (!self.attribute.items.has(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.items.get(id).child.reveal_child = false;
|
||||
timeout(REVEAL_DURATION, () => {
|
||||
self.attribute.items.get(id).destroy();
|
||||
self.attribute.items.delete(id);
|
||||
});
|
||||
}, 'removed');
|
||||
},
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const systray = SysTray();
|
||||
|
||||
return Revealer({
|
||||
transition: 'slide_right',
|
||||
|
||||
child: Box({
|
||||
children: [
|
||||
Box({
|
||||
class_name: 'sys-tray',
|
||||
children: [systray],
|
||||
}),
|
||||
|
||||
Separator(SPACING),
|
||||
],
|
||||
}),
|
||||
}).hook(SystemTray, (self) => {
|
||||
self.reveal_child = systray.get_children().length > 0;
|
||||
});
|
||||
};
|
24
modules/ags/config/ts/bar/items/tablet-toggle.ts
Normal file
24
modules/ags/config/ts/bar/items/tablet-toggle.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Box, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Tablet from '../../../services/tablet.ts';
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
class_name: 'toggle-off',
|
||||
|
||||
on_primary_click_release: () => Tablet.toggleMode(),
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(Tablet, () => {
|
||||
self.toggleClassName('toggle-on', Tablet.tabletMode);
|
||||
}, 'mode-toggled');
|
||||
},
|
||||
|
||||
child: Box({
|
||||
class_name: 'tablet-toggle',
|
||||
vertical: false,
|
||||
children: [Label(' ')],
|
||||
}),
|
||||
|
||||
});
|
172
modules/ags/config/ts/bar/items/workspaces.ts
Normal file
172
modules/ags/config/ts/bar/items/workspaces.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import { Box, Overlay, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
|
||||
const URGENT_DURATION = 1000;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import AgsOverlay from 'types/widgets/overlay.ts';
|
||||
import AgsEventBox from 'types/widgets/eventbox.ts';
|
||||
|
||||
|
||||
const Workspace = ({ id }: { id: number }) => {
|
||||
return Revealer({
|
||||
transition: 'slide_right',
|
||||
attribute: { id },
|
||||
|
||||
child: CursorBox({
|
||||
tooltip_text: `${id}`,
|
||||
|
||||
on_primary_click_release: () => {
|
||||
Hyprland.sendMessage(`dispatch workspace ${id}`);
|
||||
},
|
||||
|
||||
child: Box({
|
||||
vpack: 'center',
|
||||
class_name: 'button',
|
||||
|
||||
setup: (self) => {
|
||||
const update = (_: AgsBox, addr: string | undefined) => {
|
||||
const workspace = Hyprland.getWorkspace(id);
|
||||
const occupied = workspace && workspace.windows > 0;
|
||||
|
||||
self.toggleClassName('occupied', occupied);
|
||||
|
||||
if (!addr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deal with urgent windows
|
||||
const client = Hyprland.getClient(addr);
|
||||
const isThisUrgent = client &&
|
||||
client.workspace.id === id;
|
||||
|
||||
if (isThisUrgent) {
|
||||
self.toggleClassName('urgent', true);
|
||||
|
||||
// Only show for a sec when urgent is current workspace
|
||||
if (Hyprland.active.workspace.id === id) {
|
||||
timeout(URGENT_DURATION, () => {
|
||||
self.toggleClassName('urgent', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self
|
||||
.hook(Hyprland, update)
|
||||
|
||||
// Deal with urgent windows
|
||||
.hook(Hyprland, update, 'urgent-window')
|
||||
|
||||
.hook(Hyprland.active.workspace, () => {
|
||||
if (Hyprland.active.workspace.id === id) {
|
||||
self.toggleClassName('urgent', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const L_PADDING = 16;
|
||||
const WS_WIDTH = 30;
|
||||
|
||||
const updateHighlight = (self: AgsBox) => {
|
||||
const currentId = Hyprland.active.workspace.id;
|
||||
|
||||
const indicators = (((self.get_parent() as AgsOverlay)
|
||||
.child as AgsEventBox)
|
||||
.child as AgsBox)
|
||||
.children as Array<AgsRevealer>;
|
||||
|
||||
const currentIndex = indicators
|
||||
.findIndex((w) => w.attribute.id === currentId);
|
||||
|
||||
if (currentIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.setCss(`margin-left: ${L_PADDING + (currentIndex * WS_WIDTH)}px`);
|
||||
};
|
||||
|
||||
const highlight = Box({
|
||||
vpack: 'center',
|
||||
hpack: 'start',
|
||||
class_name: 'button active',
|
||||
|
||||
}).hook(Hyprland.active.workspace, updateHighlight);
|
||||
|
||||
const widget = Overlay({
|
||||
pass_through: true,
|
||||
overlays: [highlight],
|
||||
child: CursorBox({
|
||||
child: Box({
|
||||
class_name: 'workspaces',
|
||||
|
||||
attribute: { workspaces: [] },
|
||||
|
||||
setup: (self) => {
|
||||
const workspaces = (): Array<AgsRevealer> =>
|
||||
self.attribute.workspaces;
|
||||
|
||||
const refresh = () => {
|
||||
(self.children as Array<AgsRevealer>).forEach((rev) => {
|
||||
rev.reveal_child = false;
|
||||
});
|
||||
|
||||
workspaces().forEach((ws) => {
|
||||
ws.reveal_child = true;
|
||||
});
|
||||
};
|
||||
|
||||
const updateWorkspaces = () => {
|
||||
Hyprland.workspaces.forEach((ws) => {
|
||||
const currentWs = (self.children as Array<AgsBox>)
|
||||
.find((ch) => ch.attribute.id === ws.id);
|
||||
|
||||
if (!currentWs && ws.id > 0) {
|
||||
self.add(Workspace({ id: ws.id }));
|
||||
}
|
||||
});
|
||||
self.show_all();
|
||||
|
||||
// Make sure the order is correct
|
||||
workspaces().forEach((workspace, i) => {
|
||||
(<AgsBox> workspace.get_parent()).reorder_child(
|
||||
workspace,
|
||||
i,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
self.hook(Hyprland, () => {
|
||||
self.attribute.workspaces =
|
||||
(self.children as Array<AgsBox>).filter((ch) => {
|
||||
return Hyprland.workspaces.find((ws) => {
|
||||
return ws.id === ch.attribute.id;
|
||||
});
|
||||
}).sort((a, b) => a.attribute.id - b.attribute.id);
|
||||
|
||||
updateWorkspaces();
|
||||
refresh();
|
||||
|
||||
// Make sure the highlight doesn't go too far
|
||||
const TEMP_TIMEOUT = 10;
|
||||
|
||||
timeout(TEMP_TIMEOUT, () => updateHighlight(highlight));
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
85
modules/ags/config/ts/bar/wim.ts
Normal file
85
modules/ags/config/ts/bar/wim.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { Window, CenterBox, Box } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
|
||||
import Battery from './items/battery.ts';
|
||||
import Clock from './items/cal-opener.ts';
|
||||
import CurrentWindow from './items/current-window.ts';
|
||||
import Heart from './items/heart.ts';
|
||||
import NotifButton from './items/notif-button.ts';
|
||||
import OskToggle from './items/osk-toggle.ts';
|
||||
import QsToggle from './items/quick-settings.ts';
|
||||
import SysTray from './items/systray.ts';
|
||||
import TabletToggle from './items/tablet-toggle.ts';
|
||||
import Workspaces from './items/workspaces.ts';
|
||||
|
||||
import BarReveal from './fullscreen.ts';
|
||||
|
||||
const SPACING = 12;
|
||||
|
||||
|
||||
export default () => Window({
|
||||
name: 'bar',
|
||||
layer: 'overlay',
|
||||
anchor: ['top', 'left', 'right'],
|
||||
margins: [-1, 0, 0, 0],
|
||||
exclusivity: 'exclusive',
|
||||
child: BarReveal({
|
||||
child: CenterBox({
|
||||
css: 'margin: 6px 5px 5px 5px',
|
||||
class_name: 'bar',
|
||||
|
||||
start_widget: Box({
|
||||
hpack: 'start',
|
||||
children: [
|
||||
|
||||
OskToggle(),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
TabletToggle(),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
SysTray(),
|
||||
|
||||
Workspaces(),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
CurrentWindow(),
|
||||
|
||||
],
|
||||
}),
|
||||
|
||||
center_widget: Box({
|
||||
children: [
|
||||
Separator(SPACING),
|
||||
|
||||
Clock(),
|
||||
|
||||
Separator(SPACING),
|
||||
],
|
||||
}),
|
||||
|
||||
end_widget: Box({
|
||||
hpack: 'end',
|
||||
children: [
|
||||
Heart(),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
Battery(),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
NotifButton(),
|
||||
|
||||
Separator(SPACING),
|
||||
|
||||
QsToggle(),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
52
modules/ags/config/ts/corners/main.ts
Normal file
52
modules/ags/config/ts/corners/main.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import RoundedCorner from './screen-corners.ts';
|
||||
|
||||
|
||||
const TopLeft = () => Window({
|
||||
name: 'cornertl',
|
||||
layer: 'overlay',
|
||||
exclusivity: 'ignore',
|
||||
anchor: ['top', 'left'],
|
||||
visible: true,
|
||||
click_through: true,
|
||||
child: RoundedCorner('topleft'),
|
||||
});
|
||||
|
||||
const TopRight = () => Window({
|
||||
name: 'cornertr',
|
||||
layer: 'overlay',
|
||||
exclusivity: 'ignore',
|
||||
anchor: ['top', 'right'],
|
||||
visible: true,
|
||||
click_through: true,
|
||||
child: RoundedCorner('topright'),
|
||||
});
|
||||
|
||||
const BottomLeft = () => Window({
|
||||
name: 'cornerbl',
|
||||
layer: 'overlay',
|
||||
exclusivity: 'ignore',
|
||||
anchor: ['bottom', 'left'],
|
||||
visible: true,
|
||||
click_through: true,
|
||||
child: RoundedCorner('bottomleft'),
|
||||
});
|
||||
|
||||
const BottomRight = () => Window({
|
||||
name: 'cornerbr',
|
||||
layer: 'overlay',
|
||||
exclusivity: 'ignore',
|
||||
anchor: ['bottom', 'right'],
|
||||
visible: true,
|
||||
click_through: true,
|
||||
child: RoundedCorner('bottomright'),
|
||||
});
|
||||
|
||||
|
||||
export default () => [
|
||||
TopLeft(),
|
||||
TopRight(),
|
||||
BottomLeft(),
|
||||
BottomRight(),
|
||||
];
|
79
modules/ags/config/ts/corners/screen-corners.ts
Normal file
79
modules/ags/config/ts/corners/screen-corners.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { Box, DrawingArea } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Gtk from 'gi://Gtk';
|
||||
|
||||
export default (
|
||||
place = 'top left',
|
||||
css = 'background-color: black;',
|
||||
) => Box({
|
||||
hpack: place.includes('left') ? 'start' : 'end',
|
||||
vpack: place.includes('top') ? 'start' : 'end',
|
||||
css: `
|
||||
padding: 1px; margin:
|
||||
${place.includes('top') ? '-1px' : '0'}
|
||||
${place.includes('right') ? '-1px' : '0'}
|
||||
${place.includes('bottom') ? '-1px' : '0'}
|
||||
${place.includes('left') ? '-1px' : '0'};
|
||||
`,
|
||||
child: DrawingArea({
|
||||
css: `
|
||||
border-radius: 18px;
|
||||
border-width: 0.068rem;
|
||||
${css}
|
||||
`,
|
||||
setup: (widget) => {
|
||||
let r = widget.get_style_context()
|
||||
.get_property('border-radius', Gtk.StateFlags.NORMAL);
|
||||
|
||||
widget.set_size_request(r, r);
|
||||
widget.connect('draw', (_, cr) => {
|
||||
const c = widget.get_style_context()
|
||||
.get_property('background-color', Gtk.StateFlags.NORMAL);
|
||||
|
||||
r = widget.get_style_context()
|
||||
.get_property('border-radius', Gtk.StateFlags.NORMAL);
|
||||
|
||||
const borderColor = widget.get_style_context()
|
||||
.get_property('color', Gtk.StateFlags.NORMAL);
|
||||
|
||||
// You're going to write border-width: something anyway
|
||||
const borderWidth = widget.get_style_context()
|
||||
.get_border(Gtk.StateFlags.NORMAL).left;
|
||||
|
||||
widget.set_size_request(r, r);
|
||||
|
||||
switch (place) {
|
||||
case 'topleft':
|
||||
cr.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
|
||||
cr.lineTo(0, 0);
|
||||
break;
|
||||
|
||||
case 'topright':
|
||||
cr.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
|
||||
cr.lineTo(r, 0);
|
||||
break;
|
||||
|
||||
case 'bottomleft':
|
||||
cr.arc(r, 0, r, Math.PI / 2, Math.PI);
|
||||
cr.lineTo(0, r);
|
||||
break;
|
||||
|
||||
case 'bottomright':
|
||||
cr.arc(0, 0, r, 0, Math.PI / 2);
|
||||
cr.lineTo(r, r);
|
||||
break;
|
||||
}
|
||||
|
||||
cr.closePath();
|
||||
cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
|
||||
cr.fill();
|
||||
cr.setLineWidth(borderWidth);
|
||||
cr.setSourceRGBA(borderColor.red,
|
||||
borderColor.green,
|
||||
borderColor.blue,
|
||||
borderColor.alpha);
|
||||
cr.stroke();
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
98
modules/ags/config/ts/date.ts
Normal file
98
modules/ags/config/ts/date.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { Box, Calendar, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
const { DateTime } = imports.gi.GLib;
|
||||
|
||||
import PopupWindow from './misc/popup.ts';
|
||||
|
||||
|
||||
const Divider = () => Box({
|
||||
class_name: 'divider',
|
||||
vertical: true,
|
||||
});
|
||||
|
||||
const Time = () => Box({
|
||||
class_name: 'timebox',
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
Box({
|
||||
class_name: 'time-container',
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
|
||||
children: [
|
||||
Label({
|
||||
class_name: 'content',
|
||||
label: 'hour',
|
||||
setup: (self) => {
|
||||
self.poll(1000, () => {
|
||||
self.label = DateTime.new_now_local().format('%H');
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
Divider(),
|
||||
|
||||
Label({
|
||||
class_name: 'content',
|
||||
label: 'minute',
|
||||
setup: (self) => {
|
||||
self.poll(1000, () => {
|
||||
self.label = DateTime.new_now_local().format('%M');
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
],
|
||||
}),
|
||||
|
||||
Box({
|
||||
class_name: 'date-container',
|
||||
hpack: 'center',
|
||||
|
||||
child: Label({
|
||||
css: 'font-size: 20px',
|
||||
label: 'complete date',
|
||||
|
||||
setup: (self) => {
|
||||
self.poll(1000, () => {
|
||||
const time = DateTime.new_now_local();
|
||||
|
||||
self.label = time.format('%A, %B ') +
|
||||
time.get_day_of_month() +
|
||||
time.format(', %Y');
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
const CalendarWidget = () => Box({
|
||||
class_name: 'cal-box',
|
||||
|
||||
child: Calendar({
|
||||
show_day_names: true,
|
||||
show_heading: true,
|
||||
class_name: 'cal',
|
||||
}),
|
||||
});
|
||||
|
||||
const TOP_MARGIN = 6;
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: 'calendar',
|
||||
anchor: ['top'],
|
||||
margins: [TOP_MARGIN, 0, 0, 0],
|
||||
|
||||
child: Box({
|
||||
class_name: 'date',
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
Time(),
|
||||
CalendarWidget(),
|
||||
],
|
||||
}),
|
||||
});
|
171
modules/ags/config/ts/media-player/gesture.ts
Normal file
171
modules/ags/config/ts/media-player/gesture.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import { Box, EventBox, Overlay } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
const MAX_OFFSET = 200;
|
||||
const OFFSCREEN = 500;
|
||||
const ANIM_DURATION = 500;
|
||||
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
|
||||
opacity ${ANIM_DURATION}ms ease;`;
|
||||
|
||||
// Types
|
||||
import AgsOverlay from 'types/widgets/overlay';
|
||||
import OverlayProps from 'types/widgets/overlay';
|
||||
import AgsBox from 'types/widgets/box';
|
||||
import AgsCenterBox from 'types/widgets/centerbox';
|
||||
import { Connectable } from 'types/widgets/widget';
|
||||
type Gesture = {
|
||||
attribute?: Object
|
||||
setup?(self: Connectable<AgsOverlay> & AgsOverlay): void
|
||||
props?: OverlayProps
|
||||
};
|
||||
|
||||
|
||||
export default ({
|
||||
attribute = {},
|
||||
setup = () => {/**/},
|
||||
...props
|
||||
}: Gesture) => {
|
||||
const widget = EventBox();
|
||||
const gesture = Gtk.GestureDrag.new(widget);
|
||||
|
||||
// Have empty PlayerBox to define the size of the widget
|
||||
const emptyPlayer = Box({
|
||||
class_name: 'player',
|
||||
attribute: { empty: true },
|
||||
});
|
||||
|
||||
const content = Overlay({
|
||||
...props,
|
||||
attribute: {
|
||||
...attribute,
|
||||
dragging: false,
|
||||
|
||||
includesWidget: (playerW: AgsOverlay) => {
|
||||
return content.overlays.find((w) => w === playerW);
|
||||
},
|
||||
|
||||
showTopOnly: () => content.overlays.forEach((over) => {
|
||||
over.visible = over === content.overlays.at(-1);
|
||||
}),
|
||||
|
||||
moveToTop: (player: AgsCenterBox) => {
|
||||
player.visible = true;
|
||||
content.reorder_overlay(player, -1);
|
||||
timeout(ANIM_DURATION, () => {
|
||||
content.attribute.showTopOnly();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
child: emptyPlayer,
|
||||
|
||||
setup: (self) => {
|
||||
setup(self);
|
||||
|
||||
self
|
||||
.hook(gesture, (_, realGesture) => {
|
||||
if (realGesture) {
|
||||
self.overlays.forEach((over) => {
|
||||
over.visible = true;
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.attribute.showTopOnly();
|
||||
}
|
||||
|
||||
// Don't allow gesture when only one player
|
||||
if (self.overlays.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.dragging = true;
|
||||
let offset = gesture.get_offset()[1];
|
||||
const playerBox = self.overlays.at(-1) as AgsBox;
|
||||
|
||||
if (!offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(`
|
||||
margin-left: -${offset}px;
|
||||
margin-right: ${offset}px;
|
||||
${playerBox.attribute.bgStyle}
|
||||
`);
|
||||
}
|
||||
}, 'drag-update')
|
||||
|
||||
|
||||
.hook(gesture, () => {
|
||||
// Don't allow gesture when only one player
|
||||
if (self.overlays.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.dragging = false;
|
||||
const offset = gesture.get_offset()[1];
|
||||
|
||||
const playerBox = self.overlays.at(-1) as AgsBox;
|
||||
|
||||
// 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(`
|
||||
${TRANSITION}
|
||||
margin-left: ${OFFSCREEN}px;
|
||||
margin-right: -${OFFSCREEN}px;
|
||||
opacity: 0.7; ${playerBox.attribute.bgStyle}
|
||||
`);
|
||||
}
|
||||
|
||||
// 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
|
||||
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');
|
||||
},
|
||||
});
|
||||
|
||||
widget.add(content);
|
||||
|
||||
return widget;
|
||||
};
|
462
modules/ags/config/ts/media-player/mpris.ts
Normal file
462
modules/ags/config/ts/media-player/mpris.ts
Normal file
|
@ -0,0 +1,462 @@
|
|||
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
|
||||
|
||||
import { Button, Icon, Label, Stack, Slider, CenterBox, Box } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { execAsync, lookUpIcon, readFileAsync } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
const icons = {
|
||||
mpris: {
|
||||
fallback: 'audio-x-generic-symbolic',
|
||||
shuffle: {
|
||||
enabled: '',
|
||||
disabled: '',
|
||||
},
|
||||
loop: {
|
||||
none: '',
|
||||
track: '',
|
||||
playlist: '',
|
||||
},
|
||||
playing: ' ',
|
||||
paused: ' ',
|
||||
stopped: ' ',
|
||||
prev: '',
|
||||
next: '',
|
||||
},
|
||||
};
|
||||
|
||||
// Types
|
||||
import { MprisPlayer } from 'types/service/mpris.ts';
|
||||
import { Variable as Var } from 'types/variable';
|
||||
import AgsOverlay from 'types/widgets/overlay.ts';
|
||||
import AgsCenterBox, { CenterBoxProps } from 'types/widgets/centerbox.ts';
|
||||
import AgsLabel from 'types/widgets/label.ts';
|
||||
import AgsIcon from 'types/widgets/icon.ts';
|
||||
import AgsStack from 'types/widgets/stack.ts';
|
||||
|
||||
|
||||
export const CoverArt = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
props: CenterBoxProps,
|
||||
) => CenterBox({
|
||||
...props,
|
||||
vertical: true,
|
||||
|
||||
attribute: {
|
||||
bgStyle: '',
|
||||
player,
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
// Give temp cover art
|
||||
readFileAsync(player.cover_path).catch(() => {
|
||||
if (!colors.value && !player.track_cover_url) {
|
||||
colors.value = {
|
||||
imageAccent: '#6b4fa2',
|
||||
buttonAccent: '#ecdcff',
|
||||
buttonText: '#25005a',
|
||||
hoverAccent: '#d4baff',
|
||||
};
|
||||
|
||||
self.attribute.bgStyle = `
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 0, 0, 0.4) 30%,
|
||||
${colors.value.imageAccent}),
|
||||
rgb(0, 0, 0);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
self.setCss(self.attribute.bgStyle);
|
||||
}
|
||||
});
|
||||
|
||||
self.hook(player, () => {
|
||||
execAsync(['bash', '-c', `[[ -f "${player.cover_path}" ]] &&
|
||||
coloryou "${player.cover_path}" | grep -v Warning`])
|
||||
.then((out) => {
|
||||
if (!Mpris.players.find((p) => player === p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
colors.value = JSON.parse(out);
|
||||
|
||||
self.attribute.bgStyle = `
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 0, 0, 0.4) 30%,
|
||||
${colors.value.imageAccent}),
|
||||
url("${player.cover_path}");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
|
||||
if (!(self.get_parent() as AgsCenterBox)
|
||||
.attribute.dragging) {
|
||||
self.setCss(self.attribute.bgStyle);
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (err !== '') {
|
||||
print(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const TitleLabel = (player: MprisPlayer) => Label({
|
||||
xalign: 0,
|
||||
max_width_chars: 40,
|
||||
truncate: 'end',
|
||||
justification: 'left',
|
||||
class_name: 'title',
|
||||
label: player.bind('track_title'),
|
||||
});
|
||||
|
||||
export const ArtistLabel = (player: MprisPlayer) => Label({
|
||||
xalign: 0,
|
||||
max_width_chars: 40,
|
||||
truncate: 'end',
|
||||
justification: 'left',
|
||||
class_name: 'artist',
|
||||
label: player.bind('track_artists')
|
||||
.transform((a) => a.join(', ') || ''),
|
||||
});
|
||||
|
||||
|
||||
export const PlayerIcon = (player: MprisPlayer, overlay: AgsOverlay) => {
|
||||
const playerIcon = (
|
||||
p: MprisPlayer,
|
||||
widget?: AgsOverlay,
|
||||
over?: AgsOverlay,
|
||||
) => CursorBox({
|
||||
tooltip_text: p.identity || '',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
widget?.attribute.moveToTop(over);
|
||||
},
|
||||
|
||||
child: Icon({
|
||||
class_name: widget ? 'position-indicator' : 'player-icon',
|
||||
size: widget ? 0 : ICON_SIZE,
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(p, () => {
|
||||
self.icon = lookUpIcon(p.entry) ?
|
||||
p.entry :
|
||||
icons.mpris.fallback;
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return Box().hook(Mpris, (self) => {
|
||||
const grandPa = self.get_parent()?.get_parent();
|
||||
|
||||
if (!grandPa) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thisIndex = overlay.overlays
|
||||
.indexOf(grandPa);
|
||||
|
||||
self.children = (overlay.overlays as Array<AgsOverlay>)
|
||||
.map((over, i) => {
|
||||
self.children.push(Separator(2));
|
||||
|
||||
return i === thisIndex ?
|
||||
playerIcon(player) :
|
||||
playerIcon(over.attribute.player, overlay, over);
|
||||
})
|
||||
.reverse();
|
||||
});
|
||||
};
|
||||
|
||||
const { Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
export const PositionSlider = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => Slider({
|
||||
class_name: 'position-slider',
|
||||
vpack: 'center',
|
||||
hexpand: true,
|
||||
draw_value: false,
|
||||
|
||||
on_change: ({ value }) => {
|
||||
player.position = player.length * value;
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
const update = () => {
|
||||
if (!self.dragging) {
|
||||
self.visible = player.length > 0;
|
||||
if (player.length > 0) {
|
||||
self.value = player.position / player.length;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self
|
||||
.poll(1000, () => update())
|
||||
.hook(player, () => update(), 'position')
|
||||
.hook(colors, () => {
|
||||
if (colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
self.setCss(`
|
||||
highlight { background-color: ${c.buttonAccent}; }
|
||||
slider { background-color: ${c.buttonAccent}; }
|
||||
slider:hover { background-color: ${c.hoverAccent}; }
|
||||
trough { background-color: ${c.buttonText}; }
|
||||
`);
|
||||
}
|
||||
})
|
||||
|
||||
// OnClick
|
||||
.on('button-press-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grabbing',
|
||||
));
|
||||
})
|
||||
|
||||
// OnRelease
|
||||
.on('button-release-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
type PlayerButtonType = {
|
||||
player: MprisPlayer
|
||||
colors: Var<any>
|
||||
items: Array<[name: string, widget: AgsLabel | AgsIcon]>
|
||||
onClick: string
|
||||
prop: string
|
||||
};
|
||||
const PlayerButton = ({
|
||||
player,
|
||||
colors,
|
||||
items,
|
||||
onClick,
|
||||
prop,
|
||||
}: PlayerButtonType) => CursorBox({
|
||||
child: Button({
|
||||
attribute: { hovered: false },
|
||||
child: Stack({ items }),
|
||||
|
||||
on_primary_click_release: () => player[onClick](),
|
||||
|
||||
on_hover: (self) => {
|
||||
self.attribute.hovered = true;
|
||||
|
||||
if (prop === 'playBackStatus' && colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
items.forEach((item) => {
|
||||
item[1].setCss(`
|
||||
background-color: ${c.hoverAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 40px;
|
||||
min-width: 36px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
on_hover_lost: (self) => {
|
||||
self.attribute.hovered = false;
|
||||
if (prop === 'playBackStatus' && colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
items.forEach((item) => {
|
||||
item[1].setCss(`
|
||||
background-color: ${c.buttonAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 42px;
|
||||
min-width: 38px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(player, () => {
|
||||
(self.child as AgsStack).shown = `${player[prop]}`;
|
||||
})
|
||||
.hook(colors, () => {
|
||||
if (!Mpris.players.find((p) => player === p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
if (prop === 'playBackStatus') {
|
||||
if (self.attribute.hovered) {
|
||||
Array.from(items).forEach((item) => {
|
||||
item[1].setCss(`
|
||||
background-color: ${c.hoverAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 40px;
|
||||
min-width: 36px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
else {
|
||||
items.forEach((item) => {
|
||||
item[1].setCss(`
|
||||
background-color: ${c.buttonAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 42px;
|
||||
min-width: 38px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.setCss(`
|
||||
* { color: ${c.buttonAccent}; }
|
||||
*:hover { color: ${c.hoverAccent}; }
|
||||
`);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const ShuffleButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
items: [
|
||||
['true', Label({
|
||||
class_name: 'shuffle enabled',
|
||||
label: icons.mpris.shuffle.enabled,
|
||||
})],
|
||||
['false', Label({
|
||||
class_name: 'shuffle disabled',
|
||||
label: icons.mpris.shuffle.disabled,
|
||||
})],
|
||||
],
|
||||
onClick: 'shuffle',
|
||||
prop: 'shuffleStatus',
|
||||
});
|
||||
|
||||
export const LoopButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
items: [
|
||||
['None', Label({
|
||||
class_name: 'loop none',
|
||||
label: icons.mpris.loop.none,
|
||||
})],
|
||||
['Track', Label({
|
||||
class_name: 'loop track',
|
||||
label: icons.mpris.loop.track,
|
||||
})],
|
||||
['Playlist', Label({
|
||||
class_name: 'loop playlist',
|
||||
label: icons.mpris.loop.playlist,
|
||||
})],
|
||||
],
|
||||
onClick: 'loop',
|
||||
prop: 'loopStatus',
|
||||
});
|
||||
|
||||
export const PlayPauseButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
items: [
|
||||
['Playing', Label({
|
||||
class_name: 'pausebutton playing',
|
||||
label: icons.mpris.playing,
|
||||
})],
|
||||
['Paused', Label({
|
||||
class_name: 'pausebutton paused',
|
||||
label: icons.mpris.paused,
|
||||
})],
|
||||
['Stopped', Label({
|
||||
class_name: 'pausebutton stopped paused',
|
||||
label: icons.mpris.stopped,
|
||||
})],
|
||||
],
|
||||
onClick: 'playPause',
|
||||
prop: 'playBackStatus',
|
||||
});
|
||||
|
||||
export const PreviousButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
items: [
|
||||
['true', Label({
|
||||
class_name: 'previous',
|
||||
label: icons.mpris.prev,
|
||||
})],
|
||||
['false', Label({
|
||||
class_name: 'previous',
|
||||
label: icons.mpris.prev,
|
||||
})],
|
||||
],
|
||||
onClick: 'previous',
|
||||
prop: 'canGoPrev',
|
||||
});
|
||||
|
||||
export const NextButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
items: [
|
||||
['true', Label({
|
||||
class_name: 'next',
|
||||
label: icons.mpris.next,
|
||||
})],
|
||||
['false', Label({
|
||||
class_name: 'next',
|
||||
label: icons.mpris.next,
|
||||
})],
|
||||
],
|
||||
onClick: 'next',
|
||||
prop: 'canGoNext',
|
||||
});
|
207
modules/ags/config/ts/media-player/player.ts
Normal file
207
modules/ags/config/ts/media-player/player.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { Box, CenterBox } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import * as mpris from './mpris.ts';
|
||||
import PlayerGesture from './gesture.ts';
|
||||
import Separator from '../misc/separator.ts';
|
||||
|
||||
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
|
||||
const SPACING = 8;
|
||||
|
||||
// Types
|
||||
import { MprisPlayer } from 'types/service/mpris.ts';
|
||||
import AgsOverlay from 'types/widgets/overlay.ts';
|
||||
import { Variable as Var } from 'types/variable';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
|
||||
|
||||
const Top = (
|
||||
player: MprisPlayer,
|
||||
overlay: AgsOverlay,
|
||||
) => Box({
|
||||
class_name: 'top',
|
||||
hpack: 'start',
|
||||
vpack: 'start',
|
||||
|
||||
children: [
|
||||
mpris.PlayerIcon(player, overlay),
|
||||
],
|
||||
});
|
||||
|
||||
const Center = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => Box({
|
||||
class_name: 'center',
|
||||
|
||||
children: [
|
||||
CenterBox({
|
||||
vertical: true,
|
||||
|
||||
start_widget: Box({
|
||||
class_name: 'metadata',
|
||||
vertical: true,
|
||||
hpack: 'start',
|
||||
vpack: 'center',
|
||||
hexpand: true,
|
||||
|
||||
children: [
|
||||
mpris.TitleLabel(player),
|
||||
mpris.ArtistLabel(player),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
|
||||
CenterBox({
|
||||
vertical: true,
|
||||
|
||||
center_widget: mpris.PlayPauseButton(player, colors),
|
||||
}),
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
const Bottom = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
) => Box({
|
||||
class_name: 'bottom',
|
||||
|
||||
children: [
|
||||
mpris.PreviousButton(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.PositionSlider(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.NextButton(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.ShuffleButton(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.LoopButton(player, colors),
|
||||
],
|
||||
});
|
||||
|
||||
const PlayerBox = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<any>,
|
||||
overlay: AgsOverlay,
|
||||
) => {
|
||||
const widget = mpris.CoverArt(player, colors, {
|
||||
class_name: `player ${player.name}`,
|
||||
hexpand: true,
|
||||
|
||||
start_widget: Top(player, overlay),
|
||||
center_widget: Center(player, colors),
|
||||
end_widget: Bottom(player, colors),
|
||||
});
|
||||
|
||||
widget.visible = false;
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const content = PlayerGesture({
|
||||
attribute: {
|
||||
players: new Map(),
|
||||
setup: false,
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Mpris, (_: AgsOverlay, bus_name: string) => {
|
||||
const players = self.attribute.players;
|
||||
|
||||
if (players.has(bus_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sometimes the signal doesn't give the bus_name
|
||||
if (!bus_name) {
|
||||
const player = Mpris.players.find((p) => {
|
||||
return !players.has(p.bus_name);
|
||||
});
|
||||
|
||||
if (player) {
|
||||
bus_name = player.bus_name;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the one on top so we can move it up later
|
||||
const previousFirst = self.overlays.at(-1);
|
||||
|
||||
// Make the new player
|
||||
const player = Mpris.getPlayer(bus_name);
|
||||
const Colors = Variable(null);
|
||||
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
players.set(
|
||||
bus_name,
|
||||
PlayerBox(
|
||||
player,
|
||||
Colors,
|
||||
content.get_children()[0] as AgsOverlay,
|
||||
),
|
||||
);
|
||||
self.overlays = Array.from(players.values())
|
||||
.map((widget) => widget) as Array<AgsBox>;
|
||||
|
||||
const includes = self.attribute
|
||||
.includesWidget(previousFirst);
|
||||
|
||||
// Select favorite player at startup
|
||||
const attrs = self.attribute;
|
||||
|
||||
if (!attrs.setup && players.has(FAVE_PLAYER)) {
|
||||
attrs.moveToTop(players.get(FAVE_PLAYER));
|
||||
attrs.setup = true;
|
||||
}
|
||||
|
||||
// Move previousFirst on top again
|
||||
else if (includes) {
|
||||
attrs.moveToTop(previousFirst);
|
||||
}
|
||||
}, 'player-added')
|
||||
|
||||
.hook(Mpris, (_: AgsOverlay, bus_name: string) => {
|
||||
const players = self.attribute.players;
|
||||
|
||||
if (!bus_name || !players.has(bus_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the one on top so we can move it up later
|
||||
const previousFirst = self.overlays.at(-1);
|
||||
|
||||
// Remake overlays without deleted one
|
||||
players.delete(bus_name);
|
||||
self.overlays = Array.from(players.values())
|
||||
.map((widget) => widget) as Array<AgsBox>;
|
||||
|
||||
// Move previousFirst on top again
|
||||
const includes = self.attribute
|
||||
.includesWidget(previousFirst);
|
||||
|
||||
if (includes) {
|
||||
self.attribute.moveToTop(previousFirst);
|
||||
}
|
||||
}, 'player-closed');
|
||||
},
|
||||
});
|
||||
|
||||
return Box({
|
||||
class_name: 'media',
|
||||
child: content,
|
||||
});
|
||||
};
|
58
modules/ags/config/ts/misc/audio-icons.ts
Normal file
58
modules/ags/config/ts/misc/audio-icons.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
const speakerIcons = {
|
||||
101: 'audio-volume-overamplified-symbolic',
|
||||
67: 'audio-volume-high-symbolic',
|
||||
34: 'audio-volume-medium-symbolic',
|
||||
1: 'audio-volume-low-symbolic',
|
||||
0: 'audio-volume-muted-symbolic',
|
||||
};
|
||||
|
||||
const micIcons = {
|
||||
67: 'audio-input-microphone-high-symbolic',
|
||||
34: 'audio-input-microphone-medium-symbolic',
|
||||
1: 'audio-input-microphone-low-symbolic',
|
||||
0: 'audio-input-microphone-muted-symbolic',
|
||||
};
|
||||
|
||||
|
||||
export const SpeakerIcon = Variable('');
|
||||
Audio.connect('speaker-changed', () => {
|
||||
if (!Audio.speaker) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Audio.speaker.stream.is_muted) {
|
||||
SpeakerIcon.value = speakerIcons[0];
|
||||
}
|
||||
else {
|
||||
const vol = Audio.speaker.volume * 100;
|
||||
|
||||
for (const threshold of [-1, 0, 33, 66, 100]) {
|
||||
if (vol > threshold + 1) {
|
||||
SpeakerIcon.value = speakerIcons[threshold + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const MicIcon = Variable('');
|
||||
Audio.connect('microphone-changed', () => {
|
||||
if (!Audio.microphone) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Audio.microphone.stream.is_muted) {
|
||||
MicIcon.value = micIcons[0];
|
||||
}
|
||||
else {
|
||||
const vol = Audio.microphone.volume * 100;
|
||||
|
||||
for (const threshold of [-1, 0, 33, 66]) {
|
||||
if (vol > threshold + 1) {
|
||||
MicIcon.value = micIcons[threshold + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
15
modules/ags/config/ts/misc/background-fade.ts
Normal file
15
modules/ags/config/ts/misc/background-fade.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
|
||||
export default () => Window({
|
||||
name: 'bg-gradient',
|
||||
layer: 'background',
|
||||
exclusivity: 'ignore',
|
||||
anchor: ['top', 'bottom', 'left', 'right'],
|
||||
css: `
|
||||
background-image: -gtk-gradient (linear,
|
||||
left top, left bottom,
|
||||
from(rgba(0, 0, 0, 0.5)),
|
||||
to(rgba(0, 0, 0, 0)));
|
||||
`,
|
||||
});
|
14
modules/ags/config/ts/misc/closer.ts
Normal file
14
modules/ags/config/ts/misc/closer.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
|
||||
// Types
|
||||
import AgsWindow from 'types/widgets/window';
|
||||
|
||||
|
||||
export default () => {
|
||||
(Array.from(App.windows) as Array<[string, AgsWindow]>)
|
||||
.filter((w) => w[1].attribute?.close_on_unfocus &&
|
||||
w[1].attribute?.close_on_unfocus !== 'stay')
|
||||
.forEach((w) => {
|
||||
App.closeWindow(w[0]);
|
||||
});
|
||||
};
|
90
modules/ags/config/ts/misc/cursorbox.ts
Normal file
90
modules/ags/config/ts/misc/cursorbox.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
const { Gtk, Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
import * as EventBoxTypes from 'types/widgets/eventbox';
|
||||
type CursorBox = EventBoxTypes.EventBoxProps & {
|
||||
on_primary_click_release?(self: EventBoxTypes.default): void;
|
||||
on_hover?(self: EventBoxTypes.default): void;
|
||||
on_hover_lost?(self: EventBoxTypes.default): void;
|
||||
};
|
||||
|
||||
|
||||
export default ({
|
||||
on_primary_click_release = () => {/**/},
|
||||
on_hover = () => {/**/},
|
||||
on_hover_lost = () => {/**/},
|
||||
attribute,
|
||||
...props
|
||||
}: CursorBox) => {
|
||||
// Make this variable to know if the function should
|
||||
// be executed depending on where the click is released
|
||||
const CanRun = Variable(true);
|
||||
const Disabled = Variable(false);
|
||||
|
||||
const cursorBox = EventBox({
|
||||
...props,
|
||||
|
||||
attribute: {
|
||||
...attribute,
|
||||
disabled: Disabled,
|
||||
},
|
||||
|
||||
on_primary_click_release: (self) => {
|
||||
// Every click, do a one shot connect to
|
||||
// CanRun to wait for location of click
|
||||
const id = CanRun.connect('changed', () => {
|
||||
if (CanRun.value && !Disabled.value) {
|
||||
on_primary_click_release(self);
|
||||
}
|
||||
|
||||
CanRun.disconnect(id);
|
||||
});
|
||||
},
|
||||
|
||||
// OnHover
|
||||
}).on('enter-notify-event', (self) => {
|
||||
on_hover(self);
|
||||
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
Disabled.value ?
|
||||
'not-allowed' :
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
|
||||
// OnHoverLost
|
||||
}).on('leave-notify-event', (self) => {
|
||||
on_hover_lost(self);
|
||||
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
|
||||
// Disabled class
|
||||
}).hook(Disabled, (self) => {
|
||||
self.toggleClassName('disabled', Disabled.value);
|
||||
});
|
||||
|
||||
const gesture = Gtk.GestureLongPress.new(cursorBox);
|
||||
|
||||
cursorBox.hook(gesture, () => {
|
||||
const pointer = gesture.get_point(null);
|
||||
const x = pointer[1];
|
||||
const y = pointer[2];
|
||||
|
||||
if ((!x || !y) || (x === 0 && y === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CanRun.value = !(
|
||||
x > cursorBox.get_allocated_width() ||
|
||||
y > cursorBox.get_allocated_height()
|
||||
);
|
||||
}, 'end');
|
||||
|
||||
return cursorBox;
|
||||
};
|
52
modules/ags/config/ts/misc/persist.ts
Normal file
52
modules/ags/config/ts/misc/persist.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { execAsync, readFileAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
const { get_home_dir } = imports.gi.GLib;
|
||||
|
||||
type Persist = {
|
||||
name: string
|
||||
gobject: typeof imports.gi.GObject
|
||||
prop: string
|
||||
condition?: boolean | string // If string, compare following props to this
|
||||
whenTrue?: boolean | string
|
||||
whenFalse?: boolean | string
|
||||
signal?: string
|
||||
};
|
||||
|
||||
|
||||
export default ({
|
||||
name,
|
||||
gobject,
|
||||
prop,
|
||||
condition = true,
|
||||
whenTrue = condition,
|
||||
whenFalse = false,
|
||||
signal = 'changed',
|
||||
}: Persist) => {
|
||||
const cacheFile = `${get_home_dir()}/.cache/ags/.${name}`;
|
||||
|
||||
const stateCmd = () => ['bash', '-c',
|
||||
`echo ${gobject[prop] === condition} > ${cacheFile}`];
|
||||
|
||||
const monitorState = () => {
|
||||
gobject.connect(signal, () => {
|
||||
execAsync(stateCmd()).catch(print);
|
||||
});
|
||||
};
|
||||
|
||||
readFileAsync(cacheFile)
|
||||
.then((content) => {
|
||||
// JSON.parse was the only way I found to reliably
|
||||
// convert a string of 'true' or 'false' into a bool
|
||||
gobject[prop] = JSON.parse(content) ? whenTrue : whenFalse;
|
||||
|
||||
timeout(1000, () => {
|
||||
monitorState();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
execAsync(stateCmd())
|
||||
.then(() => {
|
||||
monitorState();
|
||||
})
|
||||
.catch(print);
|
||||
});
|
||||
};
|
379
modules/ags/config/ts/misc/popup.ts
Normal file
379
modules/ags/config/ts/misc/popup.ts
Normal file
|
@ -0,0 +1,379 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { Box, Overlay, Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
// Types
|
||||
type Allocation = typeof imports.gi.Gtk.Allocation;
|
||||
type Widget = typeof imports.gi.Gtk.Widget;
|
||||
import { RevealerProps } from 'types/widgets/revealer';
|
||||
import { WindowProps } from 'types/widgets/window';
|
||||
import AgsWindow from 'types/widgets/window';
|
||||
import AgsBox from 'types/widgets/box';
|
||||
import AgsOverlay from 'types/widgets/overlay';
|
||||
import { Binding } from 'types/service';
|
||||
type PopupWindow = WindowProps & {
|
||||
transition?: RevealerProps['transition']
|
||||
transition_duration?: number
|
||||
bezier?: string
|
||||
on_open?(self: AgsWindow): void
|
||||
on_close?(self: AgsWindow): void
|
||||
blur?: boolean
|
||||
close_on_unfocus?: 'none' | 'stay' | 'released' | 'clicked'
|
||||
anchor?: Array<string>
|
||||
name: string
|
||||
};
|
||||
|
||||
// FIXME: deal with overlay children?
|
||||
// TODO: make this a new class to be able to edit props
|
||||
|
||||
export default ({
|
||||
transition = 'slide_down',
|
||||
transition_duration = 800,
|
||||
bezier = 'cubic-bezier(0.68, -0.4, 0.32, 1.4)',
|
||||
on_open = () => {/**/},
|
||||
on_close = () => {/**/},
|
||||
|
||||
// Window props
|
||||
name,
|
||||
child = Box(),
|
||||
visible = false,
|
||||
anchor = [],
|
||||
layer = 'overlay',
|
||||
blur = false,
|
||||
close_on_unfocus = 'released',
|
||||
...props
|
||||
}: PopupWindow) => {
|
||||
const Child = Variable(child);
|
||||
const AntiClip = Variable(false);
|
||||
|
||||
const needsAnticlipping = bezier.match(/-[0-9]/) !== null &&
|
||||
transition !== 'crossfade';
|
||||
|
||||
const attribute = {
|
||||
set_x_pos: (
|
||||
alloc: Allocation,
|
||||
side = 'right' as 'left' | 'right',
|
||||
) => {
|
||||
const window = App.getWindow(name) as AgsWindow;
|
||||
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = window.get_display()
|
||||
.get_monitor_at_point(alloc.x, alloc.y)
|
||||
.get_geometry().width;
|
||||
|
||||
window.margins = [
|
||||
window.margins[0],
|
||||
|
||||
side === 'right' ?
|
||||
(width - alloc.x - alloc.width) :
|
||||
window.margins[1],
|
||||
|
||||
window.margins[2],
|
||||
|
||||
side === 'right' ?
|
||||
window.margins[3] :
|
||||
(alloc.x - alloc.width),
|
||||
];
|
||||
},
|
||||
|
||||
get_child: () => Child.value,
|
||||
|
||||
set_child: (new_child: Widget) => {
|
||||
Child.value = new_child;
|
||||
App.getWindow(name)?.child.show_all();
|
||||
},
|
||||
|
||||
// This is for my custom pointers.ts
|
||||
close_on_unfocus,
|
||||
};
|
||||
|
||||
if (transition === 'none') {
|
||||
return Window({
|
||||
name,
|
||||
layer,
|
||||
anchor,
|
||||
visible: false,
|
||||
...props,
|
||||
attribute,
|
||||
child: Child.bind(),
|
||||
});
|
||||
}
|
||||
|
||||
const window = Window({
|
||||
name,
|
||||
layer,
|
||||
anchor,
|
||||
visible: false,
|
||||
...props,
|
||||
|
||||
attribute,
|
||||
|
||||
setup: () => {
|
||||
// Add way to make window open on startup
|
||||
const id = App.connect('config-parsed', () => {
|
||||
if (visible) {
|
||||
App.openWindow(`${name}`);
|
||||
}
|
||||
App.disconnect(id);
|
||||
});
|
||||
|
||||
if (blur) {
|
||||
Hyprland.sendMessage('[[BATCH]] ' +
|
||||
`keyword layerrule ignorealpha[0.97],${name}; ` +
|
||||
`keyword layerrule blur,${name}`);
|
||||
}
|
||||
},
|
||||
|
||||
child: Overlay({
|
||||
overlays: [Box({
|
||||
setup: (self) => {
|
||||
// Make sure child doesn't
|
||||
// get bigger than it should
|
||||
const MAX_ANCHORS = 4;
|
||||
|
||||
self.hpack = 'center';
|
||||
self.vpack = 'center';
|
||||
|
||||
if (anchor.includes('top') &&
|
||||
anchor.includes('bottom')) {
|
||||
self.vpack = 'center';
|
||||
}
|
||||
else if (anchor.includes('top')) {
|
||||
self.vpack = 'start';
|
||||
}
|
||||
else if (anchor.includes('bottom')) {
|
||||
self.vpack = 'end';
|
||||
}
|
||||
|
||||
if (anchor.includes('left') &&
|
||||
anchor.includes('right')) {
|
||||
self.hpack = 'center';
|
||||
}
|
||||
else if (anchor.includes('left')) {
|
||||
self.hpack = 'start';
|
||||
}
|
||||
else if (anchor.includes('right')) {
|
||||
self.hpack = 'end';
|
||||
}
|
||||
|
||||
if (anchor.length === MAX_ANCHORS) {
|
||||
self.hpack = 'center';
|
||||
self.vpack = 'center';
|
||||
}
|
||||
|
||||
if (needsAnticlipping) {
|
||||
const reorder_child = (position: number) => {
|
||||
// If unanchored, we have another anticlip widget
|
||||
// so we can't change the order
|
||||
if (anchor.length !== 0) {
|
||||
for (const ch of self.children) {
|
||||
if (ch !== Child.value) {
|
||||
self.reorder_child(ch, position);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.hook(AntiClip, () => {
|
||||
if (transition === 'slide_down') {
|
||||
self.vertical = true;
|
||||
reorder_child(-1);
|
||||
}
|
||||
else if (transition === 'slide_up') {
|
||||
self.vertical = true;
|
||||
reorder_child(0);
|
||||
}
|
||||
else if (transition === 'slide_right') {
|
||||
self.vertical = false;
|
||||
reorder_child(-1);
|
||||
}
|
||||
else if (transition === 'slide_left') {
|
||||
self.vertical = false;
|
||||
reorder_child(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
children: Child.bind().transform((v) => {
|
||||
if (needsAnticlipping) {
|
||||
return [
|
||||
// Add an anticlip widget when unanchored
|
||||
// to not have a weird animation
|
||||
anchor.length === 0 && Box({
|
||||
css: `
|
||||
min-height: 100px;
|
||||
min-width: 100px;
|
||||
padding: 2px;
|
||||
`,
|
||||
visible: AntiClip.bind(),
|
||||
}),
|
||||
v,
|
||||
Box({
|
||||
css: `
|
||||
min-height: 100px;
|
||||
min-width: 100px;
|
||||
padding: 2px;
|
||||
`,
|
||||
visible: AntiClip.bind(),
|
||||
}),
|
||||
];
|
||||
}
|
||||
else {
|
||||
return [v];
|
||||
}
|
||||
}) as Binding<any, any, Widget[]>,
|
||||
})],
|
||||
|
||||
setup: (self) => {
|
||||
self.on('get-child-position', (_, ch) => {
|
||||
const overlay = (Child.value as Widget)
|
||||
.get_parent() as AgsOverlay;
|
||||
|
||||
if (ch === overlay) {
|
||||
const alloc = overlay.get_allocation();
|
||||
const setAlloc = (v: number) => v - 2 < 0 ? 1 : v;
|
||||
|
||||
(self.child as AgsBox).css = `
|
||||
min-height: ${setAlloc(alloc.height - 2)}px;
|
||||
min-width: ${setAlloc(alloc.width - 2)}px;
|
||||
`;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
child: Box({
|
||||
css: `
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
padding: 1px;
|
||||
`,
|
||||
|
||||
setup: (self) => {
|
||||
let currentTimeout: number;
|
||||
|
||||
self.hook(App, (_, currentName, isOpen) => {
|
||||
if (currentName === name) {
|
||||
const overlay = (Child.value as Widget)
|
||||
.get_parent() as AgsOverlay;
|
||||
|
||||
const alloc = overlay.get_allocation();
|
||||
const height = needsAnticlipping ?
|
||||
alloc.height + 100 + 10 :
|
||||
alloc.height + 10;
|
||||
|
||||
if (needsAnticlipping) {
|
||||
AntiClip.value = true;
|
||||
|
||||
const thisTimeout = timeout(
|
||||
transition_duration,
|
||||
() => {
|
||||
// Only run the timeout if there isn't a newer timeout
|
||||
if (thisTimeout === currentTimeout) {
|
||||
AntiClip.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
currentTimeout = thisTimeout;
|
||||
}
|
||||
|
||||
let css = '';
|
||||
|
||||
/* Margin: top | right | bottom | left */
|
||||
switch (transition) {
|
||||
case 'slide_down':
|
||||
css = `margin:
|
||||
-${height}px
|
||||
0
|
||||
${height}px
|
||||
0
|
||||
;`;
|
||||
break;
|
||||
|
||||
case 'slide_up':
|
||||
css = `margin:
|
||||
${height}px
|
||||
0
|
||||
-${height}px
|
||||
0
|
||||
;`;
|
||||
break;
|
||||
|
||||
case 'slide_left':
|
||||
css = `margin:
|
||||
0
|
||||
-${height}px
|
||||
0
|
||||
${height}px
|
||||
;`;
|
||||
break;
|
||||
|
||||
case 'slide_right':
|
||||
css = `margin:
|
||||
0
|
||||
${height}px
|
||||
0
|
||||
-${height}px
|
||||
;`;
|
||||
break;
|
||||
|
||||
case 'crossfade':
|
||||
css = `
|
||||
opacity: 0;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
`;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
on_open(window);
|
||||
|
||||
// To get the animation, we need to set the css
|
||||
// to hide the widget and then timeout to have
|
||||
// the animation
|
||||
overlay.css = css;
|
||||
timeout(10, () => {
|
||||
overlay.css = `
|
||||
transition: margin
|
||||
${transition_duration}ms ${bezier},
|
||||
|
||||
opacity
|
||||
${transition_duration}ms ${bezier};
|
||||
`;
|
||||
});
|
||||
}
|
||||
else {
|
||||
timeout(transition_duration, () => {
|
||||
on_close(window);
|
||||
});
|
||||
|
||||
overlay.css = `${css}
|
||||
transition: margin
|
||||
${transition_duration}ms ${bezier},
|
||||
|
||||
opacity
|
||||
${transition_duration}ms ${bezier};
|
||||
`;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
return window;
|
||||
};
|
13
modules/ags/config/ts/misc/separator.ts
Normal file
13
modules/ags/config/ts/misc/separator.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Box } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
|
||||
export default (size: number, {
|
||||
vertical = false,
|
||||
css = '',
|
||||
...props
|
||||
} = {}) => {
|
||||
return Box({
|
||||
css: `${vertical ? 'min-height' : 'min-width'}: ${size}px; ${css}`,
|
||||
...props,
|
||||
});
|
||||
};
|
269
modules/ags/config/ts/notifications/base.ts
Normal file
269
modules/ags/config/ts/notifications/base.ts
Normal file
|
@ -0,0 +1,269 @@
|
|||
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { Box, Icon, Label, Button } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
const { GLib } = imports.gi;
|
||||
|
||||
import Gesture from './gesture.ts';
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
// Types
|
||||
import { Notification as NotifObj } from 'types/service/notifications.ts';
|
||||
import AgsEventBox from 'types/widgets/eventbox.ts';
|
||||
import { Client } from 'types/service/hyprland.ts';
|
||||
type NotificationWidget = {
|
||||
notif: NotifObj
|
||||
slideIn?: 'Left' | 'Right'
|
||||
command?(): void
|
||||
};
|
||||
|
||||
const setTime = (time: number) => {
|
||||
return GLib.DateTime
|
||||
.new_from_unix_local(time)
|
||||
.format('%H:%M');
|
||||
};
|
||||
|
||||
const getDragState = (box: AgsEventBox) => (box.get_parent()?.get_parent()
|
||||
?.get_parent()?.get_parent()?.get_parent() as AgsEventBox)
|
||||
?.attribute.dragging;
|
||||
|
||||
|
||||
const NotificationIcon = (notif: NotifObj) => {
|
||||
let iconCmd = (box: AgsEventBox):void => {
|
||||
console.log(box);
|
||||
};
|
||||
|
||||
if (notif.app_entry && Applications.query(notif.app_entry).length > 0) {
|
||||
const app = Applications.query(notif.app_entry)[0];
|
||||
|
||||
let wmClass = app.app.get_string('StartupWMClass');
|
||||
|
||||
if (app.app?.get_filename()?.includes('discord')) {
|
||||
wmClass = 'discord';
|
||||
}
|
||||
|
||||
if (wmClass != null) {
|
||||
iconCmd = (box) => {
|
||||
if (!getDragState(box)) {
|
||||
if (wmClass === 'thunderbird') {
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
'togglespecialworkspace thunder');
|
||||
}
|
||||
else if (wmClass === 'Spotify') {
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
'togglespecialworkspace spot');
|
||||
}
|
||||
else {
|
||||
Hyprland.sendMessage('j/clients').then((msg) => {
|
||||
const clients = JSON.parse(msg) as Array<Client>;
|
||||
const classes = [] as Array<string>;
|
||||
|
||||
for (const key of clients) {
|
||||
if (key.class) {
|
||||
classes.push(key.class);
|
||||
}
|
||||
}
|
||||
|
||||
if (wmClass && classes.includes(wmClass)) {
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
`focuswindow ^(${wmClass})`);
|
||||
}
|
||||
else {
|
||||
Hyprland.sendMessage('dispatch workspace empty')
|
||||
.then(() => {
|
||||
app.launch();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
globalThis.closeAll();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (notif.image) {
|
||||
return CursorBox({
|
||||
on_primary_click_release: iconCmd,
|
||||
|
||||
child: Box({
|
||||
vpack: 'start',
|
||||
hexpand: false,
|
||||
class_name: 'icon img',
|
||||
css: `
|
||||
background-image: url("${notif.image}");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 78px;
|
||||
min-height: 78px;
|
||||
`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let icon = 'dialog-information-symbolic';
|
||||
|
||||
if (lookUpIcon(notif.app_icon)) {
|
||||
icon = notif.app_icon;
|
||||
}
|
||||
|
||||
|
||||
if (notif.app_entry && lookUpIcon(notif.app_entry)) {
|
||||
icon = notif.app_entry;
|
||||
}
|
||||
|
||||
|
||||
return CursorBox({
|
||||
on_primary_click_release: iconCmd,
|
||||
|
||||
child: Box({
|
||||
vpack: 'start',
|
||||
hexpand: false,
|
||||
class_name: 'icon',
|
||||
css: `
|
||||
min-width: 78px;
|
||||
min-height: 78px;
|
||||
`,
|
||||
children: [Icon({
|
||||
icon, size: 58,
|
||||
hpack: 'center',
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
vexpand: true,
|
||||
})],
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
// Make a variable to connect to for Widgets
|
||||
// to know when there are notifs or not
|
||||
export const HasNotifs = Variable(false);
|
||||
|
||||
export const Notification = ({
|
||||
notif,
|
||||
slideIn = 'Left',
|
||||
command = () => {/**/},
|
||||
}: NotificationWidget) => {
|
||||
if (!notif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const BlockedApps = [
|
||||
'Spotify',
|
||||
];
|
||||
|
||||
if (BlockedApps.find((app) => app === notif.app_name)) {
|
||||
notif.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HasNotifs.value = Notifications.notifications.length > 0;
|
||||
|
||||
// Init notif
|
||||
const notifWidget = Gesture({
|
||||
command,
|
||||
slideIn,
|
||||
id: notif.id,
|
||||
});
|
||||
|
||||
// Add body to notif
|
||||
(notifWidget.child as AgsEventBox).add(Box({
|
||||
class_name: `notification ${notif.urgency}`,
|
||||
vexpand: false,
|
||||
|
||||
// Notification
|
||||
child: Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
|
||||
// Content
|
||||
Box({
|
||||
children: [
|
||||
NotificationIcon(notif),
|
||||
|
||||
Box({
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
|
||||
// Top of Content
|
||||
Box({
|
||||
children: [
|
||||
|
||||
// Title
|
||||
Label({
|
||||
class_name: 'title',
|
||||
xalign: 0,
|
||||
justification: 'left',
|
||||
hexpand: true,
|
||||
max_width_chars: 24,
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: notif.summary,
|
||||
use_markup: notif.summary
|
||||
.startsWith('<'),
|
||||
}),
|
||||
|
||||
// Time
|
||||
Label({
|
||||
class_name: 'time',
|
||||
vpack: 'start',
|
||||
label: setTime(notif.time),
|
||||
}),
|
||||
|
||||
// Close button
|
||||
CursorBox({
|
||||
child: Button({
|
||||
class_name: 'close-button',
|
||||
vpack: 'start',
|
||||
|
||||
on_primary_click_release: () =>
|
||||
notif.close(),
|
||||
|
||||
child: Icon('window-close' +
|
||||
'-symbolic'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
// Description
|
||||
Label({
|
||||
class_name: 'description',
|
||||
hexpand: true,
|
||||
use_markup: true,
|
||||
xalign: 0,
|
||||
justification: 'left',
|
||||
label: notif.body,
|
||||
wrap: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
// Actions
|
||||
Box({
|
||||
class_name: 'actions',
|
||||
children: notif.actions.map((action) => Button({
|
||||
class_name: 'action-button',
|
||||
hexpand: true,
|
||||
|
||||
on_primary_click_release: () => notif.invoke(action.id),
|
||||
|
||||
child: Label(action.label),
|
||||
})),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
return notifWidget;
|
||||
};
|
25
modules/ags/config/ts/notifications/binto.ts
Normal file
25
modules/ags/config/ts/notifications/binto.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import NotifCenterWidget from './center.ts';
|
||||
import PopUpsWidget from './popup.ts';
|
||||
|
||||
import PopupWindow from '../misc/popup.ts';
|
||||
|
||||
|
||||
export const NotifPopups = () => Window({
|
||||
name: 'notifications',
|
||||
anchor: ['bottom', 'left'],
|
||||
monitor: 1,
|
||||
|
||||
child: PopUpsWidget(),
|
||||
});
|
||||
|
||||
|
||||
export const NotifCenter = () => PopupWindow({
|
||||
name: 'notification-center',
|
||||
anchor: ['bottom', 'right'],
|
||||
transition: 'slide_up',
|
||||
monitor: 1,
|
||||
|
||||
child: NotifCenterWidget(),
|
||||
});
|
159
modules/ags/config/ts/notifications/center.ts
Normal file
159
modules/ags/config/ts/notifications/center.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
|
||||
|
||||
import { Label, Box, Icon, Scrollable, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import { Notification, HasNotifs } from './base.ts';
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import { Notification as NotifObj } from 'resource:///com/github/Aylur/ags/service/notifications.js';
|
||||
|
||||
|
||||
const addNotif = (box: AgsBox, notif: NotifObj) => {
|
||||
if (notif) {
|
||||
const NewNotif = Notification({
|
||||
notif,
|
||||
slideIn: 'Right',
|
||||
command: () => notif.close(),
|
||||
});
|
||||
|
||||
if (NewNotif) {
|
||||
box.pack_end(NewNotif, false, false, 0);
|
||||
box.show_all();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const NotificationList = () => Box({
|
||||
vertical: true,
|
||||
vexpand: true,
|
||||
vpack: 'start',
|
||||
visible: HasNotifs.bind(),
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Notifications, (box, id) => {
|
||||
// Handle cached notifs
|
||||
if (box.children.length === 0) {
|
||||
Notifications.notifications.forEach((n) => {
|
||||
addNotif(box, n);
|
||||
});
|
||||
}
|
||||
|
||||
else if (id) {
|
||||
const notifObj = Notifications.getNotification(id);
|
||||
|
||||
if (notifObj) {
|
||||
addNotif(box, notifObj);
|
||||
}
|
||||
}
|
||||
}, 'notified')
|
||||
|
||||
.hook(Notifications, (box, id) => {
|
||||
const notif = (box.children as Array<AgsBox>)
|
||||
.find((ch) => ch.attribute.id === id);
|
||||
|
||||
if (notif?.sensitive) {
|
||||
notif.attribute.slideAway('Right');
|
||||
}
|
||||
}, 'closed');
|
||||
},
|
||||
});
|
||||
|
||||
const ClearButton = () => CursorBox({
|
||||
class_name: 'clear',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
Notifications.clear();
|
||||
timeout(1000, () => App.closeWindow('notification-center'));
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(HasNotifs, () => {
|
||||
self.attribute.disabled?.setValue(!HasNotifs.value);
|
||||
});
|
||||
},
|
||||
|
||||
child: Box({
|
||||
|
||||
children: [
|
||||
Label('Clear '),
|
||||
|
||||
Icon({
|
||||
setup: (self) => {
|
||||
self.hook(Notifications, () => {
|
||||
self.icon = Notifications.notifications.length > 0 ?
|
||||
'user-trash-full-symbolic' :
|
||||
'user-trash-symbolic';
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const Header = () => Box({
|
||||
class_name: 'header',
|
||||
children: [
|
||||
Label({
|
||||
label: 'Notifications',
|
||||
hexpand: true,
|
||||
xalign: 0,
|
||||
}),
|
||||
ClearButton(),
|
||||
],
|
||||
});
|
||||
|
||||
const Placeholder = () => Revealer({
|
||||
transition: 'crossfade',
|
||||
reveal_child: HasNotifs.bind()
|
||||
.transform((v) => !v),
|
||||
|
||||
child: Box({
|
||||
class_name: 'placeholder',
|
||||
vertical: true,
|
||||
vpack: 'center',
|
||||
hpack: 'center',
|
||||
vexpand: true,
|
||||
hexpand: true,
|
||||
|
||||
children: [
|
||||
Icon('notification-disabled-symbolic'),
|
||||
Label('Your inbox is empty'),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
export default () => Box({
|
||||
class_name: 'notification-center',
|
||||
vertical: true,
|
||||
children: [
|
||||
Header(),
|
||||
|
||||
Box({
|
||||
class_name: 'notification-wallpaper-box',
|
||||
|
||||
children: [
|
||||
Scrollable({
|
||||
class_name: 'notification-list-box',
|
||||
hscroll: 'never',
|
||||
vscroll: 'automatic',
|
||||
|
||||
child: Box({
|
||||
class_name: 'notification-list',
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
NotificationList(),
|
||||
|
||||
Placeholder(),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
211
modules/ags/config/ts/notifications/gesture.ts
Normal file
211
modules/ags/config/ts/notifications/gesture.ts
Normal file
|
@ -0,0 +1,211 @@
|
|||
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
|
||||
|
||||
import { Box, EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import { HasNotifs } from './base.ts';
|
||||
|
||||
const { Gdk, Gtk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
|
||||
const MAX_OFFSET = 200;
|
||||
const OFFSCREEN = 300;
|
||||
const ANIM_DURATION = 500;
|
||||
const SLIDE_MIN_THRESHOLD = 10;
|
||||
const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
|
||||
const SQUEEZED = 'margin-bottom: -70px; margin-top: -70px;';
|
||||
const MAX_LEFT = `
|
||||
margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
|
||||
margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
|
||||
`;
|
||||
const MAX_RIGHT = `
|
||||
margin-left: ${Number(MAX_OFFSET + OFFSCREEN)}px;
|
||||
margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
|
||||
`;
|
||||
|
||||
const slideLeft = `${TRANSITION} ${MAX_LEFT}
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
opacity: 0;`;
|
||||
const squeezeLeft = `${TRANSITION} ${MAX_LEFT} ${SQUEEZED} opacity: 0;`;
|
||||
|
||||
const slideRight = `${TRANSITION} ${MAX_RIGHT}
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
opacity: 0;`;
|
||||
const squeezeRight = `${TRANSITION} ${MAX_RIGHT} ${SQUEEZED} opacity: 0;`;
|
||||
|
||||
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
|
||||
|
||||
|
||||
export default ({
|
||||
id,
|
||||
slideIn = 'Left',
|
||||
command = () => {/**/},
|
||||
...props
|
||||
}) => {
|
||||
const widget = EventBox({
|
||||
...props,
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
// OnClick
|
||||
.on('button-press-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grabbing',
|
||||
));
|
||||
})
|
||||
|
||||
// OnRelease
|
||||
.on('button-release-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grab',
|
||||
));
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grab',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
if (!self.attribute.hovered) {
|
||||
self.attribute.hovered = true;
|
||||
}
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
|
||||
if (self.attribute.hovered) {
|
||||
self.attribute.hovered = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
attribute: {
|
||||
dragging: false,
|
||||
hovered: false,
|
||||
ready: false,
|
||||
id,
|
||||
|
||||
slideAway: (side: 'Left' | 'Right') => {
|
||||
// Slide away
|
||||
(widget.child as AgsBox)
|
||||
.setCss(side === 'Left' ? slideLeft : slideRight);
|
||||
|
||||
// Make it uninteractable
|
||||
widget.sensitive = false;
|
||||
|
||||
timeout(ANIM_DURATION - 100, () => {
|
||||
// Reduce height after sliding away
|
||||
(widget.child as AgsBox)?.setCss(side === 'Left' ?
|
||||
squeezeLeft :
|
||||
squeezeRight);
|
||||
|
||||
timeout(ANIM_DURATION, () => {
|
||||
// Kill notif and update HasNotifs after anim is done
|
||||
command();
|
||||
|
||||
HasNotifs.value = Notifications
|
||||
.notifications.length > 0;
|
||||
|
||||
(widget.get_parent() as AgsBox)?.remove(widget);
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const gesture = Gtk.GestureDrag.new(widget);
|
||||
|
||||
widget.add(Box({
|
||||
css: squeezeLeft,
|
||||
setup: (self) => {
|
||||
self
|
||||
// When dragging
|
||||
.hook(gesture, () => {
|
||||
let offset = gesture.get_offset()[1];
|
||||
|
||||
if (offset === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Slide right
|
||||
if (offset > 0) {
|
||||
self.setCss(`
|
||||
margin-top: 0px; margin-bottom: 0px;
|
||||
opacity: 1; transition: none;
|
||||
margin-left: ${offset}px;
|
||||
margin-right: -${offset}px;
|
||||
`);
|
||||
}
|
||||
|
||||
// Slide left
|
||||
else {
|
||||
offset = Math.abs(offset);
|
||||
self.setCss(`
|
||||
margin-top: 0px; margin-bottom: 0px;
|
||||
opacity: 1; transition: none;
|
||||
margin-right: ${offset}px;
|
||||
margin-left: -${offset}px;
|
||||
`);
|
||||
}
|
||||
|
||||
// Put a threshold on if a click is actually dragging
|
||||
widget.attribute.dragging =
|
||||
Math.abs(offset) > SLIDE_MIN_THRESHOLD;
|
||||
|
||||
widget.cursor = 'grabbing';
|
||||
}, 'drag-update')
|
||||
|
||||
// On drag end
|
||||
.hook(gesture, () => {
|
||||
// Make it slide in on init
|
||||
if (!widget.attribute.ready) {
|
||||
// Reverse of slideAway, so it started at squeeze, then we go to slide
|
||||
self.setCss(slideIn === 'Left' ?
|
||||
slideLeft :
|
||||
slideRight);
|
||||
|
||||
timeout(ANIM_DURATION, () => {
|
||||
// Then we go to center
|
||||
self.setCss(defaultStyle);
|
||||
timeout(ANIM_DURATION, () => {
|
||||
widget.attribute.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = gesture.get_offset()[1];
|
||||
|
||||
// If crosses threshold after letting go, slide away
|
||||
if (Math.abs(offset) > MAX_OFFSET) {
|
||||
if (offset > 0) {
|
||||
widget.attribute.slideAway('Right');
|
||||
}
|
||||
else {
|
||||
widget.attribute.slideAway('Left');
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.setCss(defaultStyle);
|
||||
widget.cursor = 'grab';
|
||||
widget.attribute.dragging = false;
|
||||
}
|
||||
}, 'drag-end');
|
||||
},
|
||||
}));
|
||||
|
||||
return widget;
|
||||
};
|
77
modules/ags/config/ts/notifications/popup.ts
Normal file
77
modules/ags/config/ts/notifications/popup.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
|
||||
|
||||
import { Box } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { interval } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import { Notification } from './base.ts';
|
||||
|
||||
const DELAY = 2000;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
|
||||
|
||||
export default () => Box({
|
||||
vertical: true,
|
||||
// Needed so it occupies space at the start
|
||||
css: 'padding: 1px;',
|
||||
|
||||
setup: (self) => {
|
||||
const addPopup = (id: number) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notif = Notifications.getNotification(id);
|
||||
|
||||
if (notif) {
|
||||
const NewNotif = Notification({
|
||||
notif,
|
||||
command: () => {
|
||||
if (notif.popup) {
|
||||
notif.dismiss();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (NewNotif) {
|
||||
// Use this instead of add to put it at the top
|
||||
self.pack_end(NewNotif, false, false, 0);
|
||||
self.show_all();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = (id: number, force = false) => {
|
||||
const notif = (self.children as Array<AgsBox>)
|
||||
.find((ch) => ch.attribute.id === id);
|
||||
|
||||
if (!notif) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If notif isn't hovered or was closed, slide away
|
||||
if (!notif.attribute.hovered || force) {
|
||||
notif.attribute.slideAway('Left');
|
||||
}
|
||||
|
||||
// If notif is hovered, delay close
|
||||
else if (notif.attribute.hovered) {
|
||||
const intervalId = interval(DELAY, () => {
|
||||
if (!notif.attribute.hovered && intervalId) {
|
||||
notif.attribute.slideAway('Left');
|
||||
|
||||
GLib.source_remove(intervalId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self
|
||||
.hook(Notifications, (_, id) => addPopup(id), 'notified')
|
||||
.hook(Notifications, (_, id) => handleDismiss(id), 'dismissed')
|
||||
.hook(Notifications, (_, id) => handleDismiss(id, true), 'closed');
|
||||
},
|
||||
});
|
22
modules/ags/config/ts/notifications/wim.ts
Normal file
22
modules/ags/config/ts/notifications/wim.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import NotifCenterWidget from './center.ts';
|
||||
import PopUpsWidget from './popup.ts';
|
||||
|
||||
import PopupWindow from '../misc/popup.ts';
|
||||
|
||||
export const NotifPopups = () => Window({
|
||||
name: 'notifications',
|
||||
anchor: ['top', 'left'],
|
||||
child: PopUpsWidget(),
|
||||
});
|
||||
|
||||
|
||||
const TOP_MARGIN = 6;
|
||||
|
||||
export const NotifCenter = () => PopupWindow({
|
||||
name: 'notification-center',
|
||||
anchor: ['top', 'right'],
|
||||
margins: [TOP_MARGIN, 0, 0, 0],
|
||||
|
||||
child: NotifCenterWidget(),
|
||||
});
|
155
modules/ags/config/ts/on-screen-keyboard/gesture.ts
Normal file
155
modules/ags/config/ts/on-screen-keyboard/gesture.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { execAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
import Tablet from '../../services/tablet.ts';
|
||||
|
||||
const KEY_N = 249;
|
||||
const HIDDEN_MARGIN = 340;
|
||||
const ANIM_DURATION = 700;
|
||||
|
||||
// Types
|
||||
import AgsWindow from 'types/widgets/window.ts';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
|
||||
|
||||
const releaseAllKeys = () => {
|
||||
const keycodes = Array.from(Array(KEY_N).keys());
|
||||
|
||||
execAsync([
|
||||
'ydotool', 'key',
|
||||
...keycodes.map((keycode) => `${keycode}:0`),
|
||||
]).catch(print);
|
||||
};
|
||||
|
||||
export default (window: AgsWindow) => {
|
||||
const gesture = Gtk.GestureDrag.new(window);
|
||||
const child = window.child as AgsBox;
|
||||
|
||||
child.setCss(`margin-bottom: -${HIDDEN_MARGIN}px;`);
|
||||
|
||||
let signals = [] as Array<number>;
|
||||
|
||||
window.attribute = {
|
||||
setVisible: (state: boolean) => {
|
||||
if (state) {
|
||||
window.visible = true;
|
||||
window.attribute.setSlideDown();
|
||||
|
||||
child.setCss(`
|
||||
transition: margin-bottom 0.7s
|
||||
cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
margin-bottom: 0px;
|
||||
`);
|
||||
}
|
||||
else {
|
||||
timeout(ANIM_DURATION + 10, () => {
|
||||
if (!Tablet.tabletMode) {
|
||||
window.visible = false;
|
||||
}
|
||||
});
|
||||
releaseAllKeys();
|
||||
window.attribute.setSlideUp();
|
||||
|
||||
child.setCss(`
|
||||
transition: margin-bottom 0.7s
|
||||
cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
margin-bottom: -${HIDDEN_MARGIN}px;
|
||||
`);
|
||||
}
|
||||
},
|
||||
|
||||
killGestureSigs: () => {
|
||||
signals.forEach((id) => {
|
||||
gesture.disconnect(id);
|
||||
});
|
||||
signals = [];
|
||||
},
|
||||
|
||||
setSlideUp: () => {
|
||||
window.attribute.killGestureSigs();
|
||||
|
||||
// Begin drag
|
||||
signals.push(
|
||||
gesture.connect('drag-begin', () => {
|
||||
Hyprland.sendMessage('j/cursorpos').then((out) => {
|
||||
gesture.startY = JSON.parse(out).y;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Update drag
|
||||
signals.push(
|
||||
gesture.connect('drag-update', () => {
|
||||
Hyprland.sendMessage('j/cursorpos').then((out) => {
|
||||
const currentY = JSON.parse(out).y;
|
||||
const offset = gesture.startY - currentY;
|
||||
|
||||
if (offset < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(window.child as AgsBox).setCss(`
|
||||
margin-bottom: ${offset - HIDDEN_MARGIN}px;
|
||||
`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// End drag
|
||||
signals.push(
|
||||
gesture.connect('drag-end', () => {
|
||||
(window.child as AgsBox).setCss(`
|
||||
transition: margin-bottom 0.5s ease-in-out;
|
||||
margin-bottom: -${HIDDEN_MARGIN}px;
|
||||
`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
setSlideDown: () => {
|
||||
window.attribute.killGestureSigs();
|
||||
|
||||
// Begin drag
|
||||
signals.push(
|
||||
gesture.connect('drag-begin', () => {
|
||||
Hyprland.sendMessage('j/cursorpos').then((out) => {
|
||||
gesture.startY = JSON.parse(out).y;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Update drag
|
||||
signals.push(
|
||||
gesture.connect('drag-update', () => {
|
||||
Hyprland.sendMessage('j/cursorpos').then((out) => {
|
||||
const currentY = JSON.parse(out).y;
|
||||
const offset = gesture.startY - currentY;
|
||||
|
||||
if (offset > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(window.child as AgsBox).setCss(`
|
||||
margin-bottom: ${offset}px;
|
||||
`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// End drag
|
||||
signals.push(
|
||||
gesture.connect('drag-end', () => {
|
||||
(window.child as AgsBox).setCss(`
|
||||
transition: margin-bottom 0.5s ease-in-out;
|
||||
margin-bottom: 0px;
|
||||
`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return window;
|
||||
};
|
104
modules/ags/config/ts/on-screen-keyboard/keyboard-layouts.ts
Normal file
104
modules/ags/config/ts/on-screen-keyboard/keyboard-layouts.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
// TODO: right Ctrl https://handwiki.org/wiki/images/4/41/KB_Canadian_Multilingual_Standard.svg
|
||||
|
||||
export const defaultOskLayout = 'qwerty_custom';
|
||||
export const oskLayouts = {
|
||||
qwerty_custom: {
|
||||
name: 'QWERTY - Custom',
|
||||
name_short: 'CSA',
|
||||
comment: 'Like physical keyboard',
|
||||
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
|
||||
// A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
|
||||
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
|
||||
keys: [
|
||||
[
|
||||
{ keytype: 'normal', label: 'Esc', shape: 'fn', keycode: 1 },
|
||||
{ keytype: 'normal', label: 'F1', shape: 'fn', keycode: 59 },
|
||||
{ keytype: 'normal', label: 'F2', shape: 'fn', keycode: 60 },
|
||||
{ keytype: 'normal', label: 'F3', shape: 'fn', keycode: 61 },
|
||||
{ keytype: 'normal', label: 'F4', shape: 'fn', keycode: 62 },
|
||||
{ keytype: 'normal', label: 'F5', shape: 'fn', keycode: 63 },
|
||||
{ keytype: 'normal', label: 'F6', shape: 'fn', keycode: 64 },
|
||||
{ keytype: 'normal', label: 'F7', shape: 'fn', keycode: 65 },
|
||||
{ keytype: 'normal', label: 'F8', shape: 'fn', keycode: 66 },
|
||||
{ keytype: 'normal', label: 'F9', shape: 'fn', keycode: 67 },
|
||||
{ keytype: 'normal', label: 'F10', shape: 'fn', keycode: 68 },
|
||||
{ keytype: 'normal', label: 'F11', shape: 'fn', keycode: 87 },
|
||||
{ keytype: 'normal', label: 'F12', shape: 'fn', keycode: 88 },
|
||||
{ keytype: 'normal', label: 'Home', shape: 'fn', keycode: 110 },
|
||||
{ keytype: 'normal', label: 'End', shape: 'fn', keycode: 115 },
|
||||
{ keytype: 'normal', label: 'Del', shape: 'fn', keycode: 111 },
|
||||
],
|
||||
[
|
||||
{ keytype: 'normal', label: '/', labelShift: '\\', labelAltGr: '|', shape: 'normal', keycode: 41 },
|
||||
{ keytype: 'normal', label: '1', labelShift: '!', shape: 'normal', keycode: 2 },
|
||||
{ keytype: 'normal', label: '2', labelShift: '@', shape: 'normal', keycode: 3 },
|
||||
{ keytype: 'normal', label: '3', labelShift: '#', labelAltGr: '¤', shape: 'normal', keycode: 4 },
|
||||
{ keytype: 'normal', label: '4', labelShift: '$', shape: 'normal', keycode: 5 },
|
||||
{ keytype: 'normal', label: '5', labelShift: '%', shape: 'normal', keycode: 6 },
|
||||
{ keytype: 'normal', label: '6', labelShift: '?', shape: 'normal', keycode: 7 },
|
||||
{ keytype: 'normal', label: '7', labelShift: '&', labelAltGr: '{', shape: 'normal', keycode: 8 },
|
||||
{ keytype: 'normal', label: '8', labelShift: '*', labelAltGr: '}', shape: 'normal', keycode: 9 },
|
||||
{ keytype: 'normal', label: '9', labelShift: '(', labelAltGr: '[', shape: 'normal', keycode: 10 },
|
||||
{ keytype: 'normal', label: '0', labelShift: ')', labelAltGr: ']', shape: 'normal', keycode: 11 },
|
||||
{ keytype: 'normal', label: '-', labelShift: '_', shape: 'normal', keycode: 12 },
|
||||
{ keytype: 'normal', label: '=', labelShift: '+', labelAltGr: '¬', shape: 'normal', keycode: 13 },
|
||||
{ keytype: 'normal', label: 'Backspace', shape: 'expand', keycode: 14 },
|
||||
],
|
||||
[
|
||||
{ keytype: 'normal', label: 'Tab', shape: 'tab', keycode: 15 },
|
||||
{ keytype: 'normal', label: 'q', labelShift: 'Q', shape: 'normal', keycode: 16 },
|
||||
{ keytype: 'normal', label: 'w', labelShift: 'W', shape: 'normal', keycode: 17 },
|
||||
{ keytype: 'normal', label: 'e', labelShift: 'E', labelAltGr: '€', shape: 'normal', keycode: 18 },
|
||||
{ keytype: 'normal', label: 'r', labelShift: 'R', shape: 'normal', keycode: 19 },
|
||||
{ keytype: 'normal', label: 't', labelShift: 'T', shape: 'normal', keycode: 20 },
|
||||
{ keytype: 'normal', label: 'y', labelShift: 'Y', shape: 'normal', keycode: 21 },
|
||||
{ keytype: 'normal', label: 'u', labelShift: 'U', shape: 'normal', keycode: 22 },
|
||||
{ keytype: 'normal', label: 'i', labelShift: 'I', shape: 'normal', keycode: 23 },
|
||||
{ keytype: 'normal', label: 'o', labelShift: 'O', shape: 'normal', keycode: 24 },
|
||||
{ keytype: 'normal', label: 'p', labelShift: 'P', shape: 'normal', keycode: 25 },
|
||||
{ keytype: 'normal', label: '^', labelShift: '"', labelAltGr: '`', shape: 'normal', keycode: 26 },
|
||||
{ keytype: 'normal', label: 'ç', labelShift: 'Ç', labelAltGr: '~', shape: 'normal', keycode: 27 },
|
||||
{ keytype: 'normal', label: 'à', labelShift: 'À', shape: 'expand', keycode: 43 },
|
||||
],
|
||||
[
|
||||
{ keytype: 'normal', label: 'Caps', shape: 'caps', keycode: 58 },
|
||||
{ keytype: 'normal', label: 'a', labelShift: 'A', shape: 'normal', keycode: 30 },
|
||||
{ keytype: 'normal', label: 's', labelShift: 'S', shape: 'normal', keycode: 31 },
|
||||
{ keytype: 'normal', label: 'd', labelShift: 'D', shape: 'normal', keycode: 32 },
|
||||
{ keytype: 'normal', label: 'f', labelShift: 'F', shape: 'normal', keycode: 33 },
|
||||
{ keytype: 'normal', label: 'g', labelShift: 'G', shape: 'normal', keycode: 34 },
|
||||
{ keytype: 'normal', label: 'h', labelShift: 'H', shape: 'normal', keycode: 35 },
|
||||
{ keytype: 'normal', label: 'j', labelShift: 'J', shape: 'normal', keycode: 36 },
|
||||
{ keytype: 'normal', label: 'k', labelShift: 'K', shape: 'normal', keycode: 37 },
|
||||
{ keytype: 'normal', label: 'l', labelShift: 'L', shape: 'normal', keycode: 38 },
|
||||
{ keytype: 'normal', label: ';', labelShift: ':', labelAltGr: '°', shape: 'normal', keycode: 39 },
|
||||
{ keytype: 'normal', label: 'è', labelShift: 'È', shape: 'normal', keycode: 40 },
|
||||
{ keytype: 'normal', label: 'Enter', shape: 'expand', keycode: 28 },
|
||||
],
|
||||
[
|
||||
{ keytype: 'modkey', label: 'Shift', shape: 'shift', keycode: 42 },
|
||||
{ keytype: 'normal', label: 'z', labelShift: 'Z', labelAltGr: '«', shape: 'normal', keycode: 44 },
|
||||
{ keytype: 'normal', label: 'x', labelShift: 'X', labelAltGr: '»', shape: 'normal', keycode: 45 },
|
||||
{ keytype: 'normal', label: 'c', labelShift: 'C', shape: 'normal', keycode: 46 },
|
||||
{ keytype: 'normal', label: 'v', labelShift: 'V', shape: 'normal', keycode: 47 },
|
||||
{ keytype: 'normal', label: 'b', labelShift: 'B', shape: 'normal', keycode: 48 },
|
||||
{ keytype: 'normal', label: 'n', labelShift: 'N', shape: 'normal', keycode: 49 },
|
||||
{ keytype: 'normal', label: 'm', labelShift: 'M', shape: 'normal', keycode: 50 },
|
||||
{ keytype: 'normal', label: ',', labelShift: "'", labelAltGr: '<', shape: 'normal', keycode: 51 },
|
||||
{ keytype: 'normal', label: '.', labelShift: '"', labelAltGr: '>', shape: 'normal', keycode: 52 },
|
||||
{ keytype: 'normal', label: 'é', labelShift: 'É', shape: 'normal', keycode: 53 },
|
||||
{ keytype: 'modkey', label: 'Shift', shape: 'expand', keycode: 54 },
|
||||
],
|
||||
[
|
||||
{ keytype: 'modkey', label: 'Ctrl', shape: 'control', keycode: 29 },
|
||||
{ keytype: 'modkey', label: 'Super', shape: 'normal', keycode: 125 },
|
||||
{ keytype: 'modkey', label: 'Alt', shape: 'normal', keycode: 56 },
|
||||
{ keytype: 'normal', label: 'Space', shape: 'space', keycode: 57 },
|
||||
{ keytype: 'normal', label: 'Space', shape: 'space', keycode: 57 },
|
||||
{ keytype: 'modkey', label: 'AltGr', shape: 'normal', keycode: 100 },
|
||||
{ keytype: 'normal', label: 'PrtSc', shape: 'fn', keycode: 99 },
|
||||
{ keytype: 'modkey', label: 'Ctrl', shape: 'control', keycode: 97 },
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
167
modules/ags/config/ts/on-screen-keyboard/keyboard.ts
Normal file
167
modules/ags/config/ts/on-screen-keyboard/keyboard.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { Box, CenterBox, Label, ToggleButton } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
const { Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
import RoundedCorner from '../corners/screen-corners.ts';
|
||||
import Key from './keys.ts';
|
||||
|
||||
import { defaultOskLayout, oskLayouts } from './keyboard-layouts.ts';
|
||||
const keyboardLayout = defaultOskLayout;
|
||||
const keyboardJson = oskLayouts[keyboardLayout];
|
||||
|
||||
const L_KEY_PER_ROW = [8, 7, 6, 6, 6, 4]; // eslint-disable-line
|
||||
const COLOR = 'rgba(0, 0, 0, 0.3)';
|
||||
const SPACING = 4;
|
||||
|
||||
// Types
|
||||
import AgsWindow from 'types/widgets/window.ts';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
|
||||
|
||||
export default (window: AgsWindow) => Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
CenterBox({
|
||||
hpack: 'center',
|
||||
|
||||
start_widget: RoundedCorner('bottomright', `
|
||||
background-color: ${COLOR};
|
||||
`),
|
||||
|
||||
center_widget: CenterBox({
|
||||
class_name: 'thingy',
|
||||
css: `background: ${COLOR};`,
|
||||
|
||||
center_widget: Box({
|
||||
hpack: 'center',
|
||||
class_name: 'settings',
|
||||
|
||||
children: [
|
||||
ToggleButton({
|
||||
class_name: 'button',
|
||||
active: true,
|
||||
vpack: 'center',
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.on('toggled', () => {
|
||||
self.toggleClassName(
|
||||
'toggled',
|
||||
self.get_active(),
|
||||
);
|
||||
window.exclusivity = self.get_active() ?
|
||||
'exclusive' :
|
||||
'normal';
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(
|
||||
Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
),
|
||||
);
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
|
||||
child: Label('Exclusive'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
|
||||
end_widget: RoundedCorner('bottomleft', `
|
||||
background-color: ${COLOR};
|
||||
`),
|
||||
}),
|
||||
|
||||
CenterBox({
|
||||
css: `background: ${COLOR};`,
|
||||
class_name: 'osk',
|
||||
hexpand: true,
|
||||
|
||||
start_widget: Box({
|
||||
class_name: 'left-side side',
|
||||
hpack: 'start',
|
||||
vertical: true,
|
||||
|
||||
children: keyboardJson.keys.map((row, rowIndex) => {
|
||||
const keys = [] as Array<AgsBox>;
|
||||
|
||||
row.forEach((key, keyIndex) => {
|
||||
if (keyIndex < L_KEY_PER_ROW[rowIndex]) {
|
||||
keys.push(Key(key));
|
||||
}
|
||||
});
|
||||
|
||||
return Box({
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
Box({
|
||||
class_name: 'row',
|
||||
|
||||
children: [
|
||||
Separator(SPACING),
|
||||
|
||||
...keys,
|
||||
],
|
||||
}),
|
||||
|
||||
Separator(SPACING, { vertical: true }),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}),
|
||||
|
||||
center_widget: Box({
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
|
||||
children: [
|
||||
],
|
||||
}),
|
||||
|
||||
end_widget: Box({
|
||||
class_name: 'right-side side',
|
||||
hpack: 'end',
|
||||
vertical: true,
|
||||
|
||||
children: keyboardJson.keys.map((row, rowIndex) => {
|
||||
const keys = [] as Array<AgsBox>;
|
||||
|
||||
row.forEach((key, keyIndex) => {
|
||||
if (keyIndex >= L_KEY_PER_ROW[rowIndex]) {
|
||||
keys.push(Key(key));
|
||||
}
|
||||
});
|
||||
|
||||
return Box({
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
Box({
|
||||
hpack: 'end',
|
||||
class_name: 'row',
|
||||
|
||||
children: keys,
|
||||
}),
|
||||
|
||||
Separator(SPACING, { vertical: true }),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
249
modules/ags/config/ts/on-screen-keyboard/keys.ts
Normal file
249
modules/ags/config/ts/on-screen-keyboard/keys.ts
Normal file
|
@ -0,0 +1,249 @@
|
|||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
import Brightness from '../../services/brightness.ts';
|
||||
|
||||
import { Box, EventBox, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
const { Gdk, Gtk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
|
||||
// Keep track of when a non modifier key
|
||||
// is clicked to release all modifiers
|
||||
const NormalClick = Variable(false);
|
||||
|
||||
// Keep track of modifier statuses
|
||||
const Super = Variable(false);
|
||||
const LAlt = Variable(false);
|
||||
const LCtrl = Variable(false);
|
||||
const AltGr = Variable(false);
|
||||
const RCtrl = Variable(false);
|
||||
|
||||
const Caps = Variable(false);
|
||||
|
||||
Brightness.connect('caps', (_, state) => {
|
||||
Caps.value = state;
|
||||
});
|
||||
|
||||
// Assume both shifts are the same for key.labelShift
|
||||
const LShift = Variable(false);
|
||||
const RShift = Variable(false);
|
||||
|
||||
const Shift = Variable(false);
|
||||
|
||||
LShift.connect('changed', () => {
|
||||
Shift.value = LShift.value || RShift.value;
|
||||
});
|
||||
RShift.connect('changed', () => {
|
||||
Shift.value = LShift.value || RShift.value;
|
||||
});
|
||||
|
||||
const SPACING = 4;
|
||||
const LSHIFT_CODE = 42;
|
||||
const LALT_CODE = 56;
|
||||
const LCTRL_CODE = 29;
|
||||
|
||||
// Types
|
||||
import { Variable as Var } from 'types/variable.ts';
|
||||
type Key = {
|
||||
keytype: string,
|
||||
label: string,
|
||||
labelShift?: string,
|
||||
labelAltGr?: string,
|
||||
shape: string,
|
||||
keycode: number
|
||||
};
|
||||
|
||||
|
||||
const ModKey = (key: Key) => {
|
||||
let Mod: Var<any>;
|
||||
|
||||
if (key.label === 'Super') {
|
||||
Mod = Super;
|
||||
}
|
||||
|
||||
// Differentiate left and right mods
|
||||
else if (key.label === 'Shift' && key.keycode === LSHIFT_CODE) {
|
||||
Mod = LShift;
|
||||
}
|
||||
|
||||
else if (key.label === 'Alt' && key.keycode === LALT_CODE) {
|
||||
Mod = LAlt;
|
||||
}
|
||||
|
||||
else if (key.label === 'Ctrl' && key.keycode === LCTRL_CODE) {
|
||||
Mod = LCtrl;
|
||||
}
|
||||
|
||||
else if (key.label === 'Shift') {
|
||||
Mod = RShift;
|
||||
}
|
||||
|
||||
else if (key.label === 'AltGr') {
|
||||
Mod = AltGr;
|
||||
}
|
||||
|
||||
else if (key.label === 'Ctrl') {
|
||||
Mod = RCtrl;
|
||||
}
|
||||
const label = Label({
|
||||
class_name: `mod ${key.label}`,
|
||||
label: key.label,
|
||||
});
|
||||
|
||||
const button = EventBox({
|
||||
class_name: 'key',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
console.log('mod toggled');
|
||||
|
||||
execAsync(`ydotool key ${key.keycode}:${Mod.value ? 0 : 1}`);
|
||||
|
||||
label.toggleClassName('active', !Mod.value);
|
||||
Mod.value = !Mod.value;
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(NormalClick, () => {
|
||||
Mod.value = false;
|
||||
|
||||
label.toggleClassName('active', false);
|
||||
execAsync(`ydotool key ${key.keycode}:0`);
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
child: label,
|
||||
});
|
||||
|
||||
return Box({
|
||||
children: [
|
||||
button,
|
||||
Separator(SPACING),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const RegularKey = (key: Key) => {
|
||||
const widget = EventBox({
|
||||
class_name: 'key',
|
||||
|
||||
child: Label({
|
||||
class_name: `normal ${key.label}`,
|
||||
label: key.label,
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Shift, () => {
|
||||
if (!key.labelShift) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.label = Shift.value ? key.labelShift : key.label;
|
||||
})
|
||||
.hook(Caps, () => {
|
||||
if (key.label === 'Caps') {
|
||||
self.toggleClassName('active', Caps.value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.labelShift) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.label.match(/[A-Za-z]/)) {
|
||||
self.label = Caps.value ?
|
||||
key.labelShift :
|
||||
key.label;
|
||||
}
|
||||
})
|
||||
.hook(AltGr, () => {
|
||||
if (!key.labelAltGr) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.toggleClassName('altgr', AltGr.value);
|
||||
self.label = AltGr.value ? key.labelAltGr : key.label;
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const gesture = Gtk.GestureLongPress.new(widget);
|
||||
|
||||
gesture.delay_factor = 1.0;
|
||||
|
||||
// Long press
|
||||
widget.hook(gesture, () => {
|
||||
const pointer = gesture.get_point(null);
|
||||
const x = pointer[1];
|
||||
const y = pointer[2];
|
||||
|
||||
if ((!x || !y) || (x === 0 && y === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Not implemented yet');
|
||||
|
||||
// TODO: popup menu for accents
|
||||
}, 'pressed');
|
||||
|
||||
// OnPrimaryClickRelease
|
||||
widget.hook(gesture, () => {
|
||||
const pointer = gesture.get_point(null);
|
||||
const x = pointer[1];
|
||||
const y = pointer[2];
|
||||
|
||||
if ((!x || !y) || (x === 0 && y === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('key clicked');
|
||||
|
||||
execAsync(`ydotool key ${key.keycode}:1`);
|
||||
execAsync(`ydotool key ${key.keycode}:0`);
|
||||
NormalClick.value = true;
|
||||
}, 'cancelled');
|
||||
|
||||
return Box({
|
||||
children: [
|
||||
widget,
|
||||
Separator(SPACING),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export default (key: Key) => key.keytype === 'normal' ?
|
||||
RegularKey(key) :
|
||||
ModKey(key);
|
37
modules/ags/config/ts/on-screen-keyboard/main.ts
Normal file
37
modules/ags/config/ts/on-screen-keyboard/main.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import Tablet from '../../services/tablet.ts';
|
||||
import Gesture from './gesture.ts';
|
||||
import Keyboard from './keyboard.ts';
|
||||
|
||||
|
||||
// Start ydotool daemon
|
||||
execAsync('ydotoold').catch(print);
|
||||
|
||||
// Window
|
||||
export default () => {
|
||||
const window = Window({
|
||||
name: 'osk',
|
||||
visible: false,
|
||||
layer: 'overlay',
|
||||
anchor: ['left', 'bottom', 'right'],
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Tablet, (_, state) => {
|
||||
self.attribute.setVisible(state);
|
||||
}, 'osk-toggled')
|
||||
|
||||
.hook(Tablet, () => {
|
||||
if (!Tablet.tabletMode && !Tablet.oskState) {
|
||||
window.visible = false;
|
||||
}
|
||||
}, 'mode-toggled');
|
||||
},
|
||||
});
|
||||
|
||||
window.child = Keyboard(window);
|
||||
|
||||
return Gesture(window);
|
||||
};
|
65
modules/ags/config/ts/osd/ctor.ts
Normal file
65
modules/ags/config/ts/osd/ctor.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { Box, Icon, ProgressBar } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
const Y_POS = 80;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box';
|
||||
import { IconProps } from 'types/widgets/icon';
|
||||
import { GObject } from 'gi://GObject';
|
||||
import AgsStack from 'types/widgets/stack';
|
||||
type Widget = typeof imports.gi.Gtk.Widget;
|
||||
import { Connectable } from 'types/widgets/widget';
|
||||
import AgsProgressBar from 'types/widgets/progressbar';
|
||||
type ConnectFunc = (self?: AgsProgressBar) => void;
|
||||
type OSD = {
|
||||
stack: AgsStack
|
||||
icon: string | IconProps
|
||||
info: {
|
||||
mod: GObject.Object
|
||||
signal?: string
|
||||
logic?(self: AgsProgressBar): void
|
||||
widget?: Widget
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default ({ stack, icon, info }: OSD) => {
|
||||
let connectFunc: ConnectFunc;
|
||||
|
||||
const osd = Box({
|
||||
css: `margin-bottom: ${Y_POS}px;`,
|
||||
children: [Box({
|
||||
class_name: 'osd',
|
||||
children: [
|
||||
Icon({
|
||||
hpack: 'start',
|
||||
// Can take a string or an object of props
|
||||
...(typeof icon === 'string' ? { icon } : icon),
|
||||
}),
|
||||
// Can take a static widget instead of a progressbar
|
||||
info.logic ?
|
||||
ProgressBar({ vpack: 'center' }) :
|
||||
info.widget,
|
||||
],
|
||||
})],
|
||||
});
|
||||
|
||||
// Handle requests to show the OSD
|
||||
// Different wether it's a bar or static
|
||||
if (info.logic) {
|
||||
connectFunc = (self) => new Promise<void>((r) => {
|
||||
if (info.logic && self) {
|
||||
info.logic(self);
|
||||
}
|
||||
r();
|
||||
}).then(() => stack.attribute.popup(osd));
|
||||
}
|
||||
else {
|
||||
connectFunc = () => stack.attribute.popup(osd);
|
||||
}
|
||||
|
||||
((osd.children[0] as AgsBox).children[1] as Connectable<AgsProgressBar>)
|
||||
.hook(info.mod, connectFunc, info.signal);
|
||||
|
||||
return osd;
|
||||
};
|
65
modules/ags/config/ts/osd/main.ts
Normal file
65
modules/ags/config/ts/osd/main.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import { Stack } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import PopupWindow from '../misc/popup.ts';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsStack from 'types/widgets/stack.ts';
|
||||
|
||||
// Import all the OSDs as an array
|
||||
const OSDList = [] as Array<(stack: AgsStack) => AgsBox>;
|
||||
|
||||
import * as Modules from './osds.ts';
|
||||
for (const osd in Modules) {
|
||||
OSDList.push(Modules[osd]);
|
||||
} // Array
|
||||
|
||||
const HIDE_DELAY = 2000;
|
||||
const transition_duration = 300;
|
||||
|
||||
|
||||
const OSDs = () => {
|
||||
const stack = Stack({
|
||||
transition: 'over_up_down',
|
||||
transition_duration,
|
||||
|
||||
attribute: { popup: () => {/**/} },
|
||||
});
|
||||
|
||||
// Send reference of stack to all items
|
||||
stack.items = OSDList.map((osd, i) => [`${i}`, osd(stack)]);
|
||||
|
||||
// Delay popup method so it
|
||||
// doesn't show any OSDs at launch
|
||||
timeout(1000, () => {
|
||||
let count = 0;
|
||||
|
||||
stack.attribute.popup = (osd: AgsBox) => {
|
||||
++count;
|
||||
stack.set_visible_child(osd);
|
||||
App.openWindow('osd');
|
||||
|
||||
timeout(HIDE_DELAY, () => {
|
||||
--count;
|
||||
|
||||
if (count === 0) {
|
||||
App.closeWindow('osd');
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return stack;
|
||||
};
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: 'osd',
|
||||
anchor: ['bottom'],
|
||||
exclusivity: 'ignore',
|
||||
close_on_unfocus: 'stay',
|
||||
transition: 'slide_up',
|
||||
transition_duration,
|
||||
bezier: 'ease',
|
||||
child: OSDs(),
|
||||
});
|
104
modules/ags/config/ts/osd/osds.ts
Normal file
104
modules/ags/config/ts/osd/osds.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import OSD from './ctor.ts';
|
||||
|
||||
import Brightness from '../../services/brightness.ts';
|
||||
import { SpeakerIcon } from '../misc/audio-icons.ts';
|
||||
import { MicIcon } from '../misc/audio-icons.ts';
|
||||
|
||||
const AUDIO_MAX = 1.5;
|
||||
|
||||
const ShowSpeaker = Variable(true);
|
||||
|
||||
globalThis.showSpeaker = () => {
|
||||
ShowSpeaker.value = !ShowSpeaker.value;
|
||||
};
|
||||
|
||||
// Types
|
||||
import AgsStack from 'types/widgets/stack.ts';
|
||||
|
||||
|
||||
export const SpeakerOSD = (stack: AgsStack) => OSD({
|
||||
stack,
|
||||
icon: { icon: SpeakerIcon.bind() },
|
||||
info: {
|
||||
mod: ShowSpeaker,
|
||||
|
||||
logic: (self) => {
|
||||
if (!Audio.speaker) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.value = Audio.speaker ?
|
||||
Audio.speaker.volume / AUDIO_MAX :
|
||||
0;
|
||||
|
||||
self.sensitive = !Audio.speaker?.stream.is_muted;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ScreenBrightnessOSD = (stack: AgsStack) => OSD({
|
||||
stack,
|
||||
icon: { icon: Brightness.bind('screenIcon') },
|
||||
info: {
|
||||
mod: Brightness,
|
||||
signal: 'screen',
|
||||
|
||||
logic: (self) => {
|
||||
self.value = Brightness.screen;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const KbdBrightnessOSD = (stack: AgsStack) => OSD({
|
||||
stack,
|
||||
icon: 'keyboard-brightness-symbolic',
|
||||
info: {
|
||||
mod: Brightness,
|
||||
signal: 'kbd',
|
||||
|
||||
logic: (self) => {
|
||||
if (!self.value) {
|
||||
self.value = Brightness.kbd / 2;
|
||||
|
||||
return;
|
||||
}
|
||||
self.value = Brightness.kbd / 2;
|
||||
self.sensitive = Brightness.kbd !== 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const MicOSD = (stack: AgsStack) => OSD({
|
||||
stack,
|
||||
icon: { icon: MicIcon.bind() },
|
||||
info: {
|
||||
mod: Audio,
|
||||
signal: 'microphone-changed',
|
||||
|
||||
logic: (self) => {
|
||||
if (!Audio.microphone) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.value = Audio.microphone ? Audio.microphone.volume : 0;
|
||||
self.sensitive = !Audio.microphone?.stream.is_muted;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const CapsLockOSD = (stack: AgsStack) => OSD({
|
||||
stack,
|
||||
icon: { icon: Brightness.bind('capsIcon') },
|
||||
info: {
|
||||
mod: Brightness,
|
||||
signal: 'caps',
|
||||
widget: Label({
|
||||
vpack: 'center',
|
||||
label: 'Caps Lock',
|
||||
}),
|
||||
},
|
||||
});
|
198
modules/ags/config/ts/overview/clients.ts
Normal file
198
modules/ags/config/ts/overview/clients.ts
Normal file
|
@ -0,0 +1,198 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Icon, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import { WindowButton } from './dragndrop.ts';
|
||||
import * as VARS from './variables.ts';
|
||||
|
||||
// Types
|
||||
import { Client as HyprClient } from 'types/service/hyprland.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsButton from 'types/widgets/button.ts';
|
||||
import AgsIcon from 'types/widgets/icon.ts';
|
||||
|
||||
const scale = (size: number) => (size * VARS.SCALE) - VARS.MARGIN;
|
||||
|
||||
const getFontSize = (client: HyprClient) => {
|
||||
const valX = scale(client.size[0]) * VARS.ICON_SCALE;
|
||||
const valY = scale(client.size[1]) * VARS.ICON_SCALE;
|
||||
|
||||
const size = Math.min(valX, valY);
|
||||
|
||||
return size <= 0 ? 0.1 : size;
|
||||
};
|
||||
|
||||
const IconStyle = (client: HyprClient) => `
|
||||
min-width: ${scale(client.size[0])}px;
|
||||
min-height: ${scale(client.size[1])}px;
|
||||
font-size: ${getFontSize(client)}px;
|
||||
`;
|
||||
|
||||
|
||||
const Client = (
|
||||
client: HyprClient,
|
||||
active: Boolean,
|
||||
clients: Array<HyprClient>,
|
||||
box: AgsBox,
|
||||
) => {
|
||||
const wsName = String(client.workspace.name).replace('special:', '');
|
||||
const wsId = client.workspace.id;
|
||||
const addr = `address:${client.address}`;
|
||||
|
||||
return Revealer({
|
||||
transition: 'crossfade',
|
||||
reveal_child: true,
|
||||
|
||||
attribute: {
|
||||
address: client.address,
|
||||
to_destroy: false,
|
||||
},
|
||||
|
||||
child: WindowButton({
|
||||
mainBox: box,
|
||||
address: client.address,
|
||||
|
||||
on_secondary_click_release: () => {
|
||||
Hyprland.sendMessage(`dispatch closewindow ${addr}`);
|
||||
},
|
||||
|
||||
on_primary_click_release: () => {
|
||||
if (wsId < 0) {
|
||||
if (client.workspace.name === 'special') {
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
`movetoworkspacesilent special:${wsId},${addr}`)
|
||||
.then(() => {
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
`togglespecialworkspace ${wsId}`)
|
||||
.then(() => {
|
||||
App.closeWindow('overview');
|
||||
}).catch(print);
|
||||
}).catch(print);
|
||||
}
|
||||
else {
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
`togglespecialworkspace ${wsName}`)
|
||||
.then(() => {
|
||||
App.closeWindow('overview');
|
||||
}).catch(print);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Close special workspace if one is opened
|
||||
const activeAddress = Hyprland.active.client.address;
|
||||
|
||||
const currentActive = clients.find((c) => {
|
||||
return c.address === activeAddress;
|
||||
});
|
||||
|
||||
if (currentActive && currentActive.workspace.id < 0) {
|
||||
const currentSpecial = `${currentActive.workspace.name}`
|
||||
.replace('special:', '');
|
||||
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
`togglespecialworkspace ${currentSpecial}`)
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
Hyprland.sendMessage(`dispatch focuswindow ${addr}`)
|
||||
.then(() => {
|
||||
App.closeWindow('overview');
|
||||
}).catch(print);
|
||||
}
|
||||
},
|
||||
|
||||
child: Icon({
|
||||
class_name: `window ${active ? 'active' : ''}`,
|
||||
css: `${IconStyle(client)} font-size: 10px;`,
|
||||
icon: client.class,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateClients = (box: AgsBox) => {
|
||||
Hyprland.sendMessage('j/clients').then((out) => {
|
||||
let clients = JSON.parse(out) as Array<HyprClient>;
|
||||
|
||||
clients = clients.filter((client) => client.class);
|
||||
|
||||
box.attribute.workspaces.forEach(
|
||||
(workspace: AgsRevealer) => {
|
||||
const fixed = workspace.attribute.get_fixed();
|
||||
const toRemove = fixed.get_children() as Array<AgsRevealer>;
|
||||
|
||||
clients.filter((client) =>
|
||||
client.workspace.id === workspace.attribute.id)
|
||||
.forEach((client) => {
|
||||
const active =
|
||||
client.address === Hyprland.active.client.address;
|
||||
|
||||
// TODO: see if this works on multi monitor setup
|
||||
const alloc = box.get_allocation();
|
||||
let monitor = box.get_display()
|
||||
.get_monitor_at_point(alloc.x, alloc.y);
|
||||
|
||||
monitor = Hyprland.monitors.find((mon) => {
|
||||
return mon.make === monitor.manufacturer &&
|
||||
mon.model === monitor.model;
|
||||
});
|
||||
|
||||
client.at[0] -= monitor.x;
|
||||
client.at[1] -= monitor.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() as Array<AgsRevealer>)
|
||||
.find((ch) =>
|
||||
ch.attribute.address === client.address),
|
||||
client.at[0] * VARS.SCALE,
|
||||
client.at[1] * VARS.SCALE,
|
||||
] as [AgsRevealer, number, number];
|
||||
|
||||
// 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 as AgsButton)
|
||||
.child as AgsIcon)
|
||||
.class_name = `window ${active}`;
|
||||
|
||||
((newClient[0].child as AgsButton)
|
||||
.child as AgsIcon).setCss(IconStyle(client));
|
||||
});
|
||||
});
|
||||
|
||||
fixed.show_all();
|
||||
toRemove.forEach((ch) => {
|
||||
if (ch.attribute.to_destroy) {
|
||||
ch.destroy();
|
||||
}
|
||||
else {
|
||||
ch.reveal_child = false;
|
||||
ch.attribute.to_destroy = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}).catch(print);
|
||||
};
|
50
modules/ags/config/ts/overview/current-workspace.ts
Normal file
50
modules/ags/config/ts/overview/current-workspace.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Box } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import * as VARS from './variables.ts';
|
||||
|
||||
const PADDING = 34;
|
||||
const MARGIN = 9;
|
||||
const DEFAULT_STYLE = `
|
||||
min-width: ${VARS.SCREEN.X * VARS.SCALE}px;
|
||||
min-height: ${(VARS.SCREEN.Y * VARS.SCALE) - (VARS.MARGIN / 2)}px;
|
||||
border-radius: 10px;
|
||||
`;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import AgsCenterBox from 'types/widgets/centerbox.ts';
|
||||
import AgsEventBox from 'types/widgets/eventbox.ts';
|
||||
|
||||
|
||||
export const Highlighter = () => Box({
|
||||
vpack: 'start',
|
||||
hpack: 'start',
|
||||
class_name: 'workspace active',
|
||||
css: DEFAULT_STYLE,
|
||||
});
|
||||
|
||||
export const updateCurrentWorkspace = (main: AgsBox, highlighter: AgsBox) => {
|
||||
const currentId = Hyprland.active.workspace.id;
|
||||
const row = Math.floor((currentId - 1) / VARS.WORKSPACE_PER_ROW);
|
||||
|
||||
const rowObject = (main.children[0] as AgsBox).children[row] as AgsRevealer;
|
||||
const workspaces = ((((rowObject.child as AgsCenterBox)
|
||||
.center_widget as AgsEventBox)
|
||||
.child as AgsBox)
|
||||
.get_children() as Array<AgsRevealer>)
|
||||
.filter((w) => w.reveal_child);
|
||||
|
||||
const currentIndex = workspaces.findIndex(
|
||||
(w) => w.attribute.id === currentId,
|
||||
);
|
||||
const left = currentIndex * ((VARS.SCREEN.X * VARS.SCALE) + 2 + PADDING);
|
||||
const height = row * ((VARS.SCREEN.Y * VARS.SCALE) + (PADDING / 2));
|
||||
|
||||
highlighter.setCss(`
|
||||
${DEFAULT_STYLE}
|
||||
margin-left: ${MARGIN + left}px;
|
||||
margin-top: ${MARGIN + height}px;
|
||||
`);
|
||||
};
|
119
modules/ags/config/ts/overview/dragndrop.ts
Normal file
119
modules/ags/config/ts/overview/dragndrop.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Button, EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import Cairo from 'cairo';
|
||||
const { Gtk, Gdk } = imports.gi;
|
||||
|
||||
import { updateClients } from './clients.ts';
|
||||
|
||||
const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsButton from 'types/widgets/button.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import { ButtonProps } from 'types/widgets/button.ts';
|
||||
import { EventBoxProps } from 'types/widgets/eventbox.ts';
|
||||
type WindowButtonType = ButtonProps & {
|
||||
address: string
|
||||
mainBox: AgsBox
|
||||
};
|
||||
|
||||
|
||||
const createSurfaceFromWidget = (widget: AgsButton) => {
|
||||
const alloc = widget.get_allocation();
|
||||
const surface = new Cairo.ImageSurface(
|
||||
Cairo.Format.ARGB32,
|
||||
alloc.width,
|
||||
alloc.height,
|
||||
);
|
||||
const cr = new Cairo.Context(surface);
|
||||
|
||||
cr.setSourceRGBA(255, 255, 255, 0);
|
||||
cr.rectangle(0, 0, alloc.width, alloc.height);
|
||||
cr.fill();
|
||||
widget.draw(cr);
|
||||
|
||||
return surface;
|
||||
};
|
||||
|
||||
let hidden = 0;
|
||||
|
||||
export const WorkspaceDrop = ({ ...props }: EventBoxProps) => 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() as AgsRevealer)?.attribute.id;
|
||||
|
||||
if (id < -1) {
|
||||
id = (self.get_parent() as AgsRevealer)?.attribute.name;
|
||||
}
|
||||
|
||||
else if (id === -1) {
|
||||
id = `special:${++hidden}`;
|
||||
}
|
||||
|
||||
else if (id === 1000) {
|
||||
id = 'empty';
|
||||
}
|
||||
|
||||
Hyprland.sendMessage('dispatch ' +
|
||||
`movetoworkspacesilent ${id},address:${data.get_text()}`)
|
||||
.catch(print);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const WindowButton = ({
|
||||
address,
|
||||
mainBox,
|
||||
...props
|
||||
}: WindowButtonType) => Button({
|
||||
...props,
|
||||
|
||||
setup: (self) => {
|
||||
self.drag_source_set(
|
||||
Gdk.ModifierType.BUTTON1_MASK,
|
||||
TARGET,
|
||||
Gdk.DragAction.COPY,
|
||||
);
|
||||
|
||||
self
|
||||
.on('drag-data-get', (_w, _c, data) => {
|
||||
data.set_text(address, address.length);
|
||||
})
|
||||
|
||||
.on('drag-begin', (_, context) => {
|
||||
Gtk.drag_set_icon_surface(
|
||||
context,
|
||||
createSurfaceFromWidget(self),
|
||||
);
|
||||
(self.get_parent() as AgsRevealer)?.set_reveal_child(false);
|
||||
})
|
||||
|
||||
.on('drag-end', () => {
|
||||
self.get_parent()?.destroy();
|
||||
|
||||
updateClients(mainBox);
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
});
|
152
modules/ags/config/ts/overview/main.ts
Normal file
152
modules/ags/config/ts/overview/main.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Box, Overlay, Window } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import { WorkspaceRow, getWorkspaces, updateWorkspaces } from './workspaces.ts';
|
||||
import { Highlighter, updateCurrentWorkspace } from './current-workspace.ts';
|
||||
import { updateClients } from './clients.ts';
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsOverlay from 'types/widgets/overlay.ts';
|
||||
|
||||
|
||||
// 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
|
||||
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,
|
||||
children: [
|
||||
WorkspaceRow('normal', 0),
|
||||
],
|
||||
}),
|
||||
|
||||
Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
WorkspaceRow('special', 0),
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(Hyprland, () => {
|
||||
if (!App.getWindow('overview')?.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.update();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const widget = Overlay({
|
||||
overlays: [highlighter, mainBox],
|
||||
|
||||
attribute: {
|
||||
get_child: () => mainBox,
|
||||
closing: false,
|
||||
},
|
||||
|
||||
// Make size of overlay big enough for content
|
||||
child: Box({
|
||||
class_name: 'overview',
|
||||
css: `
|
||||
min-height: ${mainBox.get_allocated_height()}px;
|
||||
min-width: ${mainBox.get_allocated_width()}px;
|
||||
`,
|
||||
}),
|
||||
|
||||
// TODO: throttle this?
|
||||
setup: (self) => {
|
||||
self.on('get-child-position', (_, ch) => {
|
||||
if (ch === mainBox && !self.attribute.closing) {
|
||||
(self.child as AgsBox).setCss(`
|
||||
transition: min-height 0.2s ease, min-width 0.2s ease;
|
||||
min-height: ${mainBox.get_allocated_height()}px;
|
||||
min-width: ${mainBox.get_allocated_width()}px;
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
// FIXME: can't use PopupWindow because this is an overlay already
|
||||
export default () => {
|
||||
const transition_duration = 800;
|
||||
const win = Window({
|
||||
name: 'overview',
|
||||
visible: false,
|
||||
|
||||
// Needs this to have space
|
||||
// allocated at the start
|
||||
child: Box({
|
||||
css: `
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
padding: 1px;
|
||||
`,
|
||||
}),
|
||||
|
||||
attribute: { close_on_unfocus: 'none' },
|
||||
|
||||
setup: (self) => {
|
||||
const name = 'overview';
|
||||
|
||||
Hyprland.sendMessage('[[BATCH]] ' +
|
||||
`keyword layerrule ignorealpha[0.97],${name}; ` +
|
||||
`keyword layerrule blur,${name}`);
|
||||
|
||||
self.hook(App, (_, currentName, isOpen) => {
|
||||
if (currentName === self.name) {
|
||||
if (isOpen) {
|
||||
self.child = Overview();
|
||||
self.show_all();
|
||||
|
||||
(self.child as AgsOverlay)
|
||||
.attribute.get_child().attribute.update();
|
||||
}
|
||||
else {
|
||||
(self.child as AgsOverlay).attribute.closing = true;
|
||||
|
||||
((self.child as AgsOverlay)
|
||||
.child as AgsBox).css = `
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
transition: all
|
||||
${transition_duration - 10}ms ease;
|
||||
`;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return win;
|
||||
};
|
14
modules/ags/config/ts/overview/variables.ts
Normal file
14
modules/ags/config/ts/overview/variables.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const SCALE = 0.11;
|
||||
export const ICON_SCALE = 0.8;
|
||||
export const MARGIN = 8;
|
||||
export const DEFAULT_SPECIAL = {
|
||||
SIZE_X: 1524,
|
||||
SIZE_Y: 908,
|
||||
POS_X: 197,
|
||||
POS_Y: 170,
|
||||
};
|
||||
export const WORKSPACE_PER_ROW = 6;
|
||||
export const SCREEN = {
|
||||
X: 1920,
|
||||
Y: 1200,
|
||||
};
|
201
modules/ags/config/ts/overview/workspaces.ts
Normal file
201
modules/ags/config/ts/overview/workspaces.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { Revealer, CenterBox, Box, EventBox, Fixed, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import { WorkspaceDrop } from './dragndrop.ts';
|
||||
import * as VARS from './variables.ts';
|
||||
|
||||
const EMPTY_OFFSET = 16;
|
||||
const DEFAULT_STYLE = `
|
||||
min-width: ${(VARS.SCREEN.X * VARS.SCALE) + EMPTY_OFFSET}px;
|
||||
min-height: ${VARS.SCREEN.Y * VARS.SCALE}px;
|
||||
`;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import AgsCenterBox from 'types/widgets/centerbox.ts';
|
||||
import AgsEventBox from 'types/widgets/eventbox.ts';
|
||||
|
||||
|
||||
export const getWorkspaces = (box: AgsBox) => {
|
||||
const children = [] as Array<AgsRevealer>;
|
||||
|
||||
(box.children as Array<AgsBox>).forEach((type) => {
|
||||
(type.children as Array<AgsRevealer>).forEach(
|
||||
(row) => {
|
||||
((((row.child as AgsCenterBox)
|
||||
?.center_widget as AgsEventBox)
|
||||
?.child as AgsBox)
|
||||
.children as Array<AgsRevealer>)
|
||||
.forEach((workspace) => {
|
||||
children.push(workspace);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
box.attribute.workspaces = children.sort((a, b) => {
|
||||
return a.attribute.id - b.attribute.id;
|
||||
});
|
||||
};
|
||||
|
||||
const Workspace = (id: number, name: string, normal = true) => {
|
||||
const fixed = Fixed({});
|
||||
|
||||
const workspace = Revealer({
|
||||
transition: 'slide_right',
|
||||
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.attribute.id;
|
||||
const ws = Hyprland.getWorkspace(self.attribute.id);
|
||||
|
||||
self.reveal_child =
|
||||
(ws?.windows && ws.windows > 0) || active;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
child: WorkspaceDrop({
|
||||
child: Box({
|
||||
class_name: 'workspace',
|
||||
css: normal ?
|
||||
|
||||
DEFAULT_STYLE :
|
||||
|
||||
`
|
||||
min-width: ${(VARS.SCREEN.X * VARS.SCALE / 2) +
|
||||
EMPTY_OFFSET}px;
|
||||
min-height: ${VARS.SCREEN.Y * VARS.SCALE}px;
|
||||
`,
|
||||
|
||||
children: normal ?
|
||||
|
||||
[fixed] :
|
||||
|
||||
[
|
||||
fixed,
|
||||
Label({
|
||||
label: ' +',
|
||||
css: 'font-size: 40px;',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
return workspace;
|
||||
};
|
||||
|
||||
export const WorkspaceRow = (class_name: string, i: number) => {
|
||||
const addWorkspace = Workspace(
|
||||
class_name === 'special' ? -1 : 1000,
|
||||
class_name === 'special' ? 'special' : '',
|
||||
false,
|
||||
);
|
||||
|
||||
return Revealer({
|
||||
transition: 'slide_down',
|
||||
hpack: class_name === 'special' ? 'fill' : 'start',
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(Hyprland, (rev) => {
|
||||
const minId = i * VARS.WORKSPACE_PER_ROW;
|
||||
const activeId = Hyprland.active.workspace.id;
|
||||
|
||||
const rowExists = Hyprland.workspaces.some((ws) => {
|
||||
const isInRow = ws.id > minId;
|
||||
const hasClients = ws.windows > 0;
|
||||
const isActive = ws.id === activeId;
|
||||
|
||||
return isInRow && (hasClients || isActive);
|
||||
});
|
||||
|
||||
rev.reveal_child = rowExists;
|
||||
});
|
||||
},
|
||||
|
||||
child: CenterBox({
|
||||
center_widget: EventBox({
|
||||
setup: (self) => {
|
||||
self.hook(Hyprland, () => {
|
||||
const maxId = (i + 1) * VARS.WORKSPACE_PER_ROW;
|
||||
const activeId = Hyprland.active.workspace.id;
|
||||
|
||||
const isSpecial = class_name === 'special';
|
||||
const nextRowExists = Hyprland.workspaces.some((ws) => {
|
||||
const isInNextRow = ws.id > maxId;
|
||||
const hasClients = ws.windows > 0;
|
||||
const isActive = ws.id === activeId;
|
||||
|
||||
return isInNextRow && (hasClients || isActive);
|
||||
});
|
||||
|
||||
addWorkspace.reveal_child = isSpecial || !nextRowExists;
|
||||
});
|
||||
},
|
||||
|
||||
child: Box({
|
||||
class_name,
|
||||
children: [addWorkspace],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateWorkspaces = (box: AgsBox) => {
|
||||
Hyprland.workspaces.forEach((ws) => {
|
||||
const currentWs = (box.attribute.workspaces as Array<AgsRevealer>).find(
|
||||
(ch) => ch.attribute.id === ws.id,
|
||||
);
|
||||
|
||||
if (!currentWs) {
|
||||
let type = 0;
|
||||
let rowNo = 0;
|
||||
|
||||
if (ws.id < 0) {
|
||||
// This means it's a special workspace
|
||||
type = 1;
|
||||
}
|
||||
else {
|
||||
rowNo = Math.floor((ws.id - 1) / VARS.WORKSPACE_PER_ROW);
|
||||
const wsRow = box.children[type] as AgsBox;
|
||||
const wsQty = wsRow.children.length;
|
||||
|
||||
if (rowNo >= wsQty) {
|
||||
for (let i = wsQty; i <= rowNo; ++i) {
|
||||
wsRow.add(WorkspaceRow(
|
||||
type ? 'special' : 'normal', i,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
const row = ((((box.children[type] as AgsBox)
|
||||
.children[rowNo] as AgsRevealer)
|
||||
.child as AgsCenterBox)
|
||||
.center_widget as AgsEventBox)
|
||||
.child as AgsBox;
|
||||
|
||||
row.add(Workspace(ws.id, type ? ws.name : ''));
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the order is correct
|
||||
box.attribute.workspaces.forEach(
|
||||
(workspace: AgsRevealer, i: number) => {
|
||||
(workspace?.get_parent() as AgsBox)
|
||||
?.reorder_child(workspace, i);
|
||||
},
|
||||
);
|
||||
box.show_all();
|
||||
};
|
48
modules/ags/config/ts/powermenu.ts
Normal file
48
modules/ags/config/ts/powermenu.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
import { CenterBox, Label } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import PopupWindow from './misc/popup.ts';
|
||||
import CursorBox from './misc/cursorbox.ts';
|
||||
|
||||
|
||||
const PowermenuWidget = () => CenterBox({
|
||||
class_name: 'powermenu',
|
||||
vertical: false,
|
||||
|
||||
start_widget: CursorBox({
|
||||
class_name: 'shutdown button',
|
||||
on_primary_click_release: () => execAsync(['systemctl', 'poweroff'])
|
||||
.catch(print),
|
||||
|
||||
child: Label({
|
||||
label: '襤',
|
||||
}),
|
||||
}),
|
||||
|
||||
center_widget: CursorBox({
|
||||
class_name: 'reboot button',
|
||||
on_primary_click_release: () => execAsync(['systemctl', 'reboot'])
|
||||
.catch(print),
|
||||
|
||||
child: Label({
|
||||
label: '勒',
|
||||
}),
|
||||
}),
|
||||
|
||||
end_widget: CursorBox({
|
||||
class_name: 'logout button',
|
||||
on_primary_click_release: () => Hyprland.sendMessage('dispatch exit')
|
||||
.catch(print),
|
||||
|
||||
child: Label({
|
||||
label: '',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: 'powermenu',
|
||||
child: PowermenuWidget(),
|
||||
});
|
215
modules/ags/config/ts/quick-settings/bluetooth.ts
Normal file
215
modules/ags/config/ts/quick-settings/bluetooth.ts
Normal file
|
@ -0,0 +1,215 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
|
||||
|
||||
import { Box, Icon, Label, ListBox, Overlay, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
const SCROLL_THRESH_H = 200;
|
||||
const SCROLL_THRESH_N = 7;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsScrollable from 'types/widgets/scrollable.ts';
|
||||
type ListBoxRow = typeof imports.gi.Gtk.ListBoxRow;
|
||||
import { BluetoothDevice as BTDev } from 'types/service/bluetooth.ts';
|
||||
|
||||
|
||||
const BluetoothDevice = (dev: BTDev) => Box({
|
||||
class_name: 'menu-item',
|
||||
|
||||
attribute: { dev },
|
||||
|
||||
children: [Revealer({
|
||||
reveal_child: true,
|
||||
transition: 'slide_down',
|
||||
|
||||
child: CursorBox({
|
||||
on_primary_click_release: () => dev.setConnection(true),
|
||||
|
||||
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`,
|
||||
class_name: 'scrolled-indicator',
|
||||
size: 16,
|
||||
css: '-gtk-icon-transform: rotate(180deg);',
|
||||
}),
|
||||
});
|
||||
|
||||
const bottomArrow = Revealer({
|
||||
transition: 'slide_up',
|
||||
|
||||
child: Icon({
|
||||
icon: `${App.configDir }/icons/down-large.svg`,
|
||||
class_name: 'scrolled-indicator',
|
||||
size: 16,
|
||||
}),
|
||||
});
|
||||
|
||||
return Overlay({
|
||||
pass_through: true,
|
||||
|
||||
overlays: [
|
||||
Box({
|
||||
vpack: 'start',
|
||||
hpack: 'center',
|
||||
css: 'margin-top: 12px',
|
||||
children: [topArrow],
|
||||
}),
|
||||
|
||||
Box({
|
||||
vpack: 'end',
|
||||
hpack: 'center',
|
||||
css: 'margin-bottom: 12px',
|
||||
children: [bottomArrow],
|
||||
}),
|
||||
],
|
||||
|
||||
child: Box({
|
||||
class_name: 'menu',
|
||||
|
||||
child: Scrollable({
|
||||
hscroll: 'never',
|
||||
vscroll: 'never',
|
||||
|
||||
setup: (self) => {
|
||||
self.on('edge-reached', (_, pos) => {
|
||||
// Manage scroll indicators
|
||||
if (pos === 2) {
|
||||
topArrow.reveal_child = false;
|
||||
bottomArrow.reveal_child = true;
|
||||
}
|
||||
else if (pos === 3) {
|
||||
topArrow.reveal_child = true;
|
||||
bottomArrow.reveal_child = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
child: ListBox({
|
||||
setup: (self) => {
|
||||
self.set_sort_func((a, b) => {
|
||||
const bState = (b.get_children()[0] as AgsBox)
|
||||
.attribute.dev.paired;
|
||||
|
||||
const aState = (a.get_children()[0] as AgsBox)
|
||||
.attribute.dev.paired;
|
||||
|
||||
return bState - aState;
|
||||
});
|
||||
|
||||
self.hook(Bluetooth, () => {
|
||||
// Get all devices
|
||||
const Devices = Bluetooth.devices.concat(
|
||||
Bluetooth.connected_devices,
|
||||
);
|
||||
|
||||
// Add missing devices
|
||||
Devices.forEach((dev) => {
|
||||
if (!DevList.has(dev) && dev.name) {
|
||||
DevList.set(dev, BluetoothDevice(dev));
|
||||
|
||||
self.add(DevList.get(dev));
|
||||
self.show_all();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete ones that don't exist anymore
|
||||
const difference = Array.from(DevList.keys())
|
||||
.filter((dev) => !Devices
|
||||
.find((d) => dev === d) &&
|
||||
dev.name);
|
||||
|
||||
difference.forEach((dev) => {
|
||||
const devWidget = DevList.get(dev);
|
||||
|
||||
if (devWidget) {
|
||||
if (devWidget.toDestroy) {
|
||||
devWidget.get_parent().destroy();
|
||||
DevList.delete(dev);
|
||||
}
|
||||
else {
|
||||
devWidget.child.reveal_child = false;
|
||||
devWidget.toDestroy = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start scrolling after a specified height
|
||||
// is reached by the children
|
||||
const height = Math.max(
|
||||
self.get_parent()?.get_allocated_height() || 0,
|
||||
SCROLL_THRESH_H,
|
||||
);
|
||||
|
||||
const scroll = (self.get_parent() as ListBoxRow)
|
||||
?.get_parent() as AgsScrollable;
|
||||
|
||||
if (scroll) {
|
||||
const n_child = self.get_children().length;
|
||||
|
||||
if (n_child > SCROLL_THRESH_N) {
|
||||
scroll.vscroll = 'always';
|
||||
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 {
|
||||
scroll.vscroll = 'never';
|
||||
scroll.setCss('');
|
||||
topArrow.reveal_child = false;
|
||||
bottomArrow.reveal_child = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger sort_func
|
||||
(self.get_children() as Array<ListBoxRow>)
|
||||
.forEach((ch) => {
|
||||
ch.changed();
|
||||
});
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
343
modules/ags/config/ts/quick-settings/button-grid.ts
Normal file
343
modules/ags/config/ts/quick-settings/button-grid.ts
Normal file
|
@ -0,0 +1,343 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
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';
|
||||
|
||||
import { Box, Icon, Label, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import { SpeakerIcon, MicIcon } from '../misc/audio-icons.ts';
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
import Separator from '../misc/separator.ts';
|
||||
|
||||
import { NetworkMenu } from './network.ts';
|
||||
import { BluetoothMenu } from './bluetooth.ts';
|
||||
|
||||
// Types
|
||||
import { GObject } from 'gi://GObject';
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsIcon from 'types/widgets/icon.ts';
|
||||
import AgsLabel from 'types/widgets/label.ts';
|
||||
import AgsRevealer from 'types/widgets/revealer.ts';
|
||||
import { Variable as Var } from 'types/variable.ts';
|
||||
type IconTuple = [
|
||||
GObject.Object,
|
||||
(self: AgsIcon) => void,
|
||||
signal?: string,
|
||||
];
|
||||
type IndicatorTuple = [
|
||||
GObject.Object,
|
||||
(self: AgsLabel) => void,
|
||||
signal?: string,
|
||||
];
|
||||
type GridButtonType = {
|
||||
command?(): void
|
||||
secondary_command?(): void
|
||||
on_open?(menu: AgsRevealer): void
|
||||
icon: string | IconTuple
|
||||
indicator?: IndicatorTuple
|
||||
menu?: any
|
||||
};
|
||||
|
||||
const SPACING = 28;
|
||||
const ButtonStates = [] as Array<Var<any>>;
|
||||
|
||||
|
||||
const GridButton = ({
|
||||
command = () => {/**/},
|
||||
secondary_command = () => {/**/},
|
||||
on_open = () => {/**/},
|
||||
icon,
|
||||
indicator,
|
||||
menu,
|
||||
}: GridButtonType) => {
|
||||
const Activated = Variable(false);
|
||||
|
||||
ButtonStates.push(Activated);
|
||||
let iconWidget = Icon();
|
||||
let indicatorWidget = Label();
|
||||
|
||||
// Allow setting icon dynamically or statically
|
||||
if (typeof icon === 'string') {
|
||||
iconWidget = Icon({
|
||||
class_name: 'grid-label',
|
||||
icon,
|
||||
setup: (self) => {
|
||||
self.hook(Activated, () => {
|
||||
self.setCss(`color: ${Activated.value ?
|
||||
'rgba(189, 147, 249, 0.8)' :
|
||||
'unset'};`);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (Array.isArray(icon)) {
|
||||
iconWidget = Icon({
|
||||
class_name: 'grid-label',
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(...icon)
|
||||
.hook(Activated, () => {
|
||||
self.setCss(`color: ${Activated.value ?
|
||||
'rgba(189, 147, 249, 0.8)' :
|
||||
'unset'};`);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (indicator) {
|
||||
indicatorWidget = Label({
|
||||
class_name: 'sub-label',
|
||||
justification: 'left',
|
||||
truncate: 'end',
|
||||
max_width_chars: 12,
|
||||
setup: (self) => {
|
||||
self.hook(...indicator);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (menu) {
|
||||
menu = Revealer({
|
||||
transition: 'slide_down',
|
||||
child: menu,
|
||||
reveal_child: Activated.bind(),
|
||||
});
|
||||
}
|
||||
|
||||
const widget = Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Box({
|
||||
class_name: 'grid-button',
|
||||
children: [
|
||||
|
||||
CursorBox({
|
||||
class_name: 'left-part',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
if (Activated.value) {
|
||||
secondary_command();
|
||||
}
|
||||
else {
|
||||
command();
|
||||
}
|
||||
},
|
||||
|
||||
child: iconWidget,
|
||||
}),
|
||||
|
||||
CursorBox({
|
||||
class_name: 'right-part',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
ButtonStates.forEach((state) => {
|
||||
if (state !== Activated) {
|
||||
state.value = false;
|
||||
}
|
||||
});
|
||||
Activated.value = !Activated.value;
|
||||
},
|
||||
|
||||
on_hover: (self) => {
|
||||
if (menu) {
|
||||
const rowMenu =
|
||||
((((self.get_parent() as AgsBox)
|
||||
?.get_parent() as AgsBox)
|
||||
?.get_parent() as AgsBox)
|
||||
?.get_parent() as AgsBox)
|
||||
?.children[1] as AgsBox;
|
||||
|
||||
const isSetup = (rowMenu
|
||||
.get_children() as Array<AgsBox>)
|
||||
.find((ch) => ch === menu);
|
||||
|
||||
if (!isSetup) {
|
||||
rowMenu.add(menu);
|
||||
rowMenu.show_all();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
child: Icon({
|
||||
icon: `${App.configDir }/icons/down-large.svg`,
|
||||
class_name: 'grid-chev',
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(Activated, () => {
|
||||
let deg = 270;
|
||||
|
||||
if (Activated.value) {
|
||||
deg = menu ? 360 : 450;
|
||||
on_open(menu);
|
||||
}
|
||||
self.setCss(`
|
||||
-gtk-icon-transform: rotate(${deg}deg);
|
||||
`);
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
],
|
||||
}),
|
||||
indicatorWidget,
|
||||
],
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
const Row = ({ buttons }) => {
|
||||
const child = Box({
|
||||
class_name: 'button-row',
|
||||
hpack: 'center',
|
||||
});
|
||||
|
||||
const widget = Box({
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
child,
|
||||
Box({ vertical: true }),
|
||||
],
|
||||
});
|
||||
|
||||
for (let i = 0; i < buttons.length; ++i) {
|
||||
if (i === buttons.length - 1) {
|
||||
child.add(buttons[i]);
|
||||
}
|
||||
else {
|
||||
child.add(buttons[i]);
|
||||
child.add(Separator(SPACING));
|
||||
}
|
||||
}
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
const FirstRow = () => Row({
|
||||
buttons: [
|
||||
|
||||
GridButton({
|
||||
command: () => Network.toggleWifi(),
|
||||
|
||||
secondary_command: () => {
|
||||
// TODO: connection editor
|
||||
},
|
||||
|
||||
icon: [Network, (self) => {
|
||||
self.icon = Network.wifi?.icon_name;
|
||||
}],
|
||||
|
||||
indicator: [Network, (self) => {
|
||||
self.label = Network.wifi?.ssid || Network.wired?.internet;
|
||||
}],
|
||||
|
||||
menu: NetworkMenu(),
|
||||
on_open: () => Network.wifi.scan(),
|
||||
}),
|
||||
|
||||
// TODO: do vpn
|
||||
GridButton({
|
||||
command: () => {
|
||||
//
|
||||
},
|
||||
|
||||
secondary_command: () => {
|
||||
//
|
||||
},
|
||||
|
||||
icon: 'airplane-mode-disabled-symbolic',
|
||||
}),
|
||||
|
||||
GridButton({
|
||||
command: () => Bluetooth.toggle(),
|
||||
|
||||
secondary_command: () => {
|
||||
// TODO: bluetooth connection editor
|
||||
},
|
||||
|
||||
icon: [Bluetooth, (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.connected_devices[0] ?
|
||||
`${Bluetooth.connected_devices[0]}` :
|
||||
'Disconnected';
|
||||
}, 'notify::connected-devices'],
|
||||
|
||||
menu: BluetoothMenu(),
|
||||
on_open: (menu) => {
|
||||
execAsync(`bluetoothctl scan ${menu.reveal_child ?
|
||||
'on' :
|
||||
'off'}`).catch(print);
|
||||
},
|
||||
}),
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
const SecondRow = () => Row({
|
||||
buttons: [
|
||||
GridButton({
|
||||
command: () => {
|
||||
execAsync(['pactl', 'set-sink-mute',
|
||||
'@DEFAULT_SINK@', 'toggle']).catch(print);
|
||||
},
|
||||
|
||||
secondary_command: () => {
|
||||
execAsync(['bash', '-c', 'pavucontrol'])
|
||||
.catch(print);
|
||||
},
|
||||
|
||||
icon: [SpeakerIcon, (self) => {
|
||||
self.icon = SpeakerIcon.value;
|
||||
}],
|
||||
}),
|
||||
|
||||
GridButton({
|
||||
command: () => {
|
||||
execAsync(['pactl', 'set-source-mute',
|
||||
'@DEFAULT_SOURCE@', 'toggle']).catch(print);
|
||||
},
|
||||
|
||||
secondary_command: () => {
|
||||
execAsync(['bash', '-c', 'pavucontrol'])
|
||||
.catch(print);
|
||||
},
|
||||
|
||||
icon: [MicIcon, (self) => {
|
||||
self.icon = MicIcon.value;
|
||||
}],
|
||||
}),
|
||||
|
||||
GridButton({
|
||||
command: () => {
|
||||
execAsync(['lock']).catch(print);
|
||||
},
|
||||
secondary_command: () => App.openWindow('powermenu'),
|
||||
icon: 'system-lock-screen-symbolic',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default () => Box({
|
||||
class_name: 'button-grid',
|
||||
vertical: true,
|
||||
hpack: 'center',
|
||||
children: [
|
||||
FirstRow(),
|
||||
Separator(10, { vertical: true }),
|
||||
SecondRow(),
|
||||
],
|
||||
});
|
58
modules/ags/config/ts/quick-settings/main.ts
Normal file
58
modules/ags/config/ts/quick-settings/main.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { Box, Label, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
import ButtonGrid from './button-grid.ts';
|
||||
import SliderBox from './slider-box.ts';
|
||||
import Player from '../media-player/player.ts';
|
||||
import PopupWindow from '../misc/popup.ts';
|
||||
import ToggleButton from './toggle-button.ts';
|
||||
|
||||
|
||||
const QuickSettingsWidget = () => {
|
||||
const rev = Revealer({
|
||||
transition: 'slide_down',
|
||||
child: Player(),
|
||||
});
|
||||
|
||||
return Box({
|
||||
class_name: 'qs-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
|
||||
Box({
|
||||
class_name: 'quick-settings',
|
||||
vertical: true,
|
||||
children: [
|
||||
|
||||
Label({
|
||||
label: 'Control Center',
|
||||
class_name: 'title',
|
||||
hpack: 'start',
|
||||
css: `
|
||||
margin-left: 20px;
|
||||
margin-bottom: 30px;
|
||||
`,
|
||||
}),
|
||||
|
||||
ButtonGrid(),
|
||||
|
||||
SliderBox(),
|
||||
|
||||
ToggleButton(rev),
|
||||
|
||||
],
|
||||
}),
|
||||
|
||||
rev,
|
||||
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const TOP_MARGIN = 6;
|
||||
|
||||
export default () => PopupWindow({
|
||||
name: 'quick-settings',
|
||||
anchor: ['top', 'right'],
|
||||
margins: [TOP_MARGIN, 0, 0, 0],
|
||||
child: QuickSettingsWidget(),
|
||||
});
|
251
modules/ags/config/ts/quick-settings/network.ts
Normal file
251
modules/ags/config/ts/quick-settings/network.ts
Normal file
|
@ -0,0 +1,251 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
|
||||
import { Box, Icon, Label, ListBox, Overlay, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
const SCROLL_THRESH_H = 200;
|
||||
const SCROLL_THRESH_N = 7;
|
||||
|
||||
// Types
|
||||
import AgsBox from 'types/widgets/box.ts';
|
||||
import AgsScrollable from 'types/widgets/scrollable.ts';
|
||||
type ListBoxRow = typeof imports.gi.Gtk.ListBoxRow;
|
||||
type APType = {
|
||||
bssid: string
|
||||
address: string
|
||||
lastSeen: number
|
||||
ssid: string
|
||||
active: boolean
|
||||
strength: number
|
||||
iconName: string
|
||||
};
|
||||
|
||||
|
||||
const AccessPoint = (ap: APType) => {
|
||||
const widget = Box({
|
||||
class_name: 'menu-item',
|
||||
attribute: {
|
||||
ap: Variable(ap),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const child = Box({
|
||||
hexpand: true,
|
||||
children: [
|
||||
Icon().hook(widget.attribute.ap, (self) => {
|
||||
self.icon = widget.attribute.ap.value.iconName;
|
||||
}),
|
||||
|
||||
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.attribute.ap.value.ssid ===
|
||||
Network.wifi.ssid ?
|
||||
'1' :
|
||||
'0'
|
||||
};
|
||||
`,
|
||||
);
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
widget.add(Revealer({
|
||||
reveal_child: true,
|
||||
transition: 'slide_down',
|
||||
|
||||
child: CursorBox({
|
||||
on_primary_click_release: () => {
|
||||
execAsync(`nmcli device wifi connect
|
||||
${widget.attribute.ap.value.bssid}`).catch(print);
|
||||
},
|
||||
child,
|
||||
}),
|
||||
}));
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
export const NetworkMenu = () => {
|
||||
const APList = new Map();
|
||||
|
||||
const topArrow = Revealer({
|
||||
transition: 'slide_down',
|
||||
|
||||
child: Icon({
|
||||
icon: `${App.configDir }/icons/down-large.svg`,
|
||||
class_name: 'scrolled-indicator',
|
||||
size: 16,
|
||||
css: '-gtk-icon-transform: rotate(180deg);',
|
||||
}),
|
||||
});
|
||||
|
||||
const bottomArrow = Revealer({
|
||||
transition: 'slide_up',
|
||||
|
||||
child: Icon({
|
||||
icon: `${App.configDir }/icons/down-large.svg`,
|
||||
class_name: 'scrolled-indicator',
|
||||
size: 16,
|
||||
}),
|
||||
});
|
||||
|
||||
return Overlay({
|
||||
pass_through: true,
|
||||
|
||||
overlays: [
|
||||
Box({
|
||||
vpack: 'start',
|
||||
hpack: 'center',
|
||||
css: 'margin-top: 12px',
|
||||
children: [topArrow],
|
||||
}),
|
||||
|
||||
Box({
|
||||
vpack: 'end',
|
||||
hpack: 'center',
|
||||
css: 'margin-bottom: 12px',
|
||||
children: [bottomArrow],
|
||||
}),
|
||||
],
|
||||
|
||||
child: Box({
|
||||
class_name: 'menu',
|
||||
|
||||
child: Scrollable({
|
||||
hscroll: 'never',
|
||||
vscroll: 'never',
|
||||
|
||||
setup: (self) => {
|
||||
self.on('edge-reached', (_, pos) => {
|
||||
// Manage scroll indicators
|
||||
if (pos === 2) {
|
||||
topArrow.reveal_child = false;
|
||||
bottomArrow.reveal_child = true;
|
||||
}
|
||||
else if (pos === 3) {
|
||||
topArrow.reveal_child = true;
|
||||
bottomArrow.reveal_child = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
child: ListBox({
|
||||
setup: (self) => {
|
||||
self.set_sort_func((a, b) => {
|
||||
const bState = (b.get_children()[0] as AgsBox)
|
||||
.attribute.ap.value.strength;
|
||||
|
||||
const aState = (a.get_children()[0] as AgsBox)
|
||||
.attribute.ap.value.strength;
|
||||
|
||||
return bState - aState;
|
||||
});
|
||||
|
||||
self.hook(Network, () => {
|
||||
// Add missing APs
|
||||
const currentAPs = Network.wifi
|
||||
?.access_points as Array<APType>;
|
||||
|
||||
currentAPs.forEach((ap) => {
|
||||
if (ap.ssid !== 'Unknown') {
|
||||
if (APList.has(ap.ssid)) {
|
||||
const accesPoint = APList.get(ap.ssid)
|
||||
.attribute.ap.value;
|
||||
|
||||
if (accesPoint.strength < ap.strength) {
|
||||
APList.get(ap.ssid).attribute
|
||||
.ap.value = ap;
|
||||
}
|
||||
}
|
||||
else {
|
||||
APList.set(ap.ssid, AccessPoint(ap));
|
||||
|
||||
self.add(APList.get(ap.ssid));
|
||||
self.show_all();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Delete ones that don't exist anymore
|
||||
const difference = Array.from(APList.keys())
|
||||
.filter((ssid) => !Network.wifi.access_points
|
||||
.find((ap) => ap.ssid === ssid) &&
|
||||
ssid !== 'Unknown');
|
||||
|
||||
difference.forEach((ssid) => {
|
||||
const apWidget = APList.get(ssid);
|
||||
|
||||
if (apWidget) {
|
||||
if (apWidget.toDestroy) {
|
||||
apWidget.get_parent().destroy();
|
||||
APList.delete(ssid);
|
||||
}
|
||||
else {
|
||||
apWidget.child.reveal_child = false;
|
||||
apWidget.toDestroy = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start scrolling after a specified height
|
||||
// is reached by the children
|
||||
const height = Math.max(
|
||||
self.get_parent()?.get_allocated_height() || 0,
|
||||
SCROLL_THRESH_H,
|
||||
);
|
||||
|
||||
const scroll = (self.get_parent() as ListBoxRow)
|
||||
?.get_parent() as AgsScrollable;
|
||||
|
||||
if (scroll) {
|
||||
const n_child = self.get_children().length;
|
||||
|
||||
if (n_child > SCROLL_THRESH_N) {
|
||||
scroll.vscroll = 'always';
|
||||
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 {
|
||||
scroll.vscroll = 'never';
|
||||
scroll.setCss('');
|
||||
topArrow.reveal_child = false;
|
||||
bottomArrow.reveal_child = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger sort_func
|
||||
(self.get_children() as Array<ListBoxRow>)
|
||||
.forEach((ch) => {
|
||||
ch.changed();
|
||||
});
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
143
modules/ags/config/ts/quick-settings/slider-box.ts
Normal file
143
modules/ags/config/ts/quick-settings/slider-box.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
|
||||
|
||||
import { Box, Slider, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
const { Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
import Brightness from '../../services/brightness.ts';
|
||||
import { SpeakerIcon } from '../misc/audio-icons.ts';
|
||||
|
||||
|
||||
export default () => Box({
|
||||
class_name: 'slider-box',
|
||||
vertical: true,
|
||||
hpack: 'center',
|
||||
children: [
|
||||
|
||||
Box({
|
||||
class_name: 'slider',
|
||||
vpack: 'start',
|
||||
hpack: 'center',
|
||||
|
||||
children: [
|
||||
Icon({
|
||||
size: 26,
|
||||
class_name: 'slider-label',
|
||||
icon: SpeakerIcon.bind(),
|
||||
}),
|
||||
|
||||
Slider({
|
||||
vpack: 'center',
|
||||
max: 0.999,
|
||||
draw_value: false,
|
||||
|
||||
on_change: ({ value }) => {
|
||||
if (Audio.speaker) {
|
||||
Audio.speaker.volume = value;
|
||||
}
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Audio, () => {
|
||||
self.value = Audio.speaker?.volume || 0;
|
||||
}, 'speaker-changed')
|
||||
|
||||
// OnClick
|
||||
.on('button-press-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grabbing',
|
||||
));
|
||||
})
|
||||
|
||||
// OnRelease
|
||||
.on('button-release-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
Box({
|
||||
class_name: 'slider',
|
||||
vpack: 'start',
|
||||
hpack: 'center',
|
||||
|
||||
children: [
|
||||
Icon({
|
||||
class_name: 'slider-label',
|
||||
icon: Brightness.bind('screenIcon'),
|
||||
}),
|
||||
|
||||
Slider({
|
||||
vpack: 'center',
|
||||
draw_value: false,
|
||||
|
||||
on_change: ({ value }) => {
|
||||
Brightness.screen = value;
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Brightness, () => {
|
||||
self.value = Brightness.screen;
|
||||
}, 'screen')
|
||||
|
||||
// OnClick
|
||||
.on('button-press-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grabbing',
|
||||
));
|
||||
})
|
||||
|
||||
// OnRelease
|
||||
.on('button-release-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
],
|
||||
});
|
64
modules/ags/config/ts/quick-settings/toggle-button.ts
Normal file
64
modules/ags/config/ts/quick-settings/toggle-button.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
|
||||
|
||||
import { CenterBox, Icon, ToggleButton } from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
|
||||
// Types
|
||||
import AgsRevealer from 'types/widgets/revealer';
|
||||
|
||||
const { Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
|
||||
export default (rev: AgsRevealer) => {
|
||||
const child = Icon({
|
||||
icon: `${App.configDir}/icons/down-large.svg`,
|
||||
class_name: 'arrow',
|
||||
css: '-gtk-icon-transform: rotate(180deg);',
|
||||
});
|
||||
|
||||
const button = CenterBox({
|
||||
center_widget: ToggleButton({
|
||||
setup: (self) => {
|
||||
// Open at startup if there are players
|
||||
const id = Mpris.connect('changed', () => {
|
||||
self.set_active(Mpris.players.length > 0);
|
||||
Mpris.disconnect(id);
|
||||
});
|
||||
|
||||
self
|
||||
.on('toggled', () => {
|
||||
if (self.get_active()) {
|
||||
child
|
||||
.setCss('-gtk-icon-transform: rotate(0deg);');
|
||||
rev.reveal_child = true;
|
||||
}
|
||||
else {
|
||||
child
|
||||
.setCss('-gtk-icon-transform: rotate(180deg);');
|
||||
rev.reveal_child = false;
|
||||
}
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
|
||||
child,
|
||||
}),
|
||||
});
|
||||
|
||||
return button;
|
||||
};
|
67
modules/ags/config/ts/setup.ts
Normal file
67
modules/ags/config/ts/setup.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
|
||||
|
||||
import Brightness from '../services/brightness.ts';
|
||||
import Pointers from '../services/pointers.ts';
|
||||
import Tablet from '../services/tablet.ts';
|
||||
import TouchGestures from '../services/touch-gestures.ts';
|
||||
|
||||
import closeAll from './misc/closer.ts';
|
||||
import Persist from './misc/persist.ts';
|
||||
|
||||
|
||||
export default () => {
|
||||
globalThis.Brightness = Brightness;
|
||||
globalThis.Pointers = Pointers;
|
||||
globalThis.Tablet = Tablet;
|
||||
globalThis.closeAll = closeAll;
|
||||
|
||||
Persist({
|
||||
name: 'bluetooth',
|
||||
gobject: Bluetooth,
|
||||
prop: 'enabled',
|
||||
signal: 'notify::enabled',
|
||||
});
|
||||
|
||||
TouchGestures.addGesture({
|
||||
name: 'openAppLauncher',
|
||||
gesture: 'UD',
|
||||
edge: 'T',
|
||||
command: () => App.openWindow('applauncher'),
|
||||
});
|
||||
|
||||
TouchGestures.addGesture({
|
||||
name: 'oskOn',
|
||||
gesture: 'DU',
|
||||
edge: 'B',
|
||||
command: 'busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 ' +
|
||||
'SetVisible b true',
|
||||
});
|
||||
|
||||
TouchGestures.addGesture({
|
||||
name: 'oskOff',
|
||||
gesture: 'UD',
|
||||
edge: 'B',
|
||||
command: 'busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 ' +
|
||||
'SetVisible b false',
|
||||
});
|
||||
|
||||
TouchGestures.addGesture({
|
||||
name: 'swipeSpotify1',
|
||||
gesture: 'LR',
|
||||
edge: 'L',
|
||||
command: () => Hyprland.sendMessage(
|
||||
'dispatch togglespecialworkspace spot',
|
||||
),
|
||||
});
|
||||
|
||||
TouchGestures.addGesture({
|
||||
name: 'swipeSpotify2',
|
||||
gesture: 'RL',
|
||||
edge: 'L',
|
||||
command: () => Hyprland.sendMessage(
|
||||
'dispatch togglespecialworkspace spot',
|
||||
),
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue