feat: update ags

This commit is contained in:
2023-12-30 20:38:47 +01:00
parent d2f9104fe4
commit 32d78e57a3
213 changed files with 8155 additions and 9843 deletions

View File

@@ -0,0 +1,326 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import RegularWindow from "../misc/RegularWindow.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import icons from "../icons.js";
import { getOptions, getValues } from "./option.js";
import options from "../options.js";
const optionsList = getOptions();
const categories = Array.from(
new Set(optionsList.map((opt) => opt.category)),
).filter((category) => category !== "exclude");
const currentPage = Variable(categories[0]);
const search = Variable("");
const showSearch = Variable(false);
showSearch.connect("changed", ({ value }) => {
if (!value) search.value = "";
});
/** @param {import('./option.js').Opt<string>} opt */
const EnumSetter = (opt) => {
const lbl = Widget.Label({ binds: [["label", opt]] });
const step = (dir = 1) => {
const i = opt.enums.findIndex((i) => i === lbl.label);
opt.setValue(
dir > 0
? i + dir > opt.enums.length - 1
? opt.enums[0]
: opt.enums[i + dir]
: i + dir < 0
? opt.enums[opt.enums.length - 1]
: opt.enums[i + dir],
true,
);
};
const next = Widget.Button({
child: Widget.Icon(icons.ui.arrow.right),
on_clicked: () => step(+1),
});
const prev = Widget.Button({
child: Widget.Icon(icons.ui.arrow.left),
on_clicked: () => step(-1),
});
return Widget.Box({
class_name: "enum-setter",
children: [prev, lbl, next],
});
};
/** @param {import('./option.js').Opt} opt */
const Setter = (opt) => {
switch (opt.type) {
case "number":
return Widget.SpinButton({
setup(self) {
self.set_range(0, 1000);
self.set_increments(1, 5);
},
connections: [
["value-changed", (self) => opt.setValue(self.value, true)],
[opt, (self) => (self.value = opt.value)],
],
});
case "float":
case "object":
return Widget.Entry({
on_accept: (self) => opt.setValue(JSON.parse(self.text || ""), true),
connections: [[opt, (self) => (self.text = JSON.stringify(opt.value))]],
});
case "string":
return Widget.Entry({
on_accept: (self) => opt.setValue(self.text, true),
connections: [[opt, (self) => (self.text = opt.value)]],
});
case "enum":
return EnumSetter(opt);
case "boolean":
return Widget.Switch({
connections: [
["notify::active", (self) => opt.setValue(self.active, true)],
[opt, (self) => (self.active = opt.value)],
],
});
case "img":
return Widget.FileChooserButton({
connections: [
[
"selection-changed",
(self) => {
opt.setValue(self.get_uri()?.replace("file://", ""), true);
},
],
],
});
case "font":
return Widget.FontButton({
show_size: false,
use_size: false,
connections: [
["notify::font", ({ font }) => opt.setValue(font, true)],
[opt, (self) => (self.font = opt.value)],
],
});
default:
return Widget.Label({
label: "no setter with type " + opt.type,
});
}
};
/** @param {import('./option.js').Opt} opt */
const Row = (opt) =>
Widget.Box({
class_name: "row",
setup: (self) => (self.opt = opt),
children: [
Widget.Box({
vertical: true,
vpack: "center",
children: [
opt.title &&
Widget.Label({
xalign: 0,
class_name: "summary",
label: opt.title,
}),
Widget.Label({
xalign: 0,
class_name: "id",
label: `id: "${opt.id}"`,
}),
],
}),
Widget.Box({ hexpand: true }),
Widget.Box({
vpack: "center",
vertical: true,
children: [
Widget.Box({
hpack: "end",
child: Setter(opt),
}),
opt.note &&
Widget.Label({
xalign: 1,
class_name: "note",
label: opt.note,
}),
],
}),
],
});
/** @param {string} category */
const Page = (category) =>
Widget.Scrollable({
vexpand: true,
class_name: "page",
child: Widget.Box({
class_name: "page-content vertical",
vertical: true,
connections: [
[
search,
(self) => {
for (const child of self.children) {
child.visible =
child.opt.id.includes(search.value) ||
child.opt.title.includes(search.value) ||
child.opt.note.includes(search.value);
}
},
],
],
children: optionsList
.filter((opt) => opt.category.includes(category))
.map(Row),
}),
});
const sidebar = Widget.Revealer({
binds: [["reveal-child", search, "value", (v) => !v]],
transition: "slide_right",
child: Widget.Box({
hexpand: false,
vertical: true,
children: [
Widget.Box({
class_name: "sidebar-header",
children: [
Widget.Button({
hexpand: true,
label: icons.dialog.Search + " Search",
on_clicked: () => (showSearch.value = !showSearch.value),
}),
Widget.Button({
hpack: "end",
child: Widget.Icon(icons.ui.info),
on_clicked: () => App.toggleWindow("about"),
}),
],
}),
Widget.Scrollable({
vexpand: true,
hscroll: "never",
child: Widget.Box({
class_name: "sidebar-box vertical",
vertical: true,
children: [
...categories.map((name) =>
Widget.Button({
label: (icons.dialog[name] || "") + " " + name,
xalign: 0,
binds: [
[
"class-name",
currentPage,
"value",
(v) => (v === name ? "active" : ""),
],
],
on_clicked: () => currentPage.setValue(name),
}),
),
],
}),
}),
Widget.Box({
class_name: "sidebar-footer",
child: Widget.Button({
class_name: "copy",
child: Widget.Label({
label: " Save",
xalign: 0,
}),
hexpand: true,
on_clicked: () => {
Utils.execAsync(["wl-copy", getValues()]);
Utils.execAsync([
"notify-send",
"-i",
"preferences-desktop-theme-symbolic",
"Theme copied to clipboard",
'To save it permanently, make a new theme in <span weight="bold">themes.js</span>',
]);
},
}),
}),
],
}),
});
const searchEntry = Widget.Revealer({
transition: "slide_down",
binds: [
["reveal-child", showSearch],
["transition-duration", options.transition],
],
child: Widget.Entry({
connections: [
[
showSearch,
(self) => {
if (!showSearch.value) self.text = "";
if (showSearch.value) self.grab_focus();
},
],
],
hexpand: true,
class_name: "search",
placeholder_text: "Search Options",
secondary_icon_name: icons.apps.search,
on_change: ({ text }) => (search.value = text || ""),
}),
});
const categoriesStack = Widget.Stack({
transition: "slide_left_right",
items: categories.map((name) => [name, Page(name)]),
binds: [
["shown", currentPage],
["visible", search, "value", (v) => !v],
],
});
const searchPage = Widget.Box({
binds: [["visible", search, "value", (v) => !!v]],
child: Page(""),
});
export default RegularWindow({
name: "settings-dialog",
title: "Settings",
setup: (win) => win.set_default_size(800, 500),
connections: [
[
"delete-event",
(win) => {
win.hide();
return true;
},
],
[
"key-press-event",
(self, event) => {
if (event.get_keyval()[1] === imports.gi.Gdk.KEY_Escape) {
self.text = "";
showSearch.setValue(false);
search.setValue("");
}
},
],
],
child: Widget.Box({
children: [
sidebar,
Widget.Box({
vertical: true,
children: [searchEntry, categoriesStack, searchPage],
}),
],
}),
});

