Switch to noctalia

This commit is contained in:
2025-11-23 14:46:20 +01:00
parent cfa2a6e37e
commit 75ab0bfd12
38 changed files with 150 additions and 2850 deletions

21
flake.lock generated
View File

@@ -236,6 +236,26 @@
"type": "indirect"
}
},
"noctalia": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1763904998,
"narHash": "sha256-MMJtTkftqGLSShrPtObN/ZB9eveq6JQDzrdXvQ8UwGY=",
"owner": "zoriya",
"repo": "noctalia-shell",
"rev": "8975e4f979bf7ef2c7a62446761546899c336dd8",
"type": "github"
},
"original": {
"owner": "zoriya",
"repo": "noctalia-shell",
"type": "github"
}
},
"root": {
"inputs": {
"home-manager": "home-manager",
@@ -247,6 +267,7 @@
"nixos-hardware": "nixos-hardware",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_2",
"noctalia": "noctalia",
"tmux": "tmux",
"zen-browser": "zen-browser"
}

View File

@@ -28,6 +28,10 @@
url = "github:youwen5/zen-browser-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
noctalia = {
url = "github:zoriya/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
# use tmux's master for mode 2031
tmux = {
url = "github:tmux/tmux";

View File

@@ -1,14 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = tab
indent_size = tab
max_line_length = 120
[{*.yaml,*.yml}]
indent_style = space
indent_size = 2

View File

@@ -1,4 +0,0 @@
node_modules
package-lock.json
weather_key
settings.json

View File

@@ -1,48 +0,0 @@
import Gtk from "gi://Gtk?version=3.0";
import Gdk from "gi://Gdk";
import { Bar } from "./layouts/bar.js";
import { Notifications } from "./layouts/notifications.js";
import { OSD } from "./layouts/osd.js";
import { Quicksettings } from "./layouts/quicksettings.js";
/**
* @param {Array<(monitor: number) => Gtk.Window>} widgets
*/
export function forMonitors(widgets) {
const display = Gdk.Display.get_default();
display?.connect("monitor-added", (disp, gdkmonitor) => {
let monitor = 0;
for (let i = 0; i < display.get_n_monitors(); i++) {
if (gdkmonitor === display.get_monitor(i)) {
monitor = i;
break;
}
}
widgets.forEach((win) => App.addWindow(win(monitor)));
});
display?.connect("monitor-removed", (disp, monitor) => {
App.windows.forEach((win) => {
// @ts-ignore
if (win.gdkmonitor === monitor) App.removeWindow(win);
});
});
const n = display?.get_n_monitors() || 1;
return Array.from({ length: n }, (_, i) => i).flatMap((mon) =>
widgets.map((x) => x(mon)),
);
}
App.config({
closeWindowDelay: {
quicksettings: 300,
notifications: 200,
osd: 300,
},
style: `${App.configDir}/style.css`,
windows: [...forMonitors([Bar]), Quicksettings(), Notifications(), OSD()],
});

View File

@@ -1,49 +0,0 @@
#! /usr/bin/env python3
from material_color_utilities_python import *
from PIL import Image
import json
import os
import sys
import hashlib
CACHE_DIR = os.path.expanduser("~/.cache/ags/tempcolors")
def cache_and_get_colors(file_path):
file_hash = hashlib.sha256(file_path.encode('utf-8')).hexdigest()
cache_path = os.path.join(CACHE_DIR, file_hash[:2], file_hash[2:])
os.makedirs(cache_path, exist_ok=True)
colors_path = os.path.join(cache_path, "colors.json")
if os.path.exists(colors_path):
with open(colors_path, "r") as f:
parsed_colors = json.load(f)
elif not os.path.exists(file_path):
return None
img = Image.open(file_path)
basewidth = 8
wpercent = (basewidth/float(img.size[0]))
hsize = int((float(img.size[1])*float(wpercent)))
img = img.resize((basewidth, hsize), Image.Resampling.LANCZOS)
theme = themeFromImage(img)
scheme = theme.get("schemes")
parsed_colors = {
"primary": hexFromArgb(scheme.get("dark").primary),
"onPrimary": hexFromArgb(scheme.get("dark").onPrimary),
"background": hexFromArgb(scheme.get("dark").background),
"onBackground": hexFromArgb(scheme.get("dark").onBackground),
}
with open(colors_path, "w") as f:
json.dump(parsed_colors, f)
return parsed_colors
fn = sys.argv[1]
parsed_colors = cache_and_get_colors(fn)
print(json.dumps(parsed_colors, indent=4))

View File

@@ -1,48 +0,0 @@
{pkgs, ...}: let
covercolors = pkgs.stdenv.mkDerivation {
name = "covercolors";
dontUnpack = true;
propagatedBuildInputs = [
(pkgs.python3.withPackages (pyPkgs:
with pyPkgs; [
material-color-utilities
pillow
]))
];
installPhase = "install -Dm755 ${./covercolors.py} $out/bin/covercolors";
};
# systemdTarget = "graphical-session.target";
ags = pkgs.ags_1.overrideAttrs (_: prev: {
buildInputs =
prev.buildInputs
++ [
pkgs.libdbusmenu-gtk3
];
});
in {
home.packages = with pkgs; [
ags
# TODO: Find a way to add this for ags only
covercolors
brightnessctl
blueberry
];
# systemd.user.services.ags = {
# Unit = {
# Description = " A customizable and extensible shell ";
# PartOf = systemdTarget;
# Requires = systemdTarget;
# After = systemdTarget;
# };
#
# Service = {
# Type = "simple";
# ExecStart = "${ags}/bin/ags";
# Restart = "always";
# };
#
# Install = {WantedBy = [systemdTarget];};
# };
xdg.configFile."ags".source = ./.;
}

View File

@@ -1,88 +0,0 @@
import { Clock } from "../modules/clock.js";
import * as wm from "../modules/wm.js";
import * as audio from "../modules/audio.js";
import * as network from "../modules/network.js";
import * as bluetooth from "../modules/bluetooth.js";
import * as battery from "../modules/battery.js";
import * as notifications from "../modules/notifications.js";
import * as mpris from "../modules/mpris.js";
/**
*@param {number} monitor
*/
export const Bar = (monitor) =>
Widget.Window({
monitor,
name: `bar${monitor}`,
className: "transparent",
exclusivity: "exclusive",
anchor: ["top", "left", "right"],
layer: "bottom",
child: Widget.CenterBox({
// startWidget: Widget.Box({
// children: [
// wm.Tags({
// monitor,
// labels: ["一", "二", "三", "四", "五", "六", "七", "八", "九"],
// }),
// wm.Layout({ monitor }),
// wm.ClientLabel({ monitor }),
// ],
// }),
centerWidget: Widget.Box({
hpack: "center",
children: [
Widget.Button({
css: "min-width: 200px;",
onClicked: () => App.toggleWindow("notifications"),
child: notifications.Indicator({
hexpand: true,
hpack: "center",
}),
}),
],
}),
endWidget: Widget.Box({
hpack: "end",
children: [
Widget.Box({
className: "module",
css: "margin-right: 48px",
visible: mpris.activePlayer.bind().as((x) => !!x),
children: mpris.activePlayer
.bind()
.as((player) => (player ? [mpris.LinePlayer({ player })] : [])),
}),
Widget.Button({
onClicked: () => App.toggleWindow("quicksettings"),
className: "module quicksettings",
child: Widget.Box({
children: [
audio.MicrophoneIndicator({
className: "qs-item",
}),
notifications.DNDIndicator({
className: "qs-item",
}),
network.Indicator({ className: "qs-item" }),
audio.VolumeIndicator({ className: "qs-item" }),
bluetooth.Indicator({
hideIfDisabled: true,
className: "qs-item",
}),
battery.Indicator({ className: "qs-item" }),
],
}),
}).hook(App, (self, win, visible) => {
self.toggleClassName("active", win === "quicksettings" && visible);
}),
Clock({ format: "%a %d %b", className: "module bold" }),
Clock({
format: "%H:%M",
className: "module accent bold",
css: "margin-right: 0px",
}),
],
}),
}),
});

View File

@@ -1,55 +0,0 @@
import PopupWindow from "../misc/popup.js";
import * as notifications from "../modules/notifications.js";
const notificationsService = await Service.import("notifications");
const Header = () =>
Widget.Box({
hexpand: true,
css: "margin-bottom: 40px",
children: [
notifications.DNDToggle({
className: "surface p10 round",
css: `
min-width: 24px;
min-height: 24px;
`,
hpack: "start",
hexpand: true,
}),
notifications.ClearButton({
hpack: "end",
hexpand: true,
}),
],
});
export const Notifications = () =>
PopupWindow({
name: "notifications",
exclusivity: "exclusive",
transition: "slide_down",
layout: "top-center",
duration: 300,
child: Widget.Box({
vertical: true,
className: "bgcont",
css: `
min-width: 600px;
margin: 10px;
padding: 12px;
border-radius: 20px;
`,
children:
/** @type {any} */
(
notificationsService
.bind("notifications")
.as((x) =>
x.length > 0
? [Header(), notifications.List({})]
: [Header(), notifications.Placeholder({})],
)
),
}),
});

View File

@@ -1,85 +0,0 @@
import { getIcon } from "../modules/audio.js";
import brightness from "../services/brightness.js";
const audio = await Service.import("audio");
const DELAY = 1000;
function OnScreenProgress() {
const indicator = Widget.Icon({
vpack: "start",
hpack: "center",
size: 30,
css: "padding-right: 12px;",
});
const progress = Widget.Slider({
drawValue: false,
hexpand: true,
});
const revealer = Widget.Revealer({
transition: "crossfade",
css: "opacity: 0",
revealChild: true,
vpack: "center",
hpack: "center",
child: Widget.Box({
vpack: "center",
hpack: "center",
className: "osd bgcount",
css: "padding: 20px;",
children: [indicator, progress],
}),
});
// Prevent OSD to be shown when starting ags.
Utils.timeout(DELAY * 2, () => {
revealer.css = "opacity: 1";
});
let count = 0;
/**
* @param {number} value
* @param {string} icon
*/
function show(value, icon) {
revealer.reveal_child = true;
indicator.icon = icon;
progress.value = value;
count++;
Utils.timeout(DELAY, () => {
count--;
if (count === 0) revealer.reveal_child = false;
});
}
return revealer
.hook(
brightness,
() => show(brightness.screen, "display-brightness-symbolic"),
"notify::screen",
)
.hook(
audio.speaker,
() => show(audio.speaker.volume, getIcon(audio.speaker.volume * 100)),
"notify::volume",
)
.hook(
audio.speaker,
() =>
show(
audio.speaker.is_muted ? 0 : audio.speaker.volume,
audio.speaker.is_muted
? "audio-volume-muted-symbolic"
: getIcon(audio.speaker.volume * 100),
),
"notify::is-muted",
);
}
export const OSD = () =>
Widget.Window({
name: "osd",
className: "indicator",
layer: "overlay",
clickThrough: true,
anchor: ["bottom"],
child: OnScreenProgress(),
});

View File

@@ -1,160 +0,0 @@
import Gtk from "gi://Gtk?version=3.0";
import * as audio from "../modules/audio.js";
import * as brightness from "../modules/brightness.js";
import * as network from "../modules/network.js";
import * as bluetooth from "../modules/bluetooth.js";
import * as darkmode from "../modules/darkmode.js";
import * as powerprofile from "../modules/powerprofile.js";
// import * as nightmode from "../modules/nightmode.js";
import * as mpris from "../modules/mpris.js";
import * as systray from "../modules/systray.js";
import PopupWindow from "../misc/popup.js";
import { opened, Menu } from "../misc/menu.js";
const mprisService = await Service.import("mpris");
/**
* @param {Array<Gtk.Widget>} toggles
* @param {Array<Gtk.Widget>} menus
*/
const Row = (toggles = [], menus = []) =>
Widget.Box({
vertical: true,
children: [
Widget.Box({
homogeneous: true,
children: toggles,
}),
...menus,
],
});
const Header = () =>
Widget.Box({
vertical: true,
children: [
Widget.Box({
css: "margin-bottom: 12px",
children: [
Widget.Box({
className: "avatar",
css: `background-image: url("/home/${Utils.USER}/.face");`,
}),
Widget.Box({ hexpand: true }),
Widget.Button({
child: Widget.Icon("emblem-system-symbolic"),
onClicked: () => {
Utils.execAsync("gnome-control-center");
App.closeWindow("quicksettings");
},
vpack: "center",
className: "surface sys-button",
}),
Widget.Button({
child: Widget.Icon("system-log-out-symbolic"),
onClicked: () => {
opened.value = opened.value === "sleep" ? "" : "sleep";
},
vpack: "center",
css: "margin: 12px",
className: "surface sys-button",
}),
Widget.Button({
child: Widget.Icon("system-shutdown-symbolic"),
onClicked: () => {
opened.value = opened.value === "shutdown" ? "" : "shutdown";
},
vpack: "center",
className: "surface sys-button",
}),
],
}),
VerificationMenu({
name: "sleep",
icon: "system-log-out-symbolic",
title: "Hybernate?",
command: "systemctl suspend --now",
}),
VerificationMenu({
name: "shutdown",
icon: "system-shutdown-symbolic",
title: "Shutdown?",
command: "shutdown now",
}),
],
});
/** @param {{
* name: string,
* icon: string,
* title: string,
* command: string,
* }} props */
const VerificationMenu = ({ name, icon, title, command }) =>
Menu({
name,
icon: Widget.Icon(icon),
title: title,
content: [
Widget.Button({
onClicked: () => {
opened.value = "";
App.closeWindow("quicksettings");
Utils.execAsync(command);
},
child: Widget.Label({
label: "Yes",
hpack: "start",
css: "margin-left: 12px",
}),
}),
Widget.Button({
onClicked: () => {
opened.value = "";
},
child: Widget.Label({
label: "No",
hpack: "start",
css: "margin-left: 12px",
}),
}),
],
});
export const Quicksettings = () =>
PopupWindow({
name: "quicksettings",
exclusivity: "exclusive",
transition: "slide_down",
layout: "top-right",
duration: 300,
child: Widget.Box({
vertical: true,
className: "bgcont qs-container",
children: [
Header(),
Row(
[audio.Volume({ type: "speaker" })],
[audio.SinkSelector({}), audio.AppMixer({})],
),
brightness.Brightness({}),
Row(
[network.Toggle({}), bluetooth.Toggle({})],
[network.Selection({}), bluetooth.Selection({})],
),
Widget.Box({
homogeneous: true,
children: [darkmode.Toggle(), audio.MuteToggle({})],
}),
Row(
[systray.Toggle({}), powerprofile.Toggle({})],
[systray.Selection({}), powerprofile.Selection({})],
),
Widget.Box({
children: mpris.activePlayer
.bind()
.as((player) => player ? [mpris.MprisPlayer({ player })] : []),
}),
],
}),
});

View File

@@ -1,166 +0,0 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
// -- Styling --
// min-height for diameter
// min-width for trough stroke
// padding for space between trough and progress
// margin for space between widget and parent
// background-color for trough color
// color for progress color
// -- Usage --
// font size for progress value (0-100px) (hacky i know, but i want animations)
export const AnimatedCircProg = ({
initFrom = 0,
initTo = 0,
initAnimTime = 2900,
initAnimPoints = 1,
extraSetup = () => {},
...rest
}) =>
Widget.DrawingArea({
...rest,
css: `${
initFrom !== initTo
? `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`
: ""
}`,
setup: (area) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property(
"min-height",
Gtk.StateFlags.NORMAL,
);
const height = styleContext.get_property(
"min-height",
Gtk.StateFlags.NORMAL,
);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(
Gtk.StateFlags.NORMAL,
).bottom;
area.set_size_request(
width + marginLeft + marginRight,
height + marginTop + marginBottom,
);
area.connect(
"draw",
Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property(
"min-height",
Gtk.StateFlags.NORMAL,
);
const height = styleContext.get_property(
"min-height",
Gtk.StateFlags.NORMAL,
);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(
Gtk.StateFlags.NORMAL,
).left;
const marginRight = styleContext.get_margin(
Gtk.StateFlags.NORMAL,
).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(
Gtk.StateFlags.NORMAL,
).bottom;
area.set_size_request(
width + marginLeft + marginRight,
height + marginTop + marginBottom,
);
const progressValue =
styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) /
100.0;
const bg_stroke = styleContext.get_property(
"min-width",
Gtk.StateFlags.NORMAL,
);
const fg_stroke = bg_stroke - padding;
const radius =
Math.min(width, height) / 2.0 -
Math.max(bg_stroke, fg_stroke) / 2.0;
const center_x = width / 2.0 + marginLeft;
const center_y = height / 2.0 + marginTop;
const start_angle = -Math.PI / 2.0;
const end_angle = start_angle + 2 * Math.PI * progressValue;
const start_x = center_x + Math.cos(start_angle) * radius;
const start_y = center_y + Math.sin(start_angle) * radius;
const end_x = center_x + Math.cos(end_angle) * radius;
const end_y = center_y + Math.sin(end_angle) * radius;
// Draw background
const background_color = styleContext.get_property(
"background-color",
Gtk.StateFlags.NORMAL,
);
cr.setSourceRGBA(
background_color.red,
background_color.green,
background_color.blue,
background_color.alpha,
);
cr.arc(center_x, center_y, radius, 0, 2 * Math.PI);
cr.setLineWidth(bg_stroke);
cr.stroke();
if (progressValue == 0) return;
// Draw progress
const color = styleContext.get_property(
"color",
Gtk.StateFlags.NORMAL,
);
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
cr.arc(center_x, center_y, radius, start_angle, end_angle);
cr.setLineWidth(fg_stroke);
cr.stroke();
// Draw rounded ends for progress arcs
cr.setLineWidth(0);
cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
}),
);
// Init animation
if (initFrom != initTo) {
area.css = `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`;
Utils.timeout(
20,
() => {
area.css = `font-size: ${initTo}px;`;
},
area,
);
const transitionDistance = initTo - initFrom;
const oneStep = initAnimTime / initAnimPoints;
area.css = `
font-size: ${initFrom}px;
transition: ${oneStep}ms linear;
`;
for (let i = 0; i < initAnimPoints; i++) {
Utils.timeout(Math.max(10, i * oneStep), () => {
if (!area) return;
area.css = `${
initFrom != initTo
? "font-size: " +
(initFrom + (transitionDistance / initAnimPoints) * (i + 1)) +
"px;"
: ""
}`;
});
}
} else area.css = "font-size: 0px;";
extraSetup(area);
},
});

