update system

This commit is contained in:
2024-04-17 10:03:57 +02:00
parent 6e0e34e425
commit 4c78f4b5e6
260 changed files with 8100 additions and 7799 deletions

View File

@@ -0,0 +1,84 @@
import type Gtk from "gi://Gtk?version=3.0"
import { ProfileSelector, ProfileToggle } from "./widgets/PowerProfile"
import { Header } from "./widgets/Header"
import { Volume, Microhone, SinkSelector, AppMixer } from "./widgets/Volume"
import { Brightness } from "./widgets/Brightness"
import { NetworkToggle, WifiSelection } from "./widgets/Network"
import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth"
import { DND } from "./widgets/DND"
import { DarkModeToggle } from "./widgets/DarkMode"
import { MicMute } from "./widgets/MicMute"
import { Media } from "./widgets/Media"
import PopupWindow from "widget/PopupWindow"
import options from "options"
const { bar, quicksettings } = options
const media = (await Service.import("mpris")).bind("players")
const layout = Utils.derive([bar.position, quicksettings.position], (bar, qs) =>
`${bar}-${qs}` as const,
)
const Row = (
toggles: Array<() => Gtk.Widget> = [],
menus: Array<() => Gtk.Widget> = [],
) => Widget.Box({
vertical: true,
children: [
Widget.Box({
homogeneous: true,
class_name: "row horizontal",
children: toggles.map(w => w()),
}),
...menus.map(w => w()),
],
})
const Settings = () => Widget.Box({
vertical: true,
class_name: "quicksettings vertical",
css: quicksettings.width.bind().as(w => `min-width: ${w}px;`),
children: [
Header(),
Widget.Box({
class_name: "sliders-box vertical",
vertical: true,
children: [
Row(
[Volume],
[SinkSelector, AppMixer],
),
Microhone(),
Brightness(),
],
}),
Row(
[NetworkToggle, BluetoothToggle],
[WifiSelection, BluetoothDevices],
),
Row(
[ProfileToggle, DarkModeToggle],
[ProfileSelector],
),
Row([MicMute, DND]),
Widget.Box({
visible: media.as(l => l.length > 0),
child: Media(),
}),
],
})
const QuickSettings = () => PopupWindow({
name: "quicksettings",
exclusivity: "exclusive",
transition: bar.position.bind().as(pos => pos === "top" ? "slide_down" : "slide_up"),
layout: layout.value,
child: Settings(),
})
export function setupQuickSettings() {
App.addWindow(QuickSettings())
layout.connect("changed", () => {
App.removeWindow("quicksettings")
App.addWindow(QuickSettings())
})
}

View File

@@ -0,0 +1,154 @@
import { type Props as IconProps } from "types/widgets/icon"
import { type Props as LabelProps } from "types/widgets/label"
import type GObject from "gi://GObject?version=2.0"
import type Gtk from "gi://Gtk?version=3.0"
import icons from "lib/icons"
export const opened = Variable("")
App.connect("window-toggled", (_, name: string, visible: boolean) => {
if (name === "quicksettings" && !visible)
Utils.timeout(500, () => opened.value = "")
})
export const Arrow = (name: string, activate?: false | (() => void)) => {
let deg = 0
let iconOpened = false
const icon = Widget.Icon(icons.ui.arrow.right).hook(opened, () => {
if (opened.value === name && !iconOpened || opened.value !== name && iconOpened) {
const step = opened.value === name ? 10 : -10
iconOpened = !iconOpened
for (let i = 0; i < 9; ++i) {
Utils.timeout(15 * i, () => {
deg += step
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`)
})
}
}
})
return Widget.Button({
child: icon,
class_name: "arrow",
on_clicked: () => {
opened.value = opened.value === name ? "" : name
if (typeof activate === "function")
activate()
},
})
}
type ArrowToggleButtonProps = {
name: string
icon: IconProps["icon"]
label: LabelProps["label"]
activate: () => void
deactivate: () => void
activateOnArrow?: boolean
connection: [GObject.Object, () => boolean]
}
export const ArrowToggleButton = ({
name,
icon,
label,
activate,
deactivate,
activateOnArrow = true,
connection: [service, condition],
}: ArrowToggleButtonProps) => Widget.Box({
class_name: "toggle-button",
setup: self => self.hook(service, () => {
self.toggleClassName("active", condition())
}),
children: [
Widget.Button({
child: Widget.Box({
hexpand: true,
children: [
Widget.Icon({
class_name: "icon",
icon,
}),
Widget.Label({
class_name: "label",
max_width_chars: 10,
truncate: "end",
label,
}),
],
}),
on_clicked: () => {
if (condition()) {
deactivate()
if (opened.value === name)
opened.value = ""
} else {
activate()
}
},
}),
Arrow(name, activateOnArrow && activate),
],
})
type MenuProps = {
name: string
icon: IconProps["icon"]
title: LabelProps["label"]
content: Gtk.Widget[]
}
export const Menu = ({ name, icon, title, content }: MenuProps) => Widget.Revealer({
transition: "slide_down",
reveal_child: opened.bind().as(v => v === name),
child: Widget.Box({
class_names: ["menu", name],
vertical: true,
children: [
Widget.Box({
class_name: "title-box",
children: [
Widget.Icon({
class_name: "icon",
icon,
}),
Widget.Label({
class_name: "title",
truncate: "end",
label: title,
}),
],
}),
Widget.Separator(),
Widget.Box({
vertical: true,
class_name: "content vertical",
children: content,
}),
],
}),
})
type SimpleToggleButtonProps = {
icon: IconProps["icon"]
label: LabelProps["label"]
toggle: () => void
connection: [GObject.Object, () => boolean]
}
export const SimpleToggleButton = ({
icon,
label,
toggle,
connection: [service, condition],
}: SimpleToggleButtonProps) => Widget.Button({
on_clicked: toggle,
class_name: "simple-toggle",
setup: self => self.hook(service, () => {
self.toggleClassName("active", condition())
}),
child: Widget.Box([
Widget.Icon({ icon }),
Widget.Label({
max_width_chars: 10,
truncate: "end",
label,
}),
]),
})

View File

@@ -0,0 +1,177 @@
window#quicksettings .quicksettings {
@include floating-widget;
@include spacing;
padding: $popover-padding * 1.4;
.avatar {
@include widget;
border-radius: $radius * 3;
}
.header {
@include spacing(.5);
color: transparentize($fg, .15);
button {
@include button;
padding: $padding;
image {
font-size: 1.4em;
}
}
}
.sliders-box {
@include widget;
padding: $padding;
button {
@include button($flat: true);
padding: $padding * .5;
}
.volume button.arrow:last-child {
margin-left: $spacing * .4;
}
.volume,
.brightness {
padding: $padding * .5;
}
scale {
@include slider;
margin: 0 ($spacing * .5);
&.muted highlight {
background-image: none;
background-color: transparentize($fg, $amount: .2);
}
}
}
.row {
@include spacing;
}
.menu {
@include unset;
@include widget;
padding: $padding;
margin-top: $spacing;
.icon {
margin: 0 ($spacing * .5);
margin-left: $spacing * .2;
}
.title {
font-weight: bold;
}
separator {
margin: ($radius * .5);
background-color: $border-color;
}
button {
@include button($flat: true);
padding: ($padding * .5);
image:first-child {
margin-right: $spacing * .5;
}
}
.bluetooth-devices {
@include spacing(.5);
}
switch {
@include switch;
}
}
.sliders-box .menu {
margin: ($spacing * .5) 0;
&.app-mixer {
.mixer-item {
padding: $padding * .5;
padding-left: 0;
padding-right: $padding * 2;
scale {
@include slider($width: .5em);
}
image {
font-size: 1.2em;
margin: 0 $padding;
}
}
}
}
.toggle-button {
@include button;
font-weight: bold;
image {
font-size: 1.3em;
}
label {
margin-left: $spacing * .3;
}
button {
@include button($flat: true);
&:first-child {
padding: $padding * 1.2;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
padding: $padding * .5;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.active {
background-color: $primary-bg;
label,
image {
color: $primary-fg;
}
}
}
.simple-toggle {
@include button;
font-weight: bold;
padding: $padding * 1.2;
label {
margin-left: $spacing * .3;
}
image {
font-size: 1.3em;
}
}
.media {
@include spacing;
.player {
@include media;
}
}
}

View File

@@ -0,0 +1,61 @@
import { type BluetoothDevice } from "types/service/bluetooth"
import { Menu, ArrowToggleButton } from "../ToggleButton"
import icons from "lib/icons"
const bluetooth = await Service.import("bluetooth")
export const BluetoothToggle = () => ArrowToggleButton({
name: "bluetooth",
icon: bluetooth.bind("enabled").as(p => icons.bluetooth[p ? "enabled" : "disabled"]),
label: Utils.watch("Disabled", bluetooth, () => {
if (!bluetooth.enabled)
return "Disabled"
if (bluetooth.connected_devices.length === 1)
return bluetooth.connected_devices[0].alias
return `${bluetooth.connected_devices.length} Connected`
}),
connection: [bluetooth, () => bluetooth.enabled],
deactivate: () => bluetooth.enabled = false,
activate: () => bluetooth.enabled = true,
})
const DeviceItem = (device: BluetoothDevice) => Widget.Box({
children: [
Widget.Icon(device.icon_name + "-symbolic"),
Widget.Label(device.name),
Widget.Label({
label: `${device.battery_percentage}%`,
visible: device.bind("battery_percentage").as(p => p > 0),
}),
Widget.Box({ hexpand: true }),
Widget.Spinner({
active: device.bind("connecting"),
visible: device.bind("connecting"),
}),
Widget.Switch({
active: device.connected,
visible: device.bind("connecting").as(p => !p),
setup: self => self.on("notify::active", () => {
device.setConnection(self.active)
}),
}),
],
})
export const BluetoothDevices = () => Menu({
name: "bluetooth",
icon: icons.bluetooth.disabled,
title: "Bluetooth",
content: [
Widget.Box({
class_name: "bluetooth-devices",
hexpand: true,
vertical: true,
children: bluetooth.bind("devices").as(ds => ds
.filter(d => d.name)
.map(DeviceItem)),
}),
],
})

View File

@@ -0,0 +1,23 @@
import icons from "lib/icons"
import brightness from "service/brightness"
const BrightnessSlider = () => Widget.Slider({
draw_value: false,
hexpand: true,
value: brightness.bind("screen"),
on_change: ({ value }) => brightness.screen = value,
})
export const Brightness = () => Widget.Box({
class_name: "brightness",
children: [
Widget.Button({
vpack: "center",
child: Widget.Icon(icons.brightness.indicator),
on_clicked: () => brightness.screen = 0,
tooltip_text: brightness.bind("screen").as(v =>
`Screen Brightness: ${Math.floor(v * 100)}%`),
}),
BrightnessSlider(),
],
})

View File

@@ -0,0 +1,12 @@
import { SimpleToggleButton } from "../ToggleButton"
import icons from "lib/icons"
const n = await Service.import("notifications")
const dnd = n.bind("dnd")
export const DND = () => SimpleToggleButton({
icon: dnd.as(dnd => icons.notifications[dnd ? "silent" : "noisy"]),
label: dnd.as(dnd => dnd ? "Silent" : "Noisy"),
toggle: () => n.dnd = !n.dnd,
connection: [n, () => n.dnd],
})

View File

@@ -0,0 +1,12 @@
import { SimpleToggleButton } from "../ToggleButton"
import icons from "lib/icons"
import options from "options"
const { scheme } = options.theme
export const DarkModeToggle = () => SimpleToggleButton({
icon: scheme.bind().as(s => icons.color[s]),
label: scheme.bind().as(s => s === "dark" ? "Dark" : "Light"),
toggle: () => scheme.value = scheme.value === "dark" ? "light" : "dark",
connection: [scheme, () => scheme.value === "dark"],
})

View File

@@ -0,0 +1,63 @@
import icons from "lib/icons"
import { uptime } from "lib/variables"
import options from "options"
import powermenu, { Action } from "service/powermenu"
const battery = await Service.import("battery")
const { image, size } = options.quicksettings.avatar
function up(up: number) {
const h = Math.floor(up / 60)
const m = Math.floor(up % 60)
return `${h}h ${m < 10 ? "0" + m : m}m`
}
const Avatar = () => Widget.Box({
class_name: "avatar",
css: Utils.merge([image.bind(), size.bind()], (img, size) => `
min-width: ${size}px;
min-height: ${size}px;
background-image: url('${img}');
background-size: cover;
`),
})
const SysButton = (action: Action) => Widget.Button({
vpack: "center",
child: Widget.Icon(icons.powermenu[action]),
on_clicked: () => powermenu.action(action),
})
export const Header = () => Widget.Box(
{ class_name: "header horizontal" },
Avatar(),
Widget.Box({
vertical: true,
vpack: "center",
children: [
Widget.Box({
visible: battery.bind("available"),
children: [
Widget.Icon({ icon: battery.bind("icon_name") }),
Widget.Label({ label: battery.bind("percent").as(p => `${p}%`) }),
],
}),
Widget.Box([
Widget.Icon({ icon: icons.ui.time }),
Widget.Label({ label: uptime.bind().as(up) }),
]),
],
}),
Widget.Box({ hexpand: true }),
Widget.Button({
vpack: "center",
child: Widget.Icon(icons.ui.settings),
on_clicked: () => {
App.closeWindow("quicksettings")
App.closeWindow("settings-dialog")
App.openWindow("settings-dialog")
},
}),
SysButton("logout"),
SysButton("shutdown"),
)

View File

@@ -0,0 +1,153 @@
import { type MprisPlayer } from "types/service/mpris"
import icons from "lib/icons"
import options from "options"
import { icon } from "lib/utils"
const mpris = await Service.import("mpris")
const players = mpris.bind("players")
const { media } = options.quicksettings
function lengthStr(length: number) {
const min = Math.floor(length / 60)
const sec = Math.floor(length % 60)
const sec0 = sec < 10 ? "0" : ""
return `${min}:${sec0}${sec}`
}
const Player = (player: MprisPlayer) => {
const cover = Widget.Box({
class_name: "cover",
vpack: "start",
css: Utils.merge([
player.bind("cover_path"),
player.bind("track_cover_url"),
media.coverSize.bind(),
], (path, url, size) => `
min-width: ${size}px;
min-height: ${size}px;
background-image: url('${path || url}');
`),
})
const title = Widget.Label({
class_name: "title",
max_width_chars: 20,
truncate: "end",
hpack: "start",
label: player.bind("track_title"),
})
const artist = Widget.Label({
class_name: "artist",
max_width_chars: 20,
truncate: "end",
hpack: "start",
label: player.bind("track_artists").as(a => a.join(", ")),
})
const positionSlider = Widget.Slider({
class_name: "position",
draw_value: false,
on_change: ({ value }) => player.position = value * player.length,
setup: self => {
const update = () => {
const { length, position } = player
self.visible = length > 0
self.value = length > 0 ? position / length : 0
}
self.hook(player, update)
self.hook(player, update, "position")
self.poll(1000, update)
},
})
const positionLabel = Widget.Label({
class_name: "position",
hpack: "start",
setup: self => {
const update = (_: unknown, time?: number) => {
self.label = lengthStr(time || player.position)
self.visible = player.length > 0
}
self.hook(player, update, "position")
self.poll(1000, update)
},
})
const lengthLabel = Widget.Label({
class_name: "length",
hpack: "end",
visible: player.bind("length").as(l => l > 0),
label: player.bind("length").as(lengthStr),
})
const playericon = Widget.Icon({
class_name: "icon",
hexpand: true,
hpack: "end",
vpack: "start",
tooltip_text: player.identity || "",
icon: Utils.merge([player.bind("entry"), media.monochromeIcon.bind()], (e, s) => {
const name = `${e}${s ? "-symbolic" : ""}`
return icon(name, icons.fallback.audio)
}),
})
const playPause = Widget.Button({
class_name: "play-pause",
on_clicked: () => player.playPause(),
visible: player.bind("can_play"),
child: Widget.Icon({
icon: player.bind("play_back_status").as(s => {
switch (s) {
case "Playing": return icons.mpris.playing
case "Paused":
case "Stopped": return icons.mpris.stopped
}
}),
}),
})
const prev = Widget.Button({
on_clicked: () => player.previous(),
visible: player.bind("can_go_prev"),
child: Widget.Icon(icons.mpris.prev),
})
const next = Widget.Button({
on_clicked: () => player.next(),
visible: player.bind("can_go_next"),
child: Widget.Icon(icons.mpris.next),
})
return Widget.Box(
{ class_name: "player", vexpand: false },
cover,
Widget.Box(
{ vertical: true },
Widget.Box([
title,
playericon,
]),
artist,
Widget.Box({ vexpand: true }),
positionSlider,
Widget.CenterBox({
class_name: "footer horizontal",
start_widget: positionLabel,
center_widget: Widget.Box([
prev,
playPause,
next,
]),
end_widget: lengthLabel,
}),
),
)
}
export const Media = () => Widget.Box({
vertical: true,
class_name: "media vertical",
children: players.as(p => p.map(Player)),
})

View File

@@ -0,0 +1,18 @@
import { SimpleToggleButton } from "../ToggleButton"
import icons from "lib/icons"
const { microphone } = await Service.import("audio")
const icon = () => microphone.is_muted || microphone.stream?.is_muted
? icons.audio.mic.muted
: icons.audio.mic.high
const label = () => microphone.is_muted || microphone.stream?.is_muted
? "Muted"
: "Unmuted"
export const MicMute = () => SimpleToggleButton({
icon: Utils.watch(icon(), microphone, icon),
label: Utils.watch(label(), microphone, label),
toggle: () => microphone.is_muted = !microphone.is_muted,
connection: [microphone, () => microphone?.is_muted || false],
})

View File

@@ -0,0 +1,61 @@
import { Menu, ArrowToggleButton } from "../ToggleButton"
import icons from "lib/icons.js"
import { dependencies, sh } from "lib/utils"
import options from "options"
const { wifi } = await Service.import("network")
export const NetworkToggle = () => ArrowToggleButton({
name: "network",
icon: wifi.bind("icon_name"),
label: wifi.bind("ssid").as(ssid => ssid || "Not Connected"),
connection: [wifi, () => wifi.enabled],
deactivate: () => wifi.enabled = false,
activate: () => {
wifi.enabled = true
wifi.scan()
},
})
export const WifiSelection = () => Menu({
name: "network",
icon: wifi.bind("icon_name"),
title: "Wifi Selection",
content: [
Widget.Box({
vertical: true,
setup: self => self.hook(wifi, () => self.children =
wifi.access_points.map(ap => Widget.Button({
on_clicked: () => {
if (dependencies("nmcli"))
Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`)
},
child: Widget.Box({
children: [
Widget.Icon(ap.iconName),
Widget.Label(ap.ssid || ""),
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
setup: self => Utils.idle(() => {
if (!self.is_destroyed)
self.visible = ap.active
}),
}),
],
}),
})),
),
}),
Widget.Separator(),
Widget.Button({
on_clicked: () => sh(options.quicksettings.networkSettings.value),
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Network"),
],
}),
}),
],
})