View File

@@ -0,0 +1,37 @@
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
export async function globals() {
try {
globalThis.options = (await import("../options.js")).default;
globalThis.iconBrowser = (await import("../misc/IconBrowser.js")).default;
globalThis.app = (
await import("resource:///com/github/Aylur/ags/app.js")
).default;
globalThis.audio = (
await import("resource:///com/github/Aylur/ags/service/audio.js")
).default;
globalThis.recorder = (await import("../services/screenrecord.js")).default;
globalThis.brightness = (await import("../services/brightness.js")).default;
globalThis.indicator = (
await import("../services/onScreenIndicator.js")
).default;
Mpris.players.forEach((player) => {
player.connect("changed", (player) => {
globalThis.mpris = player || Mpris.players[0];
});
});
Mpris.connect("player-added", (mpris, bus) => {
mpris.getPlayer(bus)?.connect("changed", (player) => {
globalThis.mpris = player || Mpris.players[0];
});
});
Mpris.connect("player-closed", () => {
globalThis.mpris = Mpris.players[0];
});
} catch (error) {
logError(error);
}
}

View File

@@ -0,0 +1,76 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import options from "../options.js";
import { readFile, writeFile } from "resource:///com/github/Aylur/ags/utils.js";
const noIgnorealpha = ["verification", "powermenu", "lockscreen"];
/** @param {Array<string>} batch */
function sendBatch(batch) {
const cmd = batch
.filter((x) => !!x)
.map((x) => `keyword ${x}`)
.join("; ");
Hyprland.sendMessage(`[[BATCH]]/${cmd}`);
}
/** @param {string} scss */
function getColor(scss) {
if (scss.includes("#")) return scss.replace("#", "");
if (scss.includes("$")) {
const opt = options
.list()
.find((opt) => opt.scss === scss.replace("$", ""));
return opt?.value.replace("#", "") || "ff0000";
}
}
export function hyprlandInit() {
if (readFile("/tmp/ags/hyprland-init")) return;
sendBatch(
Array.from(App.windows).flatMap(([name]) => [
`layerrule blur, ${name}`,
noIgnorealpha.some((skip) => name.includes(skip))
? ""
: `layerrule ignorealpha 0.6, ${name}`,
]),
);
writeFile("init", "/tmp/ags/hyprland-init");
}
export async function setupHyprland() {
const wm_gaps = Math.floor(
options.hypr.wm_gaps_multiplier.value * options.spacing.value,
);
const border_width = options.border.width.value;
const radii = options.radii.value;
const drop_shadow = options.desktop.drop_shadow.value;
const bar_style = options.bar.style.value;
const bar_pos = options.bar.position.value;
const inactive_border = options.hypr.inactive_border.value;
const accent = getColor(options.theme.accent.accent.value);
const batch = [];
JSON.parse(await Hyprland.sendMessage("j/monitors")).forEach(({ name }) => {
const v = bar_pos === "top" ? `-${wm_gaps},0,0,0` : `0,-${wm_gaps},0,0`;
if (bar_style !== "normal") batch.push(`monitor ${name},addreserved,${v}`);
else batch.push(`monitor ${name},addreserved,0,0,0,0`);
});
batch.push(
`general:border_size ${border_width}`,
`general:gaps_out ${wm_gaps}`,
`general:gaps_in ${Math.floor(wm_gaps / 2)}`,
`general:col.active_border rgba(${accent}ff)`,
`general:col.inactive_border ${inactive_border}`,
`decoration:rounding ${radii}`,
`decoration:drop_shadow ${drop_shadow ? "yes" : "no"}`,
);
sendBatch(batch);
}