View File

@@ -1,186 +0,0 @@
import GObject from "gi://GObject?version=2.0";
import Gtk from "gi://Gtk?version=3.0";
export const opened = Variable("");
App.connect("window-toggled", (_, name, visible) => {
if (name === "quicksettings" && !visible)
Utils.timeout(500, () => {
opened.value = "";
});
});
/**
* @param {{
* name: string,
* activate?: false | (() => void),
* } & import("../types/widgets/button").ButtonProps} props
*/
export const Arrow = ({ name, activate, ...props }) => {
let deg = 0;
let iconOpened = false;
const icon = Widget.Icon("pan-end-symbolic").hook(opened, () => {
if (
(opened.value === name && !iconOpened) ||
(opened.value !== name && iconOpened)
) {
const step = opened.value === name ? 10 : -10;
iconOpened = !iconOpened;
for (let i = 0; i < 9; ++i) {
Utils.timeout(15 * i, () => {
deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
});
}
}
});
return Widget.Button({
child: icon,
className: "qs-icon",
onClicked: () => {
opened.value = opened.value === name ? "" : name;
if (typeof activate === "function") activate();
},
...props,
});
};
/**
* @typedef {{
* name: string,
* icon: Gtk.Widget,
* label: Gtk.Widget,
* activate: () => void
* deactivate: () => void
* activateOnArrow?: boolean
* connection: [GObject.Object, () => boolean]
* } & import("../types/widgets/box").BoxProps} ArrowToggleButtonProps
* @param {ArrowToggleButtonProps} props
*/
export const ArrowToggleButton = ({
name,
icon,
label,
activate,
deactivate,
activateOnArrow = true,
connection: [service, condition],
}) =>
Widget.Box({
className: "qs-button surface",
setup: (self) =>
self.hook(service, () => {
self.toggleClassName("accent", condition());
}),
children: [
Widget.Button({
child: Widget.Box({
hexpand: true,
children: [icon, label],
}),
onClicked: () => {
if (condition()) {
deactivate();
if (opened.value === name) opened.value = "";
} else {
activate();
}
},
}),
Arrow({ name, activate: activateOnArrow && activate }),
],
});
/**
* @typedef {{
* icon: Gtk.Widget,
* label: Gtk.Widget,
* activate: () => void
* deactivate: () => void
* connection: [GObject.Object, () => boolean]
* } & import("../types/widgets/box").BoxProps} SimpleToggleButtonProps
* @param {SimpleToggleButtonProps} props
*/
export const SimpleToggleButton = ({
icon,
label,
activate,
deactivate,
connection: [service, condition],
}) =>
Widget.Box({
className: "qs-button surface",
setup: (self) =>
self.hook(service, () => {
self.toggleClassName("accent", condition());
}),
children: [
Widget.Button({
child: Widget.Box({
hexpand: true,
children: [icon, label],
}),
onClicked: () => {
if (condition()) {
deactivate();
} else {
activate();
}
},
}),
],
});
/**
* @typedef {{
* name: string,
* icon: Gtk.Widget,
* title: string,
* content: Gtk.Widget[],
* } & import("../types/widgets/revealer").RevealerProps} MenuProps
* @param {MenuProps} props
*/
export const Menu = ({ name, icon, title, content, ...props }) =>
Widget.Revealer({
transition: "slide_down",
reveal_child: opened.bind().as((v) => v === name),
child: Widget.Box({
className: "qs-submenu surface",
vertical: true,
children: [
Widget.Box({
className: "qs-sub-title accent",
children: [
icon,
Widget.Label({
className: "bold f16",
truncate: "end",
label: title,
}),
],
}),
Widget.Box({
vertical: true,
className: "qs-sub-content",
children: content,
}),
],
}),
...props,
});
/** @param {{type?: string, command?: string} & import("../types/widgets/button").ButtonProps} props */
export const SettingsButton = ({ type, command, ...props }) =>
Widget.Button({
onClicked: () => {
Utils.execAsync(command ?? `gnome-control-center ${type}`);
App.closeWindow("quicksettings");
},
hexpand: true,
child: Widget.Box({
children: [
Widget.Icon("emblem-system-symbolic"),
Widget.Label("Settings"),
],
}),
...props,
});

