diff --git a/ags/app.ts b/ags/app.ts index 8a25f48..5e863fc 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -1,21 +1,21 @@ import { App } from "astal/gtk3" - -import { OSD, OSDModes, setOSDMode } from "./window/OSD"; -import { ControlCenter } from "./window/ControlCenter"; +import { Windows } from "./windows"; +import { Wireplumber } from "./scripts/volume"; import { runStyleHandler } from "./scripts/style-handler"; import { handleArguments } from "./scripts/arg-handler"; -import { Wireplumber } from "./scripts/volume"; -import { Windows } from "./windows"; import { Time, timeout } from "astal/time"; +import { OSD, OSDModes, setOSDMode } from "./window/OSD"; +import { ControlCenter } from "./window/ControlCenter"; + let osdTimer: (Time|undefined); App.start({ instanceName: "astal", - requestHandler(request: string, res: (result: any) => void) { - console.log(`[LOG] Arguments received: ${request}`) - res(handleArguments(request)); + requestHandler(request: string, response: (result: any) => void) { + console.log(`[LOG] Arguments received: ${request}`); + response(handleArguments(request)); }, main() { console.log(`[LOG] Initialized astal instance as: ${ App.instanceName || "astal" }`); @@ -26,7 +26,6 @@ App.start({ Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => !Windows.isVisible(ControlCenter) && triggerOSD(OSDModes.SINK)); - } }); diff --git a/ags/i18n/intl.ts b/ags/i18n/intl.ts index 38a57f7..e9db088 100644 --- a/ags/i18n/intl.ts +++ b/ags/i18n/intl.ts @@ -1,14 +1,15 @@ //TODO use I18n system >.< +import en_US from "./lang/en_US"; +import pt_BR from "./lang/pt_BR"; import { GLib } from "astal"; const i18nKeys = { - "en_US": (() => import("./lang/en_US")!)(), - "pt_BR": (() => import("./lang/pt_BR")!)() -} + "en_US": en_US, + "pt_BR": pt_BR +}; -const languages: Array = (() => - Object.keys(i18nKeys))() +const languages: Array = Object.keys(i18nKeys); const defaultLanguage: string = languages[0]; let language: string = getSystemLanguage(); @@ -17,16 +18,16 @@ export function getSystemLanguage(): string { const sysLanguage: (string|null|undefined) = GLib.getenv("LANG") || GLib.getenv("LANGUAGE"); if(!sysLanguage) { - console.log(`[WARNING] Couldn't get system language, fallback to default ${defaultLanguage || "en_US"}`); + console.log(`[WARNING] Couldn't get system language, fallback to default ${defaultLanguage}`); console.log("[TIP] Please set the LANG or LANGUAGE environment variable"); - return defaultLanguage || "en_US"; + return "en_US"; } return sysLanguage.split('.')[0]; } -export function setLanguage(lang: keyof typeof i18nKeys): (string|Error) { +export function setLanguage(lang: keyof typeof i18nKeys): string { languages.map((cur: string) => { if(cur === lang) { language = lang; @@ -34,20 +35,19 @@ export function setLanguage(lang: keyof typeof i18nKeys): (string|Error) { } }); - throw new Error(`[i18n/intl] Couldn't set language: ${lang}`, { + throw new Error(`(i18n/intl) Couldn't set language: ${lang}`, { cause: `Language ${lang} not found in languages of type ${typeof languages}` }); } -export function tr(key: string): (string|undefined) { - let result = i18nKeys[language as keyof typeof i18nKeys], - defResult = i18nKeys[defaultLanguage as keyof typeof i18nKeys]; +export function tr(key: string): string { + let result = i18nKeys[language as keyof typeof i18nKeys], + defResult = i18nKeys[defaultLanguage as keyof typeof i18nKeys]; for(const keyString in key.split('.')) { - console.log(result); - result = result[keyString as keyof typeof result]; - defResult = defResult[keyString as keyof typeof defResult]; + result = result[keyString as keyof typeof result] as never; + defResult = defResult[keyString as keyof typeof defResult] as never; } - return (result as never) || (defResult as never) || undefined; + return (result as never) || (defResult as never) || "couldn't find i18n key"; } diff --git a/ags/scripts/apps.ts b/ags/scripts/apps.ts index 8b8d930..9eadab9 100644 --- a/ags/scripts/apps.ts +++ b/ags/scripts/apps.ts @@ -12,6 +12,10 @@ export function updateApps(): void { appsList = astalApps.get_list(); } +export function getAstalApps(): AstalApps.Apps { + return astalApps; +} + export function getAppsByName(appName: string): (Array|undefined) { let found: Array = []; diff --git a/ags/scripts/arg-handler.ts b/ags/scripts/arg-handler.ts index 625ad7d..920560c 100644 --- a/ags/scripts/arg-handler.ts +++ b/ags/scripts/arg-handler.ts @@ -2,6 +2,9 @@ import { Gtk } from "astal/gtk3"; import { Windows } from "../windows"; import { restartInstance } from "./reload-handler"; import { Wireplumber } from "./volume"; +import { startRunnerDefault } from "../window/Runner"; +import { AskPopup } from "../widget/AskPopup"; +import { execAsync } from "astal"; export function handleArguments(request: string): any { const args: Array = request.split(" "); @@ -20,7 +23,18 @@ export function handleArguments(request: string): any { case "reload": restartInstance(); - return "Reloading instance..." + return "Restarting instance..." + + case "runner": + startRunnerDefault(); + return "Opening runner..." + + case "test": + return AskPopup({ + onAccept: () => execAsync("notify-send -u normal haha dumb"), + text: "Would you accept?", + title: "Dumb Question" + }); default: return "command not found! try checking help"; @@ -143,6 +157,7 @@ Options: toggle [window_name]: toggles visibility of specified window. reload: creates a new astal instance and removes this one. volume: wireplumber volume controller, see "volume help". + runner: open the application runner. help, -h, --help: shows this help message. 2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License. diff --git a/ags/scripts/brightness.ts b/ags/scripts/brightness.ts index 085b31c..b98067d 100644 --- a/ags/scripts/brightness.ts +++ b/ags/scripts/brightness.ts @@ -1,4 +1,4 @@ -import { exec, execAsync, GObject, monitorFile, Process, readFileAsync, register, signal } from "astal"; +import { exec, execAsync, GObject, monitorFile, readFileAsync, register, signal } from "astal"; import { Connectable } from "astal/binding"; diff --git a/ags/scripts/notification-handler.ts b/ags/scripts/notification-handler.ts deleted file mode 100644 index bc99437..0000000 --- a/ags/scripts/notification-handler.ts +++ /dev/null @@ -1,172 +0,0 @@ -import AstalNotifd from "gi://AstalNotifd"; -import { timeout } from "astal/time"; -import { Subscribable } from "astal/binding"; -import { GObject, property, register, Variable } from "astal"; -import { Windows } from "../windows"; -import { FloatingNotifications } from "../window/FloatingNotifications"; -import { Gtk, Widget } from "astal/gtk3"; - -@register({ GTypeName: "Notifications" }) -class NotificationsClass extends GObject.Object implements Subscribable { - - private static instance: NotificationsClass; - - @property(AstalNotifd.Notifd) - private notifd: AstalNotifd.Notifd; - - @property(Boolean) - private doNotDisturb: boolean = false; - - @property() - public notificationHistory: Array = []; - - @property() - public notifications: Variable> = new Variable>([]); - - public static getDefault(): NotificationsClass { - if(!NotificationsClass.instance) { - NotificationsClass.instance = new NotificationsClass(); - } - - return NotificationsClass.instance; - } - - constructor() { - super(); - this.notifd = new AstalNotifd.Notifd({ - ignoreTimeout: true, - dontDisturb: false - } as AstalNotifd.Notifd.ConstructorProps); - - this.getNotifd().connect("notified", (daemon: AstalNotifd.Notifd, id: number) => { - const notification: (AstalNotifd.Notification|null) = daemon.get_notification(id); - if(!notification) { - console.log("[LOG] Notification is null, ignoring"); - return; - } - - if(!this.doNotDisturb) { - this.handleNotification(notification); - return; - } - - this.addHistory(notification); - }); - } - - public handleNotification(notification: AstalNotifd.Notification): void { - Windows.open(FloatingNotifications); - - let tmpArray = this.notifications.get().reverse(); - tmpArray.push(notification); - this.notifications.set(tmpArray.reverse()); - - // default timeout if undefined - let notificationTimeout = 4000; - - switch(notification.urgency) { - case AstalNotifd.Urgency.LOW: - notificationTimeout = 2000; - break; - case AstalNotifd.Urgency.NORMAL: - notificationTimeout = 4000; - break; - } - - notification.urgency !== AstalNotifd.Urgency.CRITICAL && - timeout(notificationTimeout, () => { - this.notifications.set(this.notifications.get().filter((item) => item.id !== notification.id)); - this.addHistory(notification); - }); - - } - - public addHistory(notification: AstalNotifd.Notification): void { - let tmpArray: Array = this.notificationHistory.reverse() - .filter((item: AstalNotifd.Notification) => item.id !== notification.id); - tmpArray.push(notification); - this.notificationHistory = tmpArray.reverse(); - } - - public removeHistory(notification: AstalNotifd.Notification) { - this.notificationHistory = this.notificationHistory.filter((curNotification: AstalNotifd.Notification) => - curNotification.id !== notification.id); - } - - public getNotifd(): AstalNotifd.Notifd { - return this.notifd; - } - - get() { - return this.notifications.get(); - } - - subscribe(callback: (list: Array) => void) { - return this.notifications.subscribe(callback); - } -} - -function NotificationWidget(notification: AstalNotifd.Notification): Gtk.Widget { - return new Widget.Box({ - className: "notification", - homogeneous: false, - expand: false, - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Box({ - className: "top", - orientation: Gtk.Orientation.HORIZONTAL, - hexpand: true, - vexpand: false, - children: [ - new Widget.Icon({ - className: "icon", - visible: notification.appIcon !== "", - icon: notification.appIcon || "image-missing", - iconSize: Gtk.IconSize.DND, - css: ".icon { font-size: 24px; }" - }), - new Widget.Label({ - className: "app-name", - halign: Gtk.Align.START, - label: notification.appName || "Unknown Application" - } as Widget.LabelProps), - new Widget.Button({ - className: "close nf", - onClick: () => notification.dismiss(), - label: "󰅖" - } as Widget.ButtonProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "content", - orientation: Gtk.Orientation.HORIZONTAL, - children: [ - new Widget.Box({ - className: "image", - visible: notification.image !== "", - css: `box.image { background-image: url('${notification.image}'); }` - } as Widget.BoxProps), - new Widget.Box({ - className: "text", - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Label({ - className: "summary", - useMarkup: true, - label: notification.summary - }), - new Widget.Label({ - className: "body", - useMarkup: true, - label: notification.body - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps); -} - -export const Notifications = new NotificationsClass(); diff --git a/ags/scripts/notifications.ts b/ags/scripts/notifications.ts new file mode 100644 index 0000000..9010117 --- /dev/null +++ b/ags/scripts/notifications.ts @@ -0,0 +1,103 @@ +import { GObject, property, register, signal, timeout } from "astal"; +import AstalNotifd from "gi://AstalNotifd"; + +@register({ GTypeName: "Notifications" }) +class Notifications extends GObject.Object { + private static instance: (Notifications|null) = null; + + #notifications: Array = []; + #history: Array = []; + #connections: Array; + + + @property() + public get notifications() { return this.#notifications }; + + @property() + public get history() { return this.#history }; + + + @signal(AstalNotifd.Notification) + declare notificationAdded: (notification: AstalNotifd.Notification) => void; + + @signal(Number) + declare notificationRemoved: (id: number) => void; + + @signal(AstalNotifd.Notification) + declare historyAdded: (notification: AstalNotifd.Notification) => void; + + @signal(Number) + declare historyRemoved: (id: number) => void; + + + constructor() { + super(); + + this.#connections = [ + AstalNotifd.get_default().connect("notified", (notifd, id, _replaced) => { + const notification = notifd.get_notification(id); + const notifTimeout = 4000; + + this.addNotification(notification, () => { + if(notification.urgency !== AstalNotifd.Urgency.CRITICAL) + timeout(notifTimeout, () => { + this.removeNotification(id); + }); + }); + }), + AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => { + this.removeNotification(id); + this.addHistory(notifd.get_notification(id)); + }) + ]; + + this.vfunc_dispose = () => { + this.#connections.map((id: number) => + AstalNotifd.get_default().disconnect(id)); + }; + } + + public static getDefault(): Notifications { + if(!this.instance) + this.instance = new Notifications(); + + return this.instance; + } + + private addHistory(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void { + const newArray = this.#history.reverse().filter((item) => item.id !== notif.id); + newArray.push(notif); + this.#history = newArray.reverse(); + this.notify("history"); + this.emit("history-added", notif); + onAdded && onAdded(notif); + } + + public removeHistory(notif: (AstalNotifd.Notification|number)): void { + const notifId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif; + this.#history = this.#history.filter((item: AstalNotifd.Notification) => + item.id !== notifId); + + this.notify("history"); + this.emit("history-removed", notifId); + } + + private addNotification(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void { + const newArray = this.#notifications.reverse().filter((item) => item.id !== notif.id); + newArray.push(notif); + this.#notifications = newArray.reverse(); + this.notify("notifications"); + this.emit("notification-added", notif); + onAdded && onAdded(notif); + } + + public removeNotification(notif: (AstalNotifd.Notification|number)): void { + const notifId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif; + this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) => + item.id !== notifId); + this.notify("notifications"); + this.emit("notification-removed", notifId); + } +} + +export { Notifications }; diff --git a/ags/scripts/recording.ts b/ags/scripts/recording.ts new file mode 100644 index 0000000..9491fe6 --- /dev/null +++ b/ags/scripts/recording.ts @@ -0,0 +1,88 @@ +import { execAsync, GLib, GObject, register, signal, writeFile } from "astal"; +import { Subscribable } from "astal/binding"; +import { Gdk } from "astal/gtk3"; +import { getDateTime } from "./time"; +import AstalWp from "gi://AstalWp"; + +@register({ GTypeName: "ScreenRecording" }) +class Recording extends GObject.Object implements Subscribable { + + private static instance: Recording; + + @signal() + declare started: () => void; + @signal(String) + declare stopped: (outputFile: string) => void; + @signal(String) + declare outputChanged: (newPath: string) => void; + + #recording: boolean = false; + #subs = new Set<(isRec: boolean) => void>(); + #path: string = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS) || `${GLib.get_home_dir()}/Recordings`; + /** Default extension: mp4(h264) */ + #extension: string = "mp4"; + #recordAudio: boolean|AstalWp.Endpoint = false; // TODO + + private notifySub() { + const subs = this.#subs; + for(const sub of subs) { + sub(this.recording); + } + } + + public get recording() { return this.#recording; } + private set recording(newValue: boolean) { this.#recording = newValue; } + + public get path() { return this.#path; } + public set path(newPath: string) { this.#path = newPath; } + + public get extension() { return this.#extension; } + public set extension(newExt: string) { this.#extension = newExt; } + + constructor() { + super(); + } + + public static getDefault() { + if(!this.instance) + this.instance = new Recording(); + + return this.instance; + } + + public get() { + return this.recording; + } + + private emit(id: string, ...args: any[]) { + super.emit(id, ...args); + this.notifySub(); + } + + + public startRecording(area?: Gdk.Rectangle) { + const output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension}`; + execAsync([ "wf-recorder", + `${Boolean(area) ? + `-g ${area?.x || 0},${area?.y || 0} ${area?.width || 1}x${area?.height || 1}` + : ""}`, + "-f", output ] + ).then(() => { + this.emit("stopped", `${this.path}/${output}`); + }); + writeFile("", ""); + this.emit("started"); + this.notifySub(); + } + + public stopRecording() { + + } + + public subscribe(callback: (isRec: boolean) => void) { + this.#subs.add(callback); + return () => this.#subs.delete(callback); + } +} + +export { Recording }; diff --git a/ags/scripts/runner/applications.ts b/ags/scripts/runner/applications.ts deleted file mode 100644 index e69de29..0000000 diff --git a/ags/scripts/runner/apps.ts b/ags/scripts/runner/apps.ts new file mode 100644 index 0000000..52554a5 --- /dev/null +++ b/ags/scripts/runner/apps.ts @@ -0,0 +1,15 @@ +import AstalHyprland from "gi://AstalHyprland"; +import { getAstalApps } from "../apps"; +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import AstalApps from "gi://AstalApps"; + +export function handleApplications(search: string): (Array|null) { + return getAstalApps().fuzzy_query(search).map((app: AstalApps.Application) => + new ResultWidget({ + title: app.get_name(), + description: app.get_description(), + icon: app.iconName, + onClick: () => AstalHyprland.get_default().dispatch("exec", app.get_executable()) + } as ResultWidgetProps) + ) || null; +} diff --git a/ags/scripts/runner/math.ts b/ags/scripts/runner/math.ts deleted file mode 100644 index e69de29..0000000 diff --git a/ags/scripts/runner/shell.ts b/ags/scripts/runner/shell.ts new file mode 100644 index 0000000..81ec982 --- /dev/null +++ b/ags/scripts/runner/shell.ts @@ -0,0 +1,14 @@ +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import AstalHyprland from "gi://AstalHyprland"; +import { GLib } from "astal"; + +export function handleShell(command: string): ResultWidget { + const userShell = GLib.getenv("SHELL") || "/usr/bin/env bash"; + + return new ResultWidget({ + onClick: () => AstalHyprland.get_default().dispatch("exec", `${userShell} -c "${command}"`), + title: `Run: \`${command}\``, + description: userShell, + icon: "utilities-terminal-symbolic" + } as ResultWidgetProps); +} diff --git a/ags/scripts/runner/websearch.ts b/ags/scripts/runner/websearch.ts new file mode 100644 index 0000000..b2c887a --- /dev/null +++ b/ags/scripts/runner/websearch.ts @@ -0,0 +1,42 @@ +import AstalHyprland from "gi://AstalHyprland"; +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; + +export enum SearchEngine { + GOOGLE, + DUCKDUCKGO, + YAHOO +} + +export const SearchEngineMap: Map = new Map([ + [ SearchEngine.DUCKDUCKGO, "https://duckduckgo.com/?q=" ], + [ SearchEngine.GOOGLE, "https://google.com/search?q=" ], + [ SearchEngine.YAHOO, "https://search.yahoo.com/search?p=" ] +]); + +let searchEngine: SearchEngine = SearchEngine.GOOGLE; + +export function handleWebSearch(search: string): ResultWidget { + + let engineString: string; + + switch(searchEngine as SearchEngine) { + case SearchEngine.GOOGLE: + engineString = "Google"; + case SearchEngine.YAHOO: + engineString = "Yahoo"; + case SearchEngine.DUCKDUCKGO: + engineString = "DuckDuckGo"; + default: engineString = "Web"; + + } + + return new ResultWidget({ + icon: "system-search-symbolic", + title: search || "", + description: `Search with ${engineString}`, + onClick: () => AstalHyprland.get_default().dispatch( + "exec", + `xdg-open "${SearchEngineMap.get(searchEngine)! + search.replaceAll(" ", "%20")}"` + ) + } as ResultWidgetProps); +} diff --git a/ags/scripts/style-handler.ts b/ags/scripts/style-handler.ts index fe2b327..7fdc73c 100644 --- a/ags/scripts/style-handler.ts +++ b/ags/scripts/style-handler.ts @@ -1,8 +1,10 @@ // handles reloading stylesheet and pywal colors -import { readFile, monitorFile, Process } from "astal"; +import { readFile, monitorFile, AstalIO, exec, timeout } from "astal"; import { App } from "astal/gtk3"; -import { getUserDirs } from "./user"; +import { getUserDirs } from "./utils"; + +let watchDelay: (AstalIO.Time|null); const stylePath = `${getUserDirs().state}/ags/style` const watchPaths = [ @@ -22,8 +24,8 @@ export function reloadStyle(): void { export function compileStyle(): void { console.log("[LOG] Compiling sass (stylesheet)"); - Process.exec(`mkdir -p ${stylePath}`); - Process.exec(`sh -c "sass -I ./style ./style.scss ${stylePath}/style.css"`); + exec(`mkdir -p ${stylePath}`); + exec(`sh -c "sass -I ./style ./style.scss ${stylePath}/style.css"`); } export function applyStyle(): void { @@ -34,14 +36,15 @@ export function applyStyle(): void { ); } +/** Monitor changes on stylesheet at runtime */ function watch(): void { - // Monitor changes on stylesheet at runtime watchPaths.map((path: string) => monitorFile( `${path}`, (file: string) => { // Ignore tmp files - if(!file.endsWith('~') && !Number.isNaN(file)) { + if(!watchDelay && !file.endsWith('~') && !Number.isNaN(file)) { + watchDelay = timeout(250, () => watchDelay = null); console.log(`[LOG] Stylesheet ${file} file updated`) compileStyle(); applyStyle(); @@ -54,7 +57,7 @@ function watch(): void { monitorFile( `${getUserDirs().cache}/wal/colors.scss`, (file: string) => { - Process.exec(`bash -c "cp -f ${file} ./style/_wal.scss"`) + exec(`bash -c "cp -f ${file} ./style/_wal.scss"`) } ); } diff --git a/ags/scripts/user.ts b/ags/scripts/utils.ts similarity index 53% rename from ags/scripts/user.ts rename to ags/scripts/utils.ts index db36739..bef3024 100644 --- a/ags/scripts/user.ts +++ b/ags/scripts/utils.ts @@ -1,4 +1,4 @@ -import { GLib } from "astal"; +import { execAsync, GLib } from "astal"; export function getUserDirs() { return { @@ -7,5 +7,13 @@ export function getUserDirs() { cache: GLib.getenv("XDG_CACHE_HOME"), config: GLib.getenv("XDG_CONFIG_HOME"), data: GLib.getenv("XDG_DATA_HOME") - } as const; + }; +} + +export function makeDirectory(dir: string): void { + execAsync([ "mkdir", "-p", dir ]); +} + +export function deleteFile(path: string): void { + execAsync([ "rm", "-r", path ]); } diff --git a/ags/scripts/varmap.ts b/ags/scripts/varmap.ts new file mode 100644 index 0000000..16b59e8 --- /dev/null +++ b/ags/scripts/varmap.ts @@ -0,0 +1,89 @@ +import { Subscribable } from "astal/binding"; + +export class VarMap implements Subscribable { + + #subs = new Set<(v: Map) => void>(); + #map: Map; + + constructor(initial?: Map) { + this.#map = initial || new Map(); + } + + private notifyMap() { + const subs = this.#subs; + for(const sub of subs) { + sub(this.#map); + } + } + + public get(): Map { + return this.#map; + } + + public get size(): number { + return this.#map.size; + } + + public getValue(key: K): (V|undefined) { + return this.#map.get(key); + } + + public getKeyAt(index: number): (K|undefined) { + return [...this.#map.keys()][index]; + } + + public getValueAt(index: number): (V|undefined) { + return [...this.#map.values()][index]; + } + + public set(key: K, value: V): Map { + const newMap: Map = this.#map.set(key, value); + this.notifyMap(); + + return newMap; + } + + public delete(key: K): boolean { + const deleted: boolean = this.#map.delete(key); + this.notifyMap(); + return deleted; + } + + public has(key: K): boolean { + return this.#map.has(key); + } + + public clear(): void { + this.#map.clear(); + this.notifyMap(); + } + + public entries(): MapIterator<[K, V]> { + return this.#map.entries(); + } + + public keys(): MapIterator { + return this.#map.keys(); + } + + public values(): MapIterator { + return this.#map.values(); + } + + public forEach (callback: (value: V, key: K, map: Map) => ReturnType): ReturnType[] { + const result: Array = []; + for(const entry of this.#map.entries()) { + result.push(callback(entry[1], entry[0], this.#map)); + } + + return result; + } + + public subscribe(callback: (v: Map) => void): () => void { + this.#subs.add(callback); + + return () => { + this.#subs.delete(callback); + } + } +} diff --git a/ags/style.scss b/ags/style.scss index 9a880c1..4dd37a3 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -9,6 +9,9 @@ @use "./style/control-center"; @use "./style/center-window"; @use "./style/float-notifications"; +@use "./style/logout-menu"; +@use "./style/apps-window"; +@use "./style/runner"; * { @@ -19,6 +22,96 @@ window * { @include mixins.default-styles; } +window.ask-popup { + background: rgba(black, .4); +} + +.ask-popup-box { + background: colors.$bg-translucent; + padding: 18px; + border-radius: 24px; + + & .title { + font-size: 20px; + font-weight: 700; + } + + & .buttons { + margin-top: 20px; + + & button { + background: colors.$bg-primary; + border-radius: 12px; + padding: 6px; + + margin: { + left: 4px; + right: 4px; + }; + + &:hover { + background: colors.$bg-secondary; + } + } + } +} + +.notification { + background: colors.$bg-primary; + border-radius: 16px; + + & > .top { + padding: 8px; + padding-bottom: 0; + + & .app-icon { + margin-right: 6px; + } + + & .app-name { + font-size: 12px; + } + + & button.close { + padding: 2px; + border-radius: 8px; + + &:hover { + background: colors.$bg-secondary; + } + } + + & icon.close { + font-size: 16px; + } + } + + & .content { + padding: 4px; + padding-top: 0; + & .image { + $size: 78px; + min-width: $size; + min-height: $size; + background-size: cover; + background-position: center; + margin: 6px; + border-radius: 8px; + } + + & .summary { + font-size: 17.3px; + font-weight: 700; + margin-bottom: 4px; + } + + & .body { + font-size: 14.5px; + font-weight: 400; + } + } +} + tooltip { padding: 16px; @@ -32,3 +125,40 @@ tooltip { box-shadow: 0 1px 4px 1px rgba(colors.$bg-primary, .6); } } + +menu { + padding: 4px; + background: wal.$background; + border-radius: 14px; + + & separator { + margin: 0 4px; + color: wal.$background; + } + + & menuitem { + padding: 8px 16px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; + + &:hover { + background: wal.$color1; + } + } +} + +scrollbar trough { + @include mixins.reset-props; + background: colors.$bg-translucent; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + padding: 0 2px; + + & slider { + @include mixins.reset-props; + min-width: .85em; + background: colors.$bg-tertiary; + border-radius: 12px; + } +} diff --git a/ags/style/_apps-window.scss b/ags/style/_apps-window.scss new file mode 100644 index 0000000..a2e289a --- /dev/null +++ b/ags/style/_apps-window.scss @@ -0,0 +1,5 @@ +.apps-window.container { + & > entry { + + } +} diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss index 5b74654..1425d8b 100644 --- a/ags/style/_control-center.scss +++ b/ags/style/_control-center.scss @@ -89,7 +89,7 @@ & > .content { padding: 8px; - padding-right: 0; + & > .icon { margin-right: 6px; } @@ -101,7 +101,8 @@ } & > .description { - font-size: 13px; + font-size: 12px; + color: colors.$fg-disabled; font-weight: 400; } } @@ -123,3 +124,36 @@ } } } + +.pages > .page { + background: colors.$bg-secondary; + padding: 14px; + border-radius: 24px; + + & .header { + margin-bottom: 6px; + + & > .title:first-child { + font-size: 20px; + font-weight: 600; + } + + & > .description { + font-size: 12px; + font-weight: 500; + color: colors.$fg-disabled; + } + } + + &.bluetooth { + .connections button { + @include mixins.hover-shadow; + padding: 6px; + border-radius: 12px; + + &.connected { + background: colors.$bg-tertiary; + } + } + } +} diff --git a/ags/style/_float-notifications.scss b/ags/style/_float-notifications.scss index 2144b85..91c6195 100644 --- a/ags/style/_float-notifications.scss +++ b/ags/style/_float-notifications.scss @@ -1,4 +1,5 @@ @use "./colors"; +@use "./mixins"; .floating-notifications-container { padding: { @@ -6,28 +7,11 @@ top: 6px; }; - & > .notification { - background: colors.$bg-primary; - border-radius: 16px; - padding: 12px; - margin: 6px 0; + & revealer { + padding: 6px; - & > .top { - & .app-name { - font-size: 12px; - color: colors.$fg-disabled; - } - } - - & .content { - & .image { - $size: 78px; - min-width: $size; - min-height: $size; - background-size: cover; - background-position: center 0; - margin: 6px; - } + & > .notification { + box-shadow: 0 0 4px .5px colors.$bg-translucent; } &:first-child { diff --git a/ags/style/_logout-menu.scss b/ags/style/_logout-menu.scss new file mode 100644 index 0000000..4644ef0 --- /dev/null +++ b/ags/style/_logout-menu.scss @@ -0,0 +1,37 @@ +.logout-menu { + .top { + .time { + font-size: 128px; + font-weight: 900; + } + .date { + font-size: 24px; + font-weight: 500; + } + } + .button-row { + margin: 0 150px; + + & > button { + & label { + font-size: 96px; + } + + margin: { + left: 4px; + right: 4px; + } + border-radius: 6px; + + &:first-child { + border-top-left-radius: 28px; + border-bottom-left-radius: 28px; + } + + &:last-child { + border-top-right-radius: 28px; + border-bottom-right-radius: 28px; + } + } + } +} diff --git a/ags/style/_mixins.scss b/ags/style/_mixins.scss index 23cb574..f3ad84e 100644 --- a/ags/style/_mixins.scss +++ b/ags/style/_mixins.scss @@ -14,6 +14,12 @@ color: colors.$fg-primary; } +@mixin hover-shadow { + &:hover { + box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .1); + } +} + @mixin default-styles { .button-row { & > button { @@ -38,6 +44,10 @@ } } + selection { + background: colors.$bg-tertiary; + } + label.nf, button.nf label { font-size: 12px; @@ -47,28 +57,6 @@ "Font Awesome"; } - & menu { - padding: 4px; - background: wal.$background; - border-radius: 14px; - - & separator { - margin: 0 4px; - color: wal.$background; - } - - & menuitem { - padding: 8px 16px; - border-radius: 10px; - font-size: 12px; - font-weight: 600; - - &:hover { - background: wal.$color1; - } - } - } - & trough { background: funs.toRGB(color.adjust($color: wal.$color1, $lightness: -20%)); border-radius: 8px; diff --git a/ags/style/_runner.scss b/ags/style/_runner.scss new file mode 100644 index 0000000..04c0849 --- /dev/null +++ b/ags/style/_runner.scss @@ -0,0 +1,77 @@ +@use "./colors"; + +.runner.main { + background: colors.$bg-translucent; + padding: 12px; + border-radius: 24px; + + & > * { + margin: 4px 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + & entry { + background: colors.$bg-primary; + padding: 10px 9px; + border-radius: 12px; + + &:focus { + box-shadow: inset 0 0 0 2px colors.$bg-secondary; + } + + & image.left { + margin-right: 6px; + } + } + + & list { + & eventbox:focus > box, + & eventbox:hover > box, + & listboxchild:selected eventbox > box, + & listboxchild:active eventbox > box { + background: colors.$bg-secondary; + } + } + + & list eventbox > .result { + padding: 10px; + background: colors.$bg-primary; + margin: 2px 0; + border-radius: 14px; + + & icon { + font-size: 28px; + margin-right: 6px; + } + + & .title { + font-weight: 500; + font-size: 16px; + } + + & .description { + font-size: 12px; + color: colors.$fg-disabled; + } + } + + & .not-found { + padding-top: 24px; + + & icon { + font-size: 64px; + margin-bottom: .4em; + } + + & label { + font-size: 16px; + } + } +} diff --git a/ags/style/_wal.scss b/ags/style/_wal.scss index 5c9c132..6a903c4 100644 --- a/ags/style/_wal.scss +++ b/ags/style/_wal.scss @@ -1,26 +1,26 @@ // SCSS Variables // Generated by 'wal' -$wallpaper: "/home/joaov/wallpapers/Bocchi The Rock!.png"; +$wallpaper: "/home/joaov/wallpapers/Miku Guitar.jpg"; // Special -$background: #0a0a0c; -$foreground: #c1c1c2; -$cursor: #c1c1c2; +$background: #171418; +$foreground: #c5c4c5; +$cursor: #c5c4c5; // Colors -$color0: #0a0a0c; -$color1: #935d6d; -$color2: #967e84; -$color3: #ac8486; -$color4: #bcae7a; -$color5: #a49c9c; -$color6: #bcb79c; -$color7: #8a8a96; -$color8: #565669; -$color9: #C57C92; -$color10: #C9A9B0; -$color11: #E6B1B3; -$color12: #FBE8A3; -$color13: #DBD1D0; -$color14: #FBF5D1; -$color15: #c1c1c2; +$color0: #171418; +$color1: #607985; +$color2: #208FB6; +$color3: #4C9CB4; +$color4: #63ADC9; +$color5: #C3B49C; +$color6: #89BBCF; +$color7: #96909b; +$color8: #715c71; +$color9: #6aa4bf; +$color10: #62a5bc; +$color11: #78b3c5; +$color12: #90becf; +$color13: #dac7ab; +$color14: #a7cbd9; +$color15: #c5c4c5; diff --git a/ags/tsconfig.json b/ags/tsconfig.json index 1fc22db..d79ca1e 100644 --- a/ags/tsconfig.json +++ b/ags/tsconfig.json @@ -7,7 +7,7 @@ "module": "ES2022", "moduleResolution": "Bundler", "checkJs": true, - "allowJs": true, + "allowJs": false, "jsx": "react-jsx", "jsxImportSource": "astal/gtk3" } diff --git a/ags/widget/AskPopup.ts b/ags/widget/AskPopup.ts new file mode 100644 index 0000000..36793b7 --- /dev/null +++ b/ags/widget/AskPopup.ts @@ -0,0 +1,83 @@ +import { Binding } from "astal"; +import { PopupWindow, PopupWindowProps } from "./PopupWindow"; +import { Astal, Gtk, Widget } from "astal/gtk3"; +import { Separator } from "./Separator"; + +export type AskPopupProps = { + title?: string | Binding; + text: string | Binding; + cancelText?: string; + acceptText?: string; + onAccept: () => void; + onCancel?: () => void; +}; + +/** + * A Popup Widget that asks yes or no to a certain question. + * Runs onAccept() when user accepts or else onDecline() when + * user doesn't accept or closes window. + */ +export function AskPopup(props: AskPopupProps) { + const buttons = [ + new Widget.Button({ + className: "cancel", + hexpand: true, + label: props.cancelText || "Cancel", + onClick: (_) => { + window.destroy(); + props.onCancel && props.onCancel(); + } + } as Widget.ButtonProps), + new Widget.Button({ + className: "accept", + hexpand: true, + label: props.acceptText || "Ok", + onClick: (_) => { + window.destroy(); + props.onAccept && props.onAccept(); + } + } as Widget.ButtonProps) + ]; + + const window = PopupWindow({ + namespace: "ask-popup", + visible: true, + className: "ask-popup", + exclusivity: Astal.Exclusivity.IGNORE, + widthRequest: 350, + heightRequest: 200, + onClose: (_) => { + props.onCancel && props.onCancel(); + _.destroy(); + }, + child: new Widget.Box({ + className: "ask-popup-box", + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Label({ + className: "title", + visible: Boolean(props.title), + label: props.title || "" + } as Widget.LabelProps), + Separator({ + alpha: .2, + orientation: Gtk.Orientation.VERTICAL + }), + new Widget.Label({ + className: "text", + label: props.text, + yalign: 0, + expand: true + } as Widget.LabelProps), + new Widget.Box({ + className: "buttons", + orientation: Gtk.Orientation.HORIZONTAL, + hexpand: true, + heightRequest: 38, + homogeneous: true, + children: buttons + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + } as PopupWindowProps); +} diff --git a/ags/widget/Calendar.ts b/ags/widget/Calendar.ts index 368b697..4fa5271 100644 --- a/ags/widget/Calendar.ts +++ b/ags/widget/Calendar.ts @@ -1,5 +1,4 @@ -//TODO Needs more work - +import { register, Variable } from "astal"; import { Gtk, Widget } from "astal/gtk3"; type CalendarProps = Pick & { - showWeekDays: boolean; - showHeader: boolean; - fillGrid: boolean; // I need a better name for this LMAOOO + showWeekDays?: boolean; + showHeader?: boolean; + fillGrid?: boolean; // I need a better name for this LMAOOO }; -export function Calendar(props?: Partial): Gtk.Widget { - return new Widget.Box({ - ...props, - children: [] - } as Widget.BoxProps); +@register({ GTypeName: "Calendar" }) +class Calendar extends Gtk.Box { + #showWeekDays = new Variable(true); + #showHeader = new Variable(true); + #fillGrid = new Variable(false); + + set fillGrid(newValue: boolean) { this.#fillGrid.set(newValue); } + get fillGrid() { return this.#fillGrid.get(); } + set showHeader(newValue: boolean) { this.#showHeader.set(newValue); } + get showHeader() { return this.#showHeader.get(); } + set showWeekDays(newValue: boolean) { this.#showWeekDays.set(newValue); } + get showWeekDays() { return this.#showWeekDays.get(); } + + constructor(props?: CalendarProps) { + super(); + this.add(new Widget.Box({ + ...props, + widthRequest: 128, + heightRequest: 128, + children: [ + new Widget.Box({ + className: "header", + heightRequest: 24, + hexpand: true, + + } as Widget.BoxProps) + ] + } as Widget.BoxProps)); + } } diff --git a/ags/widget/FlowBox.ts b/ags/widget/FlowBox.ts new file mode 100644 index 0000000..13d8690 --- /dev/null +++ b/ags/widget/FlowBox.ts @@ -0,0 +1,4 @@ +import { astalify, Gtk } from "astal/gtk3"; + +// TODO +export class FlowBox extends astalify(Gtk.FlowBox) {} diff --git a/ags/widget/Notification.ts b/ags/widget/Notification.ts new file mode 100644 index 0000000..d6b3166 --- /dev/null +++ b/ags/widget/Notification.ts @@ -0,0 +1,103 @@ +import { Astal, Gtk, Widget } from "astal/gtk3"; +import AstalNotifd from "gi://AstalNotifd"; +import { Separator } from "./Separator"; + +export function getUrgencyString(notif: AstalNotifd.Notification) { + switch(notif.urgency) { + case AstalNotifd.Urgency.LOW: + return "low"; + case AstalNotifd.Urgency.CRITICAL: + return "critical"; + } + + return "normal"; +} + +export function NotificationWidget(notification: AstalNotifd.Notification|number, + onClose?: (notif: AstalNotifd.Notification) => void): Gtk.Widget { + + notification = (notification instanceof AstalNotifd.Notification) ? + notification + : AstalNotifd.get_default().get_notification(notification); + + return new Widget.Box({ + className: `notification ${getUrgencyString(notification)}`, + homogeneous: false, + expand: false, + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Box({ + className: "top", + orientation: Gtk.Orientation.HORIZONTAL, + hexpand: true, + vexpand: false, + children: [ + new Widget.Icon({ + className: "icon app-icon", + icon: Astal.Icon.lookup_icon(notification.appIcon) ? + notification.appIcon + : (Astal.Icon.lookup_icon(notification.appName.toLowerCase()) ? + notification.appName.toLowerCase() + : "image-missing" + ), + setup: (_) => _.get_icon() === "image-missing" && + _.set_visible(false), + halign: Gtk.Align.START, + css: "font-size: 16px;" + }), + new Widget.Label({ + className: "app-name", + halign: Gtk.Align.START, + hexpand: true, + label: notification.appName || "Unknown Application" + } as Widget.LabelProps), + new Widget.Button({ + className: "close nf", + halign: Gtk.Align.END, + onClick: () => onClose && onClose(notification), + image: new Widget.Icon({ + className: "close icon", + icon: "window-close-symbolic" + } as Widget.IconProps) + } as Widget.ButtonProps) + ] + } as Widget.BoxProps), + Separator({ + orientation: Gtk.Orientation.VERTICAL, + alpha: 10 + }), + new Widget.Box({ + className: "content", + orientation: Gtk.Orientation.HORIZONTAL, + children: [ + new Widget.Box({ + className: "image", + visible: Boolean(notification.image), + css: `box.image { background-image: url('${notification.image}'); }` + } as Widget.BoxProps), + new Widget.Box({ + className: "text", + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Label({ + className: "summary", + useMarkup: true, + xalign: 0, + truncate: true, + label: notification.summary + }), + new Widget.Label({ + className: "body", + useMarkup: true, + xalign: 0, + expand: true, + wrap: true, + label: notification.body + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) +} diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts index 61b9b22..38ad9af 100644 --- a/ags/widget/PopupWindow.ts +++ b/ags/widget/PopupWindow.ts @@ -4,33 +4,40 @@ import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; const { TOP, BOTTOM, LEFT, RIGHT }: typeof Astal.WindowAnchor = Astal.WindowAnchor; -export interface PopupWindowProps { - className?: string | Binding; - namespace: string | Binding; - visible?: boolean | Binding; - halign?: Gtk.Align | Binding; - valign?: Gtk.Align | Binding; - hexpand?: boolean | Binding; - vexpand?: boolean | Binding; - expand?: boolean | Binding; - monitor?: number | Binding; - marginTop?: number | Binding; - marginBottom?: number | Binding; - marginLeft?: number | Binding; - marginRight?: number | Binding; - widthRequest?: number | Binding; - heightRequest?: number | Binding; - layer?: Astal.Layer | Binding; - onClose?: () => void; - child: Gtk.Widget; -} +export type PopupWindowProps = Pick & { + marginTop?: number; + marginLeft?: number; + marginBottom?: number; + marginRight?: number; + onKeyPressEvent?: (self: Widget.Window, event: Gdk.Event) => void; + /** Do something else instead of hiding window on close action(clicking outside conent / pressing Escape) + * Observation: onClose() function will still be ran after close action if defined. + */ + closeAction?: (self: Widget.Window) => void; + onClose?: (self: Widget.Window) => void; +}; export function PopupWindow(props: PopupWindowProps): Widget.Window { return new Widget.Window({ namespace: props?.namespace || "popup-window", - className: "popup-window", + className: `popup-window ${(props.namespace instanceof Binding ? + props.namespace.get() : props.namespace) || ""}`, anchor: TOP | BOTTOM | LEFT | RIGHT, - exclusivity: Astal.Exclusivity.NORMAL, + exclusivity: props.exclusivity || Astal.Exclusivity.NORMAL, keymode: Astal.Keymode.EXCLUSIVE, layer: props?.layer || Astal.Layer.OVERLAY, focusOnMap: true, @@ -44,11 +51,18 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { if((posX < childAllocation.x || posX > (childAllocation.x + childAllocation.width)) || (posY < childAllocation.y || posY > (childAllocation.y + childAllocation.height))) { _.hide(); - props?.onClose && props.onClose(); + props?.onClose && props.onClose(_); } }, - onKeyPressEvent: (_, event: Gdk.Event) => - event.get_keyval()[1] === Gdk.KEY_Escape && _.hide(), + onKeyPressEvent: (_, event: Gdk.Event) => { + if(event.get_keyval()[1] === Gdk.KEY_Escape) { + !props.closeAction ? _.hide() : props.closeAction(_); + props.onClose && props.onClose(_); + } + + props.onKeyPressEvent && + props.onKeyPressEvent(_, event); + }, child: new Widget.Box({ className: (props?.className instanceof Binding) ? props.className.as((clsName: string|undefined) => @@ -62,10 +76,13 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { hexpand: props?.hexpand || false, visible: true, vexpand: props?.vexpand || false, - marginTop: props?.marginTop || 0, - marginBottom: props?.marginBottom || 0, - marginLeft: props?.marginLeft || 0, - marginRight: props?.marginRight || 0, + css: `.popup { + margin-top: ${props.marginTop || 0}px; + margin-bottom: ${props.marginBottom || 0}px; + margin-left: ${props.marginLeft || 0}px; + margin-right: ${props.marginRight || 0}px; + }`, + onButtonPressEvent: () => true, child: props.child } as Widget.BoxProps) } as Widget.WindowProps);; diff --git a/ags/widget/Separator.ts b/ags/widget/Separator.ts index 5f3c883..ff55337 100644 --- a/ags/widget/Separator.ts +++ b/ags/widget/Separator.ts @@ -11,19 +11,26 @@ export interface SeparatorProps { } export function Separator(props: SeparatorProps) { + const alpha: number = props.alpha ? + (props.alpha > 1) ? + props.alpha / 100 + : props.alpha + : 1; + return new Widget.Box({ - className: `separator separator-${ props.orientation == Gtk.Orientation.VERTICAL ? "vertical" : "horizontal" } ${ props.class && props.class }`, + className: `separator separator-${ props.orientation == Gtk.Orientation.VERTICAL ? + "vertical" : "horizontal" } ${ props.class && props.class }`, visible: props.visible, css: `.separator { background: ${ props.cssColor || "lightgray" }; - opacity: ${ props.alpha || 1 }; + opacity: ${alpha}; } .separator-horizontal { - padding-bottom: ${props.size || 1 }px; + min-width: ${ props.size || 1 }px; margin: 4px 4px; } .separator-vertical { - padding-right: ${props.size || 1 }px; + min-height: ${ props.size || 1 }px; margin: 7px 7px; }`, } as Widget.BoxProps); diff --git a/ags/widget/bar/Audio.ts b/ags/widget/bar/Audio.ts index 24e192d..8b4341a 100644 --- a/ags/widget/bar/Audio.ts +++ b/ags/widget/bar/Audio.ts @@ -1,9 +1,9 @@ import { bind, Process } from "astal"; -import { Widget } from "astal/gtk3"; +import { Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../../scripts/volume"; import { ControlCenter } from "../../window/ControlCenter"; -export function Audio() { +export function Audio(): Gtk.Widget { return new Widget.EventBox({ className: bind(ControlCenter, "visible").as((visible: boolean) => visible ? "audio open" : "audio"), diff --git a/ags/widget/bar/Clock.ts b/ags/widget/bar/Clock.ts index 3939e1d..5c67964 100644 --- a/ags/widget/bar/Clock.ts +++ b/ags/widget/bar/Clock.ts @@ -1,10 +1,10 @@ -import { Widget } from "astal/gtk3"; +import { Gtk, Widget } from "astal/gtk3"; import { getDateTime } from "../../scripts/time"; import { bind, GLib } from "astal"; import { Windows } from "../../windows"; import { CenterWindow } from "../../window/CenterWindow"; -export function Clock(): JSX.Element { +export function Clock(): Gtk.Widget { return new Widget.Box({ className: bind(CenterWindow, "visible").as((visible: boolean) => visible ? "clock open" : "clock"), diff --git a/ags/widget/bar/FocusedClient.ts b/ags/widget/bar/FocusedClient.ts index adb58ef..24f4306 100644 --- a/ags/widget/bar/FocusedClient.ts +++ b/ags/widget/bar/FocusedClient.ts @@ -31,6 +31,7 @@ export function FocusedClient(): Gtk.Widget { new Widget.Label({ className: "class", xalign: 0, + visible: bind(focusedClient, "class").as(Boolean), maxWidthChars: 55, truncate: true, tooltipText: bind(focusedClient, "class").as((clientClass: string) => @@ -41,6 +42,7 @@ export function FocusedClient(): Gtk.Widget { className: "title", xalign: 0, maxWidthChars: 50, + visible: bind(focusedClient, "title").as(Boolean), truncate: true, tooltipText: bind(focusedClient, "title").as((clientTitle: string) => clientTitle.length > 55 ? clientTitle : ""), diff --git a/ags/widget/bar/Logo.ts b/ags/widget/bar/Logo.ts index 15083d7..4f84777 100644 --- a/ags/widget/bar/Logo.ts +++ b/ags/widget/bar/Logo.ts @@ -1,14 +1,14 @@ -import { Widget } from "astal/gtk3"; +import { Gtk, Widget } from "astal/gtk3"; import AstalHyprland from "gi://AstalHyprland"; -export function Logo() { +export function Logo(): Gtk.Widget { return new Widget.EventBox({ onClickRelease: () => AstalHyprland.get_default().dispatch("exec", "anyrun"), className: "logo", child: new Widget.Box({ child: new Widget.Label({ className: "nf", - label: "", + label: "" } as Widget.LabelProps) } as Widget.BoxProps) } as Widget.EventBoxProps); diff --git a/ags/widget/bar/Tray.ts b/ags/widget/bar/Tray.ts index 31c7987..f76f830 100644 --- a/ags/widget/bar/Tray.ts +++ b/ags/widget/bar/Tray.ts @@ -11,7 +11,7 @@ function menuFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null return menu; } -export function Tray() { +export function Tray(): Gtk.Widget { return new Widget.Box({ className: "tray", visible: bind(astalTray, "items").as((items: Array) => items.length > 0), @@ -29,9 +29,10 @@ export function Tray() { tooltipMarkup: bind(item, "tooltipMarkup"), onClick: (_, event: Astal.ClickEvent) => { if(event.button === Astal.MouseButton.SECONDARY) { + item.about_to_show(); menu.popup_at_widget(_, Gdk.Gravity.NORTH, Gdk.Gravity.SOUTH_WEST, null); } else if(event.button === Astal.MouseButton.PRIMARY) - item.secondary_activate(event.x, event.y); + item.activate(event.x, event.y); }, halign: Gtk.Align.CENTER, child: new Widget.Icon({ diff --git a/ags/widget/bar/Workspaces.ts b/ags/widget/bar/Workspaces.ts index 6dbfe11..75c013f 100644 --- a/ags/widget/bar/Workspaces.ts +++ b/ags/widget/bar/Workspaces.ts @@ -1,10 +1,10 @@ import { bind } from "astal"; -import { Gdk, Gtk, Widget } from "astal/gtk3"; +import { Gtk, Widget } from "astal/gtk3"; import AstalHyprland from "gi://AstalHyprland"; const hyprland = AstalHyprland.get_default(); -export function Workspaces() { +export function Workspaces(): Gtk.Widget { const workspacesEventBox = new Widget.EventBox({ onScroll: (_, event) => event.delta_y > 0 ? hyprland.dispatch("workspace", "e-1") : hyprland.dispatch("workspace", "e+1"), diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 8a66c12..48d4116 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -4,163 +4,165 @@ import AstalMpris from "gi://AstalMpris"; let dragTimer: (AstalIO.Time|undefined); -export const BigMedia: Gtk.Widget = new Widget.Box({ - className: "big-media", - orientation: Gtk.Orientation.VERTICAL, - homogeneous: false, - width_request: 250, - visible: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] ? true : false), - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] && [ - new Widget.Box({ - halign: Gtk.Align.CENTER, - child: new Widget.Box({ - className: "image", - hexpand: false, +export function BigMedia(): Gtk.Widget { + return new Widget.Box({ + className: "big-media", + orientation: Gtk.Orientation.VERTICAL, + homogeneous: false, + width_request: 250, + visible: bind(AstalMpris.get_default(), "players").as((players: Array) => + players[0] ? true : false), + children: bind(AstalMpris.get_default(), "players").as((players: Array) => + players[0] && [ + new Widget.Box({ + halign: Gtk.Align.CENTER, + child: new Widget.Box({ + className: "image", + hexpand: false, + orientation: Gtk.Orientation.VERTICAL, + visible: getAlbumArt(players[0]).as(Boolean), + css: getAlbumArt(players[0]).as((artUrl: string|undefined) => + artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), + width_request: 132, + height_request: 128 + } as Widget.BoxProps) + } as Widget.BoxProps), + new Widget.Box({ + className: "info", orientation: Gtk.Orientation.VERTICAL, - visible: getAlbumArt(players[0]).as(Boolean), - css: getAlbumArt(players[0]).as((artUrl: string|undefined) => - artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), - width_request: 132, - height_request: 128 - } as Widget.BoxProps) - } as Widget.BoxProps), - new Widget.Box({ - className: "info", - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Label({ - className: "title", - tooltipText: bind(players[0], "title").as((title: string) => !title ? "No Title" : title), - label: bind(players[0], "title").as((title: string) => !title ? "No Title" : title), - truncate: true, - maxWidthChars: 25, - } as Widget.LabelProps), - new Widget.Label({ - className: "artist", - tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist), - label: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist), - maxWidthChars: 28, - truncate: true, - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "progress", - hexpand: true, - visible: bind(players[0], "canSeek"), - children: [ - new Widget.Slider({ - min: 0, - hexpand: true, - max: bind(players[0], "length").as((length: number) => - Math.floor(length)), - value: bind(players[0], "position").as((position: number) => - Math.floor(position)), - onDragged: (slider: Widget.Slider) => { - if(dragTimer === undefined) - dragTimer = timeout(600, () => - players[0].set_position(Math.round(slider.value))); - else { - dragTimer.cancel(); - dragTimer = timeout(600, () => - players[0].set_position(Math.round(slider.value))); - } - } - }) - ] - }), - new Widget.CenterBox({ - className: "bottom", - homogeneous: false, - hexpand: true, - startWidget: new Widget.Label({ - className: "elapsed", - valign: Gtk.Align.START, - halign: Gtk.Align.START, - label: bind(players[0], "position").as((pos: number) => { - const sec: number = Math.floor(pos % 60); - return pos > 0 && players[0].length > 0 ? - `${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}` - : `0:00`; - }) - } as Widget.LabelProps), - centerWidget: new Widget.Box({ - className: "controls button-row", children: [ - new Widget.Button({ - className: "link nf", - label: "󰌹", - tooltipText: "Copy link to Clipboard", - visible: bind(players[0], "metadata").as((_meta: GLib.HashTable) => - players[0].get_meta("xesam:url") === null), - onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`) - } as Widget.ButtonProps), - new Widget.Button({ - className: "shuffle nf", - visible: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) => - shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), - label: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) => - shuffleStatus === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"), - tooltipText: "Toggle Shuffle", - onClick: () => players[0].shuffle() - } as Widget.ButtonProps), - new Widget.Button({ - className: "previous nf", - label: "󰒮", - tooltipText: "Previous", - onClick: () => players[0].canGoPrevious && players[0].previous() - } as Widget.ButtonProps), - new Widget.Button({ - className: "pause nf", - tooltipText: bind(players[0], "playback_status").as((status: AstalMpris.PlaybackStatus) => - status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), - label: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) => - status === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"), - onClick: () => { - players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - players[0].play() - : - players[0].pause() - } - } as Widget.ButtonProps), - new Widget.Button({ - className: "next nf", - label: "󰒭", - tooltipText: "Next", - onClick: () => players[0].canGoNext && players[0].next() - } as Widget.ButtonProps), - new Widget.Button({ - className: "repeat nf", - visible: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => - loopStatus !== AstalMpris.Loop.UNSUPPORTED), - label: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => { - switch(loopStatus) { - case AstalMpris.Loop.TRACK: return "󰑘"; - case AstalMpris.Loop.PLAYLIST: return "󰑖"; - default: return "󰑗"; - } - }), - tooltipText: "Toggle Loop", - onClick: () => players[0].loop() - } as Widget.ButtonProps) + new Widget.Label({ + className: "title", + tooltipText: bind(players[0], "title").as((title: string) => !title ? "No Title" : title), + label: bind(players[0], "title").as((title: string) => !title ? "No Title" : title), + truncate: true, + maxWidthChars: 25, + } as Widget.LabelProps), + new Widget.Label({ + className: "artist", + tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist), + label: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist), + maxWidthChars: 28, + truncate: true, + } as Widget.LabelProps) ] } as Widget.BoxProps), - endWidget: new Widget.Label({ - className: "length", - valign: Gtk.Align.START, - halign: Gtk.Align.END, - label: bind(players[0], "length").as((len/* bananananananana */: number) => { - const sec: number = Math.floor(len % 60); - return len > 0 ? - `${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}` - : "0:00"; - }) - } as Widget.LabelProps) - }) - ]) -} as Widget.BoxProps); + new Widget.Box({ + className: "progress", + hexpand: true, + visible: bind(players[0], "canSeek"), + children: [ + new Widget.Slider({ + min: 0, + hexpand: true, + max: bind(players[0], "length").as((length: number) => + Math.floor(length)), + value: bind(players[0], "position").as((position: number) => + Math.floor(position)), + onDragged: (slider: Widget.Slider) => { + if(dragTimer === undefined) + dragTimer = timeout(600, () => + players[0].set_position(Math.round(slider.value))); + else { + dragTimer.cancel(); + dragTimer = timeout(600, () => + players[0].set_position(Math.round(slider.value))); + } + } + }) + ] + }), + new Widget.CenterBox({ + className: "bottom", + homogeneous: false, + hexpand: true, + startWidget: new Widget.Label({ + className: "elapsed", + valign: Gtk.Align.START, + halign: Gtk.Align.START, + label: bind(players[0], "position").as((pos: number) => { + const sec: number = Math.floor(pos % 60); + return pos > 0 && players[0].length > 0 ? + `${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}` + : `0:00`; + }) + } as Widget.LabelProps), + centerWidget: new Widget.Box({ + className: "controls button-row", + children: [ + new Widget.Button({ + className: "link nf", + label: "󰌹", + tooltipText: "Copy link to Clipboard", + visible: bind(players[0], "metadata").as((_meta: GLib.HashTable) => + players[0].get_meta("xesam:url") === null), + onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`) + } as Widget.ButtonProps), + new Widget.Button({ + className: "shuffle nf", + visible: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) => + shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), + label: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) => + shuffleStatus === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"), + tooltipText: "Toggle Shuffle", + onClick: () => players[0].shuffle() + } as Widget.ButtonProps), + new Widget.Button({ + className: "previous nf", + label: "󰒮", + tooltipText: "Previous", + onClick: () => players[0].canGoPrevious && players[0].previous() + } as Widget.ButtonProps), + new Widget.Button({ + className: "pause nf", + tooltipText: bind(players[0], "playback_status").as((status: AstalMpris.PlaybackStatus) => + status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), + label: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) => + status === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"), + onClick: () => { + players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + players[0].play() + : + players[0].pause() + } + } as Widget.ButtonProps), + new Widget.Button({ + className: "next nf", + label: "󰒭", + tooltipText: "Next", + onClick: () => players[0].canGoNext && players[0].next() + } as Widget.ButtonProps), + new Widget.Button({ + className: "repeat nf", + visible: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => + loopStatus !== AstalMpris.Loop.UNSUPPORTED), + label: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => { + switch(loopStatus) { + case AstalMpris.Loop.TRACK: return "󰑘"; + case AstalMpris.Loop.PLAYLIST: return "󰑖"; + default: return "󰑗"; + } + }), + tooltipText: "Toggle Loop", + onClick: () => players[0].loop() + } as Widget.ButtonProps) + ] + } as Widget.BoxProps), + endWidget: new Widget.Label({ + className: "length", + valign: Gtk.Align.START, + halign: Gtk.Align.END, + label: bind(players[0], "length").as((len/* bananananananana */: number) => { + const sec: number = Math.floor(len % 60); + return len > 0 ? + `${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}` + : "0:00"; + }) + } as Widget.LabelProps) + }) + ]) + } as Widget.BoxProps); +} /** diff --git a/ags/widget/control-center/NotificationHistory.ts b/ags/widget/control-center/NotifHistory.ts similarity index 89% rename from ags/widget/control-center/NotificationHistory.ts rename to ags/widget/control-center/NotifHistory.ts index 4519489..848c7a1 100644 --- a/ags/widget/control-center/NotificationHistory.ts +++ b/ags/widget/control-center/NotifHistory.ts @@ -1,15 +1,16 @@ import { bind } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalNotifd from "gi://AstalNotifd"; -import { Notifications } from "../../scripts/notification-handler"; +import { Notifications } from "../../scripts/notifications"; -export const NotificationHistory: Gtk.Widget = new Widget.Scrollable({ +export const NotifHistory: Gtk.Widget = new Widget.Scrollable({ hscroll: Gtk.PolicyType.NEVER, vscroll: Gtk.PolicyType.AUTOMATIC, + expand: true, child: new Widget.Box({ className: "notifications", - children: bind(Notifications, "notificationHistory").as((history: Array) => - history && history.length > 0 && history.map((notification: AstalNotifd.Notification) => + children: bind(Notifications.getDefault(), "history").as((history: Array) => + history.map((notification: AstalNotifd.Notification) => new Widget.Box({ className: "notification", hexpand: true, @@ -35,7 +36,7 @@ export const NotificationHistory: Gtk.Widget = new Widget.Scrollable({ new Widget.Button({ className: "remove", label: "󱎘", - onClick: () => Notifications.removeFromNotificationHistory(notification.id) + onClick: () => Notifications.getDefault().removeHistory(notification.id) } as Widget.ButtonProps) ] } as Widget.BoxProps), diff --git a/ags/widget/control-center/Pages.ts b/ags/widget/control-center/Pages.ts index 0fa9204..4c66b43 100644 --- a/ags/widget/control-center/Pages.ts +++ b/ags/widget/control-center/Pages.ts @@ -1,40 +1,45 @@ import { timeout, Variable } from "astal"; import { Gtk, Widget } from "astal/gtk3"; +import { Page } from "./pages/Page"; -const empty = new Widget.Box(); -const page = new Variable(empty); -let connectionId: (number|undefined); +const currentPage = new Variable(undefined); export const PagesWidget: Widget.Revealer = new Widget.Revealer({ revealChild: false, + className: "pages", transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN, - transitionDuration: 250, - child: page() + transitionDuration: 360, + child: currentPage((page: (Page|undefined)) => + !page ? new Widget.Box() : page.getPage()) } as Widget.RevealerProps); -export function showPages(child: Gtk.Widget, onShow?: (self: Widget.Revealer) => void): void { - page.set(child); +export function showPages(page: Page): void { + currentPage.set(page); PagesWidget.set_reveal_child(true); - connectionId !== undefined && PagesWidget.disconnect(connectionId); - connectionId = PagesWidget.connect("show", (_) => - onShow && onShow(_)); + page.props.onOpen && page.props.onOpen(); } -export function getPage(): (Gtk.Widget|null) { - return page.get(); +export function getPage(): (Page|undefined) { + return currentPage.get(); } -export function togglePage(page: Gtk.Widget): void { - PagesWidget.revealChild ? - hidePages() - : showPages(page); +export function togglePage(page: Page): void { + if(!PagesWidget.revealChild) { + showPages(page); + return; + } + + hidePages(); } -export function hidePages(onHide?: () => void) { +export function hidePages() { PagesWidget.set_reveal_child(false); - console.log("heyyyyy"); - timeout(300, () => { - page.set(empty); - onHide && onHide(); + if(!currentPage.get()) return; + + timeout(500, () => { + if(currentPage.get() && currentPage.get()?.props.onClose) + currentPage.get()!.props.onClose!(); + + currentPage.set(undefined); }); } diff --git a/ags/widget/control-center/Sliders.ts b/ags/widget/control-center/Sliders.ts index 7dbc19c..87dda46 100644 --- a/ags/widget/control-center/Sliders.ts +++ b/ags/widget/control-center/Sliders.ts @@ -42,7 +42,7 @@ export const Sliders: Gtk.Widget = new Widget.Box({ ] } as Widget.BoxProps), /*new Widget.Box({ - className: "brightness screen", + className: "brightness", children: [ new Widget.Label({ className: "icon nf", diff --git a/ags/widget/control-center/Tiles.ts b/ags/widget/control-center/Tiles.ts index 6de110f..cb93585 100644 --- a/ags/widget/control-center/Tiles.ts +++ b/ags/widget/control-center/Tiles.ts @@ -1,10 +1,12 @@ import { Gtk, Widget } from "astal/gtk3"; -import { TileInternet } from "./tiles/Internet"; +import { TileNetwork } from "./tiles/Network"; import { TileBluetooth } from "./tiles/Bluetooth"; +import { TileRecording } from "./tiles/Recording"; export const tileList: Array = [ - TileInternet, - TileBluetooth + TileNetwork, + TileBluetooth, + TileRecording ]; export function TilesWidget(): Gtk.Widget { diff --git a/ags/widget/control-center/pages/Bluetooth.ts b/ags/widget/control-center/pages/Bluetooth.ts index e59850a..dfe5d4d 100644 --- a/ags/widget/control-center/pages/Bluetooth.ts +++ b/ags/widget/control-center/pages/Bluetooth.ts @@ -1,98 +1,108 @@ import { bind, timeout } from "astal"; import { Gtk, Widget } from "astal/gtk3"; -import AstalBluetooth from "gi://AstalBluetooth?version=0.1"; +import AstalBluetooth from "gi://AstalBluetooth"; +import { Page } from "./Page"; +import { Separator, SeparatorProps } from "../../Separator"; let watchingDevices: boolean = false; -export function BluetoothPage() { - watchNewDevices(); - - return new Widget.Box({ - className: "page bluetooth container", +export const BluetoothPage: Page = new Page({ + title: "Bluetooth Devices", + description: "Manage your Bluetooth devices and add new ones.", + className: "bluetooth", + setup: () => { + watchingDevices = true; + watchNewDevices(); + }, + onClose: stopBluetoothDevicesWatch, + pageChild: () => new Widget.Box({ + className: "connections", orientation: Gtk.Orientation.VERTICAL, + expand: true, hexpand: true, children: [ new Widget.Box({ - className: "header", - children: [ - new Widget.Label({ - hexpand: true, - className: "title", - label: "Bluetooth", - halign: Gtk.Align.START - } as Widget.LabelProps), - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "connections", + className: "paired", orientation: Gtk.Orientation.VERTICAL, - expand: true, children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => - devices.filter((device: AstalBluetooth.Device) => device.connected - ).map((dev: AstalBluetooth.Device) => - new Widget.Button({ - onClick: () => dev.connected ? dev.disconnect_device(null) : dev.connect_device(null), - child: new Widget.Box({ - className: "device", - orientation: Gtk.Orientation.HORIZONTAL, - expand: true, - children: [ - new Widget.Label({ - className: "alias", - halign: Gtk.Align.START, - label: bind(dev, "alias") - } as Widget.LabelProps), - new Widget.Label({ - className: "battery", - halign: Gtk.Align.END, - label: bind(dev, "batteryPercentage").as(String) - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - } as Widget.ButtonProps)).concat( - devices.filter((device: AstalBluetooth.Device) => !device.connected - ).map((dev: AstalBluetooth.Device) => - new Widget.Button({ - onClick: () => dev.connect_device(() => {}), - child: new Widget.Box({ - className: "device", - orientation: Gtk.Orientation.HORIZONTAL, - expand: true, - children: [ - new Widget.Label({ - className: "alias", - halign: Gtk.Align.START, - label: bind(dev, "alias") - } as Widget.LabelProps), - new Widget.Label({ - className: "battery", - halign: Gtk.Align.END, - label: bind(dev, "batteryPercentage").as(String) - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - } as Widget.ButtonProps)) + devices.filter((device: AstalBluetooth.Device) => device.connected || device.paired) + .map((dev: AstalBluetooth.Device) => + DeviceWidget(dev) + ) + ) + } as Widget.BoxProps), + Separator({ + size: .5, + orientation: Gtk.Orientation.VERTICAL, + alpha: .7 + } as SeparatorProps), + new Widget.Box({ + className: "discovered", + orientation: Gtk.Orientation.VERTICAL, + children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => + devices.filter((device: AstalBluetooth.Device) => !device.connected && !device.paired) + .map((dev: AstalBluetooth.Device) => + DeviceWidget(dev) ) ) } as Widget.BoxProps) ] } as Widget.BoxProps) +}) + +function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget { + return new Widget.Button({ + onClick: () => dev.connected ? dev.disconnect_device(null) : dev.connect_device(null), + className: bind(dev, "connected").as((connected) => connected ? "connected" : ""), + child: new Widget.Box({ + className: "device", + orientation: Gtk.Orientation.HORIZONTAL, + expand: true, + children: [ + new Widget.Icon({ + className: "icon", + icon: bind(dev, "icon").as((icon: string) => + icon ? icon : "bluetooth-active-symbolic"), + css: "font-size: 20px; margin-right: 6px;" + } as Widget.IconProps), + new Widget.Label({ + className: "alias", + halign: Gtk.Align.START, + hexpand: true, + label: bind(dev, "alias") + } as Widget.LabelProps), + new Widget.Label({ + className: "battery", + halign: Gtk.Align.END, + visible: bind(dev, "batteryPercentage").as((bat: number) => + bat <= -1 ? false : true), + label: bind(dev, "batteryPercentage").as((bat: number) => + `󰁹 ${Math.floor(bat * 100)}%`) + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps) } function watchNewDevices(): void { if(watchingDevices) { - timeout(10000, () => { - reloadDevicesList(); + timeout(8000, () => { + reloadBluetoothDevicesList(); watchNewDevices(); }); + return; } + + stopBluetoothDevicesWatch(); } -function stopDeviceWatch(): void { +export function stopBluetoothDevicesWatch(): void { watchingDevices = false; + AstalBluetooth.get_default().adapter.discovering && + AstalBluetooth.get_default().adapter.stop_discovery(); } -function reloadDevicesList(): void { +export function reloadBluetoothDevicesList(): void { AstalBluetooth.get_default().adapter.start_discovery(); - timeout(5000, () => AstalBluetooth.get_default().adapter.stop_discovery()); + timeout(4000, () => AstalBluetooth.get_default().adapter.stop_discovery()); } diff --git a/ags/widget/control-center/pages/Internet.ts b/ags/widget/control-center/pages/Internet.ts deleted file mode 100644 index 15e561b..0000000 --- a/ags/widget/control-center/pages/Internet.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { bind } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; -import AstalBluetooth from "gi://AstalBluetooth"; - -export function WifiPage() { - return new Widget.Box({ - className: "page bluetooth container", - orientation: Gtk.Orientation.VERTICAL, - hexpand: true, - children: [ - new Widget.Box({ - className: "connections", - orientation: Gtk.Orientation.VERTICAL, - expand: true, - children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => - devices && devices.filter((device: AstalBluetooth.Device) => device.connected - ).map((dev: AstalBluetooth.Device) => - new Widget.Box({ - className: "device", - orientation: Gtk.Orientation.HORIZONTAL, - expand: true, - children: [ - new Widget.Label({ - className: "alias", - halign: Gtk.Align.START, - label: bind(dev, "alias") - } as Widget.LabelProps), - new Widget.Label({ - className: "battery", - halign: Gtk.Align.END, - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - )) - } as Widget.BoxProps) - ] - } as Widget.BoxProps); -} diff --git a/ags/widget/control-center/pages/Network.ts b/ags/widget/control-center/pages/Network.ts new file mode 100644 index 0000000..28fa373 --- /dev/null +++ b/ags/widget/control-center/pages/Network.ts @@ -0,0 +1,23 @@ +import { Widget } from "astal/gtk3"; +import { Page } from "./Page"; +import AstalNetwork from "gi://AstalNetwork"; +import { bind } from "astal"; + +export const PageNetwork = new Page({ + title: "Network", + className: "network", + headerButtons: () => [ + new Widget.Button({ + className: "reload nf", + label: "󰑓", + visible: bind(AstalNetwork.get_default(), "primary").as( + (primary: AstalNetwork.Primary) => primary === AstalNetwork.Primary.WIFI + ), + tooltipText: "Re-scan connections", + onClick: () => AstalNetwork.get_default().wifi.scan() + } as Widget.ButtonProps) + ], + pageChild: () => new Widget.Box({ + } as Widget.BoxProps) +}); + diff --git a/ags/widget/control-center/pages/Page.ts b/ags/widget/control-center/pages/Page.ts new file mode 100644 index 0000000..ec7f36f --- /dev/null +++ b/ags/widget/control-center/pages/Page.ts @@ -0,0 +1,88 @@ +import { Binding, GObject, register } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; + +export type PageProps = { + setup?: () => void; + onClose?: () => void; + onOpen?: () => void; + className?: string | Binding; + title: string | Binding; + description?: string | Binding; + headerButtons?: () => Array; + pageChild: () => Gtk.Widget; +}; + +@register({ GTypeName: "Page" }) +class Page extends GObject.Object { + readonly #props: PageProps; + + get props() { return this.#props; } + + constructor(props: PageProps) { + super(); + this.#props = props; + } + + public getHeaderButtons(): (Array|null) { + return this.props.headerButtons ? + this.props.headerButtons() + : null; + } + + public getPage(): Gtk.Widget { + return new Widget.Box({ + className: (this.props.className instanceof Binding) ? + this.props.className.as((clsName: (string|undefined)) => `page ${ clsName || "" }`) : `page ${this.#props.className || ""}`, + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, + setup: this.props.setup, + children: [ + new Widget.Box({ + className: "header", + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, + children: [ + new Widget.Box({ + className: "title", + children: [ + new Widget.Label({ + hexpand: true, + className: "title", + truncate: true, + visible: (this.props.title instanceof Binding) ? + this.props.title.as(Boolean) + : (this.props.title ? true : false), + label: this.props.title, + halign: Gtk.Align.START + } as Widget.LabelProps), + new Widget.Box({ + className: "button-row", + visible: Boolean(this.getHeaderButtons()), + children: this.getHeaderButtons() || undefined + } as Widget.BoxProps) + ] + } as Widget.BoxProps), + new Widget.Label({ + className: "description", + hexpand: true, + truncate: true, + xalign: 0, + visible: (this.props.description instanceof Binding) ? + this.props.description.as(Boolean) + : this.props.description ? true : false, + label: this.props.description + } as Widget.LabelProps), + ] + } as Widget.BoxProps), + new Widget.Box({ + className: "content", + orientation: Gtk.Orientation.VERTICAL, + expand: true, + setup: (_) => _.add(this.props.pageChild()) + } as Widget.BoxProps) + ] + } as Widget.BoxProps); + } +} + +export { Page }; diff --git a/ags/widget/control-center/tiles/Bluetooth.ts b/ags/widget/control-center/tiles/Bluetooth.ts index 7bb8467..cd5e414 100644 --- a/ags/widget/control-center/tiles/Bluetooth.ts +++ b/ags/widget/control-center/tiles/Bluetooth.ts @@ -1,4 +1,4 @@ -import { bind } from "astal"; +import { bind, Variable } from "astal"; import { Tile, TileProps } from "./Tile"; import AstalBluetooth from "gi://AstalBluetooth"; import { togglePage } from "../Pages"; @@ -6,16 +6,18 @@ import { BluetoothPage } from "../pages/Bluetooth"; export const TileBluetooth = Tile({ title: "Bluetooth", - description: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => { - const connected: Array = devices.filter( - (dev: AstalBluetooth.Device) => dev.connected); - - return connected[0] ? connected[0].get_alias() : undefined; - }), + description: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => + devices.filter((dev: AstalBluetooth.Device) => dev.connected)[0]?.get_alias()), onToggledOn: () => AstalBluetooth.get_default().adapter.set_powered(true), onToggledOff: () => AstalBluetooth.get_default().adapter.set_powered(false), - onClickMore: () => togglePage(BluetoothPage()), - icon: "󰂯", + onClickMore: () => togglePage(BluetoothPage), + icon: Variable.derive([ + bind(AstalBluetooth.get_default().adapter, "powered"), + bind(AstalBluetooth.get_default(), "isConnected") + ], + (powered: boolean, isConnected: boolean) => + powered ? ( isConnected ? "󰂱" : "󰂯" ) : "󰂲" + )(), iconSize: 16, toggleState: bind(AstalBluetooth.get_default().adapter, "powered") } as TileProps); diff --git a/ags/widget/control-center/tiles/Internet.ts b/ags/widget/control-center/tiles/Internet.ts deleted file mode 100644 index 6be34af..0000000 --- a/ags/widget/control-center/tiles/Internet.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { bind, execAsync } from "astal"; -import { Tile, TileProps } from "./Tile"; -import AstalNetwork from "gi://AstalNetwork"; -import { Widget } from "astal/gtk3"; - -export const TileInternet = new Widget.Box({ - child: bind(AstalNetwork.get_default(), "wired").as((wired: AstalNetwork.Wired) => Tile({ - title: "Wired", - description: bind(wired, "internet").as((internet: AstalNetwork.Internet) => { - switch(internet) { - case AstalNetwork.Internet.CONNECTED: - return "Connected"; - case AstalNetwork.Internet.DISCONNECTED: - return "Disconnected"; - case AstalNetwork.Internet.CONNECTING: - return "Connecting..."; - } - }), - onToggledOn: () => execAsync("nmcli n on"), - onToggledOff: () => execAsync("nmcli n off"), - icon: "󰛳", - iconSize: 16, - toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => - internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED) - } as TileProps)) -} as Widget.BoxProps); diff --git a/ags/widget/control-center/tiles/Network.ts b/ags/widget/control-center/tiles/Network.ts new file mode 100644 index 0000000..ca97cb5 --- /dev/null +++ b/ags/widget/control-center/tiles/Network.ts @@ -0,0 +1,86 @@ +import { bind, execAsync, Variable } from "astal"; +import { Tile, TileProps } from "./Tile"; +import AstalNetwork from "gi://AstalNetwork"; +import { Widget } from "astal/gtk3"; +import { showPages, togglePage } from "../Pages"; +import { PageNetwork } from "../pages/Network"; + +export const TileNetwork = new Widget.Box({ + child: Variable.derive([ + bind(AstalNetwork.get_default(), "primary"), + bind(AstalNetwork.get_default(), "wired"), + bind(AstalNetwork.get_default(), "wifi") + ], + (primary: AstalNetwork.Primary, wired: AstalNetwork.Wired, wifi: AstalNetwork.Wifi) => { + if(primary === AstalNetwork.Primary.WIFI) { + return Tile({ + title: "Wireless", + description: Variable.derive( + [ bind(wifi, "ssid"), bind(wifi, "internet") ], + (ssid: string, internet: AstalNetwork.Internet) => + ssid ? ssid : (() => { + switch(internet) { + case AstalNetwork.Internet.CONNECTED: + return "Connected"; + case AstalNetwork.Internet.DISCONNECTED: + return "Disconnected"; + case AstalNetwork.Internet.CONNECTING: + return "Connecting..."; + } + })() + )(), + onToggledOn: () => wifi.set_enabled(true), + onToggledOff: () => wifi.set_enabled(false), + onClickMore: () => togglePage(PageNetwork), + icon: "󰤨", + iconSize: 16, + toggleState: bind(wifi, "enabled") + } as TileProps); + + } else if(primary === AstalNetwork.Primary.WIRED) { + return Tile({ + title: "Wired", + description: bind(wired, "internet").as((internet: AstalNetwork.Internet) => { + switch(internet) { + case AstalNetwork.Internet.CONNECTED: + return "Connected"; + case AstalNetwork.Internet.DISCONNECTED: + return "Disconnected"; + case AstalNetwork.Internet.CONNECTING: + return "Connecting..."; + } + }), + onToggledOn: () => execAsync("nmcli n on"), + onToggledOff: () => execAsync("nmcli n off"), + onClickMore: () => togglePage(PageNetwork), + icon: bind(wired, "internet").as((internet: AstalNetwork.Internet) => { + switch(internet) { + case AstalNetwork.Internet.CONNECTED: + return '󰛳'; + case AstalNetwork.Internet.DISCONNECTED: + return '󰲛'; + } + + return "󰛵"; + }), + iconSize: 16, + toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => + internet === AstalNetwork.Internet.CONNECTING + || internet === AstalNetwork.Internet.CONNECTED + ) + } as TileProps); + } + + return Tile({ + title: "Network", + description: "Disconnected", + onToggledOn: () => execAsync("nmcli n on"), + onToggledOff: () => execAsync("nmcli n off"), + onClickMore: () => togglePage(PageNetwork), + icon: "󰲛", + iconSize: 16, + toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => + internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED) + } as TileProps); + })() +} as Widget.BoxProps); diff --git a/ags/widget/control-center/tiles/Recording.ts b/ags/widget/control-center/tiles/Recording.ts new file mode 100644 index 0000000..7c29e3b --- /dev/null +++ b/ags/widget/control-center/tiles/Recording.ts @@ -0,0 +1,17 @@ +import { Tile, TileProps } from "./Tile"; +import { Recording } from "../../../scripts/recording"; +import { bind } from "astal"; + +export const TileRecording = Tile({ + title: "Screen Recording", + description: bind(Recording.getDefault(), "recording").as( + (isRecording: boolean) => isRecording ? + "Recording {time}" + : "Start a Screen Record" + ), + icon: "󰻂", + onToggledOff: () => Recording.getDefault().stopRecording(), + onToggledOn: () => Recording.getDefault().startRecording(), + iconSize: 16, + toggleState: bind(Recording.getDefault(), "recording"), +} as TileProps); diff --git a/ags/widget/control-center/tiles/Tile.ts b/ags/widget/control-center/tiles/Tile.ts index 145e725..f1650bd 100644 --- a/ags/widget/control-center/tiles/Tile.ts +++ b/ags/widget/control-center/tiles/Tile.ts @@ -55,23 +55,38 @@ export function Tile(props: TileProps): Widget.EventBox { new Widget.Label({ className: "icon nf", label: props.icon || "icon", - css: `.icon { font-size: ${props.iconSize || "12px"} }` + css: `label { font-size: ${props.iconSize || "12"}px; }` } as Widget.LabelProps), new Widget.Box({ className: "text", orientation: Gtk.Orientation.VERTICAL, vexpand: true, + hexpand: true, valign: Gtk.Align.CENTER, children: [ new Widget.Label({ className: "title", xalign: 0, + halign: Gtk.Align.START, truncate: true, label: props.title } as Widget.LabelProps), new Widget.Label({ className: "description", - visible: props.description, + visible: Boolean(props.description), + setup: (label: Widget.Label) => { + if(props.description instanceof Binding) { + const sub = props.description.subscribe((value) => { + label.set_visible(Boolean(value)); + }); + + const destroyId = label.connect("destroy-event", () => { + label.disconnect(destroyId); + sub(); + }); + } + }, + halign: Gtk.Align.START, truncate: true, xalign: 0, label: props.description diff --git a/ags/widget/runner/ResultWidget.ts b/ags/widget/runner/ResultWidget.ts new file mode 100644 index 0000000..5a98edb --- /dev/null +++ b/ags/widget/runner/ResultWidget.ts @@ -0,0 +1,76 @@ +import { register } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import { closeRunner } from "../../window/Runner"; + +export { ResultWidget, ResultWidgetProps }; + +type ResultWidgetProps = { + icon?: string; + title: string; + description?: string; + closeOnClick?: boolean; + setup?: () => void; + onClick?: () => void; +}; + +@register({ GTypeName: "ResultWidget" }) +class ResultWidget extends Widget.EventBox { + private readonly connections: Array; + public readonly onClick: ((() => void)|undefined); + public readonly icon: (string|undefined); + public readonly setup: ((() => void)|undefined); + public readonly closeOnClick: boolean = true; + + + constructor(props: ResultWidgetProps) { + super(); + if(props.icon) + this.icon = props.icon; + if(props.onClick) + this.onClick = props.onClick; + if(props.setup) + this.setup = props.setup; + if(props.closeOnClick !== undefined) + this.closeOnClick = props.closeOnClick; + + this.connections = [ + this.connect("click", () => { + this.onClick && this.onClick(); + this.closeOnClick && closeRunner(); + }), + + this.connect("destroy-event", () => this.connections.map((id: number) => + this.disconnect(id))) + ]; + + this.add(new Widget.Box({ + className: "result", + hexpand: true, + children: [ + new Widget.Icon({ + visible: Boolean(props.icon), + icon: props.icon || "image-missing" + } as Widget.IconProps), + new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.CENTER, + children: [ + new Widget.Label({ + className: "title", + xalign: 0, + truncate: true, + label: props.title + } as Widget.LabelProps), + new Widget.Label({ + className: "description", + visible: Boolean(props.description), + truncate: true, + xalign: 0, + label: props.description || "" + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + ] + } as Widget.BoxProps)); + } +} diff --git a/ags/window/AppsWindow.ts b/ags/window/AppsWindow.ts new file mode 100644 index 0000000..25dc929 --- /dev/null +++ b/ags/window/AppsWindow.ts @@ -0,0 +1,79 @@ +import { Variable } from "astal"; +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; +import { getAstalApps } from "../scripts/apps"; +import AstalApps from "gi://AstalApps"; +import AstalHyprland from "gi://AstalHyprland"; + +const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; +const searchString = new Variable(""); +const appsArray = new Variable>([]); +let searchSubscription: () => void; + +export const AppsWindow = new Widget.Window({ + namespace: "apps-window", + layer: Astal.Layer.OVERLAY, + exclusivity: Astal.Exclusivity.IGNORE, + anchor: TOP | LEFT | RIGHT | BOTTOM, + visible: false, + keymode: Astal.Keymode.EXCLUSIVE, + onKeyPressEvent: (_, event: Gdk.Event) => { + event.get_keyval()[1] === Gdk.KEY_Escape && + hideAppsWindow(_); + }, + setup: () => { + searchSubscription = searchString.subscribe((str: string) => { + appsArray.set(getAstalApps().fuzzy_query(str)); + }); + }, + child: new Widget.Box({ + className: "apps-window container", + expand: true, + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Entry({ + className: "entry", + hexpand: true, + vexpand: false, + onDraw: (_) => _.grab_focus(), + onChanged: (entry) => { + searchString.set(entry.text); + } + } as Widget.EntryProps), + new Widget.Box({ + className: "apps", + hexpand: true, + vexpand: true, + orientation: Gtk.Orientation.VERTICAL, + children: appsArray((apps: Array) => + apps.map((app: AstalApps.Application) => + new Widget.Button({ + className: "app", + onClickRelease: (_) => { + _.get_window()?.hide(); + AstalHyprland.get_default().dispatch("exec", app.get_executable()); + }, + child: new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Icon({ + className: "icon", + iconName: app.get_icon_name() + } as Widget.IconProps), + new Widget.Label({ + className: "name", + label: app.get_name() + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps) + ) + ) + } as Widget.BoxProps) + ] + } as Widget.BoxProps) +} as Widget.WindowProps); + +function hideAppsWindow(window: Widget.Window) { + searchString.set(""); + window.hide(); +} diff --git a/ags/window/CenterWindow.ts b/ags/window/CenterWindow.ts index ee10c7f..1b24a57 100644 --- a/ags/window/CenterWindow.ts +++ b/ags/window/CenterWindow.ts @@ -6,6 +6,8 @@ import { BigMedia } from "../widget/center-window/BigMedia"; import { Separator, SeparatorProps } from "../widget/Separator"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; +const BigMediaWidget = BigMedia(); + export const CenterWindow: Widget.Window = PopupWindow({ className: "center-window", namespace: "center-window", @@ -53,7 +55,7 @@ export const CenterWindow: Widget.Window = PopupWindow({ ] } as Widget.BoxProps), Separator({ - visible: bind(BigMedia, "visible"), + visible: bind(BigMediaWidget, "visible"), orientation: Gtk.Orientation.HORIZONTAL, alpha: .5, cssColor: "gray", @@ -63,7 +65,7 @@ export const CenterWindow: Widget.Window = PopupWindow({ className: "vertical right", orientation: Gtk.Orientation.VERTICAL, children: [ - BigMedia + BigMediaWidget ] } as Widget.BoxProps) ] diff --git a/ags/window/ControlCenter.ts b/ags/window/ControlCenter.ts index e5ab433..8ac873d 100644 --- a/ags/window/ControlCenter.ts +++ b/ags/window/ControlCenter.ts @@ -4,16 +4,9 @@ import { Tiles } from "../widget/control-center/Tiles"; import { Sliders } from "../widget/control-center/Sliders"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { hidePages, PagesWidget } from "../widget/control-center/Pages"; +import { NotifHistory } from "../widget/control-center/NotifHistory"; -const widgetsContainer: Widget.Box = new Widget.Box({ - className: "control-center-container", - orientation: Gtk.Orientation.VERTICAL, - widthRequest: 400, -} as Widget.BoxProps, -QuickActions, -Sliders, -Tiles, -PagesWidget); +const connections: Array = []; export const ControlCenter: Widget.Window = PopupWindow({ className: "control-center", @@ -25,5 +18,29 @@ export const ControlCenter: Widget.Window = PopupWindow({ halign: Gtk.Align.END, valign: Gtk.Align.START, visible: false, - child: widgetsContainer + vexpand: true, + child: new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + vexpand: true, + children: [ + new Widget.Box({ + className: "control-center-container", + orientation: Gtk.Orientation.VERTICAL, + widthRequest: 400, + vexpand: false, + hexpand: true, + children: [ + QuickActions, + Sliders, + Tiles, + PagesWidget + ] + } as Widget.BoxProps), + NotifHistory + ] + } as Widget.BoxProps) } as PopupWindowProps); + +connections.push(ControlCenter.connect("hide", (_) => { + hidePages(); +})); diff --git a/ags/window/FloatingNotifications.ts b/ags/window/FloatingNotifications.ts index ecdc120..d032bad 100644 --- a/ags/window/FloatingNotifications.ts +++ b/ags/window/FloatingNotifications.ts @@ -1,6 +1,13 @@ import { Astal, Gtk, Widget } from "astal/gtk3"; import AstalNotifd from "gi://AstalNotifd"; -import { Notifications } from "../scripts/notification-handler"; +import { bind } from "astal/binding"; +import { Notifications } from "../scripts/notifications"; +import { NotificationWidget } from "../widget/Notification"; +import { timeout } from "astal"; +import { VarMap } from "../scripts/varmap"; + +const connections: Array = []; +const notifWidgets = new VarMap(); export const FloatingNotifications: Widget.Window = new Widget.Window({ namespace: "floating-notifications", @@ -9,14 +16,43 @@ export const FloatingNotifications: Widget.Window = new Widget.Window({ monitor: 0, layer: Astal.Layer.OVERLAY, visible: false, - width_request: 350, + widthRequest: 450, exclusivity: Astal.Exclusivity.NORMAL, + setup: (window) => { + connections.push( + Notifications.getDefault().connect("notification-added", (_, notif: AstalNotifd.Notification) => { + !window.is_visible() && window.show(); + + notifWidgets.set(notif.id, new Widget.Revealer({ + revealChild: false, + transitionDuration: 320, + transitionType: Gtk.RevealerTransitionType.SLIDE_RIGHT, + child: NotificationWidget(notif, + () => Notifications.getDefault().removeNotification(notif.id)), + } as Widget.RevealerProps)); + + notifWidgets.getValue(notif.id)!.revealChild = true; + }), + + Notifications.getDefault().connect("notification-removed", (_, id: number) => { + notifWidgets.getValue(id)!.revealChild = false; + timeout( + (notifWidgets.getValue(id)?.get_transition_duration() || 0) + 50, + () => { + notifWidgets.delete(id); + Notifications.getDefault().notifications.length === 0 && + window.is_visible() && window.hide(); + } + ); + }) + ); + }, + onDestroy: () => connections.map(id => Notifications.getDefault().disconnect(id)), child: new Widget.Box({ className: "floating-notifications-container", orientation: Gtk.Orientation.VERTICAL, homogeneous: false, - children: Notifications.notifications().as((notifications: Array) => - notifications.map((item: AstalNotifd.Notification) => - NotificationWidget(item))) + visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0), + children: bind(notifWidgets).as((map) => [...map.values()].map((revealer) => revealer)) } as Widget.BoxProps) } as Widget.WindowProps); diff --git a/ags/window/LogoutMenu.ts b/ags/window/LogoutMenu.ts index 58a7c8a..cd1aa7a 100644 --- a/ags/window/LogoutMenu.ts +++ b/ags/window/LogoutMenu.ts @@ -1,6 +1,7 @@ import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { getDateTime } from "../scripts/time"; import { execAsync, GLib } from "astal"; +import { AskPopup } from "../widget/AskPopup"; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; @@ -26,7 +27,8 @@ export const LogoutMenu: Widget.Window = new Widget.Window({ children: [ new Widget.Box({ className: "top", - expand: false, + hexpand: true, + vexpand: false, orientation: Gtk.Orientation.VERTICAL, valign: Gtk.Align.START, children: [ @@ -45,28 +47,53 @@ export const LogoutMenu: Widget.Window = new Widget.Window({ new Widget.Box({ className: "button-row", homogeneous: true, - expand: true, + vexpand: true, valign: Gtk.Align.CENTER, + height_request: 360, children: [ new Widget.Button({ className: "poweroff nf", label: "󰐥", - onClick: () => execAsync("systemctl poweroff") + onClick: () => AskPopup({ + title: "Power Off", + text: "Are you sure you want to power off? Unsaved work will be lost.", + cancelText: "No! Let me go back", + acceptText: "Yes, shutdown", + onAccept: () => execAsync("systemctl poweroff") + }) } as Widget.ButtonProps), new Widget.Button({ className: "reboot nf", label: "󰜉", - onClick: () => execAsync("systemctl reboot") + onClick: () => AskPopup({ + title: "Reboot", + text: "Are you sure you want to Reboot? Unsaved work will be lost.", + cancelText: "No! Let me go back", + acceptText: "Yes, reboot", + onAccept: () => execAsync("systemctl reboot") + }) } as Widget.ButtonProps), new Widget.Button({ className: "suspend nf", label: "󰤄", - onClick: () => execAsync("systemctl suspend") + onClick: () => AskPopup({ + title: "Suspend", + text: "Are you sure you want to Suspend?", + cancelText: "No! Let me go back", + acceptText: "Yes, suspend", + onAccept: () => execAsync("systemctl suspend") + }) } as Widget.ButtonProps), new Widget.Button({ className: "logout nf", label: "󰗽", - onClick: () => execAsync("astal close logout-menu && bash -c 'loginctl terminate-user $USER'") + onClick: () => AskPopup({ + title: "Log out", + text: "Are you sure you want to log out? Your session will be ended.", + cancelText: "No! Let me go back", + acceptText: "Yes, please log out", + onAccept: () => execAsync(`sh -c "loginctl terminate-user ${GLib.getenv("USER") || "$USER"}"`) + }) } as Widget.ButtonProps), ] } as Widget.BoxProps) diff --git a/ags/window/Runner.ts b/ags/window/Runner.ts index 80c6ddb..f2f88b9 100644 --- a/ags/window/Runner.ts +++ b/ags/window/Runner.ts @@ -1,41 +1,184 @@ import { Variable } from "astal"; -import { Astal, Gtk, Widget } from "astal/gtk3"; +import { Gdk, Gtk, Widget } from "astal/gtk3"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; +import { updateApps } from "../scripts/apps"; +import { handleShell } from "../scripts/runner/shell"; +import { handleWebSearch } from "../scripts/runner/websearch"; +import { handleApplications } from "../scripts/runner/apps"; +import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; +import Wp05 from "gi://Wp"; -// TODO +export let runnerInstance: (Widget.Window|null) = null; -export interface RunnerProps { - halign?: Gtk.Align; - valign?: Gtk.Align; - width?: number; - height?: number; - entryPlaceHolder?: string; - resultsPlaceholder?: Array; +export function closeRunner(gtkWindow?: Widget.Window) { + const window = gtkWindow ? gtkWindow : runnerInstance; + + window?.destroy(); + runnerInstance = null; } -export function Runner(props?: RunnerProps) { +export function startRunnerDefault() { + return Runner.RunnerWindow({ + entryPlaceHolder: "Start typing...", + resultsPlaceholder: () => [ + new ResultWidget({ + icon: "utilities-terminal-symbolic", + title: "Run shell commands", + description: "Start typing with '!' prefix to run shell commands" + } as ResultWidgetProps), + new ResultWidget({ + icon: "application-x-executable-symbolic", + title: "Run your applications", + description: "Type the name of the application to search" + } as ResultWidgetProps), + new ResultWidget({ + icon: "applications-internet-symbolic", + title: "Search the Web", + description: "Start typing with '?' prefix to search the web" + } as ResultWidgetProps) + ] + } as Runner.RunnerProps); +} - const entryText: Variable = new Variable(""); +export namespace Runner { + export type RunnerProps = { + halign?: Gtk.Align; + valign?: Gtk.Align; + width?: number; + height?: number; + entryPlaceHolder?: string; + resultsPlaceholder?: () => Array; + }; - const resultsBox: Widget.Box = new Widget.Box({ - className: "results", + export const prefixes = new Map (ResultWidget|Array|null)>([ + [ "!", handleShell ], + [ "?", handleWebSearch ], + ]); + + export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) { + let subs: Array<() => void> = []; + const entryText: Variable = new Variable(""); + let results: (Array|null) = null; + let selectedResultIndex = 0; + + const searchEntry = new Widget.Entry({ + className: "search", + onChanged: (entry) => entryText.set(entry.text), + placeholderText: props?.entryPlaceHolder || "", + primary_icon_name: "system-search" + } as Widget.EntryProps); + + const resultsList: Gtk.ListBox = new Gtk.ListBox({ + visible: true, + expand: true + } as Gtk.ListBox.ConstructorProps); + + subs.push(entryText().subscribe((text: string) => { + const trimmedText = text.trim(); + const pluginResult: (ResultWidget|Array|null|undefined) = handlePrefix( + trimmedText)?.(trimmedText.replace(trimmedText.charAt(0), "")); + results = Boolean(pluginResult) ? + (!Array.isArray(pluginResult) ? + [ pluginResult! ] + : pluginResult) + : null; + + [ + new Widget.Box({ + className: "not-found", + orientation: Gtk.Orientation.VERTICAL, + visible: entryText((text: string) => text.trim().length > 0), + expand: true, + children: [ + new Widget.Icon({ + icon: "software-update-urgent-symbolic" + } as Widget.IconProps), + new Widget.Label({ + label: "Couldn't find any results with this search. Maybe try pressing F5 and searching again?", + truncate: false, + wrap: true + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + new Widget.Box({ + className: "placeholder", + orientation: Gtk.Orientation.VERTICAL, + expand: true, + visible: Boolean(props?.resultsPlaceholder), + children: props?.resultsPlaceholder && + props?.resultsPlaceholder() + } as Widget.BoxProps) + ]; + + if(resultsList.get_children().length > 0) { + resultsList.get_children().map((listItem: Gtk.Widget) => { + resultsList.remove(listItem); + listItem.destroy(); + }); + } + + if(results && results.length > 0) + results.map((resultWidget: ResultWidget) => { + resultsList.insert(resultWidget, -1); + }); + + selectedResultIndex = 0; + resultsList.select_row(resultsList.get_row_at_index(selectedResultIndex)); + })); + + if(!runnerInstance) + runnerInstance = PopupWindow({ + namespace: "runner", + halign: props?.halign || Gtk.Align.CENTER, + valign: props?.valign || Gtk.Align.CENTER, + widthRequest: props?.width || 750, + heightRequest: props?.height || 450, + onKeyPressEvent: (_, event: Gdk.Event) => { + event.get_keyval()[1] === Gdk.KEY_F5 && + updateApps(); + + if(event.get_keyval()[1] === Gdk.KEY_Down) { + resultsList.get_children().length > 0 && + resultsList.select_row(resultsList.get_row_at_index( + (selectedResultIndex + 1) > (resultsList.get_children().length - 1) ? + 0 + : selectedResultIndex + 1 + )); + } + }, + closeAction: (_) => closeRunner(_), + onClose: () => subs.map(sub => sub()), + child: new Widget.Box({ + className: "runner main", + orientation: Gtk.Orientation.VERTICAL, + children: [ + searchEntry, + new Widget.Scrollable({ + className: "results-scrollable", + vscroll: Gtk.PolicyType.AUTOMATIC, + hscroll: Gtk.PolicyType.NEVER, + expand: true, + child: resultsList + }) + ] + } as Widget.BoxProps) + } as PopupWindowProps); + + return runnerInstance; + } + + export function handlePrefix(text: string): (((a: string) => (Array|ResultWidget|null)) | null) { + const prefix = text.charAt(0); + let result: (((a: string) => ResultWidget|Array|null)|null) = null; + + if(/([a-z]|[A-Z]|[0-9])/.test(prefix)) + result = handleApplications; + + [...prefixes.keys()].map((curPrefix: string) => { + if(curPrefix === prefix) + result = prefixes.get(curPrefix)!; + }); - } as Widget.BoxProps); - - return PopupWindow({ - namespace: "runner", - halign: props?.halign || Gtk.Align.CENTER, - valign: props?.valign || Gtk.Align.CENTER, - widthRequest: props?.width || 600, - heightRequest: props?.height || 500, - child: new Widget.Box({ - className: "main", - children: [ - new Widget.Entry({ - className: "search", - onChanged: (entry) => entryText.set(entry.text), - } as Widget.EntryProps), - ] - } as Widget.BoxProps) - } as PopupWindowProps); + return result; + } } diff --git a/ags/window/Wallpaper.ts b/ags/window/Wallpaper.ts index c507d70..2d0dacb 100644 --- a/ags/window/Wallpaper.ts +++ b/ags/window/Wallpaper.ts @@ -24,10 +24,9 @@ export const Wallpaper: Widget.Window = new Widget.Window({ exclusivity: Astal.Exclusivity.IGNORE, keymode: Astal.Keymode.NONE, visible: true, + style: new Gtk.Style(), + css: ".wallpaper { all: unset; }", monitor: 0, //Needs rework for all monitors - child: new Widget.Box({ - className: "wallpaper", - } as Widget.BoxProps), onButtonPressEvent: (_, event: Gdk.Event) => { const [ , x, y ] = event.get_coords(); if(event.get_button()[1] === Gdk.BUTTON_SECONDARY) diff --git a/ags/windows.ts b/ags/windows.ts index 5a94184..b0eb897 100644 --- a/ags/windows.ts +++ b/ags/windows.ts @@ -7,7 +7,7 @@ import { CenterWindow } from "./window/CenterWindow"; import { FloatingNotifications } from "./window/FloatingNotifications"; import { GObject, register } from "astal"; import { LogoutMenu } from "./window/LogoutMenu"; -import { Wallpaper } from "./window/Wallpaper"; +import { AppsWindow } from "./window/AppsWindow"; /** * get open windows / interact with windows(e.g.: close, open or toggle) @@ -23,7 +23,7 @@ class WindowsClass extends GObject.Object { this.setWindow("center-window", CenterWindow); this.setWindow("logout-menu", LogoutMenu); this.setWindow("floating-notifications", FloatingNotifications); - this.setWindow("wallpaper", Wallpaper); + this.setWindow("apps-window", AppsWindow); } public static setWindow(name: string, window: Gtk.Window): void { diff --git a/anyrun/applications.ron b/anyrun/applications.ron index 2f77f94..b5f8e75 100644 --- a/anyrun/applications.ron +++ b/anyrun/applications.ron @@ -1,5 +1,8 @@ Config ( max_entries: 8, desktop_actions: false, - terminal: Some("kitty -c") + terminal: Some(Terminal( + command: "kitty", + args: "-c {}" + )) ) diff --git a/hypr/autostart.conf b/hypr/autostart.conf index 4ca6dbd..6e7147a 100644 --- a/hypr/autostart.conf +++ b/hypr/autostart.conf @@ -4,7 +4,7 @@ ############### # Services/Daemons -exec-once = systemctl enable --user --now hyprpolkitagent +exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 exec-once = systemctl enable --user --now hypridle exec-once = systemctl enable --user --now gnome-keyring-daemon exec-once = wl-paste --type text --watch cliphist store diff --git a/hypr/bindings.conf b/hypr/bindings.conf index dff12f2..1645426 100644 --- a/hypr/bindings.conf +++ b/hypr/bindings.conf @@ -8,7 +8,7 @@ $mainMod = SUPER # The master key $media = amberol # Media App $terminal = kitty # Terminal Emulator $fm = nautilus # File Manager -$menu = anyrun # App Runner +$menu = astal runner || anyrun # App Runner $dmenu = anyrun --plugins libstdin.so # dmenu app $lockscreen = hyprlock @@ -24,7 +24,7 @@ $screenshotSelect = hyprshot -m region -o $screenshotDir bind = $mainMod, K, exec, $terminal bind = $mainMod, Q, killactive -bind = $mainMod, E, exec, $fileManager +bind = $mainMod, E, exec, $fm bind = $mainMod, F, togglefloating bind = $mainMod, SPACE, exec, $menu bind = $mainMod, P, pseudo, diff --git a/hypr/rules.conf b/hypr/rules.conf index 3a4d3e8..9fec5ae 100644 --- a/hypr/rules.conf +++ b/hypr/rules.conf @@ -35,19 +35,22 @@ windowrulev2 = movetoworkspace e, class:org.pulseaudio.pavucontrol windowrulev2 = animation slide right, class:org.pulseaudio.pavucontrol windowrulev2 = animation slide right, class:blueberry.py windowrulev2 = animation slide right, class:io.github.kaii_lb.Overskride -layerrule = animation slide right, swaync-control-center +layerrule = animation slide, swaync-control-center layerrule = animation fade, selection layerrule = animation fade, waybar layerrule = animation fade, hyprpaper layerrule = animation slide right, swaync-notification-window layerrule = animation fade, hyprpicker layerrule = animation fade, anyrun -layerrule = animation slide right, eww-cc -layerrule = animation fade, eww-calendar -layerrule = animation fade, eww-volume -layerrule = animation fade, eww-powermenu +layerrule = animation fade, ^(eww-(.*))$ +layerrule = animation slide, eww-cc layerrule = animation fade, control-center layerrule = animation fade, center-window # Bruh i need a better name for this :skull: +layerrule = animation fade, logout-menu +layerrule = animation fade, wallpaper +layerrule = animation fade, apps-window +layerrule = animation fade, runner +layerrule = animation fade, ask-popup # Opacity windowrulev2 = opacity .95 .95, class:kitty @@ -58,10 +61,6 @@ windowrulev2 = opacity .88 .88, class:hyprpolkitagent windowrulev2 = noblur, class:^()$, title:^()$ # Removes blur from context menus windowrulev2 = noblur, class:steam(.*)$ -# Window Blur list -blurls = logout_dialog -blurls = kitty - # Layer Blur list layerrule = blur, waybar layerrule = blur, eww-bar @@ -73,13 +72,17 @@ layerrule = blur, top-bar layerrule = blur, osd layerrule = blur, control-center layerrule = blur, center-window -#layerrule = blur, logout-menu +layerrule = blur, logout-menu +layerrule = blur, runner +layerrule = blur, ask-popup +layerrule = ignorealpha .7, runner layerrule = ignorealpha .6, eww-volume layerrule = ignorealpha .55, eww-bar layerrule = ignorealpha .5, eww-calendar layerrule = ignorealpha .7, eww-cc layerrule = ignorealpha .4, osd layerrule = ignorealpha .55, top-bar +layerrule = ignorealpha .6, ask-popup layerrule = ignorealpha .7, control-center layerrule = ignorealpha .7, center-window diff --git a/hypr/scripts/change-wallpaper.sh b/hypr/scripts/change-wallpaper.sh index 2f270fb..708e974 100644 --- a/hypr/scripts/change-wallpaper.sh +++ b/hypr/scripts/change-wallpaper.sh @@ -9,7 +9,7 @@ # Made by retrozinndev (João Dias) # From https://github.com/retrozinndev/Hyprland-Dots -style="darken" # lighten / darken +style="lighten" # lighten / darken dmenu=$(sh "$XDG_CONFIG_HOME/hypr/scripts/get-dmenu.sh") if [[ -z "$WALLPAPERS_DIR" ]]; then @@ -46,7 +46,7 @@ function Reload_wallpaper() { function Reload_pywal() { echo "[LOG] Reloading pywal colorscheme" - wal -q -t --cols16 $style -i "$wall" + wal -t --cols16 $style -i "$wall" } # Prompt wallpaper list @@ -55,8 +55,7 @@ wall="$WALLPAPERS_DIR/$(ls $WALLPAPERS_DIR | $dmenu)" # Check if input wallpaper is empty if [[ $wall == "$WALLPAPERS_DIR/" ]]; then echo "No wallpaper has been selected by user!" - if [[ $RANDOM_WALLPAPER_WHEN_EMPTY == true ]] - then + if [[ $RANDOM_WALLPAPER_WHEN_EMPTY == true ]]; then wall="$WALLPAPERS_DIR/$(ls $WALLPAPERS_DIR | shuf -n 1)" echo "Selected random from $WALLPAPERS_DIR: $wall" else diff --git a/wallpapers/Mute ime44.jpg b/wallpapers/Mute ime44.jpg new file mode 100644 index 0000000..cde4fe9 Binary files /dev/null and b/wallpapers/Mute ime44.jpg differ diff --git a/wallpapers/Osage Inabakumori City.png b/wallpapers/Osage Inabakumori City.png new file mode 100644 index 0000000..91545ec Binary files /dev/null and b/wallpapers/Osage Inabakumori City.png differ