View File

@@ -0,0 +1,198 @@
import {
CACHE_DIR,
readFile,
writeFile,
} from "resource:///com/github/Aylur/ags/utils.js";
import { exec } from "resource:///com/github/Aylur/ags/utils.js";
import options from "../options.js";
import Service, { Binding } from "resource:///com/github/Aylur/ags/service.js";
import { reloadScss } from "./scss.js";
import { setupHyprland } from "./hyprland.js";
const CACHE_FILE = CACHE_DIR + "/options.json";
/** object that holds the overriedden values */
let cacheObj = JSON.parse(readFile(CACHE_FILE) || "{}");
/**
* @template T
* @typedef {Object} OptionConfig
* @property {string=} scss - name of scss variable set to "exclude" to not include it in the generated scss file
* @property {string=} unit - scss unit on numbers, default is "px"
* @property {string=} title
* @property {string=} note
* @property {string=} category
* @property {boolean=} noReload - don't reload css & hyprland on change
* @property {boolean=} persist - ignore reset call
* @property {'object' | 'string' | 'img' | 'number' | 'float' | 'font' | 'enum' =} type
* @property {Array<string> =} enums
* @property {(value: T) => any=} format
* @property {(value: T) => any=} scssFormat
*/
/** @template T */
export class Opt extends Service {
static {
Service.register(
this,
{},
{
value: ["jsobject"],
},
);
}
#value;
#scss = "";
unit = "px";
noReload = false;
persist = false;
id = "";
title = "";
note = "";
type = "";
category = "";
/** @type {Array<string>} */
enums = [];
/** @type {(v: T) => any} */
format = (v) => v;
/** @type {(v: T) => any} */
scssFormat = (v) => v;
/**
* @param {T} value
* @param {OptionConfig<T> =} config
*/
constructor(value, config) {
super();
this.#value = value;
this.defaultValue = value;
this.type = typeof value;
if (config) Object.keys(config).forEach((c) => (this[c] = config[c]));
import("../options.js").then(this.#init.bind(this));
}
set scss(scss) {
this.#scss = scss;
}
get scss() {
return this.#scss || this.id.split(".").join("-").split("_").join("-");
}
#init() {
getOptions(); // sets the ids as a side effect
if (cacheObj[this.id] !== undefined) this.setValue(cacheObj[this.id]);
const words = this.id
.split(".")
.flatMap((w) => w.split("_"))
.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
this.title ||= words.join(" ");
this.category ||= words.length === 1 ? "General" : words.at(0) || "General";
this.connect("changed", () => {
cacheObj[this.id] = this.value;
writeFile(JSON.stringify(cacheObj, null, 2), CACHE_FILE);
});
}
get value() {
return this.#value;
}
set value(value) {
this.setValue(value);
}
/** @param {T} value */
setValue(value, reload = false) {
if (typeof value !== typeof this.defaultValue) {
console.error(
Error(
`WrongType: Option "${this.id}" can't be set to ${value}, ` +
`expected "${typeof this.defaultValue}", but got "${typeof value}"`,
),
);
return;
}
if (this.value !== value) {
this.#value = this.format(value);
this.changed("value");
if (reload && !this.noReload) {
reloadScss();
setupHyprland();
}
}
}
reset(reload = false) {
if (!this.persist) this.setValue(this.defaultValue, reload);
}
}
/**
* @template T
* @param {T} value
* @param {OptionConfig<T> =} config
* @returns {Opt<T>}
*/
export function Option(value, config) {
return new Opt(value, config);
}
/** @returns {Array<Opt<any>>} */
export function getOptions(object = options, path = "") {
return Object.keys(object).flatMap((key) => {
/** @type Option<any> */
const obj = object[key];
const id = path ? path + "." + key : key;
if (obj instanceof Opt) {
obj.id = id;
return obj;
}
if (typeof obj === "object") return getOptions(obj, id);
return [];
});
}
export function resetOptions() {
exec(`rm -rf ${CACHE_FILE}`);
cacheObj = {};
getOptions().forEach((opt) => opt.reset());
}
export function getValues() {
const obj = {};
for (const opt of getOptions()) {
if (opt.category !== "exclude") obj[opt.id] = opt.value;
}
return JSON.stringify(obj, null, 2);
}
/** @param {string | object} config */
export function apply(config) {
const options = getOptions();
const settings = typeof config === "string" ? JSON.parse(config) : config;
for (const id of Object.keys(settings)) {
const opt = options.find((opt) => opt.id === id);
if (!opt) {
print(`No option with id: "${id}"`);
continue;
}
opt.setValue(settings[id]);
}
}