View File

@@ -0,0 +1,99 @@
import { ArrowToggleButton, Menu } from "../ToggleButton"
import icons from "lib/icons"
import asusctl from "service/asusctl"
const asusprof = asusctl.bind("profile")
const AsusProfileToggle = () => ArrowToggleButton({
name: "asusctl-profile",
icon: asusprof.as(p => icons.asusctl.profile[p]),
label: asusprof,
connection: [asusctl, () => asusctl.profile !== "Balanced"],
activate: () => asusctl.setProfile("Quiet"),
deactivate: () => asusctl.setProfile("Balanced"),
activateOnArrow: false,
})
const AsusProfileSelector = () => Menu({
name: "asusctl-profile",
icon: asusprof.as(p => icons.asusctl.profile[p]),
title: "Profile Selector",
content: [
Widget.Box({
vertical: true,
hexpand: true,
children: [
Widget.Box({
vertical: true,
children: asusctl.profiles.map(prof => Widget.Button({
on_clicked: () => asusctl.setProfile(prof),
child: Widget.Box({
children: [
Widget.Icon(icons.asusctl.profile[prof]),
Widget.Label(prof),
],
}),
})),
}),
],
}),
Widget.Separator(),
Widget.Button({
on_clicked: () => Utils.execAsync("rog-control-center"),
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Rog Control Center"),
],
}),
}),
],
})
const pp = await Service.import("powerprofiles")
const profile = pp.bind("active_profile")
const profiles = pp.profiles.map(p => p.Profile)
const pretty = (str: string) => str
.split("-")
.map(str => `${str.at(0)?.toUpperCase()}${str.slice(1)}`)
.join(" ")
const PowerProfileToggle = () => ArrowToggleButton({
name: "asusctl-profile",
icon: profile.as(p => icons.powerprofile[p]),
label: profile.as(pretty),
connection: [pp, () => pp.active_profile !== profiles[1]],
activate: () => pp.active_profile = profiles[0],
deactivate: () => pp.active_profile = profiles[1],
activateOnArrow: false,
})
const PowerProfileSelector = () => Menu({
name: "asusctl-profile",
icon: profile.as(p => icons.powerprofile[p]),
title: "Profile Selector",
content: [Widget.Box({
vertical: true,
hexpand: true,
child: Widget.Box({
vertical: true,
children: profiles.map(prof => Widget.Button({
on_clicked: () => pp.active_profile = prof,
child: Widget.Box({
children: [
Widget.Icon(icons.powerprofile[prof]),
Widget.Label(pretty(prof)),
],
}),
})),
}),
})],
})
export const ProfileToggle = asusctl.available
? AsusProfileToggle : PowerProfileToggle
export const ProfileSelector = asusctl.available
? AsusProfileSelector : PowerProfileSelector

