Begin work on configuration menu

This commit is contained in:
Jas Singh
2024-07-14 18:44:49 -07:00
parent 7a2fdf8647
commit abe491e6d0
8 changed files with 673 additions and 7 deletions

145
lib/icons.ts Normal file
View File

@@ -0,0 +1,145 @@
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",
}
export default {
missing: "image-missing-symbolic",
nix: {
nix: "nix-snowflake-symbolic",
},
app: {
terminal: "terminal-symbolic",
},
fallback: {
executable: "application-x-executable",
notification: "dialog-information-symbolic",
video: "video-x-generic-symbolic",
audio: "audio-x-generic-symbolic",
},
ui: {
close: "window-close-symbolic",
colorpicker: "color-select-symbolic",
info: "info-symbolic",
link: "external-link-symbolic",
lock: "system-lock-screen-symbolic",
menu: "open-menu-symbolic",
refresh: "view-refresh-symbolic",
search: "system-search-symbolic",
settings: "emblem-system-symbolic",
themes: "preferences-desktop-theme-symbolic",
tick: "object-select-symbolic",
time: "hourglass-symbolic",
toolbars: "toolbars-symbolic",
warning: "dialog-warning-symbolic",
avatar: "avatar-default-symbolic",
arrow: {
right: "pan-end-symbolic",
left: "pan-start-symbolic",
down: "pan-down-symbolic",
up: "pan-up-symbolic",
},
},
audio: {
mic: {
muted: "microphone-disabled-symbolic",
low: "microphone-sensitivity-low-symbolic",
medium: "microphone-sensitivity-medium-symbolic",
high: "microphone-sensitivity-high-symbolic",
},
volume: {
muted: "audio-volume-muted-symbolic",
low: "audio-volume-low-symbolic",
medium: "audio-volume-medium-symbolic",
high: "audio-volume-high-symbolic",
overamplified: "audio-volume-overamplified-symbolic",
},
type: {
headset: "audio-headphones-symbolic",
speaker: "audio-speakers-symbolic",
card: "audio-card-symbolic",
},
mixer: "mixer-symbolic",
},
powerprofile: {
balanced: "power-profile-balanced-symbolic",
"power-saver": "power-profile-power-saver-symbolic",
performance: "power-profile-performance-symbolic",
},
asusctl: {
profile: {
Balanced: "power-profile-balanced-symbolic",
Quiet: "power-profile-power-saver-symbolic",
Performance: "power-profile-performance-symbolic",
},
mode: {
Integrated: "processor-symbolic",
Hybrid: "controller-symbolic",
},
},
battery: {
charging: "battery-flash-symbolic",
warning: "battery-empty-symbolic",
},
bluetooth: {
enabled: "bluetooth-active-symbolic",
disabled: "bluetooth-disabled-symbolic",
},
brightness: {
indicator: "display-brightness-symbolic",
keyboard: "keyboard-brightness-symbolic",
screen: "display-brightness-symbolic",
},
powermenu: {
sleep: "weather-clear-night-symbolic",
reboot: "system-reboot-symbolic",
logout: "system-log-out-symbolic",
shutdown: "system-shutdown-symbolic",
},
recorder: {
recording: "media-record-symbolic",
},
notifications: {
noisy: "org.gnome.Settings-notifications-symbolic",
silent: "notifications-disabled-symbolic",
message: "chat-bubbles-symbolic",
},
trash: {
full: "user-trash-full-symbolic",
empty: "user-trash-symbolic",
},
mpris: {
shuffle: {
enabled: "media-playlist-shuffle-symbolic",
disabled: "media-playlist-consecutive-symbolic",
},
loop: {
none: "media-playlist-repeat-symbolic",
track: "media-playlist-repeat-song-symbolic",
playlist: "media-playlist-repeat-symbolic",
},
playing: "media-playback-pause-symbolic",
paused: "media-playback-start-symbolic",
stopped: "media-playback-start-symbolic",
prev: "media-skip-backward-symbolic",
next: "media-skip-forward-symbolic",
},
system: {
cpu: "org.gnome.SystemMonitor-symbolic",
ram: "drive-harddisk-solidstate-symbolic",
temp: "temperature-symbolic",
},
color: {
dark: "dark-mode-symbolic",
light: "light-mode-symbolic",
},
}

115
lib/option.ts Normal file
View File

