Compare commits
No commits in common. "ags" and "main" have entirely different histories.
|
|
@ -21,6 +21,4 @@
|
||||||
modules.display.scale = 2;
|
modules.display.scale = 2;
|
||||||
|
|
||||||
modules.steam.remotePlay.enable = true;
|
modules.steam.remotePlay.enable = true;
|
||||||
|
|
||||||
hardware.bluetooth.enable = true;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
node_modules/
|
|
||||||
@girs/
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { App } from "astal/gtk3"
|
|
||||||
import style from "./style.scss"
|
|
||||||
import Bar from "./widget/Bar"
|
|
||||||
import Notifications from "./widget/Notifications"
|
|
||||||
|
|
||||||
App.start({
|
|
||||||
css: style,
|
|
||||||
main() {
|
|
||||||
App.get_monitors().map(Bar)
|
|
||||||
Notifications(App.get_monitors()[0])
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const MAX_TIMEOUT = 10000
|
|
||||||
|
|
@ -1,615 +0,0 @@
|
||||||
@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;
|
|
||||||
// one step lighter than the card fill so section outlines read against
|
|
||||||
// the dropdown background and the widgets they surround
|
|
||||||
$border-color: $base03;
|
|
||||||
$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);
|
|
||||||
|
|
||||||
$bar-alpha: 0.7;
|
|
||||||
$bar-bg: transparentize($base01, 1 - $bar-alpha);
|
|
||||||
$module-bg: transparentize($base02, 0.4);
|
|
||||||
|
|
||||||
* {
|
|
||||||
all: unset;
|
|
||||||
font-family: $font-family;
|
|
||||||
font-size: $font-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Bar,
|
|
||||||
.popup-window {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
background: $bar-bg;
|
|
||||||
padding: 0;
|
|
||||||
color: $base05;
|
|
||||||
|
|
||||||
label,
|
|
||||||
icon {
|
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
|
||||||
-gtk-icon-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-corners {
|
|
||||||
background: transparent;
|
|
||||||
color: $bar-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.corner {
|
|
||||||
background: transparent;
|
|
||||||
color: $module-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar .first,
|
|
||||||
.bar .center,
|
|
||||||
.bar .end,
|
|
||||||
.bar .last {
|
|
||||||
background: $module-bg;
|
|
||||||
padding: 2px 10px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $base03;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar .first {
|
|
||||||
border-radius: 0 0 ($radius * 2.5) 0;
|
|
||||||
margin: 0 2px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar .center {
|
|
||||||
border-radius: $radius;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar .end {
|
|
||||||
border-radius: ($radius * 0.75) ($radius * 2) ($radius * 0.75) ($radius * 2);
|
|
||||||
margin: 0 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar .last {
|
|
||||||
border-radius: 0 0 0 ($radius * 2.5);
|
|
||||||
margin: 0 0 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo icon {
|
|
||||||
min-width: 20px;
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock {
|
|
||||||
font-weight: bold;
|
|
||||||
font-feature-settings: "tnum";
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock icon {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws {
|
|
||||||
min-width: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin: 0 2px;
|
|
||||||
padding: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws.active {
|
|
||||||
background: $primary-bg;
|
|
||||||
color: $primary-fg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws:hover {
|
|
||||||
background: transparentize($base05, 0.88);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quicksettings icon {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quicksettings .notif-count,
|
|
||||||
.quicksettings .bt-count {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.battery-level {
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- quicksettings popup ----------
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 7px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.battery-section {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
|
|
||||||
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: 0 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&: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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
background-color: $widget-bg;
|
|
||||||
border: $border;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 4px 0 8px;
|
|
||||||
|
|
||||||
.title-box {
|
|
||||||
margin: 2px 0 4px;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0 4px 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-separator {
|
|
||||||
background: $border-color;
|
|
||||||
min-height: 1px;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 5px 7px;
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-overlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-row {
|
|
||||||
margin: 1px 0;
|
|
||||||
|
|
||||||
.device-name {
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-battery {
|
|
||||||
font-size: 0.85em;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-connecting {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// media player card
|
|
||||||
.player {
|
|
||||||
background-color: $widget-bg;
|
|
||||||
border: $border;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 4px 0;
|
|
||||||
|
|
||||||
.cover {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background-color: $hover-bg;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
border-radius: 999px;
|
|
||||||
|
|
||||||
icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-overlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause {
|
|
||||||
background-color: $primary-bg;
|
|
||||||
|
|
||||||
icon {
|
|
||||||
color: $primary-fg;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $active-hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-position,
|
|
||||||
.player-length {
|
|
||||||
font-size: 0.85em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-section {
|
|
||||||
margin-top: 10px;
|
|
||||||
border-top: 1px solid $border-color;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-header {
|
|
||||||
label {
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-clear {
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-empty {
|
|
||||||
font-size: 0.85em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifications {
|
|
||||||
.notif {
|
|
||||||
background-color: $widget-bg;
|
|
||||||
border: $border;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-header {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
|
|
||||||
.notif-app-name {
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-time {
|
|
||||||
font-size: 0.85em;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-close {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 2px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-overlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-content {
|
|
||||||
.notif-image {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-summary {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-text {
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-action {
|
|
||||||
background: $base01;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 6px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
// paints the concave sliver below the bar at the screen edge: the r x r
|
|
||||||
// square minus the quarter disk where the wallpaper shows through
|
|
||||||
function Corner({ side }: { side: "left" | "right" }) {
|
|
||||||
return (
|
|
||||||
<drawingarea
|
|
||||||
className="corner"
|
|
||||||
widthRequest={RADIUS}
|
|
||||||
heightRequest={RADIUS}
|
|
||||||
halign={side === "left" ? Gtk.Align.START : Gtk.Align.END}
|
|
||||||
onDraw={(self, cr) => {
|
|
||||||
const w = self.get_allocated_width()
|
|
||||||
const h = self.get_allocated_height()
|
|
||||||
// bar color comes from the parent strip so the sliver gets the
|
|
||||||
// same bar-bg + module-bg stack as the pills
|
|
||||||
const bar = self
|
|
||||||
.get_parent()!
|
|
||||||
.get_style_context()
|
|
||||||
.get_color(Gtk.StateFlags.NORMAL)
|
|
||||||
const mod = self
|
|
||||||
.get_style_context()
|
|
||||||
.get_color(Gtk.StateFlags.NORMAL)
|
|
||||||
|
|
||||||
const trace = () => {
|
|
||||||
if (side === "left") {
|
|
||||||
cr.moveTo(0, 0)
|
|
||||||
cr.lineTo(w, 0)
|
|
||||||
cr.arcNegative(w, h, w, -Math.PI / 2, -Math.PI)
|
|
||||||
} else {
|
|
||||||
cr.moveTo(w, 0)
|
|
||||||
cr.lineTo(0, 0)
|
|
||||||
cr.arc(0, h, w, -Math.PI / 2, 0)
|
|
||||||
}
|
|
||||||
cr.closePath()
|
|
||||||
}
|
|
||||||
|
|
||||||
cr.setSourceRGBA(bar.red, bar.green, bar.blue, bar.alpha)
|
|
||||||
trace()
|
|
||||||
cr.fillPreserve()
|
|
||||||
cr.setSourceRGBA(mod.red, mod.green, mod.blue, mod.alpha)
|
|
||||||
cr.fill()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
|
||||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
|
||||||
const quicksettings = QuickSettings(gdkmonitor)
|
|
||||||
|
|
||||||
const right: Array<JSX.Element> = [quicksettings.button]
|
|
||||||
|
|
||||||
const popups: Array<JSX.Element> = [quicksettings.popup]
|
|
||||||
|
|
||||||
return [
|
|
||||||
(
|
|
||||||
<window
|
|
||||||
className="Bar"
|
|
||||||
namespace="ags-bar"
|
|
||||||
gdkmonitor={gdkmonitor}
|
|
||||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
|
||||||
anchor={TOP | LEFT | RIGHT}
|
|
||||||
application={App}
|
|
||||||
>
|
|
||||||
<box vertical>
|
|
||||||
<centerbox className="bar">
|
|
||||||
<box
|
|
||||||
className="bar-left"
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
valign={Gtk.Align.FILL}
|
|
||||||
>
|
|
||||||
<Logo />
|
|
||||||
<box className="center">
|
|
||||||
<Workspaces />
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
className="bar-center"
|
|
||||||
halign={Gtk.Align.CENTER}
|
|
||||||
valign={Gtk.Align.CENTER}
|
|
||||||
>
|
|
||||||
<MediaButton />
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
className="bar-right"
|
|
||||||
halign={Gtk.Align.END}
|
|
||||||
valign={Gtk.Align.FILL}
|
|
||||||
>
|
|
||||||
{right}
|
|
||||||
<box className="clock last">
|
|
||||||
<Clock />
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</centerbox>
|
|
||||||
<box className="bar-corners">
|
|
||||||
<Corner side="left" />
|
|
||||||
<box hexpand />
|
|
||||||
<Corner side="right" />
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</window>
|
|
||||||
),
|
|
||||||
...popups,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import Gtk from "gi://Gtk?version=3.0"
|
|
||||||
import AstalBattery from "gi://AstalBattery"
|
|
||||||
import { type Binding, Variable, bind } from "astal"
|
|
||||||
|
|
||||||
const battery = AstalBattery.get_default()
|
|
||||||
|
|
||||||
const CHARGING = 1
|
|
||||||
const DISCHARGING = 2
|
|
||||||
const FULLY_CHARGED = 4
|
|
||||||
|
|
||||||
function fmtTime(seconds: number) {
|
|
||||||
if (!seconds || seconds <= 0) return null
|
|
||||||
const h = Math.floor(seconds / 3600)
|
|
||||||
const m = Math.round((seconds % 3600) / 60)
|
|
||||||
return h > 0 ? `${h}h ${m}m` : `${m}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = Variable.derive(
|
|
||||||
[bind(battery, "state"), bind(battery, "time-to-empty"), bind(battery, "time-to-full")],
|
|
||||||
(state, toEmpty, toFull) => {
|
|
||||||
if (state === FULLY_CHARGED) return "fully charged"
|
|
||||||
if (state === CHARGING) {
|
|
||||||
const t = fmtTime(toFull)
|
|
||||||
return t ? `charging · ${t} to full` : "charging"
|
|
||||||
}
|
|
||||||
if (state === DISCHARGING) {
|
|
||||||
const t = fmtTime(toEmpty)
|
|
||||||
return t ? `${t} left` : "on battery"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const batteryIcon: Binding<string> | null = battery.isBattery
|
|
||||||
? bind(battery, "icon-name")
|
|
||||||
: null
|
|
||||||
|
|
||||||
export const batteryPercent: Binding<string> | null = battery.isBattery
|
|
||||||
? bind(battery, "percentage").as((p) => `${Math.round(p * 100)}%`)
|
|
||||||
: null
|
|
||||||
|
|
||||||
export function BatterySection() {
|
|
||||||
if (!battery.isBattery) return <box />
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box className="battery-section">
|
|
||||||
<icon icon={bind(battery, "icon-name")} />
|
|
||||||
<label
|
|
||||||
className="battery-percent"
|
|
||||||
label={bind(battery, "percentage").as((p) => `${Math.round(p * 100)}%`)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="battery-status"
|
|
||||||
label={status()}
|
|
||||||
visible={status().as((text) => text !== "")}
|
|
||||||
hexpand
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
import Gtk from "gi://Gtk?version=3.0"
|
|
||||||
import Gio from "gi://Gio"
|
|
||||||
import AstalBluetooth from "gi://AstalBluetooth"
|
|
||||||
import { bind, Variable } from "astal"
|
|
||||||
import { ArrowToggleButton, Menu } from "./ToggleButton"
|
|
||||||
|
|
||||||
const bt = AstalBluetooth.get_default()
|
|
||||||
|
|
||||||
function power(on: boolean) {
|
|
||||||
if (bt.adapter) bt.adapter.powered = on
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDiscovery(on: boolean) {
|
|
||||||
if (!bt.adapter) return
|
|
||||||
try {
|
|
||||||
if (on) bt.adapter.start_discovery()
|
|
||||||
else bt.adapter.stop_discovery()
|
|
||||||
} catch {
|
|
||||||
// expected when already in the requested state: bluez stops
|
|
||||||
// discovery itself on connect, auto-starts it, etc.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bluez only registers devices that are paired or seen during discovery;
|
|
||||||
// the list is mirrored into a Variable refreshed on device add/remove
|
|
||||||
const devices = Variable<AstalBluetooth.Device[]>(bt.get_devices())
|
|
||||||
const refreshDevices = () => devices.set(bt.get_devices())
|
|
||||||
bt.connect("notify::devices", refreshDevices)
|
|
||||||
bt.connect("device_added", refreshDevices)
|
|
||||||
bt.connect("device_removed", refreshDevices)
|
|
||||||
|
|
||||||
const btIcon = bind(bt, "is_powered").as((p) =>
|
|
||||||
p ? "bluetooth-active-symbolic" : "bluetooth-disabled-symbolic",
|
|
||||||
)
|
|
||||||
|
|
||||||
const connectedCount = devices().as(
|
|
||||||
(ds) => ds.filter((d) => d.connected).length,
|
|
||||||
)
|
|
||||||
|
|
||||||
const pillLabel = Variable.derive(
|
|
||||||
[bind(bt, "is_powered"), devices],
|
|
||||||
(powered, ds) => {
|
|
||||||
if (!powered) return "Disabled"
|
|
||||||
const connected = ds.filter((d) => d.connected)
|
|
||||||
if (connected.length === 1) return connected[0].alias
|
|
||||||
return `${connected.length} Connected`
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// ported from AlfheimOS: icon, name, battery, busy icon while connecting,
|
|
||||||
// whole row toggles the bluez connected state
|
|
||||||
function DeviceItem(device: AstalBluetooth.Device) {
|
|
||||||
const battery = bind(device, "battery_percentage")
|
|
||||||
const connecting = bind(device, "connecting")
|
|
||||||
const connected = Variable.derive(
|
|
||||||
[bind(device, "connected"), connecting],
|
|
||||||
(c, busy) => c && !busy,
|
|
||||||
)
|
|
||||||
// bluez reports br-connection-busy when attempts pile up, so debounce
|
|
||||||
let lastAttempt = 0
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (connecting.get()) return
|
|
||||||
// skip retries within the debounce window
|
|
||||||
const now = Date.now()
|
|
||||||
if (now - lastAttempt < 1500) return
|
|
||||||
lastAttempt = now
|
|
||||||
const state = !device.connected
|
|
||||||
// bluez rejects connects while the controller is busy scanning
|
|
||||||
if (state) setDiscovery(false)
|
|
||||||
// vala async: begin with a callback, finish() throws
|
|
||||||
const done = (_source: AstalBluetooth.Device, res: Gio.AsyncResult) => {
|
|
||||||
try {
|
|
||||||
if (state) device.connect_device_finish(res)
|
|
||||||
else device.disconnect_device_finish(res)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state) device.connect_device(done)
|
|
||||||
else device.disconnect_device(done)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className="device-row" onClicked={toggle}>
|
|
||||||
<box>
|
|
||||||
<icon
|
|
||||||
icon={
|
|
||||||
device.icon
|
|
||||||
? `${device.icon}-symbolic`
|
|
||||||
: "bluetooth-active-symbolic"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="device-name"
|
|
||||||
label={device.alias ?? device.address}
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
hexpand
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="device-battery"
|
|
||||||
visible={battery.as((p) => p > 0)}
|
|
||||||
label={battery.as((p) => `${Math.round(p * 100)}%`)}
|
|
||||||
/>
|
|
||||||
<icon
|
|
||||||
className="device-connecting"
|
|
||||||
icon="view-refresh-symbolic"
|
|
||||||
visible={connecting}
|
|
||||||
/>
|
|
||||||
<icon
|
|
||||||
visible={connected()}
|
|
||||||
icon="object-select-symbolic"
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BluetoothSection({ open }: { open: Variable<boolean> }) {
|
|
||||||
if (!bt.adapter) return null
|
|
||||||
|
|
||||||
const expanded = Variable(false)
|
|
||||||
open.subscribe((v) => {
|
|
||||||
if (!v) {
|
|
||||||
expanded.set(false)
|
|
||||||
setDiscovery(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
|
|
||||||
return { toggle, menu }
|
|
||||||
}
|
|
||||||
|
|
||||||
// bar cluster indicator: shown while bluetooth is powered, with a count
|
|
||||||
// badge when devices are connected
|
|
||||||
export const bluetoothIndicator: JSX.Element | null = bt.adapter ? (
|
|
||||||
<box className="bt-indicator" spacing={3} visible={bind(bt, "is_powered")}>
|
|
||||||
<icon icon="bluetooth-active-symbolic" />
|
|
||||||
<label
|
|
||||||
className="bt-count"
|
|
||||||
visible={connectedCount.as((n) => n > 0)}
|
|
||||||
label={connectedCount.as((n) => `${n}`)}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
) : null
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import GLib from "gi://GLib"
|
|
||||||
import Pango from "gi://Pango"
|
|
||||||
import Gtk from "gi://Gtk?version=3.0"
|
|
||||||
import { Variable, bind } from "astal"
|
|
||||||
|
|
||||||
const time = Variable("").poll(1000, () => {
|
|
||||||
const now = GLib.DateTime.new_now_local()
|
|
||||||
const clock = now.format("%I:%M %p")!.replace(/^0/, "")
|
|
||||||
return `${now.format("%a, %d %b")}, ${clock}`
|
|
||||||
})
|
|
||||||
|
|
||||||
// widest possible tick: days 1-7 always cover every weekday, sampled across
|
|
||||||
// all months with a double-digit hour in both meridiems; measured once at
|
|
||||||
// realize with the label's own font (tnum forced on) so the fixed
|
|
||||||
// widthRequest stops the bar from shifting every minute
|
|
||||||
function widestTick(self: Gtk.Label): number {
|
|
||||||
const layout = self.create_pango_layout("")
|
|
||||||
const attrs = new Pango.AttrList()
|
|
||||||
attrs.insert(Pango.attr_font_features_new("tnum"))
|
|
||||||
layout.set_attributes(attrs)
|
|
||||||
|
|
||||||
let widest = 0
|
|
||||||
for (let month = 1; month <= 12; month++) {
|
|
||||||
for (let day = 1; day <= 7; day++) {
|
|
||||||
for (const hour of [10, 22]) {
|
|
||||||
const d = GLib.DateTime.new_local(2026, month, day, hour, 0, 0)
|
|
||||||
const tick =
|
|
||||||
`${d.format("%a, %d %b")}, ${d.format("%I:%M %p")!.replace(/^0/, "")}`
|
|
||||||
layout.set_text(tick, -1)
|
|
||||||
const [w] = layout.get_pixel_size()
|
|
||||||
widest = Math.max(widest, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Math.ceil(widest) + 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Clock() {
|
|
||||||
return (
|
|
||||||
<box spacing={6}>
|
|
||||||
<icon icon="clock-hands-symbolic" />
|
|
||||||
<label
|
|
||||||
className="clock"
|
|
||||||
label={bind(time)}
|
|
||||||
onRealize={(self) => {
|
|
||||||
self.width_request = widestTick(self as Gtk.Label)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
|
||||||
import GLib from "gi://GLib"
|
|
||||||
import { Variable, bind } from "astal"
|
|
||||||
|
|
||||||
const DURATION = 180
|
|
||||||
|
|
||||||
interface DropdownProps {
|
|
||||||
gdkmonitor: Gdk.Monitor
|
|
||||||
target: Gtk.Widget
|
|
||||||
open: Variable<boolean>
|
|
||||||
width?: number
|
|
||||||
vertical?: boolean
|
|
||||||
className?: string
|
|
||||||
// cap the popup height to the screen, scrolling any overflow
|
|
||||||
maxHeight?: number
|
|
||||||
children: Array<JSX.Element>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dropdown({
|
|
||||||
gdkmonitor,
|
|
||||||
target,
|
|
||||||
open,
|
|
||||||
width = 260,
|
|
||||||
vertical = true,
|
|
||||||
className = "",
|
|
||||||
maxHeight,
|
|
||||||
children,
|
|
||||||
}: DropdownProps) {
|
|
||||||
const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor
|
|
||||||
const GAP = 8
|
|
||||||
|
|
||||||
const child = (
|
|
||||||
<box
|
|
||||||
className={`dropdown ${className}`}
|
|
||||||
vertical={vertical}
|
|
||||||
widthRequest={width}
|
|
||||||
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
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
|
|
||||||
function reposition() {
|
|
||||||
const toplevel = target.get_toplevel()
|
|
||||||
const [, tx, ty] = target.translate_coordinates(toplevel, 0, 0)
|
|
||||||
const { width: bw, height: bh } = target.get_allocation()
|
|
||||||
const { width: sw } = gdkmonitor.get_geometry()
|
|
||||||
|
|
||||||
const x = Math.min(Math.max(tx + bw / 2 - width / 2, GAP), sw - width - GAP)
|
|
||||||
const y = ty + bh + GAP
|
|
||||||
|
|
||||||
child.marginStart = Math.round(x)
|
|
||||||
child.marginTop = Math.round(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// the window outlives the animation: it hides once the slide-out
|
|
||||||
// revealer finished, and hiding the window resolves to open=false
|
|
||||||
const winVisible = Variable(false)
|
|
||||||
let closeTimer: number | undefined
|
|
||||||
|
|
||||||
open.subscribe((v) => {
|
|
||||||
if (closeTimer !== undefined) {
|
|
||||||
GLib.source_remove(closeTimer)
|
|
||||||
closeTimer = undefined
|
|
||||||
}
|
|
||||||
if (v) {
|
|
||||||
reposition()
|
|
||||||
winVisible.set(true)
|
|
||||||
} else if (winVisible.get()) {
|
|
||||||
closeTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DURATION + 20, () => {
|
|
||||||
winVisible.set(false)
|
|
||||||
closeTimer = undefined
|
|
||||||
return GLib.SOURCE_REMOVE
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<window
|
|
||||||
className="popup-window"
|
|
||||||
application={App}
|
|
||||||
gdkmonitor={gdkmonitor}
|
|
||||||
namespace="ags-bar-popup"
|
|
||||||
layer={Astal.Layer.OVERLAY}
|
|
||||||
keymode={Astal.Keymode.EXCLUSIVE}
|
|
||||||
anchor={TOP | BOTTOM | LEFT | RIGHT}
|
|
||||||
exclusivity={Astal.Exclusivity.IGNORE}
|
|
||||||
visible={bind(winVisible)}
|
|
||||||
onNotifyVisible={(self) => {
|
|
||||||
if (!self.visible && open.get()) open.set(false)
|
|
||||||
}}
|
|
||||||
onButtonPressEvent={(self, e) => {
|
|
||||||
const event = e as unknown as Gdk.Event
|
|
||||||
const [, px, py] = event.get_coords()
|
|
||||||
const { x, y, width: w, height: h } = child.get_allocation()
|
|
||||||
const xOut = px < x || px > x + w
|
|
||||||
const yOut = py < y || py > y + h
|
|
||||||
|
|
||||||
if (xOut || yOut) open.set(false)
|
|
||||||
}}
|
|
||||||
onKeyPressEvent={(_, e) => {
|
|
||||||
const event = e as unknown as Gdk.Event
|
|
||||||
if (event.get_keyval()[1] === Gdk.KEY_Escape) open.set(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<revealer
|
|
||||||
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
|
||||||
transitionDuration={DURATION}
|
|
||||||
revealChild={bind(open)}
|
|
||||||
>
|
|
||||||
{child}
|
|
||||||
</revealer>
|
|
||||||
</window>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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} />
|
|
||||||
</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,238 +0,0 @@
|
||||||
import GLib from "gi://GLib"
|
|
||||||
import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
|
||||||
import Notifd from "gi://AstalNotifd"
|
|
||||||
import { Variable, 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
|
|
||||||
|
|
||||||
// notifications restored from the daemon's cache on startup are stale
|
|
||||||
for (const n of notifd.notifications) {
|
|
||||||
n.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
notifd.connect("notified", (_, id: number) => {
|
|
||||||
const n = notifd.get_notification(id)
|
|
||||||
if (!n) 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])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
>
|
|
||||||
<box vertical>
|
|
||||||
<box className="notif-header">
|
|
||||||
<icon pixelSize={16} icon={appIcon} />
|
|
||||||
<label
|
|
||||||
className="notif-app-name"
|
|
||||||
label={appName}
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
hexpand
|
|
||||||
/>
|
|
||||||
<label className="notif-time" label={formatTime(n)} />
|
|
||||||
<button
|
|
||||||
className="notif-close"
|
|
||||||
onClicked={() => dismissNotification(n)}
|
|
||||||
>
|
|
||||||
<icon icon="window-close-symbolic" pixelSize={12} />
|
|
||||||
</button>
|
|
||||||
</box>
|
|
||||||
<box className="notif-content">
|
|
||||||
{n.image !== "" && (
|
|
||||||
<icon className="notif-image" pixelSize={36} icon={n.image} />
|
|
||||||
)}
|
|
||||||
<box vertical hexpand>
|
|
||||||
<label
|
|
||||||
className="notif-summary"
|
|
||||||
label={n.summary}
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
visible={n.summary !== ""}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="notif-text"
|
|
||||||
label={n.body}
|
|
||||||
wrap
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
visible={n.body !== ""}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
{n.actions.filter((a) => a.label).length > 0 && (
|
|
||||||
<box className="notif-actions" homogeneous spacing={4}>
|
|
||||||
{n.actions
|
|
||||||
.filter((a) => a.label)
|
|
||||||
.map((a) => (
|
|
||||||
<button
|
|
||||||
className="notif-action"
|
|
||||||
onClicked={() => a.invoke()}
|
|
||||||
>
|
|
||||||
<label label={a.label} />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Notifications(gdkmonitor: Gdk.Monitor) {
|
|
||||||
const { TOP, RIGHT } = Astal.WindowAnchor
|
|
||||||
|
|
||||||
return (
|
|
||||||
<window
|
|
||||||
className="popup-window"
|
|
||||||
namespace="ags-notifications"
|
|
||||||
application={App}
|
|
||||||
gdkmonitor={gdkmonitor}
|
|
||||||
layer={Astal.Layer.OVERLAY}
|
|
||||||
anchor={TOP | RIGHT}
|
|
||||||
exclusivity={Astal.Exclusivity.IGNORE}
|
|
||||||
keymode={Astal.Keymode.NONE}
|
|
||||||
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)),
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
</window>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
import Gtk from "gi://Gtk?version=3.0"
|
|
||||||
import GLib from "gi://GLib"
|
|
||||||
import AstalWp from "gi://AstalWp"
|
|
||||||
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"
|
|
||||||
|
|
||||||
const speaker = AstalWp.get_default().audio.default_speaker!
|
|
||||||
const mic = AstalWp.get_default().audio.default_microphone
|
|
||||||
|
|
||||||
const speakerIcon = Variable.derive(
|
|
||||||
[bind(speaker, "volume"), bind(speaker, "mute")],
|
|
||||||
(volume, mute) => {
|
|
||||||
if (mute || volume <= 0.01) return "audio-volume-muted-symbolic"
|
|
||||||
if (volume < 0.20) return "audio-volume-low-symbolic"
|
|
||||||
if (volume < 0.66) return "audio-volume-medium-symbolic"
|
|
||||||
return "audio-volume-high-symbolic"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const micIcon = mic
|
|
||||||
? Variable.derive([bind(mic, "volume"), bind(mic, "mute")], (volume, mute) =>
|
|
||||||
mute || volume <= 0.01
|
|
||||||
? "microphone-sensitivity-muted-symbolic"
|
|
||||||
: "microphone-sensitivity-high-symbolic",
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
|
|
||||||
const user = GLib.get_user_name()
|
|
||||||
|
|
||||||
const actions: { icon: string; cmd?: string[] }[] = [
|
|
||||||
{ icon: "emblem-system-symbolic" },
|
|
||||||
{ 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-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 wifiSection = WifiSection({ open })
|
|
||||||
const btSection = BluetoothSection({ open })
|
|
||||||
const toggles = [wifiSection?.toggle, btSection?.toggle].filter(
|
|
||||||
(t) => t !== undefined,
|
|
||||||
)
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<button
|
|
||||||
className="quicksettings end"
|
|
||||||
onClicked={() => open.set(!open.get())}
|
|
||||||
onScroll={(_, e) => {
|
|
||||||
const volume = Math.min(
|
|
||||||
1,
|
|
||||||
Math.max(0, speaker.volume - e.delta_y * 0.02),
|
|
||||||
)
|
|
||||||
speaker.volume = volume
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<box spacing={6}>
|
|
||||||
<box className="notif-indicator" spacing={3}>
|
|
||||||
<icon icon={dndIcon()} />
|
|
||||||
<label
|
|
||||||
className="notif-count"
|
|
||||||
label={notifCount.as((n) => `${n}`)}
|
|
||||||
visible={notifCount.as((n) => n > 0)}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
{wifiIcon && <icon icon={wifiIcon} />}
|
|
||||||
{bluetoothIndicator}
|
|
||||||
<icon icon={speakerIcon()} />
|
|
||||||
{micIcon && <icon icon={micIcon()} />}
|
|
||||||
{batteryIcon && batteryPercent && (
|
|
||||||
<box spacing={3}>
|
|
||||||
<icon icon={batteryIcon} />
|
|
||||||
<label className="battery-level" label={batteryPercent} />
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
|
|
||||||
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()} />
|
|
||||||
</button>
|
|
||||||
<slider
|
|
||||||
hexpand
|
|
||||||
className={bind(speaker, "mute").as((m) => (m ? "muted" : ""))}
|
|
||||||
value={bind(speaker, "volume")}
|
|
||||||
onDragged={(self) => (speaker.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 />
|
|
||||||
<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-clear"
|
|
||||||
onClicked={clearNotifications}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</box>
|
|
||||||
</Dropdown>
|
|
||||||
)
|
|
||||||
|
|
||||||
return { button, popup }
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import GLib from "gi://GLib"
|
|
||||||
import Pango from "gi://Pango"
|
|
||||||
import Gtk from "gi://Gtk?version=3.0"
|
|
||||||
import { bind, type Binding, type Variable } from "astal"
|
|
||||||
|
|
||||||
// rotating arrow button: icon turns 90deg in small steps while the section
|
|
||||||
// opens, ported from AlfheimOS; a single self-clearing chained timer drives
|
|
||||||
// the animation so cancellation only ever touches provably live sources
|
|
||||||
export function Arrow({
|
|
||||||
open,
|
|
||||||
onToggle,
|
|
||||||
}: {
|
|
||||||
open: Variable<boolean>
|
|
||||||
onToggle: () => void
|
|
||||||
}) {
|
|
||||||
let deg = 0
|
|
||||||
let timer: number | undefined
|
|
||||||
const icon = <icon icon="pan-end-symbolic" />
|
|
||||||
|
|
||||||
const animate = (target: number) => {
|
|
||||||
if (timer !== undefined) {
|
|
||||||
GLib.source_remove(timer)
|
|
||||||
timer = undefined
|
|
||||||
}
|
|
||||||
const tick = () => {
|
|
||||||
deg += Math.sign(target - deg) * 10
|
|
||||||
icon.css = `-gtk-icon-transform: rotate(${deg}deg);`
|
|
||||||
if (deg !== target) {
|
|
||||||
timer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 15, tick)
|
|
||||||
} else {
|
|
||||||
timer = undefined
|
|
||||||
}
|
|
||||||
return GLib.SOURCE_REMOVE
|
|
||||||
}
|
|
||||||
if (deg !== target) tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
open.subscribe((v) => animate(v ? 90 : 0))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className="arrow" onClicked={onToggle}>
|
|
||||||
{icon}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArrowToggleButtonProps = {
|
|
||||||
open: Variable<boolean>
|
|
||||||
icon: string | Binding<string>
|
|
||||||
label: string | Binding<string>
|
|
||||||
active: Binding<boolean>
|
|
||||||
onMainClick: () => void
|
|
||||||
onArrowClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// icon + label pill with a detached arrow: the pill toggles the feature,
|
|
||||||
// the arrow rotates open its device menu
|
|
||||||
export function ArrowToggleButton({
|
|
||||||
open,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
active,
|
|
||||||
onMainClick,
|
|
||||||
onArrowClick,
|
|
||||||
}: ArrowToggleButtonProps) {
|
|
||||||
return (
|
|
||||||
<box
|
|
||||||
className={active.as((v) => (v ? "toggle-button active" : "toggle-button"))}
|
|
||||||
hexpand
|
|
||||||
>
|
|
||||||
<button className="toggle-pill" hexpand onClicked={onMainClick}>
|
|
||||||
<box hexpand>
|
|
||||||
<icon className="icon" icon={icon} />
|
|
||||||
<label
|
|
||||||
className="label"
|
|
||||||
label={label}
|
|
||||||
maxWidthChars={10}
|
|
||||||
ellipsize={Pango.EllipsizeMode.END}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
<Arrow open={open} onToggle={onArrowClick} />
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MenuProps = {
|
|
||||||
open: Variable<boolean>
|
|
||||||
icon: string | Binding<string>
|
|
||||||
title: string
|
|
||||||
children: Array<JSX.Element>
|
|
||||||
}
|
|
||||||
|
|
||||||
// revealer with a titled card header, shown while the section is open
|
|
||||||
export function Menu({ open, icon, title, children }: MenuProps) {
|
|
||||||
return (
|
|
||||||
<revealer
|
|
||||||
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
|
||||||
transitionDuration={180}
|
|
||||||
revealChild={bind(open)}
|
|
||||||
>
|
|
||||||
<box className="menu" vertical>
|
|
||||||
<box className="title-box">
|
|
||||||
<icon className="icon" icon={icon} />
|
|
||||||
<label className="title" label={title} ellipsize={Pango.EllipsizeMode.END} />
|
|
||||||
</box>
|
|
||||||
<box className="menu-separator" />
|
|
||||||
<box className="content" vertical>
|
|
||||||
{children}
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</revealer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
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]
|
|
||||||
|
|
||||||
const wifi = AstalNetwork.get_default().wifi
|
|
||||||
|
|
||||||
function strongest(aps: AstalNetwork.AccessPoint[]) {
|
|
||||||
const bySsid = new Map<string, AstalNetwork.AccessPoint>()
|
|
||||||
for (const ap of aps) {
|
|
||||||
const ssid = ap.ssid ?? ""
|
|
||||||
const best = bySsid.get(ssid)
|
|
||||||
if (!best || ap.strength > best.strength) bySsid.set(ssid, ap)
|
|
||||||
}
|
|
||||||
return [...bySsid.values()].sort((a, b) => b.strength - a.strength)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wifiIcon: Binding<string> | null = wifi
|
|
||||||
? bind(wifi, "icon-name")
|
|
||||||
: null
|
|
||||||
|
|
||||||
function WifiRow(
|
|
||||||
ap: AstalNetwork.AccessPoint,
|
|
||||||
activeSsid: Binding<string>,
|
|
||||||
connect: (ap: AstalNetwork.AccessPoint) => void,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<button className="wifi-row" onClicked={() => connect(ap)}>
|
|
||||||
<box>
|
|
||||||
<icon icon={bind(ap, "icon-name")} />
|
|
||||||
<label
|
|
||||||
className="wifi-row-ssid"
|
|
||||||
label={ap.ssid ?? "(hidden network)"}
|
|
||||||
halign={Gtk.Align.START}
|
|
||||||
hexpand
|
|
||||||
/>
|
|
||||||
<icon
|
|
||||||
visible={ap.requires_password}
|
|
||||||
icon="network-wireless-encrypted-symbolic"
|
|
||||||
/>
|
|
||||||
<icon
|
|
||||||
visible={activeSsid.as((ssid) => ssid === ap.ssid)}
|
|
||||||
icon="object-select-symbolic"
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WifiSection({ open }: { open: Variable<boolean> }) {
|
|
||||||
if (!wifi) return null
|
|
||||||
|
|
||||||
const expanded = Variable(false)
|
|
||||||
const pendingAP = Variable<AstalNetwork.AccessPoint | null>(null)
|
|
||||||
const failed = Variable("")
|
|
||||||
|
|
||||||
const status = Variable.derive(
|
|
||||||
[bind(wifi, "scanning"), bind(wifi, "state"), bind(wifi, "ssid"), failed],
|
|
||||||
(scanning, state, ssid, fail) => {
|
|
||||||
if (scanning) return "scanning…"
|
|
||||||
if (CONNECTING.includes(state)) return `connecting to ${ssid ?? "network"}…`
|
|
||||||
return fail
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const activeSsid = bind(wifi, "active-access-point").as((ap) => ap?.ssid ?? "")
|
|
||||||
|
|
||||||
function connect(ap: AstalNetwork.AccessPoint) {
|
|
||||||
if (ap.requires_password) {
|
|
||||||
failed.set("")
|
|
||||||
pendingAP.set(ap)
|
|
||||||
} else {
|
|
||||||
ap.activate()
|
|
||||||
.then(() => failed.set(""))
|
|
||||||
.catch(() => failed.set(`failed to connect to ${ap.ssid ?? "network"}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordEntry = (
|
|
||||||
<entry
|
|
||||||
className="wifi-password"
|
|
||||||
hexpand
|
|
||||||
visible={pendingAP().as((ap) => ap !== null)}
|
|
||||||
placeholderText={pendingAP().as(
|
|
||||||
(ap) => (ap ? `password for ${ap.ssid ?? "network"}` : ""),
|
|
||||||
)}
|
|
||||||
onActivate={(self) => {
|
|
||||||
const ap = pendingAP.get()
|
|
||||||
if (!ap || !self.text) return
|
|
||||||
ap.activate(self.text)
|
|
||||||
.then(() => {
|
|
||||||
pendingAP.set(null)
|
|
||||||
self.text = ""
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
failed.set(`failed to connect to ${ap.ssid ?? "network"}`)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
open.subscribe((v) => {
|
|
||||||
if (v) {
|
|
||||||
wifi.scan()
|
|
||||||
} else {
|
|
||||||
expanded.set(false)
|
|
||||||
pendingAP.set(null)
|
|
||||||
failed.set("")
|
|
||||||
passwordEntry.text = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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}>
|
|
||||||
<icon icon="view-refresh-symbolic" />
|
|
||||||
<label label="Rescan" />
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
</Menu>
|
|
||||||
)
|
|
||||||
|
|
||||||
return { toggle, menu }
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import Hyprland from "gi://AstalHyprland"
|
|
||||||
import { bind } from "astal"
|
|
||||||
|
|
||||||
const hypr = Hyprland.get_default()
|
|
||||||
const focused = bind(hypr, "focused-workspace").as((ws) => ws?.id ?? -1)
|
|
||||||
|
|
||||||
export default function Workspaces() {
|
|
||||||
return (
|
|
||||||
<box className="workspaces">
|
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
|
||||||
<button
|
|
||||||
className={focused.as((f) => (f === i ? "ws active" : "ws"))}
|
|
||||||
onClicked={() => hypr.dispatch("workspace", String(i))}
|
|
||||||
>
|
|
||||||
<label label={String(i)} />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib;
|
|
||||||
let
|
|
||||||
cfg = config.modules.ags;
|
|
||||||
hyprlandCfg = config.modules.hyprland;
|
|
||||||
colors = config.lib.stylix.colors.withHashtag;
|
|
||||||
|
|
||||||
base16Names = [
|
|
||||||
"base00"
|
|
||||||
"base01"
|
|
||||||
"base02"
|
|
||||||
"base03"
|
|
||||||
"base04"
|
|
||||||
"base05"
|
|
||||||
"base06"
|
|
||||||
"base07"
|
|
||||||
"base08"
|
|
||||||
"base09"
|
|
||||||
"base0A"
|
|
||||||
"base0B"
|
|
||||||
"base0C"
|
|
||||||
"base0D"
|
|
||||||
"base0E"
|
|
||||||
"base0F"
|
|
||||||
];
|
|
||||||
|
|
||||||
palette = pkgs.writeText "palette.scss" (
|
|
||||||
concatStringsSep "\n" (
|
|
||||||
map (name: "$" + name + ": " + colors.${name} + ";") base16Names
|
|
||||||
++ [
|
|
||||||
''$font-family: "${config.stylix.fonts.sansSerif.name}";''
|
|
||||||
"$font-size: ${toString config.stylix.fonts.sizes.desktop}px;"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
notifConfig = pkgs.writeText "notif-config.ts" ''
|
|
||||||
export const MAX_TIMEOUT = ${toString cfg.timeout};
|
|
||||||
'';
|
|
||||||
|
|
||||||
astalDeps =
|
|
||||||
with pkgs.astal;
|
|
||||||
[
|
|
||||||
astal3
|
|
||||||
io
|
|
||||||
hyprland
|
|
||||||
network
|
|
||||||
battery
|
|
||||||
wireplumber
|
|
||||||
notifd
|
|
||||||
bluetooth
|
|
||||||
mpris
|
|
||||||
]
|
|
||||||
++ [ pkgs.networkmanager ];
|
|
||||||
|
|
||||||
ags-bar = pkgs.ags.bundle {
|
|
||||||
pname = "ags-bar";
|
|
||||||
version = "0.1.0";
|
|
||||||
src = ./app;
|
|
||||||
entry = "app.ts";
|
|
||||||
dependencies = astalDeps;
|
|
||||||
postUnpack = ''
|
|
||||||
cp ${palette} "$sourceRoot/palette.scss"
|
|
||||||
cp ${notifConfig} "$sourceRoot/notif-config.ts"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = mkIf cfg.enable {
|
|
||||||
home.packages = [
|
|
||||||
ags-bar
|
|
||||||
pkgs.ags
|
|
||||||
pkgs.dart-sass
|
|
||||||
pkgs.nixos-icons
|
|
||||||
];
|
|
||||||
|
|
||||||
# clock face with hands at 12 and 3; adwaita has no such icon.
|
|
||||||
# fill-only paths: gtk symbolic recoloring turns stroke shapes into solids
|
|
||||||
xdg.dataFile."icons/hicolor/scalable/actions/clock-hands-symbolic.svg".text = ''
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
||||||
<path fill="currentColor" fill-rule="evenodd" d="M8 1.5A6.5 6.5 0 1 0 8 14.5A6.5 6.5 0 1 0 8 1.5ZM8 3A5 5 0 1 0 8 13A5 5 0 1 0 8 3Z"/>
|
|
||||||
<rect x="7.4" y="3.8" width="1.2" height="4.8" rx="0.6" fill="currentColor"/>
|
|
||||||
<rect x="7.4" y="7.4" width="3.4" height="1.2" rx="0.6" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
'';
|
|
||||||
|
|
||||||
wayland.windowManager.hyprland = mkIf hyprlandCfg.enable {
|
|
||||||
settings.exec-once = [ "ags-bar" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./ags
|
|
||||||
./battlenet.nix
|
./battlenet.nix
|
||||||
./direnv.nix
|
./direnv.nix
|
||||||
./discord.nix
|
./discord.nix
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,7 @@ let
|
||||||
wav="$dir/clip.wav"
|
wav="$dir/clip.wav"
|
||||||
|
|
||||||
notify() {
|
notify() {
|
||||||
# -a sets the app name shown by the notification daemon; the bar
|
notify-send -t 2500 "Dictation" "$1" 2>/dev/null || true
|
||||||
# treats "Dictation" as transient (toast only, never listed)
|
|
||||||
notify-send -a "Dictation" -t 2500 "$1" 2>/dev/null || true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "''${1:-}" in
|
case "''${1:-}" in
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,5 @@
|
||||||
imports = [
|
imports = [
|
||||||
./binds.nix
|
./binds.nix
|
||||||
./hyprland.nix
|
./hyprland.nix
|
||||||
./hyprlock.nix
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib;
|
|
||||||
let
|
|
||||||
cfg = config.modules.hyprland;
|
|
||||||
colors = config.lib.stylix.colors;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = mkIf cfg.enable {
|
|
||||||
programs.hyprlock = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
general = {
|
|
||||||
hide_cursor = true;
|
|
||||||
ignore_empty_input = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
label = [
|
|
||||||
{
|
|
||||||
monitor = "";
|
|
||||||
text = "$TIME12";
|
|
||||||
position = "0, 100";
|
|
||||||
halign = "center";
|
|
||||||
valign = "center";
|
|
||||||
font_size = 48;
|
|
||||||
font_family = "${config.stylix.fonts.sansSerif.name}";
|
|
||||||
color = "rgb(${colors.base05})";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,5 @@ in
|
||||||
enable = true;
|
enable = true;
|
||||||
withUWSM = true;
|
withUWSM = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
security.pam.services.hyprlock = { };
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
services = {
|
services = {
|
||||||
upower.enable = true;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib;
|
|
||||||
{
|
|
||||||
options.modules.ags = {
|
|
||||||
enable = mkEnableOption "the AGS status bar";
|
|
||||||
|
|
||||||
timeout = mkOption {
|
|
||||||
type = types.ints.positive;
|
|
||||||
default = 10000;
|
|
||||||
description = ''
|
|
||||||
Maximum duration notification popups stay on screen, in milliseconds.
|
|
||||||
Critical notifications are exempt and stay until dismissed.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./ags.nix
|
|
||||||
./battlenet.nix
|
./battlenet.nix
|
||||||
./discord.nix
|
./discord.nix
|
||||||
./dictation.nix
|
./dictation.nix
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
",preferred,auto,auto"
|
",preferred,auto,auto"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
modules.ags.enable = true;
|
|
||||||
modules.plasma.enable = false;
|
modules.plasma.enable = false;
|
||||||
modules.display.manager = "gdm";
|
modules.display.manager = "gdm";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue