diff options
Diffstat (limited to '.config/ags/service')
| -rw-r--r-- | .config/ags/service/asusctl.ts | 52 | ||||
| -rw-r--r-- | .config/ags/service/brightness.ts | 69 | ||||
| -rw-r--r-- | .config/ags/service/colorpicker.ts | 56 | ||||
| -rw-r--r-- | .config/ags/service/nix.ts | 109 | ||||
| -rw-r--r-- | .config/ags/service/powermenu.ts | 43 | ||||
| -rw-r--r-- | .config/ags/service/screenrecord.ts | 102 | ||||
| -rw-r--r-- | .config/ags/service/wallpaper.ts | 127 | ||||
| -rw-r--r-- | .config/ags/service/weather.ts | 59 |
8 files changed, 617 insertions, 0 deletions
diff --git a/.config/ags/service/asusctl.ts b/.config/ags/service/asusctl.ts new file mode 100644 index 0000000..16acbd7 --- /dev/null +++ b/.config/ags/service/asusctl.ts @@ -0,0 +1,52 @@ +import { sh } from "lib/utils" + +type Profile = "Performance" | "Balanced" | "Quiet" +type Mode = "Hybrid" | "Integrated" + +class Asusctl extends Service { + static { + Service.register(this, {}, { + "profile": ["string", "r"], + "mode": ["string", "r"], + }) + } + + available = !!Utils.exec("which asusctl") + #profile: Profile = "Balanced" + #mode: Mode = "Hybrid" + + async nextProfile() { + await sh("asusctl profile -n") + const profile = await sh("asusctl profile -p") + const p = profile.split(" ")[3] as Profile + this.#profile = p + this.changed("profile") + } + + async setProfile(prof: Profile) { + await sh(`asusctl profile --profile-set ${prof}`) + this.#profile = prof + this.changed("profile") + } + + async nextMode() { + await sh(`supergfxctl -m ${this.#mode === "Hybrid" ? "Integrated" : "Hybrid"}`) + this.#mode = await sh("supergfxctl -g") as Mode + this.changed("profile") + } + + constructor() { + super() + + if (this.available) { + sh("asusctl profile -p").then(p => this.#profile = p.split(" ")[3] as Profile) + sh("supergfxctl -g").then(m => this.#mode = m as Mode) + } + } + + get profiles(): Profile[] { return ["Performance", "Balanced", "Quiet"] } + get profile() { return this.#profile } + get mode() { return this.#mode } +} + +export default new Asusctl diff --git a/.config/ags/service/brightness.ts b/.config/ags/service/brightness.ts new file mode 100644 index 0000000..a0b8eb5 --- /dev/null +++ b/.config/ags/service/brightness.ts @@ -0,0 +1,69 @@ +import { bash, dependencies, sh } from "lib/utils" + +if (!dependencies("brightnessctl")) + App.quit() + +const get = (args: string) => Number(Utils.exec(`brightnessctl ${args}`)) +const screen = await bash`ls -w1 /sys/class/backlight | head -1` +const kbd = await bash`ls -w1 /sys/class/leds | head -1` + +class Brightness extends Service { + static { + Service.register(this, {}, { + "screen": ["float", "rw"], + "kbd": ["int", "rw"], + }) + } + + #kbdMax = get(`--device ${kbd} max`) + #kbd = get(`--device ${kbd} get`) + #screenMax = get("max") + #screen = get("get") / get("max") + + get kbd() { return this.#kbd } + get screen() { return this.#screen } + + set kbd(value) { + if (value < 0 || value > this.#kbdMax) + return + + sh(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { + this.#kbd = value + this.changed("kbd") + }) + } + + set screen(percent) { + if (percent < 0) + percent = 0 + + if (percent > 1) + percent = 1 + + sh(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { + this.#screen = percent + this.changed("screen") + }) + } + + constructor() { + super() + + const screenPath = `/sys/class/backlight/${screen}/brightness` + const kbdPath = `/sys/class/leds/${kbd}/brightness` + + Utils.monitorFile(screenPath, async f => { + const v = await Utils.readFileAsync(f) + this.#screen = Number(v) / this.#screenMax + this.changed("screen") + }) + + Utils.monitorFile(kbdPath, async f => { + const v = await Utils.readFileAsync(f) + this.#kbd = Number(v) / this.#kbdMax + this.changed("kbd") + }) + } +} + +export default new Brightness diff --git a/.config/ags/service/colorpicker.ts b/.config/ags/service/colorpicker.ts new file mode 100644 index 0000000..5918f31 --- /dev/null +++ b/.config/ags/service/colorpicker.ts @@ -0,0 +1,56 @@ +import icons from "lib/icons" +import { bash, dependencies } from "lib/utils" + +const COLORS_CACHE = Utils.CACHE_DIR + "/colorpicker.json" +const MAX_NUM_COLORS = 10 + +class ColorPicker extends Service { + static { + Service.register(this, {}, { + "colors": ["jsobject"], + }) + } + + #notifID = 0 + #colors = JSON.parse(Utils.readFile(COLORS_CACHE) || "[]") as string[] + + get colors() { return [...this.#colors] } + set colors(colors) { + this.#colors = colors + this.changed("colors") + } + + // TODO: doesn't work? + async wlCopy(color: string) { + if (dependencies("wl-copy")) + bash(`wl-copy ${color}`) + } + + readonly pick = async () => { + if (!dependencies("hyprpicker")) + return + + const color = await bash("hyprpicker -a -r") + if (!color) + return + + this.wlCopy(color) + const list = this.colors + if (!list.includes(color)) { + list.push(color) + if (list.length > MAX_NUM_COLORS) + list.shift() + + this.colors = list + Utils.writeFile(JSON.stringify(list, null, 2), COLORS_CACHE) + } + + this.#notifID = await Utils.notify({ + id: this.#notifID, + iconName: icons.ui.colorpicker, + summary: color, + }) + } +} + +export default new ColorPicker diff --git a/.config/ags/service/nix.ts b/.config/ags/service/nix.ts new file mode 100644 index 0000000..3bde9fc --- /dev/null +++ b/.config/ags/service/nix.ts @@ -0,0 +1,109 @@ +import icons from "lib/icons" +import { bash, dependencies } from "lib/utils" +import options from "options" + +const CACHE = `${Utils.CACHE_DIR}/nixpkgs` +const PREFIX = "legacyPackages.x86_64-linux." +const MAX = options.launcher.nix.max +const nixpkgs = options.launcher.nix.pkgs + +export type Nixpkg = { + name: string + description: string + pname: string + version: string +} + +class Nix extends Service { + static { + Service.register(this, {}, { + "available": ["boolean", "r"], + "ready": ["boolean", "rw"], + }) + } + + #db: { [name: string]: Nixpkg } = {} + #ready = true + + private set ready(r: boolean) { + this.#ready = r + this.changed("ready") + } + + get db() { return this.#db } + get ready() { return this.#ready } + get available() { return Utils.exec("which nix") } + + constructor() { + super() + if (!this.available) + return this + + this.#updateList() + nixpkgs.connect("changed", this.#updateList) + } + + query = async (filter: string) => { + if (!dependencies("fzf", "nix") || !this.#ready) + return [] as string[] + + return bash(`cat ${CACHE} | fzf -f ${filter} -e | head -n ${MAX} `) + .then(str => str.split("\n").filter(i => i)) + } + + nix(cmd: string, bin: string, args: string) { + return Utils.execAsync(`nix ${cmd} ${nixpkgs}#${bin} --impure ${args}`) + } + + run = async (input: string) => { + if (!dependencies("nix")) + return + + try { + const [bin, ...args] = input.trim().split(/\s+/) + + this.ready = false + await this.nix("shell", bin, "--command sh -c 'exit'") + this.ready = true + + this.nix("run", bin, ["--", ...args].join(" ")) + } catch (err) { + if (typeof err === "string") + Utils.notify("NixRun Error", err, icons.nix.nix) + else + logError(err) + } finally { + this.ready = true + } + } + + #updateList = async () => { + if (!dependencies("nix")) + return + + this.ready = false + this.#db = {} + + // const search = await bash(`nix search ${nixpkgs} --json`) + const search = "" + if (!search) { + this.ready = true + return + } + + const json = Object.entries(JSON.parse(search) as { + [name: string]: Nixpkg + }) + + for (const [pkg, info] of json) { + const name = pkg.replace(PREFIX, "") + this.#db[name] = { ...info, name } + } + + const list = Object.keys(this.#db).join("\n") + await Utils.writeFile(list, CACHE) + this.ready = true + } +} + +export default new Nix diff --git a/.config/ags/service/powermenu.ts b/.config/ags/service/powermenu.ts new file mode 100644 index 0000000..fd16bc1 --- /dev/null +++ b/.config/ags/service/powermenu.ts @@ -0,0 +1,43 @@ +import options from "options" + +const { sleep, reboot, logout, shutdown } = options.powermenu + +export type Action = "sleep" | "reboot" | "logout" | "shutdown" + +class PowerMenu extends Service { + static { + Service.register(this, {}, { + "title": ["string"], + "cmd": ["string"], + }) + } + + #title = "" + #cmd = "" + + get title() { return this.#title } + get cmd() { return this.#cmd } + + action(action: Action) { + [this.#cmd, this.#title] = { + sleep: [sleep.value, "Sleep"], + reboot: [reboot.value, "Reboot"], + logout: [logout.value, "Log Out"], + shutdown: [shutdown.value, "Shutdown"], + }[action] + + this.notify("cmd") + this.notify("title") + this.emit("changed") + App.closeWindow("powermenu") + App.openWindow("verification") + } + + readonly shutdown = () => { + this.action("shutdown") + } +} + +const powermenu = new PowerMenu +Object.assign(globalThis, { powermenu }) +export default powermenu diff --git a/.config/ags/service/screenrecord.ts b/.config/ags/service/screenrecord.ts new file mode 100644 index 0000000..58721d2 --- /dev/null +++ b/.config/ags/service/screenrecord.ts @@ -0,0 +1,102 @@ +import GLib from "gi://GLib" +import icons from "lib/icons" +import { dependencies, sh, bash } from "lib/utils" + +const now = () => GLib.DateTime.new_now_local().format("%Y-%m-%d_%H-%M-%S") + +class Recorder extends Service { + static { + Service.register(this, {}, { + "timer": ["int"], + "recording": ["boolean"], + }) + } + + #recordings = Utils.HOME + "/Videos/Screencasting" + #screenshots = Utils.HOME + "/Pictures/Screenshots" + #file = "" + #interval = 0 + + recording = false + timer = 0 + + async start() { + if (!dependencies("slurp", "wf-recorder")) + return + + if (this.recording) + return + + Utils.ensureDirectory(this.#recordings) + this.#file = `${this.#recordings}/${now()}.mp4` + sh(`wf-recorder -g "${await sh("slurp")}" -f ${this.#file} --pixel-format yuv420p`) + + this.recording = true + this.changed("recording") + + this.timer = 0 + this.#interval = Utils.interval(1000, () => { + this.changed("timer") + this.timer++ + }) + } + + async stop() { + if (!this.recording) + return + + await bash("killall -INT wf-recorder") + this.recording = false + this.changed("recording") + GLib.source_remove(this.#interval) + + Utils.notify({ + iconName: icons.fallback.video, + summary: "Screenrecord", + body: this.#file, + actions: { + "Show in Files": () => sh(`xdg-open ${this.#recordings}`), + "View": () => sh(`xdg-open ${this.#file}`), + }, + }) + } + + async screenshot(full = false) { + if (!dependencies("slurp", "wayshot")) + return + + const file = `${this.#screenshots}/${now()}.png` + Utils.ensureDirectory(this.#screenshots) + + if (full) { + await sh(`wayshot -f ${file}`) + } + else { + const size = await sh("slurp") + if (!size) + return + + await sh(`wayshot -f ${file} -s "${size}"`) + } + + bash(`wl-copy < ${file}`) + + Utils.notify({ + image: file, + summary: "Screenshot", + body: file, + actions: { + "Show in Files": () => sh(`xdg-open ${this.#screenshots}`), + "View": () => sh(`xdg-open ${file}`), + "Edit": () => { + if (dependencies("swappy")) + sh(`swappy -f ${file}`) + }, + }, + }) + } +} + +const recorder = new Recorder +Object.assign(globalThis, { recorder }) +export default recorder diff --git a/.config/ags/service/wallpaper.ts b/.config/ags/service/wallpaper.ts new file mode 100644 index 0000000..865c6d9 --- /dev/null +++ b/.config/ags/service/wallpaper.ts @@ -0,0 +1,127 @@ +import options from 'options'; +import { dependencies, sh } from 'lib/utils'; + +export type Resolution = 1920 | 1366 | 3840; +export type Market = 'random' | 'en-US' | 'ja-JP' | 'en-AU' | 'en-GB' | 'de-DE' | 'en-NZ' | 'en-CA'; + +const WP = `${Utils.HOME}/pictures/wallpapers`; +const Cache = `${Utils.HOME}/Pictures/Wallpapers/Bing`; + +class Wallpaper extends Service { + static { + Service.register( + this, + {}, + { + wallpaper: ['string'], + }, + ); + } + + #blockMonitor = false; + + #wallpaper() { + if (!dependencies('swww')) return; + + sh('hyprctl cursorpos').then(pos => { + sh(['swww', 'img', '--transition-type', 'grow', '--transition-pos', pos.replace(' ', ''), WP]).then(() => { + this.changed('wallpaper'); + }); + }); + } + + async #setWallpaper(path: string) { + this.#blockMonitor = true; + + await sh(`cp "${path}" "${WP}"`); + this.#wallpaper(); + + this.#blockMonitor = false; + } + + async #fetchBing() { + // Check if wallpaper functionality is enabled + if (!options.wallpaper.enable.value) { + console.log('Wallpaper functionality is disabled.'); + return; + } + + try { + const res = await Utils.fetch('https://bing.biturl.top/', { + params: { + resolution: options.wallpaper.resolution.value, + format: 'json', + image_format: 'jpg', + index: 'random', + mkt: options.wallpaper.market.value, + }, + }); + + if (!res.ok) { + console.warn('Failed to fetch from Bing:', res.statusText); + return; + } + + const data = await res.json(); + const { url } = data; + + if (!url) { + console.warn('No URL found in Bing response:', data); + return; + } + + const file = `${Cache}/${url.replace('https://www.bing.com/th?id=', '')}`; + + Utils.ensureDirectory(Cache); + + if (!(await Utils.fileExists(file))) { + await sh(`curl "${url}" --output "${file}"`); + await this.#setWallpaper(file); + } else { + console.log(`Wallpaper already exists: ${file}`); + } + } catch (error) { + console.error('Error fetching wallpaper:', error); + } + } + + readonly random = () => { + // Check if wallpaper functionality is enabled + if (!options.wallpaper.enable.value) { + console.log('Wallpaper functionality is disabled.'); + return; + } + this.#fetchBing(); + }; + + readonly set = (path: string) => { + this.#setWallpaper(path); + }; + + get wallpaper() { + return WP; + } + constructor() { + super(); + + // Respect wallpaper.enable option + if (!options.wallpaper.enable.value) { + console.log('Wallpaper functionality is disabled, not starting swww-daemon.'); + return; + } + + if (!dependencies('swww')) return; + + // Monitor and set wallpaper if enabled + Utils.monitorFile(WP, () => { + if (!this.#blockMonitor) this.#wallpaper(); + }); + + // Start swww-daemon only when wallpaper is enabled + Utils.execAsync('swww-daemon') + .then(this.#wallpaper) + .catch(() => null); + } +} + +export default new Wallpaper(); diff --git a/.config/ags/service/weather.ts b/.config/ags/service/weather.ts new file mode 100644 index 0000000..14f2df2 --- /dev/null +++ b/.config/ags/service/weather.ts @@ -0,0 +1,59 @@ +import options from "options" + +const { interval, key, cities, unit } = options.datemenu.weather + +class Weather extends Service { + static { + Service.register(this, {}, { + "forecasts": ["jsobject"], + }) + } + + #forecasts: Forecast[] = [] + get forecasts() { return this.#forecasts } + + async #fetch(placeid: number) { + const url = "https://api.openweathermap.org/data/2.5/forecast" + const res = await Utils.fetch(url, { + params: { + id: placeid, + appid: key.value, + untis: unit.value, + }, + }) + return await res.json() + } + + constructor() { + super() + if (!key.value) + return this + + Utils.interval(interval.value, () => { + Promise.all(cities.value.map(this.#fetch)).then(forecasts => { + this.#forecasts = forecasts as Forecast[] + this.changed("forecasts") + }) + }) + } +} + +export default new Weather + +type Forecast = { + city: { + name: string, + } + list: Array<{ + dt: number + main: { + temp: number + feels_like: number + }, + weather: Array<{ + main: string, + description: string, + icon: string, + }> + }> +} |
