This commit is contained in:
xsghetti 2024-04-11 00:21:35 -04:00
parent 1f8cb3c145
commit 610604e80f
253 changed files with 27055 additions and 44 deletions

View file

@ -0,0 +1,158 @@
import Gtk from "gi://Gtk?version=3.0"
import options from '../options.js';
export const Padding = (name,
css = "",
hexpand = true,
vexpand = true,
) => Widget.EventBox({
hexpand,
vexpand,
can_focus: false,
child: Widget.Box({ css }),
setup: w => w.on("button-press-event", () => App.toggleWindow(name)),
})
const PopupRevealer = (
name,
child,
transition = "slide_down",
) => Widget.Box(
{ css: "padding: 1px;", class_name: "popup-borderbox" },
Widget.Revealer({
transition,
child: Widget.Box({
class_name: "window-content",
child,
}),
transitionDuration: options.theme.PopupTransitionDuration,
setup: self => self.hook(App, (_, wname, visible) => {
if (wname === name)
self.reveal_child = visible
}),
}),
)
const Layout = (name, child, transition) => ({
"center": () => Widget.CenterBox({},
Padding(name),
Widget.CenterBox(
{ vertical: true },
Padding(name),
PopupRevealer(name, child, transition),
Padding(name),
),
Padding(name),
),
"top": () => Widget.CenterBox({},
Padding(name),
Widget.Box(
{ vertical: true },
PopupRevealer(name, child, transition),
Padding(name),
),
Padding(name),
),
"top-right": () => Widget.Box({},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
PopupRevealer(name, child, transition),
Padding(name),
),
),
"top-center": () => Widget.Box({},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
PopupRevealer(name, child, transition),
Padding(name),
),
Padding(name),
),
"top-left": () => Widget.Box({},
Widget.Box(
{
hexpand: false,
vertical: true,
},
PopupRevealer(name, child, transition),
Padding(name),
),
Padding(name),
),
"bottom-left": () => Widget.Box({},
Widget.Box(
{
hexpand: false,
vertical: true,
},
Padding(name),
PopupRevealer(name, child, transition),
),
Padding(name),
),
"bottom-center": () => Widget.Box({},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
Padding(name),
PopupRevealer(name, child, transition),
),
Padding(name),
),
"bottom-right": () => Widget.Box({},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
Padding(name),
PopupRevealer(name, child, transition),
),
),
"right": () => Widget.Box({},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
// Padding(name),
PopupRevealer(name, child, transition),
),
),
})
export default ({
name,
child,
layout = "center",
transition,
exclusivity = "ignore",
...props
}) => Widget.Window({
name,
class_names: [name, "popup-window"],
popup: true,
visible: false,
keymode: "on-demand",
exclusivity,
layer: "top",
anchor: ["top", "bottom", "right", "left"],
child: Layout(name, child, transition)[layout](),
...props,
setup: (self => {
self.keybind("Escape", () => App.closeWindow(name))
})
})

View file

@ -0,0 +1,56 @@
import BatteryBar from "./buttons/BatteryBar.js"
import ColorPicker from "./buttons/ColorPicker.js"
import Date from "./buttons/Date.js"
import Launcher from "./buttons/Launcher.js"
import Media from "./buttons/Media.js"
import PowerMenu from "./buttons/PowerMenu.js"
import SysTray from "./buttons/SysTray.js"
import SystemIndicators from "./buttons/SystemIndicators.js"
import Taskbar from "./buttons/Taskbar.js"
import Workspaces from "./buttons/Workspaces.js"
import ScreenRecord from "./buttons/ScreenRecord.js"
import Messages from "./buttons/Messages.js"
import options from "../../options.js"
const { start, center, end } = options.bar.layout
const pos = options.bar.position
const widget = {
battery: BatteryBar,
colorpicker: ColorPicker,
date: Date,
launcher: Launcher,
media: Media,
powermenu: PowerMenu,
systray: SysTray,
system: SystemIndicators,
taskbar: Taskbar,
workspaces: Workspaces,
screenrecord: ScreenRecord,
messages: Messages,
expander: () => Widget.Box({ expand: true }),
}
export default (monitor) => Widget.Window({
monitor,
class_name: "bar",
name: `bar${monitor}`,
exclusivity: "exclusive",
layer: "top",
anchor: [pos, "right", "left"],
child: Widget.CenterBox({
css: "min-width: 2px; min-height: 2px;",
startWidget: Widget.Box({
hexpand: true,
children: start.map(w => widget[w]()),
}),
centerWidget: Widget.Box({
hpack: "center",
children: center.map(w => widget[w]()),
}),
endWidget: Widget.Box({
hexpand: true,
children: end.map(w => widget[w]()),
}),
}),
})

View file

@ -0,0 +1,38 @@
import options from "../../options.js"
export default ({
window = "",
flat,
child,
setup,
...rest
}) => Widget.Button({
child: Widget.Box({ child }),
setup: self => {
let open = false
self.toggleClassName("panel-button")
self.toggleClassName(window)
self.toggleClassName("flat", flat ?? options.bar.flatButtons)
self.hook(App, (_, win, visible) => {
if (win !== window)
return
if (open && !visible) {
open = false
self.toggleClassName("active", false)
}
if (visible) {
open = true
self.toggleClassName("active")
}
})
if (setup)
setup(self)
},
...rest,
})

View file

@ -0,0 +1,25 @@
import options from "../../options.js"
const { corners } = options.bar
export default (monitor) => Widget.Window({
monitor,
name: `corner${monitor}`,
class_name: "screen-corner",
anchor: ["top", "bottom", "right", "left"],
click_through: true,
child: Widget.Box({
class_name: "shadow",
child: Widget.Box({
class_name: "border",
expand: true,
child: Widget.Box({
class_name: "corner",
expand: true,
}),
}),
}),
setup: self => {
self.toggleClassName("corners", corners)
},
})

View file

@ -0,0 +1,234 @@
@use 'sass:color';
$bar-spacing: $spacing * .3;
$button-radius: $radius;
@mixin panel-button($flat: true, $reactive: true) {
@include accs-button($flat, $reactive);
>* {
border-radius: $button-radius;
margin: $bar-spacing;
}
label,
image {
font-weight: bold;
}
>* {
padding: $padding * 0.4 $padding * 0.8;
}
}
.bar {
background-color: $bg;
.panel-button {
@include panel-button;
&:not(.flat) {
@include accs-button($flat: false);
}
}
.launcher {
.colored {
color: transparentize($primary-bg, 0.2);
}
&:hover .colored {
color: $primary-bg;
}
&:active .colored,
&.active .colored {
color: $primary-fg;
}
}
.workspaces {
label {
font-size: 0;
min-width: 5pt;
min-height: 5pt;
border-radius: $radius*.6;
box-shadow: inset 0 0 0 $border-width $border-color;
margin: 0 $padding * .5;
transition: $transition* .5;
background-color: transparentize($fg, .8);
&.occupied {
background-color: transparentize($fg, .2);
min-width: 7pt;
min-height: 7pt;
}
&.active {
// background-color: $primary-bg;
background-image: $active-gradient;
min-width: 20pt;
min-height: 12pt;
}
}
&.active,
&:active {
label {
background-color: transparentize($primary-fg, .3);
&.occupied {
background-color: transparentize($primary-fg, .15);
}
&.active {
background-color: $primary-fg;
}
}
}
}
.media label {
margin: 0 ($spacing * .5)
}
.taskbar .indicator.active {
background-color: $primary-bg;
border-radius: $radius;
min-height: 4pt;
min-width: 6pt;
margin: 2pt;
}
.powermenu.colored,
.recorder {
image {
color: transparentize($error-bg, 0.3);
}
&:hover image {
color: transparentize($error-bg, 0.15);
}
&:active image {
color: $primary-fg;
}
}
.quicksettings>box>box {
@include spacing($spacing: if($bar-spacing==0, $padding / 2, $bar-spacing));
}
.quicksettings:not(.active):not(:active) {
.bluetooth {
color: $primary-bg;
label {
font-size: $font-size * .7;
color: $fg;
text-shadow: $text-shadow;
}
}
}
.battery-bar {
>* {
padding: 0;
}
&.bar-hidden>box {
padding: 0 $spacing * .5;
image {
margin: 0;
}
}
levelbar * {
all: unset;
transition: $transition;
}
.whole {
@if $shadows {
image {
-gtk-icon-shadow: $text-shadow;
}
label {
text-shadow: $text-shadow;
}
}
}
.regular image {
margin-left: $spacing * .5;
}
trough {
@include widget;
min-height: 12pt;
min-width: 12pt;
}
.regular trough {
margin-right: $spacing * .5;
}
block {
margin: 0;
&:last-child {
border-radius: 0 $button-radius $button-radius 0;
}
&:first-child {
border-radius: $button-radius 0 0 $button-radius;
}
}
.vertical {
block {
&:last-child {
border-radius: 0 0 $button-radius $button-radius;
}
&:first-child {
border-radius: $button-radius $button-radius 0 0;
}
}
}
@for $i from 1 through $bar-battery-blocks {
block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $primary-bg, $i*3)
}
&.low block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $error-bg, $i*3)
}
&.charging block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $charging-bg, $i*3)
}
&:active .regular block:nth-child(#{$i}).filled {
background-color: color.mix($bg, $primary-fg, $i*3)
}
}
&.low image {
color: $error-bg
}
&.charging image {
color: $charging-bg
}
&:active image {
color: $primary-fg
}
}
}

View file

@ -0,0 +1,94 @@
import icons from "../../../lib/icons.js"
import options from "../../../options.js"
import PanelButton from "../PanelButton.js"
const battery = await Service.import("battery")
let { bar, percentage, blocks, width, low } = options.bar.battery
percentage = Variable(percentage, {})
const Indicator = () => Widget.Icon({
setup: self => self.hook(battery, () => {
self.icon = battery.charging || battery.charged
? icons.battery.charging
: battery.icon_name
}),
})
const PercentLabel = () => Widget.Revealer({
transition: "slide_right",
click_through: true,
reveal_child: percentage.bind(),
child: Widget.Label({
label: battery.bind("percent").as(p => `${p}%`),
}),
})
const LevelBar = () => {
const level = Widget.LevelBar({
mode: 1,
max_value: blocks,
visible: bar !== "hidden",
value: battery.bind("percent").as(p => (p / 100) * blocks),
})
const update = () => {
level.value = (battery.percent / 100) * blocks
level.css = `block { min-width: ${width / blocks}pt; }`
}
return level
// .hook(width, update)
// .hook(blocks, update)
// .hook(bar, () => {
// level.vpack = bar.value === "whole" ? "fill" : "center"
// level.hpack = bar.value === "whole" ? "fill" : "center"
// })
}
const WholeButton = () => Widget.Overlay({
vexpand: true,
child: LevelBar(),
class_name: "whole",
pass_through: true,
overlay: Widget.Box({
hpack: "center",
children: [
Widget.Icon({
icon: icons.battery.charging,
visible: Utils.merge([
battery.bind("charging"),
battery.bind("charged"),
], (ing, ed) => ing || ed),
}),
Widget.Box({
hpack: "center",
vpack: "center",
child: PercentLabel(),
}),
],
}),
})
const Regular = () => Widget.Box({
class_name: "regular",
children: [
Indicator(),
PercentLabel(),
LevelBar(),
],
})
export default () => PanelButton({
class_name: "battery-bar",
hexpand: false,
on_clicked: () => { percentage.value = !percentage.value },
child: Widget.Box({
expand: true,
visible: battery.bind("available"),
child: bar === "whole" ? WholeButton() : Regular(),
}),
setup: self => {
self.toggleClassName("bar-hidden", bar === "hidden")
self.toggleClassName("charging", battery.charging || battery.charged)
self.toggleClassName("low", battery.percent < low)
}
})

View file

@ -0,0 +1,37 @@
import PanelButton from "../PanelButton.js"
import colorpicker from "../../../services/colorpicker.js"
import Gdk from "gi://Gdk"
const css = (color) => `
* {
background-color: ${color};
color: transparent;
}
*:hover {
color: white;
text-shadow: 2px 2px 3px rgba(0,0,0,.8);
}`
export default () => {
const menu = Widget.Menu({
class_name: "colorpicker",
children: colorpicker.bind("colors").as(c => c.map(color => Widget.MenuItem({
child: Widget.Label(color),
css: css(color),
on_activate: () => colorpicker.wlCopy(color),
}))),
})
return PanelButton({
class_name: "color-picker",
child: Widget.Icon("color-select-symbolic"),
tooltip_text: colorpicker.bind("colors").as(v => `${v.length} colors`),
on_clicked: colorpicker.pick,
on_secondary_click: self => {
if (colorpicker.colors.length === 0)
return
menu.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null)
},
})
}

View file

@ -0,0 +1,19 @@
import clock from "../../../services/clock.js"
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const { format, action } = options.bar.date
// const time = Utils.derive([clock], (c) => {
// c.format(format) || ""
// })
// const time = Variable('', {
// poll: [1000, `date "+${format}"`],
// });
export default () => PanelButton({
window: "dashboard",
on_clicked: action,
child: Widget.Label({ label: clock.bind('time').as(t => `${t.format(format)}`) }),
})

View file

@ -0,0 +1,21 @@
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const { icon, label, action } = options.bar.launcher
export default () => PanelButton({
window: "launcher",
on_clicked: action,
child: Widget.Box([
Widget.Icon({
class_name: icon.colored ? "colored" : "",
visible: !!icon.icon,
icon: icon.icon,
}),
Widget.Label({
class_name: label.colored ? "colored" : "",
visible: !!label.label,
label: label.label,
}),
]),
})

View file

@ -0,0 +1,81 @@
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
import icons from "../../../lib/icons.js"
import { icon } from "../../../lib/utils.js"
const mpris = await Service.import("mpris")
const { length, direction, preferred, monochrome } = options.bar.media
const getPlayer = (name = preferred) =>
mpris.getPlayer(name) || mpris.players[0] || null
const Content = (player) => {
const revealer = Widget.Revealer({
click_through: true,
visible: (length > 0),
transition: `slide_${direction}`,
setup: self => {
let current = ""
self.hook(player, () => {
if (current === player.track_title)
return
current = player.track_title
self.reveal_child = true
Utils.timeout(3000, () => {
!self.is_destroyed && (self.reveal_child = false)
})
})
},
child: Widget.Label({
truncate: "end",
max_width_chars: length,
label: player.bind("track_title").as(() =>
`${player.track_artists.join(", ")} - ${player.track_title}`),
}),
})
const playericon = Widget.Icon({
icon: player.bind("entry").as(entry => {
const name = `${entry}${monochrome ? "-symbolic" : ""}`
return icon(name, icons.fallback.audio)
}),
})
return Widget.Box({
attribute: { revealer },
children: direction === "right"
? [playericon, revealer] : [revealer, playericon],
})
}
export default () => {
let player = getPlayer()
const btn = PanelButton({
class_name: "media",
child: Widget.Icon(icons.fallback.audio),
})
const update = () => {
player = getPlayer()
btn.visible = !!player
if (!player)
return
const content = Content(player)
const { revealer } = content.attribute
btn.child = content
btn.on_primary_click = () => { player.playPause() }
btn.on_secondary_click = () => { player.playPause() }
btn.on_scroll_up = () => { player.next() }
btn.on_scroll_down = () => { player.previous() }
btn.on_hover = () => { revealer.reveal_child = true }
btn.on_hover_lost = () => { revealer.reveal_child = false }
}
return btn
// .hook(preferred, update)
.hook(mpris, update, "notify::players")
}