View File

@@ -0,0 +1,65 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import { getOptions } from "./option.js";
import { dependencies } from "../utils.js";
export function scssWatcher() {
return Utils.subprocess(
[
"inotifywait",
"--recursive",
"--event",
"create,modify",
"-m",
App.configDir + "/scss",
],
reloadScss,
() => print("missing dependancy for css hotreload: inotify-tools"),
);
}
/**
* generate an scss file that makes every option available as a variable
* based on the passed scss parameter or the path in the object
*
* e.g
* options.bar.style.value => $bar-style
*/
export async function reloadScss() {
if (!dependencies(["sassc"])) return;
const opts = getOptions();
const vars = opts.map((opt) => {
if (opt.scss === "exclude") return "";
const unit = typeof opt.value === "number" ? opt.unit : "";
const value = opt.scssFormat ? opt.scssFormat(opt.value) : opt.value;
return `$${opt.scss}: ${value}${unit};`;
});
const bar_style = opts.find((opt) => opt.id === "bar.style")?.value || "";
const additional =
bar_style === "normal"
? "//"
: `
window#quicksettings .window-content {
margin-right: $wm-gaps;
}
`;
try {
const tmp = "/tmp/ags/scss";
Utils.ensureDirectory(tmp);
await Utils.writeFile(vars.join("\n"), `${tmp}/options.scss`);
await Utils.writeFile(additional, `${tmp}/additional.scss`);
await Utils.execAsync(
`sassc ${App.configDir}/scss/main.scss ${tmp}/style.css`,
);
App.resetCss();
App.applyCss(`${tmp}/style.css`);
} catch (error) {
if (error instanceof Error) console.error(error.message);
if (typeof error === "string") console.error(error);
}
}

View File