View File

@@ -1,198 +0,0 @@
// Stollen from https://github.com/Aylur/dotfiles/blob/main/ags/widget/PopupWindow.ts#
/** @typedef {import('../types/widgets/window').WindowProps} WindowProps */
/** @typedef {import('../types/widgets/revealer').RevealerProps} RevealerProps */
/** @typedef {import('../types/widgets/eventbox').EventBoxProps} EventBoxProps */
/** @typedef {import('gi://Gtk?version=3.0')} Gtk */
/**
* @param {string} name
* @param {EventBoxProps}
* @returns {any}
*/
export const Padding = (
name,
{ css = "", hexpand = true, vexpand = true } = {},
) =>
// Widget.Box({});
Widget.EventBox({
hexpand,
vexpand,
can_focus: false,
child: Widget.Box({ css }),
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
});
/**
* @param {string} name
* @param {Child} child
* @param {Transition} [transition="slide_down"]
* @param {number} duration
*/
const PopupRevealer = (
name,
child,
transition = "slide_down",
duration = 500,
) =>
Widget.Box(
{ css: "padding: 1px;" },
Widget.Revealer({
transition,
child: Widget.Box({
class_name: "window-content",
child,
}),
transitionDuration: duration,
setup: (self) =>
self.hook(App, (_, wname, visible) => {
if (wname === name) self.reveal_child = visible;
}),
}),
);
/**
* @param {string} name
* @param {Child} child
* @param {Transition} [transition]
* @param {number} [duration]
* @returns {{ center: () => any; top: () => any; "top-right": () => any; "top-center": () => any; "top-left": () => any; "bottom-left": () => any; "bottom-center": () => any; "bottom-right": () => any; }}
*/
const Layout = (name, child, transition, duration) => ({
center: () =>
Widget.CenterBox(
{},
Padding(name),
Widget.CenterBox(
{ vertical: true },
Padding(name),
PopupRevealer(name, child, transition, duration),
Padding(name),
),
Padding(name),
),
top: () =>
Widget.CenterBox(
{},
Padding(name),
Widget.Box(
{ vertical: true },
PopupRevealer(name, child, transition, duration),
Padding(name),
),
Padding(name),
),
"top-right": () =>
Widget.Box(
{},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
PopupRevealer(name, child, transition, duration),
Padding(name),
),
),
"top-center": () =>
Widget.Box(
{},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
PopupRevealer(name, child, transition, duration),
Padding(name),
),
Padding(name),
),
"top-left": () =>
Widget.Box(
{},
Widget.Box(
{
hexpand: false,
vertical: true,
},
PopupRevealer(name, child, transition, duration),
Padding(name),
),
Padding(name),
),
"bottom-left": () =>
Widget.Box(
{},
Widget.Box(
{
hexpand: false,
vertical: true,
},
Padding(name),
PopupRevealer(name, child, transition, duration),
),
Padding(name),
),
"bottom-center": () =>
Widget.Box(
{},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
Padding(name),
PopupRevealer(name, child, transition, duration),
),
Padding(name),
),
"bottom-right": () =>
Widget.Box(
{},
Padding(name),
Widget.Box(
{
hexpand: false,
vertical: true,
},
Padding(name),
PopupRevealer(name, child, transition, duration),
),
),
});
/** @typedef {RevealerProps["transition"]} Transition */
/** @typedef {WindowProps["child"]} Child */
/**
* @typedef {Omit<WindowProps, "name"> & {
* name: string
* layout?: keyof ReturnType<typeof Layout>
* transition?: Transition,
* duration?: number
* }} PopupWindowProps
* @param {PopupWindowProps} props
*/
export default ({
name,
child,
layout = "center",
transition,
exclusivity = "ignore",
duration,
...props
}) =>
Widget.Window({
name,
class_names: [name, "popup-window"],
setup: (w) => w.keybind("Escape", () => App.closeWindow(name)),
visible: false,
keymode: "on-demand",
exclusivity,
layer: "top",
// anchor: ["top", "right"],
anchor: ["top", "bottom", "right", "left"],
child: Layout(name, child, transition, duration)[layout](),
...props,
});

View File