View file

@ -0,0 +1,16 @@
import icons from "../../../lib/icons.js"
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const n = await Service.import("notifications")
const notifs = n.bind("notifications")
const action = options.bar.messages.action
export default () => PanelButton({
class_name: "messages",
on_clicked: action,
visible: notifs.as(n => n.length > 0),
child: Widget.Box([
Widget.Icon(icons.notifications.message),
]),
})

View file

@ -0,0 +1,15 @@
import icons from "../../../lib/icons.js"
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
const { monochrome, action } = options.bar.powermenu
export default () => PanelButton({
window: "powermenu",
on_clicked: action,
child: Widget.Icon(icons.powermenu.shutdown),
setup: self => {
self.toggleClassName("colored", !monochrome)
self.toggleClassName("box")
},
})

View file

@ -0,0 +1,21 @@
import PanelButton from "../PanelButton.js"
import screenrecord from "../../../services/screenrecord.js"
import icons from "../../../lib/icons.js"
export default () => PanelButton({
class_name: "recorder",
on_clicked: () => screenrecord.stop(),
visible: screenrecord.bind("recording"),
child: Widget.Box({
children: [
Widget.Icon(icons.recorder.recording),
Widget.Label({
label: screenrecord.bind("timer").as(time => {
const sec = time % 60
const min = Math.floor(time / 60)
return `${min}:${sec < 10 ? "0" + sec : sec}`
}),
}),
],
}),
})

View file

@ -0,0 +1,39 @@
import PanelButton from "../PanelButton.js"
import Gdk from "gi://Gdk"
import options from "../../../options.js"
const systemtray = await Service.import("systemtray")
const { ignore } = options.bar.systray
const SysTrayItem = (item) => PanelButton({
class_name: "tray-item",
child: Widget.Icon({ icon: item.bind("icon") }),
tooltip_markup: item.bind("tooltip_markup"),
setup: self => {
const menu = item.menu
if (!menu)
return
const id = item.menu?.connect("popped-up", () => {
self.toggleClassName("active")
menu.connect("notify::visible", () => {
self.toggleClassName("active", menu.visible)
})
menu.disconnect(id)
})
if (id)
self.connect("destroy", () => item.menu?.disconnect(id))
},
on_primary_click: btn => item.menu?.popup_at_widget(
btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null),
on_secondary_click: btn => item.menu?.popup_at_widget(
btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null),
})
export default () => Widget.Box()
.bind("children", systemtray, "items", i => i
.filter(({ id }) => !ignore.includes(id))
.map(SysTrayItem))

View file

@ -0,0 +1,79 @@
import PanelButton from "../PanelButton.js"
import icons from "../../../lib/icons.js"
import asusctl from "../../../services/asusctl.js"
const notifications = await Service.import("notifications")
const bluetooth = await Service.import("bluetooth")
const audio = await Service.import("audio")
const network = await Service.import("network")
const ProfileIndicator = () => Widget.Icon()
.bind("visible", asusctl, "profile", p => p !== "Balanced")
.bind("icon", asusctl, "profile", p => icons.asusctl.profile[p])
const ModeIndicator = () => Widget.Icon()
.bind("visible", asusctl, "mode", m => m !== "Hybrid")
.bind("icon", asusctl, "mode", m => icons.asusctl.mode[m])
const MicrophoneIndicator = () => Widget.Icon()
.hook(audio, self => self.visible =
audio.recorders.length > 0
|| audio.microphone.stream?.is_muted
|| audio.microphone.is_muted)
.hook(audio.microphone, self => {
const vol = audio.microphone.stream.is_muted ? 0 : audio.microphone.volume
const { muted, low, medium, high } = icons.audio.mic
const cons = [[67, high], [34, medium], [1, low], [0, muted]]
self.icon = cons.find(([n]) => n <= vol * 100)?.[1] || ""
})
const DNDIndicator = () => Widget.Icon({
visible: notifications.bind("dnd"),
icon: icons.notifications.silent,
})
const BluetoothIndicator = () => Widget.Overlay({
class_name: "bluetooth",
passThrough: true,
child: Widget.Icon({
icon: icons.bluetooth.enabled,
visible: bluetooth.bind("enabled"),
}),
overlay: Widget.Label({
hpack: "end",
vpack: "start",
label: bluetooth.bind("connected_devices").as(c => `${c.length}`),
visible: bluetooth.bind("connected_devices").as(c => c.length > 0),
}),
})
const NetworkIndicator = () => Widget.Icon().hook(network, self => {
const icon = network[network.primary || "wifi"]?.icon_name
self.icon = icon || ""
self.visible = !!icon
})
const AudioIndicator = () => Widget.Icon({
icon: audio.speaker.bind("volume").as(vol => {
const { muted, low, medium, high, overamplified } = icons.audio.volume
const cons = [[101, overamplified], [67, high], [34, medium], [1, low], [0, muted]]
const icon = cons.find(([n]) => n <= vol * 100)?.[1] || ""
return audio.speaker.is_muted ? muted : icon
}),
})
export default () => PanelButton({
class_name: "quicksettings panel-button",
on_clicked: () => App.toggleWindow("quicksettings"),
on_scroll_up: () => audio.speaker.volume += 0.02,
on_scroll_down: () => audio.speaker.volume -= 0.02,
child: Widget.Box([
asusctl?.available && ProfileIndicator(),
asusctl?.available && ModeIndicator(),
DNDIndicator(),
BluetoothIndicator(),
NetworkIndicator(),
AudioIndicator(),
MicrophoneIndicator(),
]),
})

View file

@ -0,0 +1,86 @@
import { launchApp, icon } from "../../../lib/utils.js"
import icons from "../../../lib/icons.js"
import options from "../../../options.js"
import { watch } from "../../../lib/experiments.js"
import PanelButton from "../PanelButton.js"
const hyprland = await Service.import("hyprland")
const apps = await Service.import("applications")
const { monochrome, exclusive } = options.bar.taskbar
const { position } = options.bar
const focus = (address) => hyprland.messageAsync(
`dispatch focuswindow address:${address}`)
const DummyItem = (address) => Widget.Box({
attribute: { address },
visible: false,
})
const AppItem = (address) => {
const client = hyprland.getClient(address)
if (!client || client.class === "")
return DummyItem(address)
const app = apps.list.find(app => app.match(client.class))
const btn = PanelButton({
class_name: "panel-button",
tooltip_text: client.title,
on_primary_click: () => focus(address),
on_middle_click: () => app && launchApp(app),
visible: watch(true, [hyprland], () => {
return exclusive
? hyprland.active.workspace.id === client.workspace.id
: true
}),
child: Widget.Icon({
icon: icon(
(app?.icon_name || client.class) + (monochrome ? "-symbolic" : ""),
icons.fallback.executable,
),
}),
})
return Widget.Box(
{ attribute: { address } },
Widget.Overlay({
child: btn,
pass_through: true,
overlay: Widget.Box({
className: "indicator",
hpack: "center",
vpack: (position === "top" ? "start" : "end"),
setup: w => w.hook(hyprland, () => {
w.toggleClassName("active", hyprland.active.client.address === address)
}),
}),
}),
)
}
function sortItems(arr) {
return arr.sort(({ attribute: a }, { attribute: b }) => {
const aclient = hyprland.getClient(a.address)
const bclient = hyprland.getClient(b.address)
return aclient.workspace.id - bclient.workspace.id
})
}
export default () => Widget.Box({
class_name: "taskbar",
children: sortItems(hyprland.clients.map(c => AppItem(c.address))),
setup: w => w
.hook(hyprland, (w, address) => {
if (typeof address === "string")
w.children = w.children.filter(ch => ch.attribute.address !== address)
}, "client-removed")
.hook(hyprland, (w, address) => {
if (typeof address === "string")
w.children = sortItems([...w.children, AppItem(address)])
}, "client-added")
.hook(hyprland, (w, event) => {
if (event === "movewindow")
w.children = sortItems(w.children)
}, "event"),
})

View file

@ -0,0 +1,38 @@
import PanelButton from "../PanelButton.js"
import options from "../../../options.js"
import { sh, range } from "../../../lib/utils.js"
const hyprland = await Service.import("hyprland")
const { workspaces } = options.bar.workspaces
const dispatch = (arg) => {
sh(`hyprctl dispatch workspace ${arg}`)
}
const Workspaces = (ws) => Widget.Box({
children: range(ws || 20).map(i => Widget.Label({
attribute: i,
vpack: "center",
label: `${i}`,
setup: self => self.hook(hyprland, () => {
self.toggleClassName("active", hyprland.active.workspace.id === i)
self.toggleClassName("occupied", (hyprland.getWorkspace(i)?.windows || 0) > 0)
}),
})),
setup: box => {
if (ws === 0) {
box.hook(hyprland.active.workspace, () => box.children.map(btn => {
btn.visible = hyprland.workspaces.some(ws => ws.id === btn.attribute)
}))
}
},
})
export default () => PanelButton({
window: "overview",
class_name: "workspaces",
on_scroll_up: () => dispatch("m+1"),
on_scroll_down: () => dispatch("m-1"),
on_clicked: () => App.toggleWindow("overview"),
child: Workspaces(workspaces),
})

View file

@ -0,0 +1,50 @@
$_shadow-size: $padding;
$_radius: $radius * $hyprland-gaps-multiplier;
$_margin: 99px;
window.screen-corner {
box.shadow {
margin-right: $_margin * -1;
margin-left: $_margin * -1;
@if $shadows {
box-shadow: inset 0 0 $_shadow-size 0 $shadow-color;
}
@if $bar-position =="top" {
margin-bottom: $_margin * -1;
}
@if $bar-position =="bottom" {
margin-top: $_margin * -1;
}
}
box.border {
@if $bar-position =="top" {
border-top: $border-width solid $bg;
}
@if $bar-position =="bottom" {
border-bottom: $border-width solid $bg;
}
margin-right: $_margin;
margin-left: $_margin;
}
box.corner {
box-shadow: 0 0 0 $border-width $border-color;
}
&.corners {
box.border {
border-radius: if($radius>0, $radius * $hyprland-gaps-multiplier, 0);
box-shadow: 0 0 0 $_radius $bg;
}
box.corner {
border-radius: if($radius>0, $radius * $hyprland-gaps-multiplier, 0);
}
}
}

View file

@ -0,0 +1,38 @@
import { uptime } from "../../lib/variables.js"
import clock from "../../services/clock.js"
function up(up) {
const h = Math.floor(up / 60)
const m = Math.floor(up % 60)
return `uptime: ${h}:${m < 10 ? "0" + m : m}`
}
export default () => Widget.Box({
vertical: true,
class_name: "date-column vertical",
children: [
Widget.Box({
class_name: "clock-box",
vertical: true,
children: [
Widget.Label({
class_name: "clock",
label: clock.bind('time').as(t => t.format("%H:%M")),
}),
Widget.Label({
class_name: "uptime",
label: uptime.bind().as(up),
}),
],
}),
Widget.Box({
class_name: "calendar",
children: [
Widget.Calendar({
hexpand: true,
hpack: "center",
}),
],
}),
],
})

View file

@ -0,0 +1,33 @@
import PopupWindow from "../PopupWindow.js"
import DateColumn from "./DateColumn.js"
import options from "../../options.js"
const { bar, datemenu } = options
const pos = bar.position
const layout = `${bar.position}-${datemenu.position}`
const Settings = () => Widget.Box({
class_name: "datemenu horizontal",
vexpand: false,
children: [
// NotificationColumn(),
// Widget.Separator({ orientation: 1 }),
DateColumn(),
],
})
const DateMenu = () => PopupWindow({
name: "datemenu",
exclusivity: "exclusive",
transition: pos === "top" ? "slide_down" : "slide_up",
layout: layout,
child: Settings(),
})
export function setupDateMenu() {
App.addWindow(DateMenu())
// layout.connect("changed", () => {
// App.removeWindow("datemenu")
// App.addWindow(DateMenu())
// })
}

View file

@ -0,0 +1,31 @@
// import Gtk from 'gi://Gtk';
/** @param {string} windowName */
const Padding = windowName => Widget.EventBox({
class_name: 'padding',
hexpand: true,
vexpand: true,
setup: w => w.on('button-press-event', () => App.toggleWindow(windowName)),
});
const Dock = () => {
return Widget.Box({
name: 'dock-box',
child: Widget.Label({
label: "Yo Yo YO YO bfbhgjhdfvbfb",
})
})
}
export default () => Widget.Window({
name: 'dock',
anchor: ['bottom'],
exclusivity: 'normal',
keymode: 'on-demand',
layer: 'background',
// popup: true,
// monitor: 0,
visible: true,
child: Dock(),
})

View file

@ -0,0 +1,156 @@
import GLib from 'gi://GLib'
const time = Widget.Label({
name: 'lockscreen-time',
label: GLib.DateTime.new_now_local().format("%H:%M"),
setup: (self) => self.poll(1000, label => {
label.label = GLib.DateTime.new_now_local().format("%H:%M");
}),
justification: 'left',
hexpand: true,
hpack: 'start',
})
const date = Widget.Label({
name: 'lockscreen-date',
label: GLib.DateTime.new_now_local().format("%e, %B %A"),
setup: (self) => self.poll(60000, label => {
label.label = GLib.DateTime.new_now_local().format("%e, %B %A");
}),
justification: 'left',
})
const network = await Service.import('network')
const battery = await Service.import('battery')
const Lockscreen = () => {
const WifiIndicator = () => Widget.Box({
child: Widget.Icon({
icon: network.wifi.bind('icon_name'),
}),
})
const WiredIndicator = () => Widget.Icon({
icon: network.wired.bind('icon_name'),
})
const batPercent = Widget.Revealer({
revealChild: false,
transitionDuration: 600,
transition: 'slide_left',
child: Widget.Label({
name: "lockscreen-revealer",
label: battery.bind('percent').transform(p => "" + p + "%"),
}),
})
const batInfo = Widget.Box({
"class-name": 'lockscreen-smolbox',
hexpand: false,
hpack: 'end',
children: [
batPercent,
Widget.EventBox({
child: Widget.Box({
child: Widget.Label({
label: battery.bind('percent').transform(p => ['', '', '', '', ''][Math.floor(p / 20)]),
class_name: 'lockscreen-battery',
hexpand: false,
setup: icon => icon.hook(battery, () => {
icon.toggleClassName('lockscreen-charging', battery.charging);
icon.toggleClassName('lockscreen-charged', battery.charged);
icon.toggleClassName('lockscreen-low', battery.percent < 30);
}),
}),
}),
onHover: () => {
batPercent.reveal_child = true;
print("revealed");
},
onHoverLost: () => {
batPercent.reveal_child = false;
}
})
]
})
const networkName = Widget.Revealer({
revealChild: false,
transitionDuration: 600,
transition: 'slide_left',
child: Widget.Label({
name: "lockscreen-revealer",
label: network.wifi.bind('ssid')
.as(ssid => ssid || 'Unknown'),
}),
})
const networkStatus = Widget.Box({
class_name: "lockscreen-smolbox",
hexpand: false,
hpack: 'end',
children: [
networkName,
Widget.EventBox({
child: Widget.Stack({
class_name: "lockscreen-network",
children: {
'wifi': WifiIndicator(),
'wired': WiredIndicator()
},
shown: network.bind('primary').transform(p => p || 'wifi'),
}),
onHover: () => {
networkName.reveal_child = true;
},
onHoverLost: () => {
networkName.reveal_child = false;
}
})
]
})
const boxRight = Widget.Box({
homogeneous: false,
name: "lockscreen-boxRight",
vertical: true,
hexpand: false,
children: [networkStatus, batInfo],
vpack: 'end',
hpack: 'end',
})
const boxLeft = Widget.Box({
homogeneous: false,
vertical: true,
children: [time, date],
vpack: 'end',
hpack: 'start',
})
return Widget.Box({
children: [boxLeft, boxRight],
vertical: false,
})
}
function randomImage() {
const a = Utils.exec(`bash -c "ls ${App.configDir + '/modules/lock/images/'} | shuf -n 1"`);
const b = `background-image: url("${App.configDir + '/modules/lock/images/' + a}");`;
print(b);
return b;
}
export default () => Widget.Window({
name: 'lockscreen',
css: randomImage(),
anchor: ['top', 'left', 'right', 'bottom'],
exclusivity: 'normal',
keymode: 'on-demand',
layer: 'top',
visible: false,
monitor: 0,
child: Lockscreen(),
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -0,0 +1,139 @@
import GLib from "gi://GLib"
import icons from "../../lib/icons.js"
const time = (time, format = "%H:%M") => GLib.DateTime
.new_from_unix_local(time)
.format(format)
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon img",
css: `
background-image: url("${image}");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
min-width: 78px;
min-height: 78px;
`,
})
}
let icon = icons.fallback.notification
if (Utils.lookUpIcon(app_icon))
icon = app_icon
if (Utils.lookUpIcon(app_entry || ""))
icon = app_entry || ""
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon",
css: `
min-width: 78px;
min-height: 78px;
`,
child: Widget.Icon({
icon,
size: 58,
hpack: "center", hexpand: true,
vpack: "center", vexpand: true,
}),
})
}
export default (notification) => {
const content = Widget.Box({
class_name: "content",
children: [
NotificationIcon(notification),
Widget.Box({
hexpand: true,
vertical: true,
children: [
Widget.Box({
children: [
Widget.Label({
class_name: "title",
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: notification.summary.trim(),
use_markup: true,
}),
Widget.Label({
class_name: "time",
vpack: "start",
label: time(notification.time),
}),
Widget.Button({
class_name: "close-button",
vpack: "start",
hexpand: false,
vexpand: false,
child: Widget.Icon("window-close-symbolic"),
on_clicked: notification.close,
}),
],
}),
Widget.Label({
class_name: "description",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
label: notification.body.trim(),
max_width_chars: 24,
wrap: true,
}),
],
}),
],
})
const actionsbox = notification.actions.length > 0 ? Widget.Revealer({
transition: "slide_down",
child: Widget.EventBox({
child: Widget.Box({
class_name: "actions horizontal",
children: notification.actions.map(action => Widget.Button({
class_name: "action-button",
on_clicked: () => notification.invoke(action.id),
hexpand: true,
child: Widget.Label(action.label),
})),
}),
}),
}) : null
const eventbox = Widget.EventBox({
vexpand: false,
on_primary_click: notification.dismiss,
on_hover() {
if (actionsbox)
actionsbox.reveal_child = true
},
on_hover_lost() {
if (actionsbox)
actionsbox.reveal_child = true
notification.dismiss()
},
child: Widget.Box({
vertical: true,
children: actionsbox ? [content, actionsbox] : [content],
}),
})
return Widget.Box({
class_name: `notification ${notification.urgency}`,
child: eventbox,
})
}

View file

@ -0,0 +1,95 @@
import Notification from "./Notification.js"
import options from "../../options.js"
const notifications = await Service.import("notifications")
const { transitionDuration } = options
const { position, blacklist } = options.notifications
const { timeout, idle } = Utils
function Animated(id) {
const n = notifications.getNotification(id)
const widget = Notification(n)
const inner = Widget.Revealer({
css: "border: 1px solid magenta;",
transition: "slide_left",
transition_duration: transitionDuration,
child: widget,
})
const outer = Widget.Revealer({
css: "border: 1px solid yellow;",
transition: "slide_down",
transition_duration: transitionDuration,
child: inner,
})
const box = Widget.Box({
hpack: "end",
child: outer,
})
idle(() => {
outer.reveal_child = true
timeout(transitionDuration, () => {
inner.reveal_child = true
})
})
return Object.assign(box, {
dismiss() {
inner.reveal_child = false
timeout(transitionDuration, () => {
outer.reveal_child = false
timeout(transitionDuration, () => {
box.destroy()
})
})
},
})
}
function PopupList() {
const map = new Map
const box = Widget.Box({
hpack: "end",
vertical: true,
css: `min-width: ${options.notifications.width}px;`,
})
function remove(_, id) {
map.get(id)?.dismiss()
map.delete(id)
}
return box
.hook(notifications, (_, id) => {
if (id !== undefined) {
if (map.has(id))
remove(null, id)
if (blacklist.includes(notifications.getNotification(id).app_name))
return
if (notifications.dnd)
return
const w = Animated(id)
map.set(id, w)
box.children = [w, ...box.children]
}
}, "notified")
.hook(notifications, remove, "dismissed")
.hook(notifications, remove, "closed")
}
export default (monitor) => Widget.Window({
monitor,
name: `notifications${monitor}`,
anchor: position,
class_name: "notifications",
child: Widget.Box({
css: "padding: 2px;",
child: PopupList(),
}),
})

View file

@ -0,0 +1,111 @@
import { icon } from "../../lib/utils.js"
import icons from "../../lib/icons.js"
import Progress from "./Progress.js"
import brightness from "../../services/brightness.js"
import options from "../../options.js"
const audio = await Service.import("audio")
const { progress, microphone } = options.osd
const DELAY = 2500
function OnScreenProgress(vertical) {
const indicator = Widget.Icon({
size: 42,
vpack: "start",
})
const progress = Progress({
vertical,
width: vertical ? 42 : 300,
height: vertical ? 300 : 42,
child: indicator,
})
const revealer = Widget.Revealer({
transition: "slide_left",
child: progress,
})
let count = 0
function show(value, icon) {
revealer.reveal_child = true
indicator.icon = icon
progress.setValue(value)
count++
Utils.timeout(DELAY, () => {
count--
if (count === 0)
revealer.reveal_child = false
})
}
return revealer
.hook(brightness, () => show(
brightness.screen,
icons.brightness.screen,
), "notify::screen")
.hook(brightness, () => show(
brightness.kbd,
icons.brightness.keyboard,
), "notify::kbd")
.hook(audio.speaker, () => show(
audio.speaker.volume,
icon(audio.speaker.icon_name || "", icons.audio.type.speaker),
), "notify::volume")
}
function MicrophoneMute() {
const icon = Widget.Icon({
class_name: "microphone",
})
const revealer = Widget.Revealer({
transition: "slide_up",
child: icon,
})
let count = 0
let mute = audio.microphone.stream?.is_muted ?? false
return revealer.hook(audio.microphone, () => Utils.idle(() => {
if (mute !== audio.microphone.stream?.is_muted) {
mute = audio.microphone.stream.is_muted
icon.icon = icons.audio.mic[mute ? "muted" : "high"]
revealer.reveal_child = true
count++
Utils.timeout(DELAY, () => {
count--
if (count === 0)
revealer.reveal_child = false
})
}
}))
}
export default (monitor) => Widget.Window({
monitor,
name: `indicator${monitor}`,
class_name: "indicator",
layer: "overlay",
click_through: true,
anchor: ["right", "left", "top", "bottom"],
child: Widget.Box({
css: "padding: 2px;",
expand: true,
child: Widget.Overlay(
{ child: Widget.Box({ expand: true }) },
Widget.Box({
hpack: progress.pack.h,
vpack: progress.pack.v,
child: OnScreenProgress(progress.vertical),
}),
Widget.Box({
hpack: microphone.pack.h,
vpack: microphone.pack.v,
child: MicrophoneMute(),
}),
),
}),
})

View file

@ -0,0 +1,66 @@
import GLib from "gi://GLib?version=2.0"
import { range } from "../../lib/utils.js"
import options from "../../options.js"
export default ({
height = 18,
width = 180,
vertical = false,
child,
}) => {
const fill = Widget.Box({
class_name: "fill",
hexpand: vertical,
vexpand: !vertical,
hpack: vertical ? "fill" : "start",
vpack: vertical ? "end" : "fill",
child,
})
const container = Widget.Box({
class_name: "progress",
child: fill,
css: `
min-width: ${width}px;
min-height: ${height}px;
`,
})
let fill_size = 0
let animations = []
return Object.assign(container, {
setValue(value) {
if (value < 0)
return
if (animations.length > 0) {
for (const id of animations)
GLib.source_remove(id)
animations = []
}
const axis = vertical ? "height" : "width"
const axisv = vertical ? height : width
const min = vertical ? width : height
const preferred = (axisv - min) * value + min
if (!fill_size) {
fill_size = preferred
fill.css = `min-${axis}: ${preferred}px;`
return
}
const frames = options.transition / 10
const goal = preferred - fill_size
const step = goal / frames
animations = range(frames, 0).map(i => Utils.timeout(5 * i, () => {
fill_size += step
fill.css = `min-${axis}: ${fill_size}px`
animations.shift()
}))
},
})
}

View file

@ -0,0 +1,43 @@
import PopupWindow from "../PopupWindow.js"
import Workspace from "./Workspace.js"
import options from "../../options.js"
import { range } from "../../lib/utils.js"
const hyprland = await Service.import("hyprland")
const Overview = (ws) => Widget.Box({
class_name: "overview horizontal",
// spacing: 10,
children: ws > 0
? range(ws).map(Workspace)
: hyprland.workspaces
.map(({ id }) => Workspace(id))
.sort((a, b) => a.attribute.id - b.attribute.id),
setup: w => {
if (ws > 0)
return
w.hook(hyprland, (w, id) => {
if (id === undefined)
return
w.children = w.children
.filter(ch => ch.attribute.id !== Number(id))
}, "workspace-removed")
w.hook(hyprland, (w, id) => {
if (id === undefined)
return
w.children = [...w.children, Workspace(Number(id))]
.sort((a, b) => a.attribute.id - b.attribute.id)
}, "workspace-added")
},
})
export default () => PopupWindow({
name: "overview",
layout: "center",
transition: "slide_down",
child: Overview(options.overview.workspaces),
})

View file

@ -0,0 +1,51 @@
import { createSurfaceFromWidget, icon } from "../../lib/utils.js"
import Gdk from "gi://Gdk"
import Gtk from "gi://Gtk?version=3.0"
import options from "../../options.js"
import icons from "../../lib/icons.js"
const monochrome = options.overview.monochromeIcon
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)]
const scale = (size) => (options.overview.scale / 100) * size
const hyprland = await Service.import("hyprland")
const apps = await Service.import("applications")
const dispatch = (args) => hyprland.messageAsync(`dispatch ${args}`)
const Icon = (m, c) => {
const app = apps.list.find(app => app.match(c))
if (!app)
return icons.fallback.executable
return icon(
app.icon_name + (m ? "-symbolic" : ""),
icons.fallback.executable,
)
}
export default ({ address, size: [w, h], class: c, title }) => Widget.Button({
class_name: "client",
attribute: { address },
tooltip_text: `${title}`,
child: Widget.Icon({
class_name: "overview-icon",
css: `
min-width: ${scale(w)}px;
min-height: ${scale(h)}px;
`,
icon: Icon(monochrome, c),
}),
on_secondary_click: () => dispatch(`closewindow address:${address}`),
on_clicked: () => {
dispatch(`focuswindow address:${address}`)
App.closeWindow("overview")
},
setup: btn => btn
.on("drag-data-get", (_w, _c, data) => data.set_text(address, address.length))
.on("drag-begin", (_, context) => {
Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(btn))
btn.toggleClassName("hidden", true)
})
.on("drag-end", () => btn.toggleClassName("hidden", false))
.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.COPY),
})

View file

@ -0,0 +1,59 @@
import Window from "./Window.js"
import Gdk from "gi://Gdk"
import Gtk from "gi://Gtk?version=3.0"
import options from "../../options.js"
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)]
const scale = (size) => (options.overview.scale / 100) * size
const hyprland = await Service.import("hyprland")
const dispatch = (args) => hyprland.messageAsync(`dispatch ${args}`)
const size = (id) => {
const def = { h: 1080, w: 1920 }
const ws = hyprland.getWorkspace(id)
if (!ws)
return def
const mon = hyprland.getMonitor(ws.monitorID)
return mon ? { h: mon.height, w: mon.width } : def
}
export default (id) => Widget.Box({
attribute: { id },
tooltipText: `${id}`,
class_name: "workspace",
vpack: "center",
css: `
min-width: ${scale(size(id).w)}px;
min-height: ${scale(size(id).h)}px;
`,
setup: box => box.hook(hyprland, () => {
box.toggleClassName("active", hyprland.active.workspace.id === id)
}),
child: Widget.EventBox({
expand: true,
on_primary_click: () => {
App.closeWindow("overview")
dispatch(`workspace ${id}`)
},
setup: eventbox => {
eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY)
eventbox.connect("drag-data-received", (_w, _c, _x, _y, data) => {
const address = new TextDecoder().decode(data.get_data())
dispatch(`movetoworkspacesilent ${id},address:${address}`)
})
},
child: Widget.Fixed().hook(hyprland, fixed => {
fixed.get_children().forEach(ch => ch.destroy())
hyprland.clients
.filter(({ workspace }) => workspace.id === id)
.forEach(c => {
const x = c.at[0] - (hyprland.getMonitor(c.monitor)?.x || 0)
const y = c.at[1] - (hyprland.getMonitor(c.monitor)?.y || 0)
c.mapped && fixed.put(Window(c), scale(x), scale(y))
})
fixed.show_all()
}, "notify::clients"),
}),
})