@@ -0,0 +1,114 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import options from "../options.js";
import icons from "../icons.js";
import { reloadScss, scssWatcher } from "./scss.js";
import { wallpaper } from "./wallpaper.js";
import { hyprlandInit, setupHyprland } from "./hyprland.js";
import { globals } from "./globals.js";
import { showAbout } from "../about/about.js";
import Gtk from "gi://Gtk";
export function init() {
notificationBlacklist();
warnOnLowBattery();
globals();
tmux();
gsettigsColorScheme();
gtkFontSettings();
scssWatcher();
dependandOptions();
reloadScss();
hyprlandInit();
setupHyprland();
wallpaper();
showAbout();
}
function dependandOptions() {
options.bar.style.connect("changed", ({ value }) => {
if (value !== "normal")
options.desktop.screen_corners.setValue(false, true);
});
}
function tmux() {
if (!Utils.exec("which tmux")) return;
/** @param {string} scss */
function getColor(scss) {
if (scss.includes("#")) return scss;
if (scss.includes("$")) {
const opt = options
.list()
.find((opt) => opt.scss === scss.replace("$", ""));
return opt?.value;
}
}
options.theme.accent.accent.connect("changed", ({ value }) =>
Utils.execAsync(`tmux set @main_accent ${getColor(value)}`).catch((err) =>
console.error(err.message),
),
);
}
function gsettigsColorScheme() {
if (!Utils.exec("which gsettings")) return;
options.theme.scheme.connect("changed", ({ value }) => {
const gsettings = "gsettings set org.gnome.desktop.interface color-scheme";
Utils.execAsync(`${gsettings} "prefer-${value}"`).catch((err) =>
console.error(err.message),
);
});
}
function gtkFontSettings() {
const settings = Gtk.Settings.get_default();
if (!settings) {
console.error(Error("Gtk.Settings unavailable"));
return;
}
const callback = () => {
const { size, font } = options.font;
settings.gtk_font_name = `${font.value} ${size.value}`;
};
options.font.font.connect("notify::value", callback);
options.font.size.connect("notify::value", callback);
}
function notificationBlacklist() {
Notifications.connect("notified", (_, id) => {
const n = Notifications.getNotification(id);
options.notifications.black_list.value.forEach((item) => {
if (n?.app_name.includes(item) || n?.app_entry?.includes(item)) n.close();
});
});
}
function warnOnLowBattery() {
Battery.connect("notify::percent", () => {
const low = options.battery.low.value;
if (
Battery.percent !== low ||
Battery.percent !== low / 2 ||
!Battery.charging
)
return;
Utils.execAsync([
"notify-send",
`${Battery.percent}% Battery Percentage`,
"-i",
icons.battery.warning,
"-u",
"critical",
]);
});
}

View File

@@ -0,0 +1,55 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import options from "../options.js";
import themes from "../themes.js";
import { reloadScss } from "./scss.js";
import { setupHyprland } from "./hyprland.js";
import { wallpaper } from "./wallpaper.js";
/** @param {string} name */
export function setTheme(name) {
options.reset();
const theme = themes.find((t) => t.name === name);
if (!theme) return print("No theme named " + name);
options.apply(theme.options);
reloadScss();
setupHyprland();
wallpaper();
}
export const WP = App.configDir + "/assets/";
export const lightColors = {
"theme.scheme": "light",
"color.red": "#e55f86",
"color.green": "#00D787",
"color.yellow": "#EBFF71",
"color.blue": "#51a4e7",
"color.magenta": "#9077e7",
"color.teal": "#51e6e6",
"color.orange": "#E79E64",
"theme.bg": "#fffffa",
"theme.fg": "#141414",
};
export const Theme = ({ name, icon = " ", ...options }) => ({
name,
icon,
options: {
"theme.name": name,
"theme.icon": icon,
...options,
},
});
let settingsDialog;
export async function openSettings() {
if (settingsDialog) return settingsDialog.present();
try {
settingsDialog = (await import("./SettingsDialog.js")).default;
settingsDialog.present();
} catch (error) {
if (error instanceof Error) console.error(error.message);
}
}

View File

@@ -0,0 +1,25 @@
import options from "../options.js";
import { exec, execAsync } from "resource:///com/github/Aylur/ags/utils.js";
import { dependencies } from "../utils.js";
export function initWallpaper() {
if (dependencies(["swww"])) {
exec("swww init");
options.desktop.wallpaper.img.connect("changed", wallpaper);
}
}
export function wallpaper() {
if (!dependencies(["swww"])) return;
execAsync([
"swww",
"img",
"--transition-type",
"grow",
"--transition-pos",
exec("hyprctl cursorpos").replace(" ", ""),
options.desktop.wallpaper.img.value,
]).catch((err) => console.error(err));
}