@@ -1,32 +0,0 @@
import GLib from "gi://GLib?version=2.0";
/**
* @param {string | null | undefined} name
* @param {string | null | undefined} [fallback]
*/
export function icon(name, fallback) {
if (!name) return fallback || "";
if (typeof name !== "string") return name;
const sub = substitutes[name];
if (sub && Utils.lookUpIcon(sub)) return sub;
if (Utils.lookUpIcon(name)) return name;
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
return fallback || "";
}
export const substitutes = {
"transmission-gtk": "transmission",
"blueberry.py": "blueberry",
Caprine: "facebook-messenger",
"com.raggesilver.BlackBox-symbolic": "terminal-symbolic",
"org.wezfurlong.wezterm-symbolic": "terminal-symbolic",
"audio-headset-bluetooth": "audio-headphones-symbolic",
"audio-card-analog-usb": "audio-speakers-symbolic",
"audio-card-analog-pci": "audio-card-symbolic",
"preferences-system": "emblem-system-symbolic",
"com.github.Aylur.ags-symbolic": "controls-symbolic",
"com.github.Aylur.ags": "controls-symbolic",
};

View File

@@ -1,228 +0,0 @@
import { icon } from "../misc/utils.js";
import {
Arrow,
Menu,
SettingsButton,
SimpleToggleButton,
} from "../misc/menu.js";
const audio = await Service.import("audio");
const volumeIcons = /** @type {const} */ ([
[101, "overamplified"],
[67, "high"],
[34, "medium"],
[1, "low"],
[0, "muted"],
]);
/** @param {number} volume */
export const getIcon = (volume) => {
const icon = volumeIcons.find(([threshold]) => threshold <= volume)?.[1];
return `audio-volume-${icon}-symbolic`;
};
/** @param {{type?: "speaker" | "microphone"} & import("../types/widgets/icon.js").IconProps} props */
export const VolumeIndicator = ({ type = "speaker", ...props }) =>
Widget.Icon(props).hook(audio, (self) => {
if (audio[type].is_muted) {
self.icon = "audio-volume-muted-symbolic";
self.tooltip_text = "Muted";
return;
}
const vol = audio[type].volume * 100;
self.icon = getIcon(vol);
self.tooltip_text = `Volume: ${Math.floor(vol)}%`;
});
/** @param {import("../types/widgets/icon.js").IconProps} props */
export const MicrophoneIndicator = (props) =>
Widget.Icon(props).hook(audio, (self) => {
self.visible = audio.microphone.is_muted || audio.recorders.length > 0;
if (audio.microphone.is_muted) self.icon = "microphone-disabled-symbolic";
else if (audio.recorders.length > 0)
self.icon = "microphone-sensitivity-high-symbolic";
});
/** @param {{type?: "speaker" | "microphone"} & import("../types/widgets/slider.js").SliderProps} props */
const VolumeSlider = ({ type = "speaker", ...props }) =>
Widget.Slider({
hexpand: true,
drawValue: false,
onChange: ({ value, dragging }) => {
if (dragging) {
audio[type].volume = value;
audio[type].is_muted = false;
}
},
value: audio[type].bind("volume"),
css: audio[type].bind("is_muted").as((x) => `opacity: ${x ? 0.7 : 1}`),
...props,
});
/** @param {{type?: "speaker" | "microphone"} & import("../types/widgets/box.js").BoxProps} props */
export const Volume = ({ type = "speaker", ...props }) =>
Widget.Box({
className: "qs-slider",
children: [
Widget.Button({
onClicked: () => {
audio[type].is_muted = !audio[type].is_muted;
},
child: VolumeIndicator({ type }),
}),
VolumeSlider({ type }),
Widget.Label({
label: audio[type].bind("volume").as(
(vol) =>
`${Math.floor(vol * 100)
.toString()
.padStart(3)}%`,
),
}),
Widget.Box({
vpack: "center",
child: Arrow({ name: "sink-selector" }),
}),
Widget.Box({
vpack: "center",
child: Arrow({ name: "app-mixer" }),
visible: audio.bind("apps").as((a) => a.length > 0),
}),
],
...props,
});
/** @param {Partial<import("../misc/menu.js").SimpleToggleButtonProps>} props */
export const MuteToggle = ({ ...props } = {}) =>
SimpleToggleButton({
icon: Widget.Icon({
className: "qs-icon",
icon: audio.microphone
.bind("is_muted")
.as((x) =>
x
? "microphone-disabled-symbolic"
: "microphone-sensitivity-high-symbolic",
),
}),
label: Widget.Label({
label: audio.microphone
.bind("is_muted")
.as((x) => (x ? "Unmute" : "Mute")),
}),
activate: () => {
audio.microphone.is_muted = true;
},
deactivate: () => {
audio.microphone.is_muted = false;
},
connection: [audio.microphone, () => audio.microphone.is_muted || false],
...props,
});
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const SinkSelector = (props) =>
Menu({
name: "sink-selector",
icon: Widget.Icon("audio-headphones-symbolic"),
title: "Sink Selector",
content: [
Widget.Box({
className: "qs-sub-sub-content",
vertical: true,
children: audio.bind("speakers").as((a) => a.map(SinkItem)),
}),
Widget.Separator({ className: "accent" }),
SettingsButton({ type: "sound" }),
],
...props,
});
/** @param {import("../types/service/audio.js").Stream} stream */
const SinkItem = (stream) =>
Widget.Button({
hexpand: true,
onClicked: () => {
audio.speaker = stream;
},
child: Widget.Box({
children: [
Widget.Icon({
icon: icon(stream.icon_name, "audio-x-generic-symbolic"),
tooltip_text: stream.icon_name || "",
}),
Widget.Label({
label: (stream.description || "").split(" ").slice(0, 4).join(" "),
}),
Widget.Icon({
icon: "object-select-symbolic",
hexpand: true,
hpack: "end",
visible: audio.speaker.bind("stream").as((s) => s === stream.stream),
}),
],
}),
});
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const AppMixer = (props) =>
Menu({
name: "app-mixer",
icon: Widget.Icon("audio-volume-high-symbolic"),
title: "App Mixer",
content: [
Widget.Box({
vertical: true,
className: "qs-sub-sub-content",
children: audio.bind("apps").as((a) => a.map(MixerItem)),
}),
Widget.Separator({ className: "accent" }),
SettingsButton({ type: "sound" }),
],
...props,
});
/** @param {import("../types/service/audio.js").Stream} stream */
const MixerItem = (stream) =>
Widget.Box({
hexpand: true,
children: [
Widget.Icon({
tooltipText: stream.bind("name").as((n) => n || ""),
icon: stream
.bind("name")
.as((n) =>
n && Utils.lookUpIcon(n) ? n : "audio-x-generic-symbolic",
),
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
xalign: 0,
truncate: "end",
max_width_chars: 28,
label: stream.bind("description").as((d) => d || ""),
}),
Widget.Slider({
hexpand: true,
draw_value: false,
value: stream.bind("volume"),
onChange: ({ value }) => {
stream.volume = value;
},
}),
],
}),
Widget.Label({
css: "padding: 12px",
label: stream.bind("volume").as(
(x) =>
`${Math.floor(x * 100)
.toString()
.padStart(3)}%`,
),
}),
],
});

View File

@@ -1,28 +0,0 @@
const battery = await Service.import("battery");
/** @param {import("../types/widgets/box").BoxProps} props */
export const Indicator = ({ ...props }) =>
Widget.Box({
children: [
Widget.Icon({
icon: battery.bind("icon_name"),
className: Utils.merge(
[
battery.bind("charging"),
battery.bind("charged"),
battery.bind("percent"),
],
(charging, charged, percent) => {
if (charging || charged) return "green";
if (percent < 30) return "red";
return "";
},
),
}),
Widget.Label({
label: battery.bind("percent").as((x) => `${x}%`),
}),
],
visible: battery.bind("available"),
...props,
});

View File

@@ -1,94 +0,0 @@
import { ArrowToggleButton, Menu, SettingsButton } from "../misc/menu.js";
const bluetooth = await Service.import("bluetooth");
const connected = Utils.merge(
[bluetooth.bind("enabled"), bluetooth.bind("connected_devices")],
(enabled, devices) => enabled && devices.length > 0,
);
/** @param {{hideIfDisabled?: boolean} & import("../types/widgets/icon.js").IconProps} props */
export const Indicator = ({ hideIfDisabled = false, ...props } = {}) =>
Widget.Icon({
icon: connected.as(
(x) => `bluetooth-${x ? "active" : "disabled"}-symbolic`,
),
visible: connected.as((x) => x || !hideIfDisabled),
...props,
});
/** @param {import("../types/widgets/label.js").LabelProps} props */
export const ConnectedLabel = (props) =>
Widget.Label(props).hook(bluetooth, (self) => {
if (!bluetooth.enabled) self.label = "Disabled";
if (bluetooth.connected_devices.length === 0) self.label = "Disconnected";
else if (bluetooth.connected_devices.length === 1)
self.label = bluetooth.connected_devices[0].alias;
else self.label = `${bluetooth.connected_devices.length} Connected`;
});
/** @param {Partial<import("../misc/menu.js").ArrowToggleButtonProps>} props */
export const Toggle = (props) =>
ArrowToggleButton({
name: "bluetooth",
icon: Indicator({ className: "qs-icon" }),
label: ConnectedLabel({
max_width_chars: 20,
}),
activate: () => {
bluetooth.enabled = true;
},
deactivate: () => {
bluetooth.enabled = false;
},
connection: [bluetooth, () => bluetooth.enabled],
...props,
});
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const Selection = (props) =>
Menu({
name: "bluetooth",
icon: Indicator({}),
title: "Bluetooth",
content: [
Widget.Box({
vertical: true,
className: "qs-sub-sub-content",
children: bluetooth.bind("devices").as((x) => x.map(DeviceItem)),
}),
Widget.Separator({ className: "accent" }),
SettingsButton({ command: "blueberry" }),
],
...props,
});
/** @param {import("../types/service/bluetooth.js").BluetoothDevice} device */
const DeviceItem = (device) =>
Widget.Box({
children: [
Widget.Icon(`${device.icon_name}-symbolic`),
Widget.Label(device.name),
Widget.Box({ hexpand: true }),
Widget.Label({
label: `${device.battery_percentage}%`,
css: "padding-right: 24px;",
visible: device.bind("battery_percentage").as((x) => x > 0),
}),
Widget.Spinner({
active: device.bind("connecting"),
visible: device.bind("connecting"),
}),
Widget.Switch({
active: device.bind("connected"),
visible: device.bind("connecting").as((p) => !p),
setup: (self) =>
// TODO: If connecting to the device failed, reset back the switch to `active: false`.
self.connect("state_set", () => {
device.setConnection(self.active);
return true;
}),
}),
],
});

