feat(ags): add wifi widget
All checks were successful
Discord / discord commits (push) Has been skipped
All checks were successful
Discord / discord commits (push) Has been skipped
This commit is contained in:
parent
8f82b1885a
commit
6bc32a8d8e
11 changed files with 388 additions and 68 deletions
|
@ -12,6 +12,7 @@ import Calendar from '../widgets/date/wim';
|
||||||
import Clipboard from '../widgets/clipboard/main';
|
import Clipboard from '../widgets/clipboard/main';
|
||||||
import Corners from '../widgets/corners/main';
|
import Corners from '../widgets/corners/main';
|
||||||
import IconBrowser from '../widgets/icon-browser/main';
|
import IconBrowser from '../widgets/icon-browser/main';
|
||||||
|
import NetworkWindow from '../widgets/network/wim';
|
||||||
import { NotifPopups, NotifCenter } from '../widgets/notifs/wim';
|
import { NotifPopups, NotifCenter } from '../widgets/notifs/wim';
|
||||||
import OnScreenDisplay from '../widgets/on-screen-display/main';
|
import OnScreenDisplay from '../widgets/on-screen-display/main';
|
||||||
import OnScreenKeyboard from '../widgets/on-screen-keyboard/main';
|
import OnScreenKeyboard from '../widgets/on-screen-keyboard/main';
|
||||||
|
@ -100,6 +101,7 @@ export default () => {
|
||||||
Clipboard();
|
Clipboard();
|
||||||
Corners();
|
Corners();
|
||||||
IconBrowser();
|
IconBrowser();
|
||||||
|
NetworkWindow();
|
||||||
NotifPopups();
|
NotifPopups();
|
||||||
NotifCenter();
|
NotifCenter();
|
||||||
OnScreenDisplay();
|
OnScreenDisplay();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { idle } from 'astal';
|
import { idle, subprocess } from 'astal';
|
||||||
import { App, Gdk, Gtk } from 'astal/gtk3';
|
import { App, Gdk, Gtk } from 'astal/gtk3';
|
||||||
|
|
||||||
import AstalHyprland from 'gi://AstalHyprland';
|
import AstalHyprland from 'gi://AstalHyprland';
|
||||||
|
@ -201,3 +201,68 @@ export const perMonitor = (window: (monitor: Gdk.Monitor) => Gtk.Widget) => idle
|
||||||
windows.delete(monitor);
|
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<number>((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}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -1,76 +1,12 @@
|
||||||
import { execAsync, subprocess } from 'astal';
|
import { execAsync, subprocess } from 'astal';
|
||||||
import GObject, { register } from 'astal/gobject';
|
import GObject, { register } from 'astal/gobject';
|
||||||
|
|
||||||
/* Types */
|
import { notifySend } from '../lib';
|
||||||
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 APP_NAME = 'gpu-screen-recorder';
|
const APP_NAME = 'gpu-screen-recorder';
|
||||||
const ICON_NAME = 'nvidia';
|
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<number>((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()
|
@register()
|
||||||
export default class GpuScreenRecorder extends GObject.Object {
|
export default class GpuScreenRecorder extends GObject.Object {
|
||||||
private _lastNotifID: number | undefined;
|
private _lastNotifID: number | undefined;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
@use '../widgets/date';
|
@use '../widgets/date';
|
||||||
@use '../widgets/icon-browser';
|
@use '../widgets/icon-browser';
|
||||||
@use '../widgets/misc';
|
@use '../widgets/misc';
|
||||||
|
@use '../widgets/network';
|
||||||
@use '../widgets/notifs';
|
@use '../widgets/notifs';
|
||||||
@use '../widgets/on-screen-display';
|
@use '../widgets/on-screen-display';
|
||||||
@use '../widgets/on-screen-keyboard';
|
@use '../widgets/on-screen-keyboard';
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { bind, Variable } from 'astal';
|
import { bind, Variable } from 'astal';
|
||||||
import { Gtk } from 'astal/gtk3';
|
import { App, Gtk } from 'astal/gtk3';
|
||||||
|
|
||||||
import AstalNetwork from 'gi://AstalNetwork';
|
import AstalNetwork from 'gi://AstalNetwork';
|
||||||
|
|
||||||
|
/* Types */
|
||||||
|
import PopupWindow from '../../misc/popup-window';
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const network = AstalNetwork.get_default();
|
const network = AstalNetwork.get_default();
|
||||||
|
@ -16,6 +19,17 @@ export default () => {
|
||||||
|
|
||||||
onHover={() => Hovered.set(true)}
|
onHover={() => Hovered.set(true)}
|
||||||
onHoverLost={() => Hovered.set(false)}
|
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) => {
|
{bind(network, 'primary').as((primary) => {
|
||||||
if (primary === AstalNetwork.Primary.UNKNOWN) {
|
if (primary === AstalNetwork.Primary.UNKNOWN) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use 'sass:color';
|
@use 'sass:color';
|
||||||
@use '../../style/colors';
|
@use '../../style/colors';
|
||||||
|
|
||||||
.bluetooth {
|
.bluetooth.widget {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
32
modules/ags/config/widgets/network/_index.scss
Normal file
32
modules/ags/config/widgets/network/_index.scss
Normal file
|
@ -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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
137
modules/ags/config/widgets/network/access-point.tsx
Normal file
137
modules/ags/config/widgets/network/access-point.tsx
Normal file
|
@ -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 = (
|
||||||
|
<revealer
|
||||||
|
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
||||||
|
>
|
||||||
|
<box vertical halign={Gtk.Align.FILL} hexpand>
|
||||||
|
<Separator size={8} vertical />
|
||||||
|
|
||||||
|
<centerbox>
|
||||||
|
<label
|
||||||
|
label="Connected"
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<box />
|
||||||
|
|
||||||
|
<switch
|
||||||
|
cursor="pointer"
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
halign={Gtk.Align.END}
|
||||||
|
|
||||||
|
state={bind(wifi, 'activeAccessPoint')
|
||||||
|
.as((activeAp) => aps.includes(activeAp))}
|
||||||
|
|
||||||
|
onButtonReleaseEvent={(self) => {
|
||||||
|
if (self.state) {
|
||||||
|
apDisconnect(this.getStrongest());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
apConnect(this.getStrongest());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</centerbox>
|
||||||
|
|
||||||
|
<Separator size={8} vertical />
|
||||||
|
</box>
|
||||||
|
</revealer>
|
||||||
|
) as Widget.Revealer;
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<button
|
||||||
|
cursor="pointer"
|
||||||
|
onButtonReleaseEvent={() => {
|
||||||
|
rev.revealChild = !rev.revealChild;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box>
|
||||||
|
<icon
|
||||||
|
icon="check-active-symbolic"
|
||||||
|
|
||||||
|
css={bind(wifi, 'activeAccessPoint').as((activeAp) => aps.includes(activeAp) ?
|
||||||
|
'' :
|
||||||
|
'opacity: 0;')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator size={8} />
|
||||||
|
|
||||||
|
<icon
|
||||||
|
icon={bind(aps[0], 'iconName')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator size={8} />
|
||||||
|
|
||||||
|
<label label={aps[0].get_ssid()!} />
|
||||||
|
</box>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
super({
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
button,
|
||||||
|
rev,
|
||||||
|
(<Separator size={8} vertical />),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.aps = aps;
|
||||||
|
};
|
||||||
|
};
|
117
modules/ags/config/widgets/network/main.tsx
Normal file
117
modules/ags/config/widgets/network/main.tsx
Normal file
|
@ -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<boolean>(false);
|
||||||
|
const AccessPoints = Variable<AstalNetwork.AccessPoint[]>(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 = (
|
||||||
|
<scrollable
|
||||||
|
className="list"
|
||||||
|
|
||||||
|
css="min-height: 300px;"
|
||||||
|
hscroll={Gtk.PolicyType.NEVER}
|
||||||
|
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||||
|
>
|
||||||
|
<box vertical>
|
||||||
|
{bind(AccessPoints).as(() => {
|
||||||
|
const joined = new Map<string, AstalNetwork.AccessPoint[]>();
|
||||||
|
|
||||||
|
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) => <AccessPointWidget aps={aps} />);
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
</scrollable>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
className="network widget"
|
||||||
|
vertical
|
||||||
|
>
|
||||||
|
<centerbox homogeneous>
|
||||||
|
<switch
|
||||||
|
cursor="pointer"
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
|
||||||
|
active={bind(wifi, 'enabled')}
|
||||||
|
|
||||||
|
setup={(self) => {
|
||||||
|
self.connect('notify::active', () => {
|
||||||
|
wifi.set_enabled(self.active);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<box />
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
cursor="pointer"
|
||||||
|
halign={Gtk.Align.END}
|
||||||
|
|
||||||
|
className="toggle-button"
|
||||||
|
|
||||||
|
sensitive={bind(wifi, 'enabled')}
|
||||||
|
active={bind(IsRefreshing)}
|
||||||
|
|
||||||
|
onToggled={(self) => {
|
||||||
|
self.toggleClassName('active', self.active);
|
||||||
|
IsRefreshing.set(self.active);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<icon icon="emblem-synchronizing-symbolic" css="font-size: 30px;" />
|
||||||
|
</ToggleButton>
|
||||||
|
</centerbox>
|
||||||
|
|
||||||
|
<Separator size={8} vertical />
|
||||||
|
|
||||||
|
{apList}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
15
modules/ags/config/widgets/network/wim.tsx
Normal file
15
modules/ags/config/widgets/network/wim.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Astal } from 'astal/gtk3';
|
||||||
|
|
||||||
|
import PopupWindow from '../misc/popup-window';
|
||||||
|
|
||||||
|
import NetworkWidget from './main';
|
||||||
|
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<PopupWindow
|
||||||
|
name="network"
|
||||||
|
anchor={Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.TOP}
|
||||||
|
>
|
||||||
|
<NetworkWidget />
|
||||||
|
</PopupWindow>
|
||||||
|
);
|
|
@ -74,6 +74,7 @@ in {
|
||||||
++ (attrValues {
|
++ (attrValues {
|
||||||
inherit
|
inherit
|
||||||
(pkgs)
|
(pkgs)
|
||||||
|
networkmanagerapplet
|
||||||
playerctl
|
playerctl
|
||||||
wayfreeze
|
wayfreeze
|
||||||
;
|
;
|
||||||
|
|
Loading…
Reference in a new issue