View File

@@ -0,0 +1,150 @@
import { type Stream } from "types/service/audio"
import { Arrow, Menu } from "../ToggleButton"
import { dependencies, icon, sh } from "lib/utils"
import icons from "lib/icons.js"
const audio = await Service.import("audio")
type Type = "microphone" | "speaker"
const VolumeIndicator = (type: Type = "speaker") => Widget.Button({
vpack: "center",
on_clicked: () => audio[type].is_muted = !audio[type].is_muted,
child: Widget.Icon({
icon: audio[type].bind("icon_name")
.as(i => icon(i || "", icons.audio.mic.high)),
tooltipText: audio[type].bind("volume")
.as(vol => `Volume: ${Math.floor(vol * 100)}%`),
}),
})
const VolumeSlider = (type: Type = "speaker") => Widget.Slider({
hexpand: true,
draw_value: false,
on_change: ({ value, dragging }) => {
if (dragging) {
audio[type].volume = value
audio[type].is_muted = false
}
},
value: audio[type].bind("volume"),
class_name: audio[type].bind("is_muted").as(m => m ? "muted" : ""),
})
export const Volume = () => Widget.Box({
class_name: "volume",
children: [
VolumeIndicator("speaker"),
VolumeSlider("speaker"),
Widget.Box({
vpack: "center",
child: Arrow("sink-selector"),
}),
Widget.Box({
vpack: "center",
child: Arrow("app-mixer"),
visible: audio.bind("apps").as(a => a.length > 0),
}),
],
})
export const Microhone = () => Widget.Box({
class_name: "slider horizontal",
visible: audio.bind("recorders").as(a => a.length > 0),
children: [
VolumeIndicator("microphone"),
VolumeSlider("microphone"),
],
})
const MixerItem = (stream: Stream) => Widget.Box(
{
hexpand: true,
class_name: "mixer-item horizontal",
},
Widget.Icon({
tooltip_text: stream.bind("name").as(n => n || ""),
icon: stream.bind("name").as(n => {
return Utils.lookUpIcon(n || "")
? (n || "")
: icons.fallback.audio
}),
}),
Widget.Box(
{ vertical: true },
Widget.Label({
xalign: 0,
truncate: "end",
max_width_chars: 28,
label: stream.bind("description").as(d => d || ""),
}),
Widget.Slider({
hexpand: true,
draw_value: false,
value: stream.bind("volume"),
on_change: ({ value }) => stream.volume = value,
}),
),
)
const SinkItem = (stream: Stream) => Widget.Button({
hexpand: true,
on_clicked: () => audio.speaker = stream,
child: Widget.Box({
children: [
Widget.Icon({
icon: icon(stream.icon_name || "", icons.fallback.audio),
tooltip_text: stream.icon_name || "",
}),
Widget.Label((stream.description || "").split(" ").slice(0, 4).join(" ")),
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
visible: audio.speaker.bind("stream").as(s => s === stream.stream),
}),
],
}),
})
const SettingsButton = () => Widget.Button({
on_clicked: () => {
if (dependencies("pavucontrol"))
sh("pavucontrol")
},
hexpand: true,
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Settings"),
],
}),
})
export const AppMixer = () => Menu({
name: "app-mixer",
icon: icons.audio.mixer,
title: "App Mixer",
content: [
Widget.Box({
vertical: true,
class_name: "vertical mixer-item-box",
children: audio.bind("apps").as(a => a.map(MixerItem)),
}),
Widget.Separator(),
SettingsButton(),
],
})
export const SinkSelector = () => Menu({
name: "sink-selector",
icon: icons.audio.type.headset,
title: "Sink Selector",
content: [
Widget.Box({
vertical: true,
children: audio.bind("speakers").as(a => a.map(SinkItem)),
}),
Widget.Separator(),
SettingsButton(),
],
})