View File

@@ -1,38 +0,0 @@
import brightness from "../services/brightness.js";
/** @param {import("../types/widgets/slider.js").SliderProps} props */
const BrightnessSlider = (props) =>
Widget.Slider({
drawValue: false,
hexpand: true,
value: brightness.bind("screen"),
onChange: ({ value }) => {
brightness.screen = value;
},
...props,
});
/** @param {import("../types/widgets/box.js").BoxProps} props */
export const Brightness = (props) =>
Widget.Box({
className: "qs-slider",
children: [
Widget.Icon({
vpack: "center",
icon: "display-brightness-symbolic",
tooltipText: brightness
.bind("screen")
.as((x) => `Screen Brightness: ${Math.floor(x * 100)}%`),
}),
BrightnessSlider({}),
Widget.Label({
label: brightness.bind("screen").as(
(x) =>
`${Math.floor(x * 100)
.toString()
.padStart(3)}%`,
),
}),
],
...props,
});

View File

@@ -1,14 +0,0 @@
import GLib from "gi://GLib";
export const clock = Variable(GLib.DateTime.new_now_local(), {
poll: [1000, () => GLib.DateTime.new_now_local()],
});
/**
* @param {{format?: string} & import("../types/widgets/label").LabelProps} props
*/
export const Clock = ({ format = "%a %d %b %H:%M ", ...props } = {}) =>
Widget.Label({
...props,
label: Utils.derive([clock], (c) => c.format(format) || "").bind(),
});

View File

@@ -1,57 +0,0 @@
import Gio from "gi://Gio";
import { SimpleToggleButton } from "../misc/menu.js";
const interfaceXml = `
<node name="/nl/whynothugo/darkman">
<interface name="nl.whynothugo.darkman">
<signal name="ModeChanged">
<arg name="NewMode" type="s" />
</signal>
<property name="Mode" type="s" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true" />
</property>
</interface>
</node>
`;
const Darkman = Gio.DBusProxy.makeProxyWrapper(interfaceXml);
const theme = Variable(/** @type {"light" | "dark"} */ ("light"));
/** @param {Partial<import("../misc/menu.js").SimpleToggleButtonProps>} props */
export const Toggle = ({ ...props } = {}) =>
SimpleToggleButton({
icon: Widget.Icon({
className: "qs-icon",
icon: theme
.bind()
.as((x) =>
x === "light"
? "weather-clear-symbolic"
: "weather-clear-night-symbolic",
),
}),
label: Widget.Label({
label: theme.bind().as((x) => (x === "light" ? "Light" : "Dark")),
}),
activate: () => theme.setValue("dark"),
deactivate: () => theme.setValue("light"),
connection: [theme, () => theme.value === "dark"],
...props,
});
function init() {
const darkman = Darkman(
Gio.DBus.session,
"nl.whynothugo.darkman",
"/nl/whynothugo/darkman",
);
theme.value = darkman.Mode;
theme.connect("changed", () => {
darkman.Mode = theme.value;
});
darkman.connectSignal("ModeChanged", (_proxy, _senderName, nTheme) => {
theme.value = nTheme[0];
});
}
init();

View File