View file

@ -0,0 +1,144 @@
// import Gtk from 'gi://Gtk';
import { uptime } from '../../lib/variables.js';
import powermenu from '../../services/powermenu.js';
import PopupWindow from "../PopupWindow.js";
const options = [
{
label: "lock",
icon: ""
},
{
label: "sleep",
icon: ""
},
{
label: "logout",
icon: ""
},
{
label: "shutdown",
icon: ""
},
{
label: "reboot",
icon: ""
}
];
function buttonCreator(option, flag = 'none') {
const but = Widget.Button({
className: 'powermenu-buttonBox',
child: Widget.Box({
child: Widget.Label({
className: 'powermenu-buttonLabel',
label: option['icon'],
}),
}),
onClicked: () => powermenu.action(option['label']),//Utils.execAsync(option['cmd']),
})
if (flag == 'first') {
but.toggleClassName('powermenu-buttonBoxFirst', true);
} else if (flag == 'last') {
but.toggleClassName('powermenu-buttonBoxLast', true);
}
return but;
}
const Powermenu = () => {
let powerbutton = Widget.Box({
hpack: 'start',
vpack: 'start',
className: 'powermenu-powerbutton',
child: Widget.Label({
label: "",
hpack: 'start',
}),
})
const topBox = Widget.Box({
vertical: false,
homogeneous: false,
name: "powermenu-topbox",
css: `background-image: url('${App.configDir + '/modules/powermenu/imag2.png'}'); background-position: center;`,
spacing: 0,
children: [
Widget.EventBox({
vexpand: false,
vpack: 'start',
child: powerbutton,
onPrimaryClick: () => {
App.toggleWindow('powermenu');
}
}),
Widget.Box({
hpack: 'start',
vpack: 'start',
className: 'powermenu-username',
child: Widget.Label({
hpack: 'start',
label: `${Utils.exec('whoami')}`
})
})
]
})
const midBox = Widget.Box({
hpack: 'fill',
className: 'powermenu-midbox',
child: Widget.Label({
label: uptime.bind().as(value => 'Uptime : ' + Math.floor(value / 60) + ' hours ' + Math.floor(value % 60) + ' minutes'),
setup: self => self.hook(uptime, () => {
self.label = uptime.value.toString();
})
})
})
let bottomBox = Widget.Box({
vertical: false,
homogeneous: false,
name: 'powermenu-bottombox',
spacing: 20,
children: [
buttonCreator(options[0], 'first'),
buttonCreator(options[1]),
buttonCreator(options[2]),
buttonCreator(options[3]),
buttonCreator(options[4], 'last'),
],
})
let motherBox = Widget.Box({
homogeneous: false,
vertical: true,
name: 'powermenu-motherbox',
// hexpand: true,
// vexpand: false,
hpack: 'center',
vpack: 'center',
children: [topBox, midBox, bottomBox],
})
return motherBox;
}
export default () => PopupWindow({
name: 'powermenu',
transition: "slide_down",
child: Powermenu(),
})
// export default () => Widget.Window({
// name: 'powermenu',
// anchor: ['top', 'left', 'right', 'bottom'],
// exclusivity: 'normal',
// keymode: 'exclusive',
// layer: 'top',
// popup: true,
// // monitor: 0,
// visible: false,
// child: Powermenu(),
// })

View file

@ -0,0 +1,70 @@
import PopupWindow from '../PopupWindow.js';
import powermenu from '../../services/powermenu.js';
export default () => PopupWindow({
name: "verification",
transition: "crossfade",
layout: 'center',
child: Widget.Box({
class_name: "verification",
vertical: true,
homogeneous: false,
children: [
Widget.Box({
class_name: "verification-label",
vertical: true,
children: [
Widget.Label({
class_name: "desc",
label: powermenu.bind("title"),
}),
],
}),
Widget.Box({
vexpand: true,
vpack: "end",
spacing: 20,
homogeneous: false,
children: [
Widget.Button({
class_name: "verification-buttonBox verification-buttonBoxFirst",
child: Widget.Label({
label: "No",
hpack: "center",
vpack: "center",
}),
on_clicked: () => App.toggleWindow("verification"),
setup: self => self.hook(App, (_, name, visible) => {
if (name === "verification" && visible)
self.grab_focus()
}),
}),
Widget.Button({
class_name: "verification-buttonBox verification-buttonBoxLast",
child: Widget.Label({
label: "Yes",
hpack: "center",
vpack: "center",
}),
on_clicked: () => Utils.exec(powermenu.cmd),
}),
],
}),
],
}),
})
// export default () => Widget.Window({
// name: 'verification',
// // transition: 'crossfade',
// anchor: ['top', 'left', 'right', 'bottom'],
// exclusivity: 'normal',
// keymode: 'exclusive',
// layer: 'top',
// popup: true,
// // monitor: 0,
// visible: false,
// child: Verification(),
// })

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

View file

@ -0,0 +1,117 @@
import Notification from "../notifications/Notification.js"
import options from "../../options.js"
import icons from "../../lib/icons.js"
const notifications = await Service.import("notifications")
const notifs = notifications.bind("notifications")
const Animated = (n) => Widget.Revealer({
transition_duration: options.transition,
transition: "slide_down",
child: Notification(n),
setup: self => Utils.timeout(options.transition, () => {
if (!self.is_destroyed)
self.reveal_child = true
}),
})
const ClearButton = () => Widget.Button({
on_clicked: notifications.clear,
sensitive: notifs.as(n => n.length > 0),
child: Widget.Box({
children: [
Widget.Label("Clear "),
Widget.Icon({
icon: notifs.as(n => icons.trash[n.length > 0 ? "full" : "empty"]),
}),
],
}),
})
const Header = () => Widget.Box({
class_name: "header",
children: [
Widget.Label({ label: "Notifications", hexpand: true, xalign: 0 }),
ClearButton(),
],
})
const NotificationList = () => {
const map = new Map
const box = Widget.Box({
vertical: true,
children: notifications.notifications.map(n => {
const w = Animated(n)
map.set(n.id, w)
return w
}),
visible: notifs.as(n => n.length > 0),
})
function remove(_, id) {
const n = map.get(id)
if (n) {
n.reveal_child = false
Utils.timeout(options.transition, () => {
n.destroy()
map.delete(id)
})
}
}
return box
.hook(notifications, remove, "closed")
.hook(notifications, (_, id) => {
if (id !== undefined) {
if (map.has(id))
remove(null, id)
const n = notifications.getNotification(id)
const w = Animated(n)
map.set(id, w)
box.children = [w, ...box.children]
}
}, "notified")
}
const Placeholder = () => Widget.Box({
class_name: "placeholder",
vertical: true,
vpack: "center",
hpack: "center",
vexpand: true,
hexpand: true,
visible: notifs.as(n => n.length === 0),
children: [
Widget.Icon(icons.notifications.silent),
Widget.Label("Your inbox is empty"),
],
})
export default () => Widget.Box({
class_name: "notifications",
css: `min-width: ${options.notifications.width}px`,
// vpack: "fill",
vexpand: true,
vertical: true,
children: [
Header(),
Widget.Scrollable({
vexpand: true,
// vpack: "fill",
hscroll: "never",
class_name: "notification-scrollable",
child: Widget.Box({
class_name: "notification-list vertical",
vertical: true,
vexpand: true,
// vpack: "fill",
// vexpand: true,
children: [
NotificationList(),
Placeholder(),
],
}),
}),
],
})

