Compare commits
No commits in common. "dbac79e67a8812e39de0d1f8aa3d345af0b0b860" and "ef896285e4c4cbb35d5d039760785a47c0397258" have entirely different histories.
dbac79e67a
...
ef896285e4
|
|
@ -1,23 +1,6 @@
|
|||
@use "palette" as *;
|
||||
|
||||
// theme vars ported from AlfheimOS, mapped to the stylix palette
|
||||
$radius: 12px; // keep in sync with RADIUS in Bar.tsx
|
||||
$spacing: 8px;
|
||||
$padding: 8px;
|
||||
$border-width: 1px;
|
||||
$border-color: $base02;
|
||||
$border: $border-width solid $border-color;
|
||||
$transition: 180ms;
|
||||
$fg: $base05;
|
||||
$primary-bg: $base0D;
|
||||
$primary-fg: $base00;
|
||||
$widget-bg: transparentize($base02, 0.45);
|
||||
$hover-bg: transparentize($base02, 0.2);
|
||||
$hover-overlay: transparentize($base05, 0.92);
|
||||
$active-hover: transparentize($primary-bg, 0.15);
|
||||
$active-gradient: linear-gradient(to right, $primary-bg, darken($primary-bg, 4%));
|
||||
$toast-bg: transparentize($base02, 0.15);
|
||||
|
||||
$radius: 12px;
|
||||
$bar-alpha: 0.7;
|
||||
$bar-bg: transparentize($base01, 1 - $bar-alpha);
|
||||
$module-bg: transparentize($base02, 0.4);
|
||||
|
|
@ -79,7 +62,7 @@ $module-bg: transparentize($base02, 0.4);
|
|||
|
||||
.bar .end {
|
||||
border-radius: ($radius * 0.75) ($radius * 2) ($radius * 0.75) ($radius * 2);
|
||||
margin: 0 2px 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.bar .last {
|
||||
|
|
@ -109,8 +92,8 @@ $module-bg: transparentize($base02, 0.4);
|
|||
}
|
||||
|
||||
.ws.active {
|
||||
background: $primary-bg;
|
||||
color: $primary-fg;
|
||||
background: $base0D;
|
||||
color: $base00;
|
||||
}
|
||||
|
||||
.ws:hover {
|
||||
|
|
@ -121,240 +104,62 @@ $module-bg: transparentize($base02, 0.4);
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quicksettings .notif-count,
|
||||
.quicksettings .bt-count {
|
||||
font-size: 0.8em;
|
||||
.wifi icon,
|
||||
.wifi-header icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.battery-level {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.toggle-button {
|
||||
margin: 4px 0;
|
||||
|
||||
// ---------- quicksettings popup ----------
|
||||
.toggle-pill {
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
.dropdown {
|
||||
background: $base01;
|
||||
border: $border-width solid transparentize($base02, 0.5);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
color: $fg;
|
||||
}
|
||||
|
||||
scrollbar {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0 2px;
|
||||
|
||||
slider {
|
||||
background: $base03;
|
||||
border-radius: 999px;
|
||||
min-width: 6px;
|
||||
min-height: 6px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 6px;
|
||||
color: transparentize($fg, 0.15);
|
||||
|
||||
.avatar {
|
||||
padding: 7px;
|
||||
border-radius: 10px;
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
&:hover {
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 7px 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
&.active .toggle-pill {
|
||||
background: $base0D;
|
||||
color: $base00;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
background: transparentize($base0D, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.battery-section {
|
||||
margin-bottom: 6px;
|
||||
.arrow {
|
||||
padding: 4px 6px;
|
||||
border-radius: 8px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-percent {
|
||||
font-weight: bold;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.battery-status {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// volume, microphone and brightness rows on one widget card
|
||||
.sliders-box {
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
|
||||
.volume,
|
||||
.microphone,
|
||||
.brightness {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 7px;
|
||||
border-radius: 6px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shared slider look: bordered trough, accent gradient highlight,
|
||||
// thumb only appears on hover, .muted turns the highlight gray
|
||||
scale {
|
||||
trough {
|
||||
border-radius: 999px;
|
||||
border: $border;
|
||||
background-color: $base01;
|
||||
min-height: 8px;
|
||||
|
||||
highlight {
|
||||
border-radius: 999px;
|
||||
background-image: $active-gradient;
|
||||
min-height: 8px;
|
||||
}
|
||||
|
||||
slider {
|
||||
background-color: transparent;
|
||||
border: $border-width solid transparent;
|
||||
border-radius: 999px;
|
||||
min-width: 10px;
|
||||
min-height: 10px;
|
||||
}
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
trough {
|
||||
background-color: transparentize($base01, 0.4);
|
||||
}
|
||||
|
||||
trough slider {
|
||||
background-color: $base04;
|
||||
border-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.muted trough highlight {
|
||||
background-image: none;
|
||||
background-color: transparentize($fg, 0.2);
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
// icon + label pill with a detached arrow
|
||||
.toggle-button {
|
||||
margin: 4px 0;
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
|
||||
button:first-child {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
padding: 4px 8px;
|
||||
border-radius: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: $hover-overlay;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $primary-bg;
|
||||
|
||||
label,
|
||||
icon {
|
||||
color: $primary-fg;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: $active-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arrow-less toggle pill (do not disturb)
|
||||
.simple-toggle {
|
||||
margin: 4px 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $primary-bg;
|
||||
|
||||
label,
|
||||
icon {
|
||||
color: $primary-fg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $active-hover;
|
||||
}
|
||||
}
|
||||
.bluetooth-section {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid $base02;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
margin: 4px 0 8px;
|
||||
|
||||
.title-box {
|
||||
|
|
@ -362,7 +167,7 @@ scale {
|
|||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
margin: 0 4px 0 2px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -372,78 +177,14 @@ scale {
|
|||
}
|
||||
|
||||
.menu-separator {
|
||||
background: $border-color;
|
||||
background: $base02;
|
||||
min-height: 1px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 7px;
|
||||
border-radius: 6px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-overlay;
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
background: $base03;
|
||||
border-radius: 999px;
|
||||
min-width: 34px;
|
||||
min-height: 16px;
|
||||
margin: 1px 0;
|
||||
|
||||
slider {
|
||||
background: $base05;
|
||||
border-radius: 999px;
|
||||
min-width: 12px;
|
||||
min-height: 12px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
switch:checked {
|
||||
background: $primary-bg;
|
||||
|
||||
slider {
|
||||
background: $primary-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-status,
|
||||
.wifi-empty {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.wifi-password {
|
||||
margin: 4px 0;
|
||||
padding: 5px 8px;
|
||||
border-radius: 6px;
|
||||
background: $base01;
|
||||
color: $fg;
|
||||
}
|
||||
|
||||
.wifi-row {
|
||||
margin: 1px 0;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.wifi-row-ssid {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-item {
|
||||
padding: 5px 7px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
margin: 1px 0;
|
||||
|
||||
|
|
@ -468,90 +209,206 @@ scale {
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-overlay;
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
// media player card
|
||||
.player {
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
.dropdown {
|
||||
background: $base01;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
padding: 8px 12px;
|
||||
color: $base05;
|
||||
}
|
||||
|
||||
.power-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.power-action {
|
||||
padding: 8px 0;
|
||||
border-radius: 8px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
.qs-row {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.cover {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background-color: $hover-bg;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
.qs-toggle {
|
||||
margin-right: 8px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.player-title {
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
.player-artist {
|
||||
font-size: 0.9em;
|
||||
color: transparentize($fg, 0.2);
|
||||
}
|
||||
|
||||
scale {
|
||||
margin: 4px 0;
|
||||
|
||||
trough {
|
||||
border: none;
|
||||
}
|
||||
|
||||
trough highlight {
|
||||
background-image: linear-gradient($fg, $fg);
|
||||
}
|
||||
}
|
||||
|
||||
.player-ctl {
|
||||
padding: 4px 6px;
|
||||
.vol-slider {
|
||||
trough {
|
||||
background: $base03;
|
||||
border-radius: 999px;
|
||||
min-height: 12px;
|
||||
|
||||
icon {
|
||||
font-size: 14px;
|
||||
highlight {
|
||||
background: $base0D;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-overlay;
|
||||
slider {
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
background: $base05;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-section {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid $base02;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.wifi-toggle {
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-header {
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.wifi-rescan {
|
||||
padding: 0 2px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-header,
|
||||
.device-item {
|
||||
switch {
|
||||
background: $base03;
|
||||
border-radius: 999px;
|
||||
min-width: 34px;
|
||||
min-height: 16px;
|
||||
margin: 1px 0;
|
||||
|
||||
slider {
|
||||
background: $base05;
|
||||
border-radius: 999px;
|
||||
min-width: 12px;
|
||||
min-height: 12px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.play-pause {
|
||||
background-color: $primary-bg;
|
||||
switch:checked {
|
||||
background: $base0D;
|
||||
|
||||
icon {
|
||||
color: $primary-fg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $active-hover;
|
||||
slider {
|
||||
background: $base00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player-position,
|
||||
.player-length {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
.wifi-status,
|
||||
.wifi-empty {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.wifi-password {
|
||||
margin: 4px 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
background: $base02;
|
||||
color: $base05;
|
||||
}
|
||||
|
||||
.wifi-row {
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
margin: 1px 0;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.wifi-row-ssid {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-row:hover {
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
|
||||
.battery-level {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.battery-section {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid $base02;
|
||||
padding-top: 8px;
|
||||
|
||||
icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-percent {
|
||||
font-weight: bold;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.battery-status {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.notif-section {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid $border-color;
|
||||
border-top: 1px solid $base02;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.notif-header {
|
||||
.notif-toggle {
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
.notif-clear {
|
||||
|
|
@ -560,7 +417,7 @@ scale {
|
|||
font-size: 0.85em;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -569,29 +426,31 @@ scale {
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.quicksettings .notif-count {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.quicksettings .bt-count {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.notif-section .notif {
|
||||
background: $base02;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
.notif {
|
||||
background-color: $widget-bg;
|
||||
border: $border;
|
||||
background: $base01;
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
padding: 8px 12px;
|
||||
color: $base05;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
background: transparentize($base05, 0.88);
|
||||
}
|
||||
|
||||
&.critical {
|
||||
border-color: $base08;
|
||||
}
|
||||
|
||||
// popup toasts are more opaque than list entries and become
|
||||
// completely opaque while hovered
|
||||
&.toast {
|
||||
background-color: $toast-bg;
|
||||
|
||||
&:hover {
|
||||
background-color: $base02;
|
||||
}
|
||||
border: 1px solid $base08;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -612,10 +471,6 @@ scale {
|
|||
.notif-close {
|
||||
border-radius: 6px;
|
||||
padding: 0 2px;
|
||||
|
||||
&:hover {
|
||||
background: $hover-overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -635,13 +490,13 @@ scale {
|
|||
}
|
||||
|
||||
.notif-action {
|
||||
background: $base01;
|
||||
background: $base02;
|
||||
border-radius: 6px;
|
||||
margin-top: 6px;
|
||||
padding: 2px 8px;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
background: $base03;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
|||
import Workspaces from "./Workspaces"
|
||||
import Logo from "./Logo"
|
||||
import Clock from "./Clock"
|
||||
import { MediaButton } from "./Media"
|
||||
import QuickSettings from "./QuickSettings"
|
||||
|
||||
const RADIUS = 12
|
||||
|
|
@ -86,9 +85,7 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
|
|||
className="bar-center"
|
||||
halign={Gtk.Align.CENTER}
|
||||
valign={Gtk.Align.CENTER}
|
||||
>
|
||||
<MediaButton />
|
||||
</box>
|
||||
/>
|
||||
<box
|
||||
className="bar-right"
|
||||
halign={Gtk.Align.END}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function DeviceItem(device: AstalBluetooth.Device) {
|
|||
}
|
||||
|
||||
export function BluetoothSection({ open }: { open: Variable<boolean> }) {
|
||||
if (!bt.adapter) return null
|
||||
if (!bt.adapter) return <box />
|
||||
|
||||
const expanded = Variable(false)
|
||||
open.subscribe((v) => {
|
||||
|
|
@ -120,58 +120,63 @@ export function BluetoothSection({ open }: { open: Variable<boolean> }) {
|
|||
}
|
||||
})
|
||||
|
||||
const toggle = (
|
||||
<ArrowToggleButton
|
||||
open={expanded}
|
||||
icon={btIcon}
|
||||
label={pillLabel()}
|
||||
active={bind(bt, "is_powered")}
|
||||
onMainClick={() => {
|
||||
if (bt.is_powered) {
|
||||
power(false)
|
||||
expanded.set(false)
|
||||
setDiscovery(false)
|
||||
} else {
|
||||
power(true)
|
||||
}
|
||||
}}
|
||||
onArrowClick={() => {
|
||||
const next = !expanded.get()
|
||||
expanded.set(next)
|
||||
if (next) {
|
||||
if (!bt.is_powered) power(true)
|
||||
setDiscovery(true)
|
||||
} else {
|
||||
setDiscovery(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const menu = (
|
||||
<Menu open={expanded} icon="bluetooth-active-symbolic" title="Bluetooth">
|
||||
<label
|
||||
className="wifi-empty"
|
||||
label="bluetooth is off"
|
||||
visible={bind(bt, "is_powered").as((p) => !p)}
|
||||
return (
|
||||
<box className="bluetooth-section" vertical>
|
||||
<ArrowToggleButton
|
||||
open={expanded}
|
||||
icon={btIcon}
|
||||
label={pillLabel()}
|
||||
active={bind(bt, "is_powered")}
|
||||
onMainClick={() => {
|
||||
if (bt.is_powered) {
|
||||
power(false)
|
||||
expanded.set(false)
|
||||
setDiscovery(false)
|
||||
} else {
|
||||
power(true)
|
||||
}
|
||||
}}
|
||||
onArrowClick={() => {
|
||||
const next = !expanded.get()
|
||||
expanded.set(next)
|
||||
if (next) {
|
||||
if (!bt.is_powered) power(true)
|
||||
setDiscovery(true)
|
||||
} else {
|
||||
setDiscovery(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="wifi-empty"
|
||||
label="searching for devices…"
|
||||
visible={Variable.derive(
|
||||
[bind(bt, "is_powered"), devices],
|
||||
(powered, ds) => powered && ds.filter((d) => d.name).length === 0,
|
||||
)()}
|
||||
/>
|
||||
<box vertical visible={bind(bt, "is_powered")}>
|
||||
{devices().as((ds) =>
|
||||
ds.filter((d) => d.name).map(DeviceItem),
|
||||
)}
|
||||
</box>
|
||||
</Menu>
|
||||
<Menu open={expanded} icon="bluetooth-active-symbolic" title="Bluetooth">
|
||||
<label
|
||||
className="wifi-empty"
|
||||
label="bluetooth is off"
|
||||
visible={bind(bt, "is_powered").as((p) => !p)}
|
||||
/>
|
||||
<label
|
||||
className="wifi-empty"
|
||||
label="searching for devices…"
|
||||
visible={Variable.derive(
|
||||
[bind(bt, "is_powered"), devices],
|
||||
(powered, ds) => powered && ds.filter((d) => d.name).length === 0,
|
||||
)()}
|
||||
/>
|
||||
<scrollable
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
maxContentHeight={360}
|
||||
propagateNaturalHeight
|
||||
visible={bind(bt, "is_powered")}
|
||||
>
|
||||
<box vertical>
|
||||
{devices().as((ds) =>
|
||||
ds.filter((d) => d.name).map(DeviceItem),
|
||||
)}
|
||||
</box>
|
||||
</scrollable>
|
||||
</Menu>
|
||||
</box>
|
||||
)
|
||||
|
||||
return { toggle, menu }
|
||||
}
|
||||
|
||||
// bar cluster indicator: shown while bluetooth is powered, with a count
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import { execAsync, Variable, bind } from "astal"
|
||||
|
||||
// no astal.brightness lib in nixpkgs: poll brightnessctl for the current
|
||||
// level as a 0..1 fraction, pushing changes through brightnessctl set
|
||||
const percent = Variable(0).poll(
|
||||
2000,
|
||||
["sh", "-c", "echo $(( $(brightnessctl get) * 100 / $(brightnessctl max) ))"],
|
||||
(out) => parseInt(out) / 100,
|
||||
)
|
||||
|
||||
export function BrightnessRow() {
|
||||
return (
|
||||
<box className="brightness" spacing={6}>
|
||||
<button>
|
||||
<icon icon="display-brightness-symbolic" />
|
||||
</button>
|
||||
<slider
|
||||
hexpand
|
||||
value={bind(percent)}
|
||||
onDragged={(self) => {
|
||||
const pct = Math.round(self.value * 100)
|
||||
percent.set(pct / 100)
|
||||
execAsync(["brightnessctl", "set", `${pct}%`]).catch(
|
||||
console.error,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ interface DropdownProps {
|
|||
width?: number
|
||||
vertical?: boolean
|
||||
className?: string
|
||||
// cap the popup height to the screen, scrolling any overflow
|
||||
maxHeight?: number
|
||||
children: Array<JSX.Element>
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +21,6 @@ export default function Dropdown({
|
|||
width = 260,
|
||||
vertical = true,
|
||||
className = "",
|
||||
maxHeight,
|
||||
children,
|
||||
}: DropdownProps) {
|
||||
const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor
|
||||
|
|
@ -37,19 +34,7 @@ export default function Dropdown({
|
|||
halign={Gtk.Align.START}
|
||||
valign={Gtk.Align.START}
|
||||
>
|
||||
{maxHeight !== undefined ? (
|
||||
<scrollable
|
||||
hexpand
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
maxContentHeight={maxHeight}
|
||||
propagateNaturalHeight
|
||||
>
|
||||
<box vertical>{children}</box>
|
||||
</scrollable>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{children}
|
||||
</box>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Gtk from "gi://Gtk?version=3.0"
|
|||
export default function Logo() {
|
||||
return (
|
||||
<box className="logo first" valign={Gtk.Align.FILL}>
|
||||
<icon icon="nix-snowflake" pixelSize={20} />
|
||||
<icon icon="nix-snowflake-white" pixelSize={20} />
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
import Gtk from "gi://Gtk?version=3.0"
|
||||
import GLib from "gi://GLib"
|
||||
import Pango from "gi://Pango"
|
||||
import AstalMpris from "gi://AstalMpris"
|
||||
import { Variable, bind } from "astal"
|
||||
|
||||
const mpris = AstalMpris.get_default()
|
||||
|
||||
const PREV = "media-skip-backward-symbolic"
|
||||
const PLAY = "media-playback-start-symbolic"
|
||||
const PAUSE = "media-playback-pause-symbolic"
|
||||
const NEXT = "media-skip-forward-symbolic"
|
||||
const NO_ART = "audio-x-generic-symbolic"
|
||||
|
||||
function fmtTime(seconds: number) {
|
||||
if (!seconds || seconds < 0) return "0:00"
|
||||
const min = Math.floor(seconds / 60)
|
||||
const sec = Math.floor(seconds % 60)
|
||||
return `${min}:${sec < 10 ? "0" : ""}${sec}`
|
||||
}
|
||||
|
||||
function playerIcon(entry: string) {
|
||||
const name = entry ? `${entry.toLowerCase()}-symbolic` : ""
|
||||
const theme = Gtk.IconTheme.get_default()!
|
||||
return name !== "" && theme.has_icon(name) ? name : NO_ART
|
||||
}
|
||||
|
||||
function PlayerCard(player: AstalMpris.Player) {
|
||||
// position only changes on the bus when seeking, so poll every second;
|
||||
// the ticker stops itself when the player disconnects so we never
|
||||
// touch a disposed GObject
|
||||
const position = Variable(-1)
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
|
||||
if (!mpris.players.includes(player)) return GLib.SOURCE_REMOVE
|
||||
position.set(player.available ? player.position : -1)
|
||||
return GLib.SOURCE_CONTINUE
|
||||
})
|
||||
|
||||
return (
|
||||
<box className="player" spacing={8}>
|
||||
<box
|
||||
className="cover"
|
||||
css={bind(player, "cover_art").as((c) =>
|
||||
c !== "" ? `background-image: url('${encodeURI(`file://${c}`)}')` : "",
|
||||
)}
|
||||
>
|
||||
{/* kept allocated (opacity, not visible) so the padded box
|
||||
stays 48px and the icon sits dead center with no art */}
|
||||
<icon
|
||||
pixelSize={24}
|
||||
icon={NO_ART}
|
||||
opacity={bind(player, "cover_art").as((c) => (c !== "" ? 0 : 1))}
|
||||
/>
|
||||
</box>
|
||||
<box vertical spacing={4}>
|
||||
<box spacing={6}>
|
||||
<label
|
||||
className="player-title"
|
||||
hexpand
|
||||
halign={Gtk.Align.START}
|
||||
label={bind(player, "title").as((t) => t || "")}
|
||||
maxWidthChars={20}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
<icon
|
||||
className="player-icon"
|
||||
icon={bind(player, "entry").as(playerIcon)}
|
||||
tooltipText={bind(player, "identity")}
|
||||
/>
|
||||
</box>
|
||||
<label
|
||||
className="player-artist"
|
||||
halign={Gtk.Align.START}
|
||||
label={bind(player, "artist").as((a) => a || "")}
|
||||
maxWidthChars={24}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
visible={bind(player, "artist").as((a) => (a || "") !== "")}
|
||||
/>
|
||||
<slider
|
||||
value={position().as((p) =>
|
||||
player.length > 0
|
||||
? Math.min(1, Math.max(0, p / player.length))
|
||||
: 0,
|
||||
)}
|
||||
onDragged={(self) => {
|
||||
if (player.length > 0) player.position = self.value * player.length
|
||||
}}
|
||||
visible={bind(player, "length").as((l) => l > 0)}
|
||||
/>
|
||||
<box className="player-footer" visible={bind(player, "length").as((l) => l > 0)}>
|
||||
<label
|
||||
className="player-position"
|
||||
label={position().as((p) => fmtTime(p))}
|
||||
/>
|
||||
<box hexpand />
|
||||
<button
|
||||
className="player-ctl"
|
||||
onClicked={() => player.previous()}
|
||||
visible={bind(player, "can_go_previous")}
|
||||
>
|
||||
<icon icon={PREV} />
|
||||
</button>
|
||||
<button
|
||||
className="player-ctl play-pause"
|
||||
onClicked={() => player.play_pause()}
|
||||
visible={bind(player, "can_play")}
|
||||
>
|
||||
<icon
|
||||
icon={bind(player, "playback_status").as((s) =>
|
||||
s === AstalMpris.PlaybackStatus.PLAYING ? PAUSE : PLAY,
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="player-ctl"
|
||||
onClicked={() => player.next()}
|
||||
visible={bind(player, "can_go_next")}
|
||||
>
|
||||
<icon icon={NEXT} />
|
||||
</button>
|
||||
<box hexpand />
|
||||
<label
|
||||
className="player-length"
|
||||
label={bind(player, "length").as((l) => fmtTime(l))}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
export function MediaSection() {
|
||||
const players = bind(mpris, "players")
|
||||
return (
|
||||
<box className="media-section" vertical visible={players.as((ps) => ps.length > 0)}>
|
||||
{players.as((ps) => ps.map((p) => PlayerCard(p)))}
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
// the players property only notifies on connect/disconnect, so the bar
|
||||
// button tracks the first player directly to follow track changes
|
||||
let player: AstalMpris.Player | null = null
|
||||
let hookIds: Array<number> = []
|
||||
|
||||
const hasPlayer = Variable(false)
|
||||
const title = Variable("")
|
||||
const icon = Variable(NO_ART)
|
||||
|
||||
function sync() {
|
||||
const current = mpris.players[0] ?? null
|
||||
if (current !== player) {
|
||||
hookIds.forEach((id) => player?.disconnect(id))
|
||||
hookIds = []
|
||||
player = current
|
||||
if (player) {
|
||||
hookIds = [
|
||||
player.connect("notify::title", sync),
|
||||
player.connect("notify::entry", sync),
|
||||
]
|
||||
}
|
||||
}
|
||||
hasPlayer.set(player !== null)
|
||||
title.set(player?.title || "")
|
||||
icon.set(player ? playerIcon(player.entry) : "")
|
||||
}
|
||||
|
||||
mpris.connect("notify::players", sync)
|
||||
sync()
|
||||
|
||||
export function MediaButton() {
|
||||
return (
|
||||
<button
|
||||
className="media center"
|
||||
visible={bind(hasPlayer)}
|
||||
onClicked={() => {
|
||||
if (player && player.can_play) player.play_pause()
|
||||
}}
|
||||
onScroll={(_, e) => {
|
||||
if (!player) return
|
||||
if (e.delta_y < 0) player.next()
|
||||
else player.previous()
|
||||
}}
|
||||
>
|
||||
<box spacing={6}>
|
||||
<icon className="media-icon" icon={bind(icon)} />
|
||||
<label
|
||||
className="media-title"
|
||||
label={bind(title)}
|
||||
maxWidthChars={30}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
</box>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,168 +1,61 @@
|
|||
import GLib from "gi://GLib"
|
||||
import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
||||
import Notifd from "gi://AstalNotifd"
|
||||
import { Variable, bind } from "astal"
|
||||
import { bind } from "astal"
|
||||
import { MAX_TIMEOUT } from "../notif-config"
|
||||
|
||||
const notifd = Notifd.get_default()
|
||||
|
||||
export const dndIcon = Variable.derive([bind(notifd, "dontDisturb")], (dnd) =>
|
||||
dnd
|
||||
? "notifications-disabled-symbolic"
|
||||
: "preferences-system-notifications-symbolic",
|
||||
)
|
||||
|
||||
// every notification received this session is kept until it is explicitly
|
||||
// cleared, so popups that time out remain in the dropdown's list. only
|
||||
// notifications dismissed directly from their popup skip the list, and
|
||||
// transient status toasts never enter it at all.
|
||||
export const history = Variable<Notifd.Notification[]>([])
|
||||
|
||||
export const notifCount = bind(history).as((ns) => ns.length)
|
||||
|
||||
// apps whose notifications are transient status updates: they surface as
|
||||
// toasts only and never enter the dropdown history or the bar count
|
||||
const EPHEMERAL_APPS = new Set(["dictation"])
|
||||
|
||||
// irregular ids the generic capitalization rule can't fix up
|
||||
const FRIENDLY_NAMES: Record<string, string> = {
|
||||
"notify-send": "Notification",
|
||||
}
|
||||
|
||||
// per-app icons for notifications that don't supply one
|
||||
const APP_ICONS: Record<string, string> = {
|
||||
dictation: "audio-input-microphone-symbolic",
|
||||
}
|
||||
|
||||
// apps like discord report a bare binary id; make words of those while
|
||||
// names with any uppercase ("Spotify") pass through untouched
|
||||
function displayName(n: Notifd.Notification) {
|
||||
const raw = n.app_name || "Notification"
|
||||
|
||||
const mapped = FRIENDLY_NAMES[raw.toLowerCase()]
|
||||
if (mapped) return mapped
|
||||
|
||||
if (raw === raw.toLowerCase() && /[a-z]/.test(raw)) {
|
||||
return raw
|
||||
.split(/[-_ ]+/)
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(" ")
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
export function DndToggle() {
|
||||
return (
|
||||
<button
|
||||
className={bind(notifd, "dontDisturb").as((v) =>
|
||||
v ? "simple-toggle active" : "simple-toggle",
|
||||
)}
|
||||
onClicked={() => (notifd.dontDisturb = !notifd.dontDisturb)}
|
||||
>
|
||||
<box spacing={8}>
|
||||
<icon className="icon" icon={dndIcon()} />
|
||||
<label label="Do Not Disturb" />
|
||||
</box>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// popups are timed out entirely client-side: the daemon never resolves them
|
||||
// on its own, so a popup under the pointer can be kept alive indefinitely
|
||||
notifd.ignore_timeout = true
|
||||
notifd.default_timeout = -1
|
||||
notifd.default_timeout = MAX_TIMEOUT
|
||||
|
||||
// notifications restored from the daemon's cache on startup are stale
|
||||
for (const n of notifd.notifications) {
|
||||
n.dismiss()
|
||||
}
|
||||
|
||||
// the daemon honors app-requested timeouts, so enforce our own cap for
|
||||
// notifications that ask for more (or none at all). critical notifications
|
||||
// are exempt and stay until dismissed.
|
||||
notifd.connect("notified", (_, id: number) => {
|
||||
const n = notifd.get_notification(id)
|
||||
if (!n) return
|
||||
if (!n || n.urgency === Notifd.Urgency.CRITICAL) return
|
||||
|
||||
if (EPHEMERAL_APPS.has(n.app_name.toLowerCase())) return
|
||||
|
||||
// re-notified ids update the existing entry instead of adding a duplicate
|
||||
if (!history.get().some((m) => m.id === id)) {
|
||||
history.set([...history.get(), n])
|
||||
if (n.expire_timeout <= 0 || n.expire_timeout > MAX_TIMEOUT) {
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, MAX_TIMEOUT, () => {
|
||||
if (notifd.notifications.includes(n)) n.dismiss()
|
||||
return GLib.SOURCE_REMOVE
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ids of notifications whose popup is currently under the pointer
|
||||
const hovered = new Set<number>()
|
||||
|
||||
// enforce our own cap for popups, honoring shorter app-requested timeouts.
|
||||
// critical notifications are exempt and stay until dismissed, as are popups
|
||||
// being hovered.
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
|
||||
const now = GLib.DateTime.new_now_local().to_unix()
|
||||
|
||||
for (const n of [...notifd.notifications]) {
|
||||
if (n.urgency === Notifd.Urgency.CRITICAL || hovered.has(n.id)) continue
|
||||
|
||||
const timeout =
|
||||
n.expire_timeout > 0
|
||||
? Math.min(n.expire_timeout, MAX_TIMEOUT)
|
||||
: MAX_TIMEOUT
|
||||
|
||||
if (now - n.time >= timeout / 1000) n.dismiss()
|
||||
}
|
||||
|
||||
return GLib.SOURCE_CONTINUE
|
||||
})
|
||||
|
||||
// drop a notification from the list and withdraw its popup; used when a
|
||||
// notification is dismissed directly instead of timing out
|
||||
export function dismissNotification(n: Notifd.Notification) {
|
||||
hovered.delete(n.id)
|
||||
history.set(history.get().filter((m) => m.id !== n.id))
|
||||
notifd.get_notification(n.id)?.dismiss()
|
||||
}
|
||||
|
||||
export function clearNotifications() {
|
||||
for (const n of [...notifd.notifications]) {
|
||||
dismissNotification(n)
|
||||
}
|
||||
history.set([])
|
||||
}
|
||||
|
||||
function formatTime(n: Notifd.Notification) {
|
||||
return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")!
|
||||
}
|
||||
|
||||
export function NotificationCard(n: Notifd.Notification, toast = false) {
|
||||
export function NotificationCard(n: Notifd.Notification) {
|
||||
const critical = n.urgency === Notifd.Urgency.CRITICAL
|
||||
const appName = displayName(n)
|
||||
const appIcon =
|
||||
n.app_icon ||
|
||||
APP_ICONS[appName.toLowerCase()] ||
|
||||
"preferences-system-notifications-symbolic"
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`notif${critical ? " critical" : ""}${toast ? " toast" : ""}`}
|
||||
onClicked={() => dismissNotification(n)}
|
||||
{...(toast
|
||||
? {
|
||||
onHover: () => hovered.add(n.id),
|
||||
onHoverLost: () => hovered.delete(n.id),
|
||||
}
|
||||
: {})}
|
||||
className={`notif${critical ? " critical" : ""}`}
|
||||
onClicked={() => n.dismiss()}
|
||||
>
|
||||
<box vertical>
|
||||
<box className="notif-header">
|
||||
<icon pixelSize={16} icon={appIcon} />
|
||||
<icon
|
||||
pixelSize={16}
|
||||
icon={n.app_icon || "preferences-system-notifications-symbolic"}
|
||||
/>
|
||||
<label
|
||||
className="notif-app-name"
|
||||
label={appName}
|
||||
label={n.app_name || "Notification"}
|
||||
halign={Gtk.Align.START}
|
||||
hexpand
|
||||
/>
|
||||
<label className="notif-time" label={formatTime(n)} />
|
||||
<button
|
||||
className="notif-close"
|
||||
onClicked={() => dismissNotification(n)}
|
||||
onClicked={() => n.dismiss()}
|
||||
>
|
||||
<icon icon="window-close-symbolic" pixelSize={12} />
|
||||
</button>
|
||||
|
|
@ -222,15 +115,12 @@ export default function Notifications(gdkmonitor: Gdk.Monitor) {
|
|||
marginTop={54}
|
||||
marginRight={8}
|
||||
visible={bind(notifd, "notifications").as((ns) => ns.length > 0)}
|
||||
onNotifyVisible={(self) => {
|
||||
if (!self.visible) hovered.clear()
|
||||
}}
|
||||
>
|
||||
<box className="notifications" vertical spacing={8} widthRequest={360}>
|
||||
{bind(notifd, "notifications").as((ns) =>
|
||||
[...ns]
|
||||
.sort((a, b) => b.time - a.time)
|
||||
.map((n) => NotificationCard(n, true)),
|
||||
.map((n) => NotificationCard(n)),
|
||||
)}
|
||||
</box>
|
||||
</window>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
import Gtk from "gi://Gtk?version=3.0"
|
||||
import GLib from "gi://GLib"
|
||||
import AstalWp from "gi://AstalWp"
|
||||
import Notifd from "gi://AstalNotifd"
|
||||
import { execAsync, Variable, bind } from "astal"
|
||||
import Dropdown from "./Dropdown"
|
||||
import { wifiIcon, WifiSection } from "./Wifi"
|
||||
import { bluetoothIndicator, BluetoothSection } from "./Bluetooth"
|
||||
import { BrightnessRow } from "./Brightness"
|
||||
import { MediaSection } from "./Media"
|
||||
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
|
||||
import {
|
||||
clearNotifications,
|
||||
dndIcon,
|
||||
DndToggle,
|
||||
history,
|
||||
notifCount,
|
||||
NotificationCard,
|
||||
} from "./Notifications"
|
||||
import { NotificationCard } from "./Notifications"
|
||||
|
||||
const speaker = AstalWp.get_default().audio.default_speaker!
|
||||
const mic = AstalWp.get_default().audio.default_microphone
|
||||
const notifd = Notifd.get_default()
|
||||
|
||||
const notifCount = bind(notifd, "notifications").as((ns) => ns.length)
|
||||
|
||||
const speakerIcon = Variable.derive(
|
||||
[bind(speaker, "volume"), bind(speaker, "mute")],
|
||||
|
|
@ -38,42 +32,21 @@ const micIcon = mic
|
|||
)
|
||||
: null
|
||||
|
||||
const user = GLib.get_user_name()
|
||||
|
||||
const actions: { icon: string; cmd?: string[] }[] = [
|
||||
{ icon: "emblem-system-symbolic" },
|
||||
const actions = [
|
||||
{ icon: "system-lock-screen-symbolic", cmd: ["hyprlock"] },
|
||||
{ icon: "weather-clear-night-symbolic", cmd: ["systemctl", "suspend"] },
|
||||
{ icon: "system-reboot-symbolic", cmd: ["systemctl", "reboot"] },
|
||||
{ icon: "system-log-out-symbolic", cmd: ["hyprctl", "dispatch", "exit"] },
|
||||
{ icon: "system-reboot-symbolic", cmd: ["systemctl", "reboot"] },
|
||||
{ icon: "system-shutdown-symbolic", cmd: ["systemctl", "poweroff"] },
|
||||
]
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<box className="header" spacing={8}>
|
||||
<box className="avatar">
|
||||
<icon icon="nix-snowflake-white" pixelSize={16} />
|
||||
</box>
|
||||
<label className="username" label={user} valign={Gtk.Align.CENTER} />
|
||||
<box hexpand />
|
||||
{actions.map((action) => (
|
||||
<button onClicked={() => action.cmd && execAsync(action.cmd)}>
|
||||
<icon icon={action.icon} />
|
||||
</button>
|
||||
))}
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
||||
const open = Variable(false)
|
||||
const notifOpen = Variable(false)
|
||||
|
||||
const wifiSection = WifiSection({ open })
|
||||
const btSection = BluetoothSection({ open })
|
||||
const toggles = [wifiSection?.toggle, btSection?.toggle].filter(
|
||||
(t) => t !== undefined,
|
||||
)
|
||||
open.subscribe((v) => {
|
||||
if (!v) notifOpen.set(false)
|
||||
})
|
||||
|
||||
const button = (
|
||||
<button
|
||||
|
|
@ -89,7 +62,7 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
|||
>
|
||||
<box spacing={6}>
|
||||
<box className="notif-indicator" spacing={3}>
|
||||
<icon icon={dndIcon()} />
|
||||
<icon icon="preferences-system-notifications-symbolic" />
|
||||
<label
|
||||
className="notif-count"
|
||||
label={notifCount.as((n) => `${n}`)}
|
||||
|
|
@ -111,77 +84,93 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
|||
)
|
||||
|
||||
const popup = (
|
||||
<Dropdown
|
||||
gdkmonitor={gdkmonitor}
|
||||
target={button}
|
||||
open={open}
|
||||
width={280}
|
||||
maxHeight={gdkmonitor.get_geometry().height - 200}
|
||||
>
|
||||
<Header />
|
||||
<BatterySection />
|
||||
<box className="sliders-box" vertical spacing={2}>
|
||||
<box className="volume" spacing={6}>
|
||||
<button onClicked={() => (speaker.mute = !speaker.mute)}>
|
||||
<icon icon={speakerIcon()} />
|
||||
<Dropdown gdkmonitor={gdkmonitor} target={button} open={open} width={280}>
|
||||
<box className="power-header" homogeneous>
|
||||
{actions.map((action) => (
|
||||
<button
|
||||
className="power-action"
|
||||
onClicked={() => execAsync(action.cmd)}
|
||||
>
|
||||
<icon icon={action.icon} />
|
||||
</button>
|
||||
))}
|
||||
</box>
|
||||
<box className="qs-row">
|
||||
<button
|
||||
className="qs-toggle"
|
||||
onClicked={() => (speaker.mute = !speaker.mute)}
|
||||
>
|
||||
<icon icon={speakerIcon()} />
|
||||
</button>
|
||||
<slider
|
||||
className="vol-slider"
|
||||
hexpand
|
||||
value={bind(speaker, "volume")}
|
||||
onDragged={(self) => (speaker.volume = self.value)}
|
||||
/>
|
||||
</box>
|
||||
{mic && micIcon && (
|
||||
<box className="qs-row">
|
||||
<button
|
||||
className="qs-toggle"
|
||||
onClicked={() => (mic.mute = !mic.mute)}
|
||||
>
|
||||
<icon icon={micIcon()} />
|
||||
</button>
|
||||
<slider
|
||||
className="vol-slider"
|
||||
hexpand
|
||||
className={bind(speaker, "mute").as((m) => (m ? "muted" : ""))}
|
||||
value={bind(speaker, "volume")}
|
||||
onDragged={(self) => (speaker.volume = self.value)}
|
||||
value={bind(mic, "volume")}
|
||||
onDragged={(self) => (mic.volume = self.value)}
|
||||
/>
|
||||
</box>
|
||||
{mic && micIcon && (
|
||||
<box className="microphone" spacing={6}>
|
||||
<button onClicked={() => (mic.mute = !mic.mute)}>
|
||||
<icon icon={micIcon()} />
|
||||
</button>
|
||||
<slider
|
||||
hexpand
|
||||
className={bind(mic, "mute").as((m) => (m ? "muted" : ""))}
|
||||
value={bind(mic, "volume")}
|
||||
onDragged={(self) => (mic.volume = self.value)}
|
||||
/>
|
||||
</box>
|
||||
)}
|
||||
<BrightnessRow />
|
||||
</box>
|
||||
<DndToggle />
|
||||
{toggles.length > 0 && (
|
||||
<box homogeneous spacing={6}>
|
||||
{toggles}
|
||||
</box>
|
||||
)}
|
||||
{wifiSection?.menu}
|
||||
{btSection?.menu}
|
||||
<MediaSection />
|
||||
<WifiSection open={open} />
|
||||
<BluetoothSection open={open} />
|
||||
<BatterySection />
|
||||
<box className="notif-section" vertical spacing={6}>
|
||||
<box className="notif-header-row" spacing={6}>
|
||||
<box className="notif-header" spacing={6}>
|
||||
<icon icon="preferences-system-notifications-symbolic" />
|
||||
<label label="Notifications" />
|
||||
</box>
|
||||
<box hexpand />
|
||||
<button
|
||||
className="notif-toggle"
|
||||
hexpand
|
||||
halign={Gtk.Align.START}
|
||||
onClicked={() => notifOpen.set(!notifOpen.get())}
|
||||
>
|
||||
<box spacing={6}>
|
||||
<icon icon="preferences-system-notifications-symbolic" />
|
||||
<label label="Notifications" />
|
||||
<icon
|
||||
icon={notifOpen().as((v) =>
|
||||
v ? "pan-down-symbolic" : "pan-end-symbolic",
|
||||
)}
|
||||
/>
|
||||
</box>
|
||||
</button>
|
||||
<button
|
||||
className="notif-clear"
|
||||
onClicked={clearNotifications}
|
||||
onClicked={() => [...notifd.notifications].forEach((n) => n.dismiss())}
|
||||
>
|
||||
<label label="Clear" />
|
||||
</button>
|
||||
</box>
|
||||
<box className="notifications" vertical>
|
||||
<label
|
||||
className="notif-empty"
|
||||
label="no notifications"
|
||||
visible={notifCount.as((n) => n === 0)}
|
||||
/>
|
||||
{bind(history).as((ns) =>
|
||||
[...ns]
|
||||
.sort((a, b) => b.time - a.time)
|
||||
.map((n) => NotificationCard(n)),
|
||||
)}
|
||||
</box>
|
||||
<revealer
|
||||
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
||||
transitionDuration={180}
|
||||
revealChild={bind(notifOpen)}
|
||||
>
|
||||
<box className="notifications" vertical>
|
||||
<label
|
||||
className="notif-empty"
|
||||
label="no notifications"
|
||||
visible={notifCount.as((n) => n === 0)}
|
||||
/>
|
||||
{bind(notifd, "notifications").as((ns) =>
|
||||
[...ns]
|
||||
.sort((a, b) => b.time - a.time)
|
||||
.map((n) => NotificationCard(n)),
|
||||
)}
|
||||
</box>
|
||||
</revealer>
|
||||
</box>
|
||||
</Dropdown>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ export function ArrowToggleButton({
|
|||
return (
|
||||
<box
|
||||
className={active.as((v) => (v ? "toggle-button active" : "toggle-button"))}
|
||||
hexpand
|
||||
>
|
||||
<button className="toggle-pill" hexpand onClicked={onMainClick}>
|
||||
<box hexpand>
|
||||
|
|
@ -86,7 +85,7 @@ export function ArrowToggleButton({
|
|||
|
||||
type MenuProps = {
|
||||
open: Variable<boolean>
|
||||
icon: string | Binding<string>
|
||||
icon: string
|
||||
title: string
|
||||
children: Array<JSX.Element>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Gtk from "gi://Gtk?version=3.0"
|
||||
import AstalNetwork from "gi://AstalNetwork"
|
||||
import { type Binding, type Variable, Variable, bind } from "astal"
|
||||
import { ArrowToggleButton, Menu } from "./ToggleButton"
|
||||
|
||||
const CONNECTING = [40, 50, 60, 70, 80, 90]
|
||||
|
||||
|
|
@ -50,7 +49,7 @@ function WifiRow(
|
|||
}
|
||||
|
||||
export function WifiSection({ open }: { open: Variable<boolean> }) {
|
||||
if (!wifi) return null
|
||||
if (!wifi) return <box />
|
||||
|
||||
const expanded = Variable(false)
|
||||
const pendingAP = Variable<AstalNetwork.AccessPoint | null>(null)
|
||||
|
|
@ -112,49 +111,68 @@ export function WifiSection({ open }: { open: Variable<boolean> }) {
|
|||
}
|
||||
})
|
||||
|
||||
const toggle = (
|
||||
<ArrowToggleButton
|
||||
open={expanded}
|
||||
icon={bind(wifi, "icon-name")}
|
||||
label={bind(wifi, "ssid").as((ssid) => ssid || "Not Connected")}
|
||||
active={bind(wifi, "enabled")}
|
||||
onMainClick={() => {
|
||||
wifi.enabled = !wifi.enabled
|
||||
if (wifi.enabled) wifi.scan()
|
||||
}}
|
||||
onArrowClick={() => expanded.set(!expanded.get())}
|
||||
/>
|
||||
)
|
||||
|
||||
const menu = (
|
||||
<Menu open={expanded} icon={bind(wifi, "icon-name")} title="Wi-Fi">
|
||||
<label
|
||||
className="wifi-status"
|
||||
label={status()}
|
||||
visible={status().as((text) => text !== "")}
|
||||
/>
|
||||
{passwordEntry}
|
||||
<label
|
||||
className="wifi-empty"
|
||||
label="wi-fi is off"
|
||||
visible={bind(wifi, "enabled").as((enabled) => !enabled)}
|
||||
/>
|
||||
<box vertical visible={bind(wifi, "enabled")}>
|
||||
{bind(wifi, "access-points").as((aps) =>
|
||||
strongest(aps).map((ap) =>
|
||||
WifiRow(ap, activeSsid, connect)
|
||||
),
|
||||
)}
|
||||
</box>
|
||||
<box className="menu-separator" />
|
||||
<button className="wifi-rescan" onClicked={() => wifi.scan()}>
|
||||
<box spacing={6}>
|
||||
return (
|
||||
<box className="wifi-section" vertical>
|
||||
<box className="wifi-header">
|
||||
<button
|
||||
className="wifi-toggle"
|
||||
onClicked={() => expanded.set(!expanded.get())}
|
||||
>
|
||||
<box spacing={6}>
|
||||
<icon icon={bind(wifi, "icon-name")} />
|
||||
<label label="Wi-Fi" />
|
||||
<icon
|
||||
icon={expanded().as((v) =>
|
||||
v ? "pan-down-symbolic" : "pan-end-symbolic",
|
||||
)}
|
||||
/>
|
||||
</box>
|
||||
</button>
|
||||
<button className="wifi-rescan" onClicked={() => wifi.scan()}>
|
||||
<icon icon="view-refresh-symbolic" />
|
||||
<label label="Rescan" />
|
||||
</button>
|
||||
<switch
|
||||
active={bind(wifi, "enabled")}
|
||||
onStateSet={(_, state) => {
|
||||
wifi.enabled = state
|
||||
return true
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
<revealer
|
||||
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
||||
transitionDuration={180}
|
||||
revealChild={bind(expanded)}
|
||||
>
|
||||
<box vertical>
|
||||
<label
|
||||
className="wifi-status"
|
||||
label={status()}
|
||||
visible={status().as((text) => text !== "")}
|
||||
/>
|
||||
{passwordEntry}
|
||||
<label
|
||||
className="wifi-empty"
|
||||
label="wi-fi is off"
|
||||
visible={bind(wifi, "enabled").as((enabled) => !enabled)}
|
||||
/>
|
||||
<scrollable
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
maxContentHeight={360}
|
||||
propagateNaturalHeight
|
||||
visible={bind(wifi, "enabled")}
|
||||
>
|
||||
<box vertical>
|
||||
{bind(wifi, "access-points").as((aps) =>
|
||||
strongest(aps).map((ap) =>
|
||||
WifiRow(ap, activeSsid, connect)
|
||||
),
|
||||
)}
|
||||
</box>
|
||||
</scrollable>
|
||||
</box>
|
||||
</button>
|
||||
</Menu>
|
||||
</revealer>
|
||||
</box>
|
||||
)
|
||||
|
||||
return { toggle, menu }
|
||||
}
|
||||
|
|
@ -54,7 +54,6 @@ let
|
|||
wireplumber
|
||||
notifd
|
||||
bluetooth
|
||||
mpris
|
||||
]
|
||||
++ [ pkgs.networkmanager ];
|
||||
|
||||
|
|
|
|||
|
|
@ -45,9 +45,7 @@ let
|
|||
wav="$dir/clip.wav"
|
||||
|
||||
notify() {
|
||||
# -a sets the app name shown by the notification daemon; the bar
|
||||
# treats "Dictation" as transient (toast only, never listed)
|
||||
notify-send -a "Dictation" -t 2500 "$1" 2>/dev/null || true
|
||||
notify-send -t 2500 "Dictation" "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
case "''${1:-}" in
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
with lib;
|
||||
{
|
||||
options.modules.ags = {
|
||||
enable = mkEnableOption "the AGS status bar";
|
||||
enable = mkEnableOption {
|
||||
default = false;
|
||||
description = "Enable the AGS status bar";
|
||||
};
|
||||
|
||||
timeout = mkOption {
|
||||
type = types.ints.positive;
|
||||
|
|
|
|||
Loading…
Reference in New Issue