@@ -1,290 +0,0 @@
import { AnimatedCircProg } from "../misc/circular-progress.js";
const mpris = await Service.import("mpris");
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/icon").IconProps} props */
const PlayerIcon = ({ player, ...props }) =>
Widget.Icon({
size: 24,
hpack: "start",
vpack: "start",
tooltipText: player.identity || "",
icon: player.bind("entry").transform((entry) => {
const name = `${entry}-symbolic`;
return Utils.lookUpIcon(name)
? name
: Utils.lookUpIcon(entry)
? entry
: "audio-x-generic-symbolic";
}),
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/label").LabelProps} props */
const TitleLabel = ({ player, ...props }) =>
Widget.Label({
wrap: true,
truncate: "end",
hpack: "start",
label: player.bind("track_title"),
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/label").LabelProps} props */
const ArtistLabel = ({ player, ...props }) =>
Widget.Label({
wrap: true,
truncate: "end",
hpack: "start",
label: player.bind("track_artists").transform((x) => x.map(y => y.replace(/- Topic$/, "")).join(", ")),
...props,
});
/** @param {{
* player: import("types/service/mpris").MprisPlayer,
* iconProps?: import("types/widgets/icon").IconProps,
* } & import("types/widgets/button").ButtonProps} props */
export const PlayPause = ({ player, iconProps = {}, ...props }) =>
Widget.Button({
child: Widget.Icon({
icon: player.bind("play_back_status").as(
(x) =>
({
Playing: "media-playback-pause-symbolic",
Paused: "media-playback-start-symbolic",
Stopped: "media-playback-start-symbolic",
})[x],
),
...iconProps,
}),
onClicked: () => player.playPause(),
visible: player.bind("can_play"),
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */
const PreviousButton = ({ player, ...props }) =>
Widget.Button({
child: Widget.Icon({ icon: "media-skip-backward-symbolic" }),
onClicked: () => player.previous(),
visible: player.bind("can_go_prev"),
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/button").ButtonProps} props */
const NextButton = ({ player, ...props }) =>
Widget.Button({
child: Widget.Icon({ icon: "media-skip-forward-symbolic" }),
onClicked: () => player.next(),
visible: player.bind("can_go_next"),
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/slider").SliderProps} props */
const PositionSlider = ({ player, ...props }) =>
Widget.Slider({
className: "mpris-position-slider",
drawValue: false,
onChange: ({ value }) => {
player.position = value * player.length;
},
visible: player.bind("length").as((l) => l > 0),
setup: (self) => {
function update() {
const value = player.position / player.length;
self.value = value > 0 ? value : 0;
}
self.hook(player, update);
self.hook(player, update, "position");
self.poll(1000, update);
},
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/box").BoxProps} props */
const PositionCircle = ({ player, child, ...props }) =>
Widget.Box({
child: Widget.Overlay({
child: AnimatedCircProg({
css: `
min-width: 0.136rem;
min-height: 1.636rem;
padding: 0rem;
`,
vpack: "center",
hpack: "center",
className: "accent-rev",
extraSetup: (self) => {
function update() {
const value = player.position / player.length;
self.css = `
font-size: ${Math.max(value * 100, 0)}px;
min-width: 0.136rem;
min-height: 1.636rem;
padding: 0rem;
`;
}
self.hook(player, update);
self.hook(player, update, "position");
self.poll(1000, update);
},
}),
overlays: [child],
}),
...props,
});
/** @param {{player: import("types/service/mpris").MprisPlayer} & import("../types/widgets/box").BoxProps} props */
export const LinePlayer = ({ player, ...props }) =>
Widget.Box({
visible: player.bind("play_back_status").as((x) => x !== "Stopped"),
children: [
PositionCircle({
player,
child: PlayPause({ player, iconProps: { size: 10 } }),
css: "margin-right: 12px;",
}),
Widget.Label({
label: Utils.merge(
[player.bind("track_title"), player.bind("track_artists")],
(title, artists) => `${title} - ${artists.map(x => x.replace(/- Topic$/, "")).join(", ")}`,
),
maxWidthChars: 30,
wrap: true,
truncate: "end",
hpack: "start",
}),
],
...props,
});
export const activePlayer = Variable(mpris.players[0]);
mpris.connect("player-added", (_, bus) => {
mpris.getPlayer(bus)?.connect("changed", (player) => {
if (player?.play_back_status !== "Stopped") {
activePlayer.value = player || mpris.players[0];
} else {
activePlayer.value = mpris.players[0];
}
});
});
mpris.connect("player-closed", (_, bus) => {
if (activePlayer.value.bus_name === bus)
activePlayer.value = mpris.players[0];
});
/** @param {{player?: import("types/service/mpris").MprisPlayer | null} & import("../types/widgets/box").BoxProps} props */
export const MprisPlayer = ({ player, ...props }) => {
if (!player) return Widget.Box({ visible: false });
const colors = getMaterialColors(player);
return Widget.Box({
visible: player.bind("play_back_status").as((x) => x !== "Stopped"),
className: `mpris-cover-art ${props.className}`,
css: Utils.merge(
[colors.bind(), player.bind("cover_path")],
(colors, cover) => `
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.4) 30%, ${colors.primary}), url("${cover}"); \
color: ${colors.onBackground};
`,
),
vexpand: false,
children: [
Widget.CenterBox({
vertical: true,
hexpand: true,
startWidget: Widget.Box({
vertical: true,
vexpand: true,
css: colors.bind().as((x) => `color: ${x.onBackground}`),
child: PlayerIcon({ player }),
}),
centerWidget: Widget.Box({
hexpand: true,
children: [
Widget.Box({
css: colors.bind().as(
(x) => `
color: ${x.onBackground};
margin-right: 12px;
`,
),
vertical: true,
vpack: "center",
hexpand: true,
children: [
TitleLabel({
player,
css: "font-weight: 600; font-size: 19px;",
maxWidthChars: 33,
}),
ArtistLabel({
player,
css: "font-weight: 400; font-size: 17px;",
maxWidthChars: 40,
}),
],
}),
Widget.Box({
children: [
PlayPause({
player,
hpack: "end",
className: "mpris-play",
css: colors.bind().as(
(x) => `
background-color: ${x.primary};
color: ${x.onPrimary};
`,
),
}),
],
}),
],
}),
endWidget: Widget.Box({
css: colors.bind().as((x) => `color: ${x.onBackground}`),
vpack: "end",
children: [
Widget.Box({
hexpand: true,
children: [
PreviousButton({ player, css: "margin-left: 16px;" }),
PositionSlider({ player, hexpand: true }),
NextButton({ player, css: "margin-right: 16px;" }),
],
}),
],
}),
}),
],
...props,
});
};
// TODO: Move ret inside getMaterialColors to support multiple players.
const ret = Variable({
primary: "#222222",
onPrimary: "#ffffff",
background: "#222222",
onBackground: "#ffffff",
});
/** @param {import("types/service/mpris").MprisPlayer} player */
export const getMaterialColors = (player) => {
// TODO: Move that to a hook to allow graceful disconnections
player.connect("changed", (player) => {
const cover = player.cover_path;
// TODO: Wait for the cover to be downloaded, currently we hope that it's ready in <100ms
Utils.timeout(100, () => {
Utils.execAsync(["covercolors", cover])
.then((colors) => {
const col = JSON.parse(colors);
if (!col) return;
ret.setValue(col);
})
.catch(print);
});
});
return ret;
};

View File

@@ -1,114 +0,0 @@
import { ArrowToggleButton, Menu, SettingsButton } from "../misc/menu.js";
const network = await Service.import("network");
/** @param {import("../types/widgets/icon.js").IconProps} props*/
export const Indicator = (props) =>
Widget.Icon(props).hook(network, (self) => {
self.icon =
network[network.primary || "wifi"].icon_name ??
"network-wireless-offline-symbolic";
});
/** @param {import("../types/widgets/label.js").LabelProps} props */
export const SSIDLabel = (props) =>
Widget.Label({
truncate: "end",
...props,
}).hook(network, (self) => {
if (network.primary === "wifi")
self.label = network.wifi.ssid || "Not Connected";
else
self.label =
network.wired.internet !== "disconnected" ? "Wired" : "Not Connected";
});
/** @param {Partial<import("../misc/menu.js").ArrowToggleButtonProps>} props */
export const Toggle = (props) =>
ArrowToggleButton({
name: "network",
icon: Indicator({ className: "qs-icon" }),
label: SSIDLabel({
max_width_chars: 20,
}),
activate: () => {
network.wifi.enabled = true;
network.wifi.scan();
},
deactivate: () => {
network.wifi.enabled = false;
},
connection: [network.wifi, () => network.wifi.enabled],
...props,
});
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const Selection = (props) =>
Menu({
name: "network",
icon: Indicator({}),
title: "Network Selection",
content: [
Wired(),
Widget.Box({
vertical: true,
className: "qs-sub-sub-content",
children: network.wifi.bind("access_points").as((x) =>
x
.sort((a, b) => b.strength - a.strength)
.reduce((acc, x) => {
if (!acc.find((y) => y.ssid === x.ssid)) acc.push(x);
return acc;
}, [])
.slice(0, 10)
.map(WifiItem),
),
}),
Widget.Separator({ className: "accent" }),
SettingsButton({ type: "wifi" }),
],
...props,
});
const Wired = () =>
Widget.Button({
// onClicked:
// visible: network.wired.bind("state").as(x => (console.log(x), true)),
child: Widget.Box({
children: [
Widget.Icon({ icon: network.wired.bind("icon_name") }),
Widget.Label("Wired"),
Widget.Icon({
icon: "object-select-symbolic",
hexpand: true,
hpack: "end",
visible: network.bind("primary").as((x) => x === "wired"),
}),
],
}),
});
/** @param {import("../types/service/network.js").Wifi["access_points"][0]} wifi */
const WifiItem = (wifi) =>
Widget.Button({
onClicked: () => Utils.execAsync(`nmcli device wifi connect ${wifi.bssid}`),
child: Widget.Box({
children: [
Widget.Icon(wifi.iconName),
Widget.Label({
truncate: "end",
max_width_chars: 28,
label: wifi.ssid || "",
}),
Widget.Icon({
icon: "object-select-symbolic",
hexpand: true,
hpack: "end",
setup: (self) =>
Utils.idle(() => {
if (!self.is_destroyed) self.visible = wifi.active;
}),
}),
],
}),
});

View File

@@ -1,270 +0,0 @@
const notifications = await Service.import("notifications");
notifications.popupTimeout = 2000; //in seconds
notifications.forceTimeout = true; //force all notifications to timeout
/** @param {import("../types/widgets/icon").IconProps} props */
export const DNDIndicator = (props) =>
Widget.Icon({
visible: notifications.bind("dnd"),
icon: "notifications-disabled-symbolic",
...props,
});
/** @param {import("../types/widgets/button").ButtonProps} props */
export const DNDToggle = (props) =>
Widget.Button({
onClicked: () => {
notifications.dnd = !notifications.dnd;
},
child: Widget.Icon({
icon: notifications
.bind("dnd")
.as((x) =>
x
? "preferences-system-notifications-symbolic"
: "notifications-disabled-symbolic",
),
}),
className: notifications.bind("dnd").as((x) => (x ? "on" : "")),
...props,
});
/** @param {import("../types/widgets/box").BoxProps} props */
export const Indicator = ({ ...props }) =>
Widget.Box({
visible: Utils.merge(
[
notifications.bind("notifications").as((x) => x.length > 0),
notifications.bind("dnd"),
],
(hasNotif, dnd) => hasNotif && !dnd,
),
children: [
Widget.Icon({ icon: "preferences-system-notifications-symbolic" }),
Widget.Revealer({
transition: "slide_right",
revealChild: notifications.bind("popups").as((x) => x.length > 0),
child: Widget.Label({
use_markup: true,
truncate: "end",
wrap: false,
label: notifications.bind("popups").as((x) => {
const notif = x[x.length - 1];
// Keep the text of the old notif for the fade out animation.
if (!notif) return old_notif;
const summary = notif.summary.substring(0, 18).trim();
const body = notif.body.substring(0, 45).trim();
old_notif = `${summary}: ${body}`.replaceAll("\n", " ");
return old_notif;
}),
}),
}),
],
...props,
});
let old_notif = "";
/** @param {import("../types/service/notifications").Notification} param */
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Widget.Box({
vpack: "start",
hexpand: false,
className: "r20",
css: `
background-image: url("${image}");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
min-width: 78px;
min-height: 78px;
margin-right: 12px;
`,
});
}
let icon = "dialog-information-symbolic";
if (Utils.lookUpIcon(app_icon)) icon = app_icon;
if (Utils.lookUpIcon(app_entry || "")) icon = app_entry || "";
return Widget.Box({
vpack: "start",
hexpand: false,
className: "r20",
css: `
min-width: 78px;
min-height: 78px;
margin-right: 12px;
`,
child: Widget.Icon({
icon,
size: 58,
hpack: "center",
hexpand: true,
vpack: "center",
vexpand: true,
}),
});
};
import GLib from "gi://GLib";
/** @param {number} time */
const time = (time, format = "%H:%M") =>
GLib.DateTime.new_from_unix_local(time).format(format);
/** @param {import("../types/service/notifications").Notification} notification */
export const Notification = (notification) => {
const content = Widget.Box({
children: [
NotificationIcon(notification),
Widget.Box({
hexpand: true,
vertical: true,
children: [
Widget.Box({
children: [
Widget.Label({
css: `
font-size: 1.1em;
margin-right: 12pt;
`,
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: notification.summary.trim(),
use_markup: true,
}),
Widget.Label({
vpack: "start",
label: time(notification.time),
}),
Widget.Button({
css: `
margin-left: 6pt;
min-width: 1.2em;
min-height: 1.2em;
`,
vpack: "start",
child: Widget.Icon("window-close-symbolic"),
on_clicked: notification.close,
}),
],
}),
Widget.Label({
css: "font-size: .9em;",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
label: notification.body.trim(),
max_width_chars: 24,
wrap: true,
}),
],
}),
],
});
const actionsbox =
notification.actions.length > 0
? Widget.Revealer({
transition: "slide_down",
child: Widget.EventBox({
child: Widget.Box({
class_name: "actions horizontal",
children: notification.actions.map((action) =>
Widget.Button({
className: "button",
on_clicked: () => notification.invoke(action.id),
hexpand: true,
child: Widget.Label(action.label),
}),
),
}),
}),
})
: null;
const eventbox = Widget.EventBox({
vexpand: false,
on_primary_click: notification.dismiss,
on_hover() {
if (actionsbox) actionsbox.reveal_child = true;
},
on_hover_lost() {
if (actionsbox) actionsbox.reveal_child = true;
notification.dismiss();
},
child: Widget.Box({
vertical: true,
children: actionsbox ? [content, actionsbox] : [content],
}),
});
return Widget.Box({
className: "surface r20 p10",
css: "margin: 8px 0;",
child: eventbox,
});
};
/** @param {import("../types/widgets/scrollable").ScrollableProps} props */
export const List = (props) =>
Widget.Scrollable({
vscroll: "automatic",
hscroll: "never",
css: "min-height: 500px;",
child: Widget.Box({
vertical: true,
children: notifications
.bind("notifications")
.as((x) => x.map(Notification)),
}),
...props,
});
/** @param {import("../types/widgets/box").BoxProps} props */
export const Placeholder = (props) =>
Widget.Box({
vertical: true,
vpack: "center",
hpack: "center",
children: [
Widget.Icon({
icon: "notifications-disabled-symbolic",
size: 75,
css: "margin: 50px 0",
}),
Widget.Label({
label: "Your inbox is empty",
css: "margin-bottom: 150px;",
}),
],
...props,
});
/** @param {import("../types/widgets/button").ButtonProps} props */
export const ClearButton = (props) =>
Widget.Button({
className: "surface r20 p10",
onClicked: () => notifications.clear(),
sensitive: notifications.bind("notifications").as((x) => x.length > 0),
child: Widget.Box({
children: [
Widget.Label({ label: "Clear " }),
Widget.Icon({
icon: notifications
.bind("notifications")
.as((x) =>
x.length > 0 ? "user-trash-full-symbolic" : "user-trash-symbolic",
),
}),
],
}),
...props,
});

View File

@@ -1,62 +0,0 @@
import { ArrowToggleButton, Menu } from "../misc/menu.js";
const powerProfiles = await Service.import("powerprofiles");
/** @param {string} x */
const capitalize = (x) => x.charAt(0).toUpperCase() + x.slice(1);
/** @param {Partial<import("../misc/menu.js").ArrowToggleButtonProps>} props */
export const Toggle = (props) =>
ArrowToggleButton({
name: "powerprofile",
icon: Widget.Icon({
icon: powerProfiles.bind("icon_name"),
className: "qs-icon",
}),
label: Widget.Label({
label: powerProfiles.bind("active_profile").as(capitalize),
truncate: "end",
max_width_chars: 20,
}),
activate: () => {},
deactivate: () => {},
connection: [powerProfiles, () => false],
...props,
});
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const Selection = (props) =>
Menu({
name: "powerprofile",
icon: Widget.Icon({ icon: powerProfiles.bind("icon_name") }),
title: "Power profile",
// Hard coding the list of profiles since ags does not give them.
content: ["power-saver", "balanced", "performance"].map(ProfileItem),
...props,
});
/** @param {string} profile */
const ProfileItem = (profile) =>
Widget.Button({
onClicked: () => (powerProfiles.active_profile = profile),
child: Widget.Box({
children: [
Widget.Icon({
icon: `power-profile-${profile}-symbolic`,
}),
Widget.Label({
label: capitalize(profile),
truncate: "end",
maxWidthChars: 28,
}),
Widget.Icon({
icon: "object-select-symbolic",
hexpand: true,
hpack: "end",
visible: powerProfiles
.bind("active_profile")
.as((x) => x === profile),
}),
],
}),
});

View File

@@ -1,52 +0,0 @@
import { icon } from "../misc/utils.js";
import { ArrowToggleButton, Menu } from "../misc/menu.js";
const systemtray = await Service.import("systemtray");
/** @param {Partial<import("../misc/menu.js").ArrowToggleButtonProps>} props */
export const Toggle = (props) =>
ArrowToggleButton({
name: "systray",
icon: Widget.Icon({ icon: "open-menu-symbolic", className: "qs-icon" }),
label: Widget.Label("Systray"),
activate: () => {},
deactivate: () => {},
connection: [systemtray, () => systemtray.items.length > 0],
...props,
});
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const Selection = (props) =>
Menu({
name: "systray",
icon: Widget.Icon("open-menu-symbolic"),
title: "Systray",
content: [
Widget.Box({
vertical: true,
className: "qs-sub-sub-content",
children: systemtray.bind("items").as((i) => i.map(SysTrayItem)),
}),
],
...props,
});
/** @param {import('../types/service/systemtray.js').TrayItem} item */
const SysTrayItem = (item) =>
Widget.Button({
css: "margin: 12px;",
child: Widget.Box({
children: [
Widget.Icon({ icon: item.bind("icon").as(icon) }),
Widget.Label({
truncate: "end",
maxWidthChars: 28,
}).hook(item, (self) => {
self.label = item.title || item.tooltip_markup;
}),
],
}),
tooltipMarkup: item.bind("tooltip_markup"),
onPrimaryClick: (_, event) => item.activate(event),
onSecondaryClick: (_, event) => item.openMenu(event),
});

View File

@@ -1,96 +0,0 @@
import Gdk from "gi://Gdk?version=3.0";
// const hyprland = await Service.import("hyprland");
//
// const display = Gdk.Display.get_default();
// /** @param {number} monitor */
// const getMonitorName = (monitor) => {
// return display.get_default_screen().get_monitor_plug_name(monitor);
// };
//
// /** @type {Record<number, boolean>} */
// const urgents = {}
//
// /** @param {{monitor: number, labels: string[]} & import("types/widgets/box").BoxProps} props */
// export const Tags = ({ monitor, labels, ...props }) => {
// const monName = getMonitorName(monitor);
// // @ts-ignore
// return Widget.Box({
// ...props,
// children: Array.from({ length: 9 }, (_, i) => i).map((i) =>
// Widget.EventBox({
// child: Widget.Label({ label: labels[i], className: "tags" }),
// attribute: i + 1,
// onPrimaryClickRelease: () => {
// hyprland.message(`dispatch workspace ${i + 1}`);
// },
// }),
// ),
// setup: (self) => {
// self.hook(hyprland, (_, address) => {
// const client = hyprland.getClient(address);
// if (!client) return;
// const ws = client.workspace.id;
// urgents[ws] = true;
// }, "urgent-window");
//
// self.hook(hyprland, () =>
// self.children.forEach((btn) => {
// const mon = hyprland.monitors.find((x) => x.name === monName);
// const ws = hyprland.workspaces.find((x) => x.id === btn.attribute);
//
// const occupied = (ws?.windows ?? 0) > 0;
// const selected = mon?.activeWorkspace?.id === btn.attribute;
// if (selected) urgents[btn.attribute] = false;
// const urgent = urgents[btn.attribute];
//
// btn.visible = occupied || selected;
// btn.class_names = [
// selected ? "accent" : "",
// urgent ? "secondary" : "",
// ];
// }),
// );
// },
// });
// };
//
// /** @param {{monitor: number } & import("types/widgets/label").LabelProps} props */
// export const Layout = ({ monitor, ...props }) => {
// const monName = getMonitorName(monitor);
// return Widget.Label({
// className: "module",
// ...props,
// }).hook(
// hyprland,
// (self) => {
// const mon = hyprland.monitors.find((x) => x.name === monName);
// const ws = hyprland.workspaces.find(
// (x) => x.id === mon?.activeWorkspace?.id,
// );
// self.label = ws?.windows ? `[${ws?.windows}]` : "";
// },
// "changed",
// );
// }
//
// /** @param {{monitor: number, fallback?: string} & import("types/widgets/label").LabelProps} props */
// export const ClientLabel = ({ monitor, fallback = "", ...props }) => {
// const monName = getMonitorName(monitor);
// return Widget.Label({
// truncate: "end",
// maxWidthChars: 25,
// className: "module",
// ...props,
// }).hook(
// hyprland,
// (self) => {
// const mon = hyprland.monitors.find((x) => x.name === monName);
// const ws = hyprland.workspaces.find(
// (x) => x.id === mon?.activeWorkspace?.id,
// );
// self.label = ws?.lastwindowtitle || fallback;
// },
// "changed",
// );
// };

View File

@@ -1,44 +0,0 @@
const screen = await Utils.execAsync(
"/bin/sh -c 'ls -w1 /sys/class/backlight | head -n 1'",
);
const max = Number(await Utils.execAsync("brightnessctl m"));
class Brightness extends Service {
static {
Service.register(this, {}, { screen: ["float", "rw"] });
}
#screen = 0;
get screen() {
return this.#screen;
}
set screen(percent) {
if (percent < 0) percent = 0;
if (percent > 1) percent = 1;
Utils.execAsync(`brightnessctl s ${percent * 100}% -q`)
.then(() => {
this.#screen = percent;
this.emit("changed");
this.notify("screen");
})
.catch(print);
}
constructor() {
super();
this.#screen = Number(Utils.exec("brightnessctl g")) / max;
const screenPath = `/sys/class/backlight/${screen}/brightness`;
Utils.monitorFile(screenPath, async (f) => {
const v = await Utils.readFileAsync(f);
this.#screen = Number(v) / max;
this.changed("screen");
});
}
}
export default new Brightness();

View File

@@ -1,232 +0,0 @@
* {
font-family: monospace;
padding: 0;
margin: 0;
}
button {
all: unset;
}
/* Colors */
.transparent {
background-color: rgba(0, 0, 0, 0.6);
}
.bgcont {
background-color: #1E1E2E;
border: 1px solid #11111B;
}
.surface {
background-color: #11111B;
color: #CDD6F4;
}
.accent {
background-color: #94e2d5;
color: #1e1e2e;
}
.accent-rev {
color: #94e2d5;
background-color: #1e1e2e;
}
.secondary {
background-color: #cba6f7;
color: #1e1e2e;
}
.red {
color: #F38BA8;
}
.green {
color: #A6E3A1;
}
/* Misc */
.bold {
font-weight: bold;
}
.f16 {
font-size: 16px;
}
.round {
border-radius: 50%;
}
.r20 {
border-radius: 20px;
}
.r100 {
border-radius: 100px;
}
.p10 {
padding: 10px;
}
separator {
border-radius: 10px;
min-height: 1px;
min-width: 1px;
}
/* Slider */
slider {
box-shadow: none;
background-color: transparent;
border: 1px solid transparent;
transition: 200ms;
min-height: 1rem;
border-radius: 100px;
}
trough {
transition: 200ms;
border: 1px;
background-color: #000;
border-radius: 100px;
}
highlight, fill {
background-color: #94e2d5;
border-radius: 100px;
transition: 200ms;
}
mark {
background-color: #F38BA8;
min-width: 1px;
min-height: 3px;
}
switch {
background-color: #1E1E2E;
border-radius: 100px;
}
switch slider {
all: unset;
transition: 200ms;
background-color: #94e2d5;
border-radius: 100px;
min-width: 1rem;
min-height: 1rem;
}
switch image {
all: unset;
color: transparent;
}
/* Bar */
.module {
padding: 1px 10px;
margin: 0 4px;
}
.tags {
min-width: 2rem;
}
.qs-item {
padding: 3px;
}
/* On Screen Display */
.osd {
border-radius: 20px;
margin-bottom: 150px;
min-width: 175px;
min-height: 30px;
}
/* Quick settings */
.qs-container {
min-width: 400px;
padding: 25px;
margin: 10px;
border-radius: 20px;
}
.qs-button {
padding: 0px;
margin: 5px;
border-radius: 20px;
min-width: 225px;
min-height: 75px;
transition: background-color 0.3s ease-in-out;
transition: color 0.3s ease-in-out;
}
.qs-icon {
padding: 12px;
}
.qs-slider > * {
margin: 5px;
}
.qs-submenu {
margin-top: 8px;
margin-bottom: 8px;
padding: 12px;
border-radius: 20px;
}
.qs-sub-title {
padding: 14px 20px;
border-radius: 100px;
margin-bottom: 8px;
}
.qs-sub-title > *:first-child {
margin-right: 14px;
}
.qs-sub-content image {
margin-right: 8px;
}
.qs-sub-content > * {
margin: 6px;
}
.qs-sub-sub-content > * {
margin: 6px 0px;
}
.avatar {
border-radius: 50%;
min-width: 70px;
min-height: 70px;
background-size: cover;
}
.sys-button {
min-width: 40px;
min-height: 40px;
border-radius: 15px;
}
.mpris-cover-art {
background-size: cover;
background-position: center;
margin: 10px 0;
min-height: 205px;
border-radius: 20px;
transition: all 0.2s;
}
.mpris-cover-art > * {
padding: 20px;
}
.mpris-play {
min-width: 60px;
min-height: 60px;
border-radius: 20px;
padding: 0;
transition: all 0.3s;
}
.mpris-position-slider {
margin: 0 10px;
}
.mpris-position-slider trough {
border-radius: 100px;
background-color: rgba(255, 255, 255, 0.6);
min-height: 4px;
}
.mpris-position-slider highlight, .mpris-position-slider progress {
background-color: #fff;
border-radius: 100px;
}
.mpris-position-slider slider {
border-radius: 100px;
min-height: 4px;
min-width: 4px;
background-color: #fff;
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022"
],
"allowJs": true,
"checkJs": true,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"typeRoots": [
"./types"
],
"skipLibCheck": true
}
}

View File

@@ -1 +0,0 @@
/nix/store/g5s50dy9za9inrw5zps05s7kh4ypnbpa-ags-1.8.2/share/com.github.Aylur.ags/types

View File

@@ -1,6 +1,7 @@
{
pkgs,
lib,
noctalia,
...
}: let
wallpaper = pkgs.writeShellScriptBin "wallpaper" ''
@@ -39,9 +40,9 @@
in {
imports = [
./rofi
./quickshell
./fcitx5.nix
./hyprlock.nix
noctalia.homeModules.default
];
home.packages = [
wallpaper
@@ -89,4 +90,127 @@ in {
button-layout = "";
};
};
programs.noctalia-shell = {
enable = true;
systemd.enable = true;
settings = {
bar = {
capsuleOpacity = 0.5;
showCapsule = false;
outerCorners = false;
widgets = {
left = [
{
id = "TaskbarGrouped";
hideUnoccupied = true;
labelMode = "none";
}
];
center = [
{
id = "NotificationHistory";
}
];
right = [
{
id = "MediaMini";
maxWidth = 250;
showArtistFirst = false;
}
{
id = "Spacer";
}
{
id = "Tray";
}
{
id = "Battery";
displayMode = "alwaysShow";
warningThreshold = 30;
}
{
id = "Volume";
}
{
id = "Bluetooth";
}
{
id = "WiFi";
}
{
id = "Spacer";
}
{
id = "Clock";
formatHorizontal = "HH:mm\\nyyyy-MM-dd";
}
];
};
};
controlCenter = {
position = "top_center";
};
notifications = {
enabled = true;
location = "bar";
lowUrgencyDuration = 3;
normalUrgencyDuration = 3;
criticalUrgencyDuration = 3;
};
osd = {
enabled = true;
location = "bottom";
};
sessionMenu = {
enableCountdown = true;
position = "top_right";
powerOptions = [
{
action = "lock";
countdownEnabled = false;
enabled = true;
}
{
action = "suspend";
countdownEnabled = false;
enabled = true;
}
{
action = "hibernate";
countdownEnabled = true;
enabled = true;
}
{
action = "reboot";
countdownEnabled = true;
enabled = true;
}
{
action = "logout";
countdownEnabled = false;
enabled = true;
}
{
action = "shutdown";
countdownEnabled = true;
enabled = true;
}
];
showHeader = true;
};
settingsVersion = 23;
setupCompleted = true;
general = {
lockOnSuspend = false;
showScreenCorners = false;
};
colorSchemes = {
predefinedScheme = "Catppuccin";
};
wallpaper.enabled = false;
dock.enabled = false;
nightLight.enabled = false;
};
};
}

View File

@@ -1 +0,0 @@
UseTabs=true

View File

@@ -1 +0,0 @@
/run/user/1000/quickshell/vfs/61f729b565ae1eaaa7b71145d10755fe/.qmlls.ini

View File

@@ -1,10 +0,0 @@
{
programs.quickshell = {
enable = true;
systemd.enable = true;
configs = {
default = ./.;
};
activeConfig = "default";
};
}

View File

@@ -1,16 +0,0 @@
import Quickshell
import QtQuick
import qs.widgets
ShellRoot {
Bar {}
Connections {
function onReloadCompleted() {
Quickshell.inhibitReloadPopup();
}
target: Quickshell
}
}

View File

@@ -1,13 +0,0 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property QtObject colors
colors: QtObject {
readonly property color base: "#313244"
}
}

View File

@@ -1,37 +0,0 @@
import Quickshell
import QtQuick
import qs.utils
Scope {
Variants {
model: Quickshell.screens
delegate: Component {
PanelWindow {
required property var modelData
screen: modelData
anchors {
top: true
left: true
right: true
}
color: Settings.colors.base
implicitHeight: 30
Text {
anchors.centerIn: parent
text: Qt.formatDateTime(clock.date, "hh:mm")
}
}
}
}
SystemClock {
id: clock
precision: SystemClock.Minutes
}
}