View file

@ -0,0 +1,87 @@
import { ProfileSelector, ProfileToggle } from "./widgets/AsusProfile.js"
// import { Header } from "./widgets/Header.js"
import { Volume, Microhone, SinkSelector, AppMixer } from "./widgets/Volume.js"
import { Brightness } from "./widgets/Brightness.js"
import { NetworkToggle, WifiSelection } from "./widgets/Network.js"
import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth.js"
import { DND } from "./widgets/DND.js"
// import { DarkModeToggle } from "./widgets/DarkMode.js"
import { MicMute } from "./widgets/MicMute.js"
import { Media } from "./widgets/Media.js"
import PopupWindow from "../PopupWindow.js"
import NotificationColumn from "./NotificationColumn.js"
import options from "../../options.js"
const { bar, quicksettings } = options
const media = (await Service.import("mpris")).bind("players")
const layout = `${quicksettings.position}`
const Row = (
toggles = [],
menus = [],
) => 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,
vexpand: true,
vpack: "fill",
class_name: "quicksettings vertical",
css: `min-width: ${quicksettings.width}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],
[ProfileSelector],
),
Row([MicMute, DND]),
Widget.Box({
visible: media.as(l => l.length > 0),
child: Media(),
}),
NotificationColumn(),
],
})
const QuickSettings = () => PopupWindow({
name: "quicksettings",
transition: "slide_left", //bar.position === "top" ? "slide_down" : "slide_up",
// anchor: ["right", "top"],
keymode: 'on-demand',
layout: layout,
child: Settings(),
vpack: "fill",
// hpack: "fill",
vexpand: true,
// hexpand: true,
})
export function setupQuickSettings() {
App.addWindow(QuickSettings())
}

View file

@ -0,0 +1,129 @@
import icons from "../../lib/icons.js"
export const opened = Variable("")
App.connect("window-toggled", (_, name, visible) => {
if (name === "quicksettings" && !visible)
Utils.timeout(500, () => opened.value = "")
})
export const Arrow = (name, activate) => {
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()
},
})
}
export const ArrowToggleButton = ({
name,
icon,
label,
activate,
deactivate,
activateOnArrow = true,
connection: [service, condition],
}) => 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),
],
})
export const Menu = ({ name, icon, title, content }) => 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,
}),
],
}),
})
export const SimpleToggleButton = ({
icon,
label,
toggle,
connection: [service, condition],
}) => 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,51 @@
import { ArrowToggleButton, Menu } from "../ToggleButton.js"
import icons from "../../../lib/icons.js"
import asusctl from "../../../services/asusctl.js"
const profile = asusctl.bind("profile")
export const ProfileToggle = () => ArrowToggleButton({
name: "asusctl-profile",
icon: profile.as(p => icons.asusctl.profile[p]),
label: profile,
connection: [asusctl, () => asusctl.profile !== "Balanced"],
activate: () => asusctl.setProfile("Quiet"),
deactivate: () => asusctl.setProfile("Balanced"),
activateOnArrow: false,
})
export const ProfileSelector = () => Menu({
name: "asusctl-profile",
icon: profile.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"),
],
}),
}),
],
})

View file

@ -0,0 +1,62 @@
import { Menu, ArrowToggleButton } from "../ToggleButton.js"
import icons from "../../../lib/icons.js"
import { watch } from "../../../lib/experiments.js"
const bluetooth = await Service.import("bluetooth")
const title = 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`
})
export const BluetoothToggle = () => ArrowToggleButton({
name: "bluetooth",
icon: bluetooth.bind("enabled").as(p => icons.bluetooth[p ? "enabled" : "disabled"]),
label: title,
connection: [bluetooth, () => bluetooth.enabled],
deactivate: () => bluetooth.enabled = false,
activate: () => bluetooth.enabled = true,
})
const DeviceItem = (device) => 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.js"
import brightness from "../../../services/brightness.js"
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.js"
import icons from "../../../lib/icons.js"
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,10 @@
import { SimpleToggleButton } from "../ToggleButton.js"
import icons from "../../../lib/icons.js"
let scheme = "dark"
export const DarkModeToggle = () => SimpleToggleButton({
icon: icons.color[scheme],
label: scheme === "dark" ? "Dark" : "Light",
toggle: () => scheme = scheme === "dark" ? "light" : "dark",
})

View file

@ -0,0 +1,60 @@
import icons from "../../../lib/icons.js"
import { uptime } from "../../../lib/variables.js"
import options from "../../../options.js"
import powermenu from "../../../services/powermenu.js"
const battery = await Service.import("battery")
const { image, size } = options.quicksettings.avatar
function up(up) {
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: `
min-width: ${size}px;
min-height: ${size}px;
background-image: url('${image}');
background-size: cover;
`,
})
const SysButton = (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([
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,148 @@
import icons from "../../../lib/icons.js"
import options from "../../../options.js"
import { icon } from "../../../lib/utils.js"
const mpris = await Service.import("mpris")
const players = mpris.bind("players")
const { media } = options.quicksettings
function lengthStr(length) {
const min = Math.floor(length / 60)
const sec = Math.floor(length % 60)
const sec0 = sec < 10 ? "0" : ""
return `${min}:${sec0}${sec}`
}
const Player = (player) => {
const cover = Widget.Box({
class_name: "cover",
vpack: "start",
css: player.bind("cover_path").as(path => `
min-width: ${media.coverSize}px;
min-height: ${media.coverSize}px;
background-image: url('${path}');
`),
})
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 = (_, time) => {
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: player.bind("entry").as(e => {
const name = `${e}${media.monochromeIcon ? "-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,26 @@
import { SimpleToggleButton } from "../ToggleButton.js"
import icons from "../../../lib/icons.js"
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"
// TODO: Variable watch option
const ico = Variable(icon())
microphone.connect("changed", () => ico.value = icon())
// TODO: Variable watch option
const lbl = Variable(label())
microphone.connect("changed", () => lbl.value = label())
export const MicMute = () => SimpleToggleButton({
icon: ico.bind(),
label: lbl.bind(),
toggle: () => microphone.is_muted = !microphone.is_muted,
connection: [microphone, () => microphone?.is_muted || false],
})

View file

@ -0,0 +1,61 @@
import { Menu, ArrowToggleButton } from "../ToggleButton.js"
import icons from "../../../lib/icons.js"
import { dependencies, sh } from "../../../lib/utils.js"
import options from "../../../options.js"
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),
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Network"),
],
}),
}),
],
})

View file

@ -0,0 +1,141 @@
import { Arrow, Menu } from "../ToggleButton.js"
import { dependencies, icon, sh } from "../../../lib/utils.js"
import icons from "../../../lib/icons.js"
const audio = await Service.import("audio")
const VolumeIndicator = (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 = "speaker") => Widget.Slider({
hexpand: true,
draw_value: false,
on_change: ({ value, dragging }) => dragging && (audio[type].volume = value),
value: audio[type].bind("volume"),
})
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) => 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) => 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(),
],
})