@@ -0,0 +1,115 @@
import { Variable } from "resource:///com/github/Aylur/ags/variable.js"
type OptProps = {
persistent?: boolean
}
export class Opt<T = unknown> extends Variable<T> {
static { Service.register(this) }
constructor(initial: T, { persistent = false }: OptProps = {}) {
super(initial)
this.initial = initial
this.persistent = persistent
}
initial: T
id = ""
persistent: boolean
toString() { return `${this.value}` }
toJSON() { return `opt:${this.value}` }
getValue = (): T => {
return super.getValue()
}
init(cacheFile: string) {
const cacheV = JSON.parse(Utils.readFile(cacheFile) || "{}")[this.id]
if (cacheV !== undefined)
this.value = cacheV
this.connect("changed", () => {
const cache = JSON.parse(Utils.readFile(cacheFile) || "{}")
cache[this.id] = this.value
Utils.writeFileSync(JSON.stringify(cache, null, 2), cacheFile)
})
}
reset() {
if (this.persistent)
return
if (JSON.stringify(this.value) !== JSON.stringify(this.initial)) {
this.value = this.initial
return this.id
}
}
}
export const opt = <T>(initial: T, opts?: OptProps) => new Opt(initial, opts)
function getOptions(object: object, path = ""): Opt[] {
return Object.keys(object).flatMap(key => {
const obj: Opt = object[key]
const id = path ? path + "." + key : key
if (obj instanceof Variable) {
obj.id = id
return obj
}
if (typeof obj === "object")
return getOptions(obj, id)
return []
})
}
export function mkOptions<T extends object>(cacheFile: string, object: T) {
for (const opt of getOptions(object))
opt.init(cacheFile)
Utils.ensureDirectory(cacheFile.split("/").slice(0, -1).join("/"))
const configFile = `${TMP}/config.json`
const values = getOptions(object).reduce((obj, { id, value }) => ({ [id]: value, ...obj }), {})
Utils.writeFileSync(JSON.stringify(values, null, 2), configFile)
Utils.monitorFile(configFile, () => {
const cache = JSON.parse(Utils.readFile(configFile) || "{}")
for (const opt of getOptions(object)) {
if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value))
opt.value = cache[opt.id]
}
})
function sleep(ms = 0): Promise<T> {
return new Promise(r => setTimeout(r, ms))
}
async function reset(
[opt, ...list] = getOptions(object),
id = opt?.reset(),
): Promise<Array<string>> {
if (!opt)
return sleep().then(() => [])
return id
? [id, ...(await sleep(50).then(() => reset(list)))]
: await sleep().then(() => reset(list))
}
return Object.assign(object, {
configFile,
array: () => getOptions(object),
async reset() {
return (await reset()).join("\n")
},
handler(deps: string[], callback: () => void) {
for (const opt of getOptions(object)) {
if (deps.some(i => opt.id.startsWith(i)))
opt.connect("changed", callback)
}
},
})
}

16
lib/session.ts Normal file
View File

@@ -0,0 +1,16 @@
import GLib from "gi://GLib?version=2.0"
declare global {
const OPTIONS: string
const TMP: string
const USER: string
}
Object.assign(globalThis, {
OPTIONS: `${GLib.get_user_cache_dir()}/ags/options.json`,
TMP: `${GLib.get_tmp_dir()}/asztal`,
USER: GLib.get_user_name(),
})
Utils.ensureDirectory(TMP)
App.addIcons(`${App.configDir}/assets`)

113
lib/utils.ts Normal file
View File

@@ -0,0 +1,113 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Application } from "types/service/applications"
import icons, { substitutes } from "./icons"
import Gtk from "gi://Gtk?version=3.0"
import Gdk from "gi://Gdk"
import GLib from "gi://GLib?version=2.0"
export type Binding<T> = import("types/service").Binding<any, any, T>
/**
* @returns substitute icon || name || fallback icon
*/
export function icon(name: string | null, fallback = icons.missing) {
if (!name)
return fallback || ""
if (GLib.file_test(name, GLib.FileTest.EXISTS))
return name
const icon = (substitutes[name] || name)
if (Utils.lookUpIcon(icon))
return icon
print(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`)
return fallback
}
/**
* @returns execAsync(["bash", "-c", cmd])
*/
export async function bash(strings: TemplateStringsArray | string, ...values: unknown[]) {
const cmd = typeof strings === "string" ? strings : strings
.flatMap((str, i) => str + `${values[i] ?? ""}`)
.join("")
return Utils.execAsync(["bash", "-c", cmd]).catch(err => {
console.error(cmd, err)
return ""
})
}
/**
* @returns execAsync(cmd)
*/
export async function sh(cmd: string | string[]) {
return Utils.execAsync(cmd).catch(err => {
console.error(typeof cmd === "string" ? cmd : cmd.join(" "), err)
return ""
})
}
export function forMonitors(widget: (monitor: number) => Gtk.Window) {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1
return range(n, 0).flatMap(widget)
}
/**
* @returns [start...length]
*/
export function range(length: number, start = 1) {
return Array.from({ length }, (_, i) => i + start)
}
/**
* @returns true if all of the `bins` are found
*/
export function dependencies(...bins: string[]) {
const missing = bins.filter(bin => Utils.exec({
cmd: `which ${bin}`,
out: () => false,
err: () => true,
}))
if (missing.length > 0) {
console.warn(Error(`missing dependencies: ${missing.join(", ")}`))
Utils.notify(`missing dependencies: ${missing.join(", ")}`)
}
return missing.length === 0
}
/**
* run app detached
*/
export function launchApp(app: Application) {
const exe = app.executable
.split(/\s+/)
.filter(str => !str.startsWith("%") && !str.startsWith("@"))
.join(" ")
bash(`${exe} &`)
app.frequency += 1
}
/**
* to use with drag and drop
*/
export function createSurfaceFromWidget(widget: Gtk.Widget) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cairo = imports.gi.cairo as any
const alloc = widget.get_allocation()
const surface = new cairo.ImageSurface(
cairo.Format.ARGB32,
alloc.width,
alloc.height,
)
const cr = new cairo.Context(surface)
cr.setSourceRGBA(255, 255, 255, 0)
cr.rectangle(0, 0, alloc.width, alloc.height)
cr.fill()
widget.draw(cr)
return surface
}

16
lib/variables.ts Normal file
View File

@@ -0,0 +1,16 @@
import GLib from "gi://GLib"
export const clock = Variable(GLib.DateTime.new_now_local(), {
poll: [1000, () => GLib.DateTime.new_now_local()],
})
export const uptime = Variable(0, {
poll: [60_000, "cat /proc/uptime", line =>
Number.parseInt(line.split(".")[0]) / 60,
],
})
export const distro = {
id: GLib.get_os_info("ID"),
logo: GLib.get_os_info("LOGO"),
}