diff --git a/modules/ags/config/configurations/wim.ts b/modules/ags/config/configurations/wim.ts index e3041366..1d561089 100644 --- a/modules/ags/config/configurations/wim.ts +++ b/modules/ags/config/configurations/wim.ts @@ -12,6 +12,7 @@ import Calendar from '../widgets/date/wim'; import Clipboard from '../widgets/clipboard/main'; import Corners from '../widgets/corners/main'; import IconBrowser from '../widgets/icon-browser/main'; +import NetworkWindow from '../widgets/network/wim'; import { NotifPopups, NotifCenter } from '../widgets/notifs/wim'; import OnScreenDisplay from '../widgets/on-screen-display/main'; import OnScreenKeyboard from '../widgets/on-screen-keyboard/main'; @@ -100,6 +101,7 @@ export default () => { Clipboard(); Corners(); IconBrowser(); + NetworkWindow(); NotifPopups(); NotifCenter(); OnScreenDisplay(); diff --git a/modules/ags/config/lib.ts b/modules/ags/config/lib.ts index c9edfeb0..b769d392 100644 --- a/modules/ags/config/lib.ts +++ b/modules/ags/config/lib.ts @@ -1,4 +1,4 @@ -import { idle } from 'astal'; +import { idle, subprocess } from 'astal'; import { App, Gdk, Gtk } from 'astal/gtk3'; import AstalHyprland from 'gi://AstalHyprland'; @@ -201,3 +201,68 @@ export const perMonitor = (window: (monitor: Gdk.Monitor) => Gtk.Widget) => idle windows.delete(monitor); }); }); + +interface NotifyAction { + id: string + label: string + callback: () => void +} +interface NotifySendProps { + actions?: NotifyAction[] + appName?: string + body?: string + category?: string + hint?: string + iconName: string + replaceId?: number + title: string + urgency?: 'low' | 'normal' | 'critical' +} + +const escapeShellArg = (arg: string): string => `'${arg?.replace(/'/g, '\'\\\'\'')}'`; + +export const notifySend = ({ + actions = [], + appName, + body, + category, + hint, + iconName, + replaceId, + title, + urgency = 'normal', +}: NotifySendProps) => new Promise((resolve) => { + let printedId = false; + + const cmd = [ + 'notify-send', + '--print-id', + `--icon=${escapeShellArg(iconName)}`, + escapeShellArg(title), + escapeShellArg(body ?? ''), + // Optional params + appName ? `--app-name=${escapeShellArg(appName)}` : '', + category ? `--category=${escapeShellArg(category)}` : '', + hint ? `--hint=${escapeShellArg(hint)}` : '', + replaceId ? `--replace-id=${replaceId.toString()}` : '', + `--urgency=${urgency}`, + ].concat( + actions.map(({ id, label }) => `--action=${escapeShellArg(id)}=${escapeShellArg(label)}`), + ).join(' '); + + subprocess( + cmd, + (out) => { + if (!printedId) { + resolve(parseInt(out)); + printedId = true; + } + else { + actions.find((action) => action.id === out)?.callback(); + } + }, + (err) => { + console.error(`[Notify] ${err}`); + }, + ); +}); diff --git a/modules/ags/config/services/gpu-screen-recorder.ts b/modules/ags/config/services/gpu-screen-recorder.ts index f2f8bd74..7b7251d7 100644 --- a/modules/ags/config/services/gpu-screen-recorder.ts +++ b/modules/ags/config/services/gpu-screen-recorder.ts @@ -1,76 +1,12 @@ import { execAsync, subprocess } from 'astal'; import GObject, { register } from 'astal/gobject'; -/* Types */ -interface NotifyAction { - id: string - label: string - callback: () => void -} -interface NotifySendProps { - actions?: NotifyAction[] - appName?: string - body?: string - category?: string - hint?: string - iconName: string - replaceId?: number - title: string - urgency?: 'low' | 'normal' | 'critical' -} +import { notifySend } from '../lib'; const APP_NAME = 'gpu-screen-recorder'; const ICON_NAME = 'nvidia'; -const escapeShellArg = (arg: string): string => `'${arg.replace(/'/g, '\'\\\'\'')}'`; - -const notifySend = ({ - actions = [], - appName, - body, - category, - hint, - iconName, - replaceId, - title, - urgency = 'normal', -}: NotifySendProps) => new Promise((resolve) => { - let printedId = false; - - const cmd = [ - 'notify-send', - '--print-id', - `--icon=${escapeShellArg(iconName)}`, - escapeShellArg(title), - escapeShellArg(body ?? ''), - // Optional params - appName ? `--app-name=${escapeShellArg(appName)}` : '', - category ? `--category=${escapeShellArg(category)}` : '', - hint ? `--hint=${escapeShellArg(hint)}` : '', - replaceId ? `--replace-id=${replaceId.toString()}` : '', - `--urgency=${urgency}`, - ].concat( - actions.map(({ id, label }) => `--action=${escapeShellArg(id)}=${escapeShellArg(label)}`), - ).join(' '); - - subprocess( - cmd, - (out) => { - if (!printedId) { - resolve(parseInt(out)); - printedId = true; - } - else { - actions.find((action) => action.id === out)?.callback(); - } - }, - (err) => { - console.error(`[Notify] ${err}`); - }, - ); -}); - @register() export default class GpuScreenRecorder extends GObject.Object { private _lastNotifID: number | undefined; diff --git a/modules/ags/config/style/main.scss b/modules/ags/config/style/main.scss index 39045afb..81980eb5 100644 --- a/modules/ags/config/style/main.scss +++ b/modules/ags/config/style/main.scss @@ -8,6 +8,7 @@ @use '../widgets/date'; @use '../widgets/icon-browser'; @use '../widgets/misc'; +@use '../widgets/network'; @use '../widgets/notifs'; @use '../widgets/on-screen-display'; @use '../widgets/on-screen-keyboard'; diff --git a/modules/ags/config/widgets/bar/items/network.tsx b/modules/ags/config/widgets/bar/items/network.tsx index ccfe2471..733285df 100644 --- a/modules/ags/config/widgets/bar/items/network.tsx +++ b/modules/ags/config/widgets/bar/items/network.tsx @@ -1,8 +1,11 @@ import { bind, Variable } from 'astal'; -import { Gtk } from 'astal/gtk3'; +import { App, Gtk } from 'astal/gtk3'; import AstalNetwork from 'gi://AstalNetwork'; +/* Types */ +import PopupWindow from '../../misc/popup-window'; + export default () => { const network = AstalNetwork.get_default(); @@ -16,6 +19,17 @@ export default () => { onHover={() => Hovered.set(true)} onHoverLost={() => Hovered.set(false)} + + onButtonReleaseEvent={(self) => { + const win = App.get_window('win-network') as PopupWindow; + + win.set_x_pos( + self.get_allocation(), + 'right', + ); + + win.visible = !win.visible; + }} > {bind(network, 'primary').as((primary) => { if (primary === AstalNetwork.Primary.UNKNOWN) { diff --git a/modules/ags/config/widgets/bluetooth/_index.scss b/modules/ags/config/widgets/bluetooth/_index.scss index fa1b24a9..96c5194e 100644 --- a/modules/ags/config/widgets/bluetooth/_index.scss +++ b/modules/ags/config/widgets/bluetooth/_index.scss @@ -1,7 +1,7 @@ @use 'sass:color'; @use '../../style/colors'; -.bluetooth { +.bluetooth.widget { margin-top: 0; * { diff --git a/modules/ags/config/widgets/network/_index.scss b/modules/ags/config/widgets/network/_index.scss new file mode 100644 index 00000000..01ec31e0 --- /dev/null +++ b/modules/ags/config/widgets/network/_index.scss @@ -0,0 +1,32 @@ +@use 'sass:color'; +@use '../../style/colors'; + +.network.widget { + margin-top: 0; + + * { + font-size: 20px; + } + + row { + all: unset; + } + + .toggle-button { + padding: 4px; + min-width: 30px; + min-height: 30px; + + transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out; + + box-shadow: 2px 1px 2px colors.$accent-color; + margin: 4px 4px 4px 4px; + background-color: colors.$window_bg_color; + + &.active { + box-shadow: 0 0 0 white; + margin: 6px 4px 2px 4px; + background-color: color.adjust(colors.$window_bg_color, $lightness: -3%); + } + } +} diff --git a/modules/ags/config/widgets/network/access-point.tsx b/modules/ags/config/widgets/network/access-point.tsx new file mode 100644 index 00000000..227d87b2 --- /dev/null +++ b/modules/ags/config/widgets/network/access-point.tsx @@ -0,0 +1,137 @@ +import { bind, execAsync } from 'astal'; +import { Gtk, Widget } from 'astal/gtk3'; +import { register } from 'astal/gobject'; + +import AstalNetwork from 'gi://AstalNetwork'; + +import Separator from '../misc/separator'; +import { notifySend } from '../../lib'; + + +const apCommand = (ap: AstalNetwork.AccessPoint, cmd: string[]): void => { + execAsync([ + 'nmcli', + ...cmd, + ap.get_ssid()!, + ]).catch((e) => notifySend({ + title: 'Network', + iconName: ap.iconName, + body: (e as Error).message, + actions: [ + { + id: 'open', + label: 'Open network manager', + callback: () => + execAsync('nm-connection-editor'), + }, + ], + })).catch((e) => console.error(e)); +}; + +const apConnect = (ap: AstalNetwork.AccessPoint): void => { + execAsync(['nmcli', 'connection', 'show', ap.get_ssid()!]) + .catch(() => apCommand(ap, ['device', 'wifi', 'connect'])) + .then(() => apCommand(ap, ['connection', 'up'])); +}; + +const apDisconnect = (ap: AstalNetwork.AccessPoint): void => { + apCommand(ap, ['connection', 'down']); +}; + +@register() +export default class AccessPointWidget extends Widget.Box { + readonly aps: AstalNetwork.AccessPoint[]; + + getStrongest() { + return this.aps.sort((apA, apB) => apB.get_strength() - apA.get_strength())[0]; + } + + constructor({ aps }: { aps: AstalNetwork.AccessPoint[] }) { + const wifi = AstalNetwork.get_default().get_wifi(); + + if (!wifi) { + throw new Error('Could not find wifi device.'); + } + + const rev = ( + + + + + + + + + + + ) as Widget.Revealer; + + const button = ( + + ); + + super({ + vertical: true, + children: [ + button, + rev, + (), + ], + }); + + this.aps = aps; + }; +}; diff --git a/modules/ags/config/widgets/network/main.tsx b/modules/ags/config/widgets/network/main.tsx new file mode 100644 index 00000000..3ea1b602 --- /dev/null +++ b/modules/ags/config/widgets/network/main.tsx @@ -0,0 +1,117 @@ +import { bind, Variable } from 'astal'; +import { Gtk } from 'astal/gtk3'; + +import AstalNetwork from 'gi://AstalNetwork'; + +import { ToggleButton } from '../misc/subclasses'; +import Separator from '../misc/separator'; + +import AccessPointWidget from './access-point'; + + +export default () => { + const wifi = AstalNetwork.get_default().get_wifi(); + + if (!wifi) { + throw new Error('Could not find wifi device.'); + } + + const IsRefreshing = Variable(false); + const AccessPoints = Variable(wifi.get_access_points()); + + wifi.connect('notify::access-points', () => { + if (IsRefreshing.get()) { + AccessPoints.set(wifi.get_access_points()); + } + }); + + IsRefreshing.subscribe(() => { + if (IsRefreshing.get()) { + AccessPoints.set(wifi.get_access_points()); + } + }); + + const apList = ( + + + {bind(AccessPoints).as(() => { + const joined = new Map(); + + AccessPoints.get() + .filter((ap) => ap.get_ssid()) + .sort((apA, apB) => { + const sort = apB.get_strength() - apA.get_strength(); + + return sort !== 0 ? + sort : + apA.get_ssid()!.localeCompare(apB.get_ssid()!); + }) + .forEach((ap) => { + const arr = joined.get(ap.get_ssid()!); + + if (arr) { + arr.push(ap); + } + else { + joined.set(ap.get_ssid()!, [ap]); + } + }); + + return [...joined.values()].map((aps) => ); + })} + + + ); + + return ( + + + { + self.connect('notify::active', () => { + wifi.set_enabled(self.active); + }); + }} + /> + + + + { + self.toggleClassName('active', self.active); + IsRefreshing.set(self.active); + }} + > + + + + + + + {apList} + + ); +}; diff --git a/modules/ags/config/widgets/network/wim.tsx b/modules/ags/config/widgets/network/wim.tsx new file mode 100644 index 00000000..589fe06e --- /dev/null +++ b/modules/ags/config/widgets/network/wim.tsx @@ -0,0 +1,15 @@ +import { Astal } from 'astal/gtk3'; + +import PopupWindow from '../misc/popup-window'; + +import NetworkWidget from './main'; + + +export default () => ( + + + +); diff --git a/modules/ags/packages.nix b/modules/ags/packages.nix index 86031e3d..3b15d58d 100644 --- a/modules/ags/packages.nix +++ b/modules/ags/packages.nix @@ -74,6 +74,7 @@ in { ++ (attrValues { inherit (pkgs) + networkmanagerapplet playerctl wayfreeze ;