diff options
Diffstat (limited to '.config/ags/lib')
| -rw-r--r-- | .config/ags/lib/battery.ts | 16 | ||||
| -rw-r--r-- | .config/ags/lib/client.js | 134 | ||||
| -rw-r--r-- | .config/ags/lib/cursorhover.js | 86 | ||||
| -rw-r--r-- | .config/ags/lib/gtk.ts | 16 | ||||
| -rw-r--r-- | .config/ags/lib/hyprland.ts | 80 | ||||
| -rw-r--r-- | .config/ags/lib/iconUtils.js | 46 | ||||
| -rw-r--r-- | .config/ags/lib/icons.ts | 186 | ||||
| -rw-r--r-- | .config/ags/lib/init.ts | 19 | ||||
| -rw-r--r-- | .config/ags/lib/matugen.ts | 113 | ||||
| -rw-r--r-- | .config/ags/lib/notifications.ts | 16 | ||||
| -rw-r--r-- | .config/ags/lib/option.ts | 115 | ||||
| -rw-r--r-- | .config/ags/lib/session.ts | 16 | ||||
| -rw-r--r-- | .config/ags/lib/tmux.ts | 14 | ||||
| -rw-r--r-- | .config/ags/lib/utils.ts | 113 | ||||
| -rw-r--r-- | .config/ags/lib/variables.ts | 47 |
15 files changed, 1017 insertions, 0 deletions
diff --git a/.config/ags/lib/battery.ts b/.config/ags/lib/battery.ts new file mode 100644 index 0000000..3817260 --- /dev/null +++ b/.config/ags/lib/battery.ts @@ -0,0 +1,16 @@ +import icons from "./icons" + +export default async function init() { + const bat = await Service.import("battery") + bat.connect("notify::percent", ({ percent, charging }) => { + const low = 30 + if (percent !== low || percent !== low / 2 || !charging) + return + + Utils.notify({ + summary: `${percent}% Battery Percentage`, + iconName: icons.battery.warning, + urgency: "critical", + }) + }) +} diff --git a/.config/ags/lib/client.js b/.config/ags/lib/client.js new file mode 100644 index 0000000..9fb9164 --- /dev/null +++ b/.config/ags/lib/client.js @@ -0,0 +1,134 @@ +import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js'; +import { find_icon } from "./iconUtils.js"; +import { lookUpIcon, timeout } from 'resource:///com/github/Aylur/ags/utils.js'; + + +export let clientMapWorkSpace = {}; + +export function substitute(str) { + const subs = [ + { from: "code-url-handler", to: "visual-studio-code" }, + { from: "Code", to: "visual-studio-code" }, + { from: "GitHub Desktop", to: "github-desktop" }, + { from: "wpsoffice", to: "wps-office2019-kprometheus" }, + { from: "gnome-tweaks", to: "org.gnome.tweaks" }, + { from: "Minecraft* 1.20.1", to: "minecraft" }, + { from: "", to: "image-missing" }, + ]; + + for (const { from, to } of subs) { + if (from === str) { + return to; + } + } + + return str; +} + +function titleToClient(title, className) { + const subs = [ + { from: "musicfox", to: "musicfox" }, + ]; + + for (const { from, to } of subs) { + if (title.indexOf(from) !== -1) { + return to; + } + } + + return className +} + +export const getClientByAdrees = function(address) { + + const clients = Hyprland.clients + + const client = clients.find(item => { + return item.address === address + }) + + return client +} + +//Fullscreen client +export const getFullScreenClientAddress = function(workspace_id) { + + const clients = Hyprland.clients + const client = clients.find(item => { + return item.fullscreen && item.workspace.id === workspace_id + }) + return client +} + +export const ignoreAppsClass = [ + 'image-missing', + 'fcitx', + 'rofi' +] + +export const getClientIcon = (clientClass, title = "") => { + + clientClass.toLowerCase() + clientClass = clientClass.replace(" ", "_"); + + + if (title.length > 0) { + clientClass = titleToClient(title, clientClass) + } + + const awesome_icon = find_icon(clientClass) + if (awesome_icon) { + return awesome_icon + } + + if (lookUpIcon(clientClass)) { + return clientClass + } + + if (find_icon('system')) { + return find_icon('system') + } + + return "" +} + + +export const focus = (client) => { + //client + const { address } = client; + const liveClient = getClientByAdrees(address); + + //special window + if (liveClient.workspace.id < 0) { + const oldWorkSpace = clientMapWorkSpace[address]; + if (oldWorkSpace) { + Utils.exec( + `hyprctl dispatch movetoworkspace ${oldWorkSpace},address:${address}`, + ); + Utils.exec(`hyprctl dispatch workspace ${oldWorkSpace}`); + } + } + + //fullscreen + if (liveClient.fullscreen) { + Utils.exec("hyprctl dispatch focuswindow address:" + address); + return; + } + + //workspace fullscreen client + const currentFullScreenAddress = getFullScreenClientAddress( + liveClient.workspace.id, + ); + if (currentFullScreenAddress) { + const fullScreenAdress = currentFullScreenAddress.address; + Utils.exec("hyprctl dispatch focuswindow address:" + fullScreenAdress); + Utils.exec("hyprctl dispatch fullscreen 1"); + } + + Utils.exec("hyprctl dispatch focuswindow address:" + address); + // Utils.exec('hyprctl dispatch cyclenext') + Utils.exec("hyprctl dispatch alterzorder top,address:" + address); + if (currentFullScreenAddress) { + Utils.exec("hyprctl dispatch fullscreen 1"); + } +}; diff --git a/.config/ags/lib/cursorhover.js b/.config/ags/lib/cursorhover.js new file mode 100644 index 0000000..d93d021 --- /dev/null +++ b/.config/ags/lib/cursorhover.js @@ -0,0 +1,86 @@ +const { Gdk, Gtk } = imports.gi; + +const CLICK_BRIGHTEN_AMOUNT = 0.13; + +export function setupCursorHover(button) { + const display = Gdk.Display.get_default(); + button.connect('enter-notify-event', () => { + const cursor = Gdk.Cursor.new_from_name(display, 'pointer'); + button.get_window().set_cursor(cursor); + }); + + button.connect('leave-notify-event', () => { + const cursor = Gdk.Cursor.new_from_name(display, 'default'); + button.get_window().set_cursor(cursor); + }); + +} + +export function setupCursorHoverAim(button) { + button.connect('enter-notify-event', () => { + const display = Gdk.Display.get_default(); + const cursor = Gdk.Cursor.new_from_name(display, 'crosshair'); + button.get_window().set_cursor(cursor); + }); + + button.connect('leave-notify-event', () => { + const display = Gdk.Display.get_default(); + const cursor = Gdk.Cursor.new_from_name(display, 'default'); + button.get_window().set_cursor(cursor); + }); +} + +export function setupCursorHoverGrab(button) { + button.connect('enter-notify-event', () => { + const display = Gdk.Display.get_default(); + const cursor = Gdk.Cursor.new_from_name(display, 'grab'); + button.get_window().set_cursor(cursor); + }); + + button.connect('leave-notify-event', () => { + const display = Gdk.Display.get_default(); + const cursor = Gdk.Cursor.new_from_name(display, 'default'); + button.get_window().set_cursor(cursor); + }); +} + +// failed radial ripple experiment +// +// var clicked = false; +// var dummy = false; +// var cursorX = 0; +// var cursorY = 0; +// const styleContext = button.get_style_context(); +// var clickColor = styleContext.get_property('background-color', Gtk.StateFlags.HOVER); +// clickColor.green += CLICK_BRIGHTEN_AMOUNT; +// clickColor.blue += CLICK_BRIGHTEN_AMOUNT; +// clickColor.red += CLICK_BRIGHTEN_AMOUNT; +// clickColor = clickColor.to_string(); +// button.add_events(Gdk.EventMask.POINTER_MOTION_MASK); +// button.connect('motion-notify-event', (widget, event) => { +// [dummy, cursorX, cursorY] = event.get_coords(); // Get the mouse coordinates relative to the widget +// if(!clicked) widget.css = ` +// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%); +// `; +// }); + +// button.connect('button-press-event', (widget, event) => { +// clicked = true; +// [dummy, cursorX, cursorY] = event.get_coords(); // Get the mouse coordinates relative to the widget +// cursorX = Math.round(cursorX); cursorY = Math.round(cursorY); +// widget.css = ` +// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%); +// `; +// widget.toggleClassName('growingRadial', true); +// widget.css = ` +// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 70%, ${clickColor} 70%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%); +// ` +// }); +// button.connect('button-release-event', (widget, event) => { +// widget.toggleClassName('growingRadial', false); +// widget.toggleClassName('fadingRadial', false); +// widget.css = ` +// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%); +// ` +// clicked = false; +// }); diff --git a/.config/ags/lib/gtk.ts b/.config/ags/lib/gtk.ts new file mode 100644 index 0000000..8cd60a3 --- /dev/null +++ b/.config/ags/lib/gtk.ts @@ -0,0 +1,16 @@ +import Gio from "gi://Gio" +import options from "options" + +const settings = new Gio.Settings({ + schema: "org.gnome.desktop.interface", +}) + +function gtk() { + const scheme = options.theme.scheme.value + settings.set_string("color-scheme", `prefer-${scheme}`) +} + +export default function init() { + options.theme.scheme.connect("changed", gtk) + gtk() +} diff --git a/.config/ags/lib/hyprland.ts b/.config/ags/lib/hyprland.ts new file mode 100644 index 0000000..7f6a68c --- /dev/null +++ b/.config/ags/lib/hyprland.ts @@ -0,0 +1,80 @@ +import options from "options" +const { messageAsync } = await Service.import("hyprland") + +const { + hyprland, + theme: { + spacing, + radius, + border: { width }, + blur, + shadows, + dark: { + primary: { bg: darkActive }, + }, + light: { + primary: { bg: lightActive }, + }, + scheme, + }, +} = options + +const deps = [ + "hyprland", + spacing.id, + radius.id, + blur.id, + width.id, + shadows.id, + darkActive.id, + lightActive.id, + scheme.id, +] + +function activeBorder() { + const color = scheme.value === "dark" + ? darkActive.value + : lightActive.value + + return color.replace("#", "") +} + +function sendBatch(batch: string[]) { + const cmd = batch + .filter(x => !!x) + .map(x => `keyword ${x}`) + .join("; ") + + return messageAsync(`[[BATCH]]/${cmd}`) +} + +async function setupHyprland() { + const wm_gaps = Math.floor(hyprland.gaps.value * spacing.value) + + //sendBatch([ + // `general:border_size ${width}`, + // `general:gaps_out ${wm_gaps}`, + // `general:gaps_in ${Math.floor(wm_gaps / 2)}`, + // `general:col.active_border rgba(${activeBorder()}ff)`, + // `general:col.inactive_border rgba(${hyprland.inactiveBorder.value})`, + // `decoration:rounding ${radius}`, + // `decoration:drop_shadow ${shadows.value ? "yes" : "no"}`, + // `dwindle:no_gaps_when_only ${hyprland.gapsWhenOnly.value ? 0 : 1}`, + // `master:no_gaps_when_only ${hyprland.gapsWhenOnly.value ? 0 : 1}`, + //]) + + //await sendBatch(App.windows.map(({ name }) => `layerrule unset, ${name}`)) + + if (blur.value > 0) { + sendBatch(App.windows.flatMap(({ name }) => [ + `layerrule unset, ${name}`, + `layerrule blur, ${name}`, + `layerrule ignorealpha ${/* based on shadow color */.29}, ${name}`, + ])) + } +} + +export default function init() { + options.handler(deps, setupHyprland) + setupHyprland() +} diff --git a/.config/ags/lib/iconUtils.js b/.config/ags/lib/iconUtils.js new file mode 100644 index 0000000..baba660 --- /dev/null +++ b/.config/ags/lib/iconUtils.js @@ -0,0 +1,46 @@ +const { Gio, Gdk, Gtk } = imports.gi; + +function fileExists(filePath) { + let file = Gio.File.new_for_path(filePath); + return file.query_exists(null); +} + +function cartesianProduct(arrays) { + if (arrays.length === 0) { + return [[]]; + } + + const [head, ...tail] = arrays; + const tailCartesian = cartesianProduct(tail); + const result = []; + + for (const item of head) { + for (const tailItem of tailCartesian) { + result.push([item, ...tailItem]); + } + } + return result; +} +import { HOME } from '../utils.ts'; +export const find_icon = app_class => { + const themPath = [ + [`${HOME}/.local/share/icons/WhiteSur/`, `${HOME}/.local/share//icons/WhiteSur-dark/`], + ['512x512/', '128x128/', '64x64/', '96x96/', '72x72/', '48x48/', '36x36/'], + ['apps/', ''], + [app_class + '.png', app_class + '.svg', app_class + '.xpm'], + ]; + + let real_path = ''; + const all_icon_dir = cartesianProduct(themPath); + + for (let index = 0; index < all_icon_dir.length; index++) { + const pathItem = all_icon_dir[index]; + const icon_path = pathItem.join(''); + if (fileExists(icon_path)) { + real_path = icon_path; + break; + } + } + + return real_path; +}; diff --git a/.config/ags/lib/icons.ts b/.config/ags/lib/icons.ts new file mode 100644 index 0000000..dfc9e18 --- /dev/null +++ b/.config/ags/lib/icons.ts @@ -0,0 +1,186 @@ +export const substitutes = { + "transmission-gtk": "transmission", + "blueberry.py": "blueberry", + "Caprine": "facebook-messenger", + "phototonic": "terminal-symbolic", + "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-volume-medium-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", + audio: "audio-volume-medium-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-medium-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", + }, + ui: { + arch: "archlinux-logo", + close: "window-close", + colorpicker: "color-select", + info: "info", + link: "external-link", + lock: "system-lock-screen", + menu: "open-menu", + refresh: "view-refresh", + search: "system-search", + settings: "emblem-system", + themes: "preferences-desktop-theme", + tick: "object-select", + time: "hourglass", + toolbars: "toolbars-symbolic", + warning: "dialog-warning", + avatar: "avatar-default", + tbox_osk: "osk", + tbox_appkill: "bomb-kill", + tbox_close: "tbox-close", + tbox_rotate: "rotation", + tbox_moveup: "arrows-up", + tbox_movedown: "arrows-down", + tbox_moveleft: "arrows-left", + tbox_moveright: "arrows-right", + tbox_workspacenext: "wp-next", + tbox_workspaceprev: "wp-prev", + tbox_fullscreen: "fullscreen", + tbox_swapnext: "swapnext", + tbox_float: "float", + tbox_pinned: "pinned", + tbox_split: "togglesplit", + arrow: { + right: "pan-end", + left: "pan-start", + down: "pan-down", + up: "pan-up", + }, + }, +} diff --git a/.config/ags/lib/init.ts b/.config/ags/lib/init.ts new file mode 100644 index 0000000..e9e396c --- /dev/null +++ b/.config/ags/lib/init.ts @@ -0,0 +1,19 @@ +import matugen from './matugen'; +import hyprland from './hyprland'; +import tmux from './tmux'; +import gtk from './gtk'; +import lowBattery from './battery'; +import notifications from './notifications'; + +export default function init() { + try { + gtk(); + tmux(); + matugen(); + lowBattery(); + notifications(); + hyprland(); + } catch (error) { + logError(error); + } +} diff --git a/.config/ags/lib/matugen.ts b/.config/ags/lib/matugen.ts new file mode 100644 index 0000000..dfccccf --- /dev/null +++ b/.config/ags/lib/matugen.ts @@ -0,0 +1,113 @@ +import wallpaper from "service/wallpaper" +import options from "options" +import { sh, dependencies } from "./utils" + +export default function init() { + wallpaper.connect("changed", () => matugen()) + options.autotheme.connect("changed", () => matugen()) +} + +function animate(...setters: Array<() => void>) { + const delay = options.transition.value / 2 + setters.forEach((fn, i) => Utils.timeout(delay * i, fn)) +} + +export async function matugen( + type: "image" | "color" = "image", + arg = wallpaper.wallpaper, +) { + if (!options.autotheme.value || !dependencies("matugen")) + return + + const colors = await sh(`matugen --dry-run -j hex ${type} ${arg}`) + const c = JSON.parse(colors).colors as { light: Colors, dark: Colors } + const { dark, light } = options.theme + + animate( + () => { + dark.widget.value = c.dark.on_surface + light.widget.value = c.light.on_surface + }, + () => { + dark.border.value = c.dark.outline + light.border.value = c.light.outline + }, + () => { + dark.bg.value = c.dark.surface + light.bg.value = c.light.surface + }, + () => { + dark.fg.value = c.dark.on_surface + light.fg.value = c.light.on_surface + }, + () => { + dark.primary.bg.value = c.dark.primary + light.primary.bg.value = c.light.primary + options.bar.battery.charging.value = options.theme.scheme.value === "dark" + ? c.dark.primary : c.light.primary + }, + () => { + dark.primary.fg.value = c.dark.on_primary + light.primary.fg.value = c.light.on_primary + }, + () => { + dark.error.bg.value = c.dark.error + light.error.bg.value = c.light.error + }, + () => { + dark.error.fg.value = c.dark.on_error + light.error.fg.value = c.light.on_error + }, + ) +} + +type Colors = { + background: string + error: string + error_container: string + inverse_on_surface: string + inverse_primary: string + inverse_surface: string + on_background: string + on_error: string + on_error_container: string + on_primary: string + on_primary_container: string + on_primary_fixed: string + on_primary_fixed_variant: string + on_secondary: string + on_secondary_container: string + on_secondary_fixed: string + on_secondary_fixed_variant: string + on_surface: string + on_surface_variant: string + on_tertiary: string + on_tertiary_container: string + on_tertiary_fixed: string + on_tertiary_fixed_variant: string + outline: string + outline_variant: string + primary: string + primary_container: string + primary_fixed: string + primary_fixed_dim: string + scrim: string + secondary: string + secondary_container: string + secondary_fixed: string + secondary_fixed_dim: string + shadow: string + surface: string + surface_bright: string + surface_container: string + surface_container_high: string + surface_container_highest: string + surface_container_low: string + surface_container_lowest: string + surface_dim: string + surface_variant: string + tertiary: string + tertiary_container: string + tertiary_fixed: string + tertiary_fixed_dim: string +} diff --git a/.config/ags/lib/notifications.ts b/.config/ags/lib/notifications.ts new file mode 100644 index 0000000..0000831 --- /dev/null +++ b/.config/ags/lib/notifications.ts @@ -0,0 +1,16 @@ +import options from "options" +const notifs = await Service.import("notifications") + +// TODO: consider adding this to upstream + +const { blacklist } = options.notifications + +export default function init() { + const notify = notifs.constructor.prototype.Notify.bind(notifs) + notifs.constructor.prototype.Notify = function(appName: string, ...rest: unknown[]) { + if (blacklist.value.includes(appName)) + return Number.MAX_SAFE_INTEGER + + return notify(appName, ...rest) + } +} diff --git a/.config/ags/lib/option.ts b/.config/ags/lib/option.ts new file mode 100644 index 0000000..2d73978 --- /dev/null +++ b/.config/ags/lib/option.ts @@ -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) { + 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) + } + }, + }) +} + diff --git a/.config/ags/lib/session.ts b/.config/ags/lib/session.ts new file mode 100644 index 0000000..0e3e0cf --- /dev/null +++ b/.config/ags/lib/session.ts @@ -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`) diff --git a/.config/ags/lib/tmux.ts b/.config/ags/lib/tmux.ts new file mode 100644 index 0000000..1372eb2 --- /dev/null +++ b/.config/ags/lib/tmux.ts @@ -0,0 +1,14 @@ +import options from "options" +import { sh } from "./utils" + +export async function tmux() { + const { scheme, dark, light } = options.theme + const hex = scheme.value === "dark" ? dark.primary.bg.value : light.primary.bg.value + if (await sh("which tmux")) + sh(`tmux set @main_accent "${hex}"`) +} + +export default function init() { + options.theme.dark.primary.bg.connect("changed", tmux) + options.theme.light.primary.bg.connect("changed", tmux) +} diff --git a/.config/ags/lib/utils.ts b/.config/ags/lib/utils.ts new file mode 100644 index 0000000..f3ff2e3 --- /dev/null +++ b/.config/ags/lib/utils.ts @@ -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 const HOME = GLib.get_home_dir(); + +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).map(widget).flat(1) +} + +/** + * @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 => { + return !Utils.exec(`which ${bin}`) + }) + + if (missing.length > 0) { + console.warn("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 +} diff --git a/.config/ags/lib/variables.ts b/.config/ags/lib/variables.ts new file mode 100644 index 0000000..78d8793 --- /dev/null +++ b/.config/ags/lib/variables.ts @@ -0,0 +1,47 @@ +import GLib from "gi://GLib" +// import options from "options" +// +// const intval = options.system.fetchInterval.value +// const tempPath = options.system.temperature.value + +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 user = { + name: GLib.get_user_name() +} + +export const distro = { + id: GLib.get_os_info("ID"), + logo: GLib.get_os_info("LOGO"), +} + +// const divide = ([total, free]: string[]) => Number.parseInt(free) / Number.parseInt(total) +// +// export const cpu = Variable(0, { +// poll: [intval, "top -b -n 1", out => divide(["100", out.split("\n") +// .find(line => line.includes("Cpu(s)")) +// ?.split(/\s+/)[1] +// .replace(",", ".") || "0"])], +// }) +// +// export const ram = Variable(0, { +// poll: [intval, "free", out => divide(out.split("\n") +// .find(line => line.includes("Mem:")) +// ?.split(/\s+/) +// .splice(1, 2) || ["1", "1"])], +// }) +// +// export const temperature = Variable(0, { +// poll: [intval, `cat ${tempPath}`, n => { +// return Number.parseInt(n) / 100_000 +// }], +// }) |
