diff --git a/src/app.ts b/src/app.ts index ecb9812..4847b58 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,6 +30,7 @@ import AstalNotifd from "gi://AstalNotifd"; import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; import Adw from "gi://Adw?version=1"; +import GdkPixbuf from "gi://GdkPixbuf?version=2.0"; const runnerPlugins: Array = [ @@ -41,7 +42,7 @@ const runnerPlugins: Array = [ PluginClipboard ]; -const defaultWindows: Array = []; +const defaultWindows: Array = [ "bar" ]; Gtk.init(); Adw.init(); @@ -57,6 +58,7 @@ export class Shell extends Gtk.Application { #stylesheet: Uint8Array|undefined; #styleProvider: Gtk.CssProvider; #gresource: Gio.Resource|null = null; + #icons: Record = {}; get scope() { return this.#scope; } @@ -64,12 +66,42 @@ export class Shell extends Gtk.Application { super({ applicationId: "io.github.retrozinndev.colorshell", flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE, - version: "1.1.0", + version: COLORSHELL_VERSION ?? "0.0.0-unknown", }); this.#styleProvider = Gtk.CssProvider.new(); try { - this.#gresource = Gio.Resource.load(GRESOURCES_FILE); + // load gresource from build-defined value + support env variables + this.#gresource = Gio.Resource.load(GRESOURCES_FILE.split('/').filter(s => + s !== "" + ).map(path => { + if(/^\$/.test(path)) { + const env = GLib.getenv(path.replace(/^\$/, "")); + + if(env === null) + throw new Error(`Couldn't get environment variable: ${path}`); + + return env; + } + + return path; + }).join('/')); + Gio.resources_register(this.#gresource); + + // add icons + Gio.resources_enumerate_children( + "/io/github/retrozinndev/colorshell", + Gio.ResourceLookupFlags.NONE + ).filter(name => + /symbolic$/.test(name) || name.endsWith("svg") + ).map(name => + `/io/github/retrozinndev/colorshell/${name}` + ).forEach(path => { + const name = path.split('/')[path.split('/').length - 1]; + const iconBytes = Gio.resources_lookup_data(path, null); + + this.#icons[name] = Gio.BytesIcon.new(iconBytes); + }); } catch(_e) { const e = _e as Error; console.error(`Error: couldn't load gresource! Stderr: ${e.message}\n${e.stack}`); @@ -91,6 +123,13 @@ export class Shell extends Gtk.Application { ); } + public getGIcon(name: string): Gio.BytesIcon { + if(!Object.hasOwn(this.#icons, name)) + throw new Error(`Colorshell: No gicon found with name "${name}"`); + + return this.#icons[name]; + } + public applyStyle(stylesheet: string): void { const previous = this.#stylesheet ? decoder.decode(this.#stylesheet) : undefined; let final = ""; @@ -127,8 +166,9 @@ export class Shell extends Gtk.Application { return 1; } } else { - if(args[1]) { - printerr("Error: colorshell not running. Try to clean-run before using arguments"); + if(args.length > 0) { + cmd.printerr_literal("Error: colorshell not running. Try to clean-run before using arguments"); + cmd.done(); return 1; } @@ -151,7 +191,7 @@ export class Shell extends Gtk.Application { console.log(`Colorshell: initializing`); this.#scope = getScope(); - Stylesheet.getDefault().compileApply(); + Stylesheet.getDefault(); // Init clipboard module Clipboard.getDefault(); diff --git a/src/scripts/stylesheet.ts b/src/scripts/stylesheet.ts index d3affd5..db6a7e9 100644 --- a/src/scripts/stylesheet.ts +++ b/src/scripts/stylesheet.ts @@ -1,9 +1,9 @@ import { monitorFile, readFile } from "ags/file"; -import { timeout } from "ags/time"; -import { exec, execAsync } from "ags/process"; +import { decoder } from "./utils"; +import { Wallpaper } from "./wallpaper"; import { Shell } from "../app"; +import { exec } from "ags/process"; -import AstalIO from "gi://AstalIO"; import Gio from "gi://Gio?version=2.0"; import GLib from "gi://GLib?version=2.0"; @@ -11,43 +11,20 @@ import GLib from "gi://GLib?version=2.0"; /** handles stylesheet compiling and reloading */ export class Stylesheet { private static instance: Stylesheet; - #watchDelay: (AstalIO.Time|undefined); #outputPath = Gio.File.new_for_path(`${GLib.get_user_cache_dir()}/colorshell/style`); - #styles = [ "./style", "./style.scss" ]; + #sassStyles!: { + colors: string; + general: string; + }; public get stylePath() { return this.#outputPath.get_path()!; } - public async compileSass(): Promise { + public compileSass(): string { console.log("Stylesheet: Compiling Sass"); + exec(`echo '${this.#sassStyles.colors}\n${this.#sassStyles.general}' \ + | sass --stdin --no-source-map -s "${this.stylePath}.css"`); - exec(`bash -c "sass ${this.#styles.map(style => `-I ${style}`).join('\s')} ${ - this.#outputPath.get_path()!}/style.css"`); - } - - public async reapply(cssFilePath: string): Promise { - console.log("Stylesheet: Applying stylesheet"); - - const content = readFile(cssFilePath); - - if(content?.trim()) { - Shell.getDefault().resetStyle(); - Shell.getDefault().applyStyle(content); - - console.log("Stylesheet: done applying stylesheet to shell"); - return; - } - - console.error(`Stylesheet: An error occurred while trying to read the css file: ${ - cssFilePath}`); - } - - public async compileApply(): Promise { - await this.compileSass().then(() => - this.reapply(this.#outputPath.get_path()! + "/style.css") - ).catch((err: Error) => - console.error(`Stylesheet: An error occurred and Sass couldn't be compiled. Stderr:\n${ - err.message}\n${err.stack}`) - ); + return readFile(`${this.stylePath}/style.css`); } public static getDefault(): Stylesheet { @@ -57,31 +34,83 @@ export class Stylesheet { return this.instance; } - constructor() { - (async () => !this.#outputPath.query_exists(null) && - this.#outputPath.make_directory_with_parents(null))(); + public getStyleSheet(): string { + const stylesNames: Array = Gio.resources_enumerate_children( + "/io/github/retrozinndev/colorshell", + Gio.ResourceLookupFlags.NONE + ).filter(name => + name.startsWith("style") + ).map(name => + `/io/github/retrozinndev/colorshell/${name}` + ); - this.#styles.map((path: string) => - monitorFile( - `${path}`, - (file: string) => { - if(this.#watchDelay || file.endsWith('~') || Number.isNaN(file)) - return; + return stylesNames.map(path => + Gio.resources_lookup_data(path, Gio.ResourceLookupFlags.NONE) + ).map(bytes => decoder.decode(bytes.get_data()!)).join('\n'); + } - this.#watchDelay = timeout(250, () => this.#watchDelay = undefined); - console.log(`Stylesheet: \`${file.startsWith(GLib.get_home_dir()) ? - file.replace(GLib.get_home_dir(), '~') - : file}\` changed`) + /* + private objectToStyleSheet(colors: object & Record): string { + return Object.keys(colors).map(name => { + const isBg = name.toLowerCase().startsWith('bg') || name.toLowerCase() === "background", + color = colors[name as keyof typeof colors]; - this.compileApply(); + // this will transform the color name's casing, example: bgPrimary -> bg-primary + return ` + .${this.kebabify(name)} { + ${isBg ? `background: ${color}` : `color: ${color}`} } - ) - ) + `.trim(); + }).join('\n') + } - monitorFile(`${GLib.get_user_cache_dir()}/wal/colors.scss`, (file: string) => { - execAsync(`bash -c "cp -f ${file} ./style/_wal.scss"`).catch(r => { - console.error(`Stylesheet: Failed to copy pywal stylesheet to style dir. Stderr: ${r}`); - }); + private kebabify(str: string) { + return str.replace(/[A-Z]/, (c) => `-${c.toLowerCase()}`); + } + */ + + public getColors(): string { + const data = Wallpaper.getDefault().getData(); + const colors = { + bgPrimary: `color.adjust($color: ${data.colors.color1}, $lightness: -28%)`, + bgSecondary: `color.adjust($color: ${data.colors.color1}, $lightness: -16%)`, + bgTertiary: `color.adjust($color: ${data.colors.color1}, $lightness: -4%)`, + bgLight: data.special.foreground, + bgTranslucent: `rgba(color.adjust($color: ${data.colors.color1}, $lightness: -28%), .7)`, + bgTranslucentPrimary: `rgba(color.adjust($color: ${data.colors.color1}, $lightness: -28%), .7)`, + bgTranslucentSecondary: `rgba(color.adjust($color: ${data.colors.color1}, $lightness: -16%), .7)`, + fgPrimary: data.special.foreground, + fgLight: `color.adjust($color: ${data.colors.color1}, $lightness: -28%)`, + fgDisabled: `color.adjust($color: ${data.special.foreground}, $lightness: -11%)` + }; + + return Object.keys(colors).map(name => + `$${name}: ${colors[name as keyof typeof colors]};` + ).join('\n'); + } + + private updateColors(): void { + this.#sassStyles.colors = this.getColors(); + Shell.getDefault().applyStyle(this.compileSass()); + } + + constructor() { + try { + !this.#outputPath.query_exists(null) && + this.#outputPath.make_directory_with_parents(null); + } catch(_e) { + const e = _e as Error; + console.error(`Stylesheet: couldn't create output path. Stderr: ${e.message}\n${e.stack}`); + } + + this.#sassStyles = { + colors: this.getColors(), + general: this.getStyleSheet().replace(/colors\.[$]/g, "\$") + }; + Shell.getDefault().applyStyle(this.compileSass()); + + monitorFile(`${GLib.get_user_cache_dir()}/wal/colors.json`, () => { + this.updateColors(); }); } } diff --git a/src/scripts/wallpaper.ts b/src/scripts/wallpaper.ts index 2f3d917..4683c47 100644 --- a/src/scripts/wallpaper.ts +++ b/src/scripts/wallpaper.ts @@ -1,7 +1,7 @@ import { execAsync } from "ags/process"; import { timeout } from "ags/time"; import GObject, { register, getter } from "ags/gobject"; -import { monitorFile } from "ags/file"; +import { monitorFile, readFile } from "ags/file"; import AstalIO from "gi://AstalIO"; import Gio from "gi://Gio?version=2.0"; @@ -10,6 +10,35 @@ import GLib from "gi://GLib?version=2.0"; export { Wallpaper }; +type WalData = { + checksum: string; + wallpaper: string; + alpha: number; + special: { + background: string; + foreground: string; + cursor: string; + }; + colors: { + color0: string; + color1: string; + color2: string; + color3: string; + color4: string; + color5: string; + color6: string; + color7: string; + color8: string; + color9: string; + color10: string; + color11: string; + color12: string; + color13: string; + color14: string; + color15: string; + }; +}; + @register({ GTypeName: "Wallpaper" }) class Wallpaper extends GObject.Object { private static instance: Wallpaper; @@ -137,6 +166,11 @@ class Wallpaper extends GObject.Object { ); } + public getData(): WalData { + const content = readFile(`${GLib.getenv("XDG_CACHE_HOME")}/wal/colors.json`); + return JSON.parse(content) as WalData; + } + public async getWallpaper(): Promise { return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => { const loaded: (string|undefined) = stdout.split('=')[1]?.trim(); diff --git a/src/widget/bar/Media.tsx b/src/widget/bar/Media.tsx index caaab0b..4c1400c 100644 --- a/src/widget/bar/Media.tsx +++ b/src/widget/bar/Media.tsx @@ -3,14 +3,18 @@ import { Gtk } from "ags/gtk4"; import { Separator } from "../Separator"; import { Windows } from "../../windows"; import { Clipboard } from "../../scripts/clipboard"; +import { decoder, getPlayerIconFromBusName, variableToBoolean } from "../../scripts/utils"; import GObject from "ags/gobject"; import AstalMpris from "gi://AstalMpris"; import Pango from "gi://Pango?version=1.0"; -import { decoder, getPlayerIconFromBusName, variableToBoolean } from "../../scripts/utils"; -export const dummyPlayer = AstalMpris.Player.new("colorshellDummy"); +export const dummyPlayer = { + available: false, + busName: "dummy_player", + bus_name: "dummy_player" +} as AstalMpris.Player; export let [player, setPlayer] = createState(dummyPlayer); export const Media = () => { diff --git a/src/widget/center-window/BigMedia.tsx b/src/widget/center-window/BigMedia.tsx index 804d450..8e77d16 100644 --- a/src/widget/center-window/BigMedia.tsx +++ b/src/widget/center-window/BigMedia.tsx @@ -58,7 +58,7 @@ class PlayerWidget extends Gtk.Box { this.append( diff --git a/src/window/Bar.tsx b/src/window/Bar.tsx index eadaaac..40925e1 100644 --- a/src/window/Bar.tsx +++ b/src/window/Bar.tsx @@ -2,7 +2,6 @@ import { Astal, Gtk } from "ags/gtk4"; import { Tray } from "../widget/bar/Tray"; import { Workspaces } from "../widget/bar/Workspaces"; import { FocusedClient } from "../widget/bar/FocusedClient"; -import { Media } from "../widget/bar/Media"; import { Apps } from "../widget/bar/Apps"; import { Clock } from "../widget/bar/Clock"; import { Status } from "../widget/bar/Status"; @@ -12,8 +11,9 @@ export const Bar = (mon: number) => { const widgetSpacing = 4; return + exclusivity={Astal.Exclusivity.EXCLUSIVE} heightRequest={46} monitor={mon} + canFocus={false}> + { $type="center"> - * @throws Error if there are no monitors connected */ - public createWindowForMonitors(create: (mon: number, scope: Scope) => GObject.Object|Astal.Window): (() => Array) { + public createWindowForMonitors(create: (mon: number, scope: ReturnType) => GObject.Object|Astal.Window): (() => Array) { const monitors = AstalHyprland.get_default().get_monitors(); if(monitors.length < 1) @@ -213,7 +213,7 @@ class Windows extends GObject.Object { * @returns a function that when called, returns a Astal.Window instance * @throws Error if no focused monitor is found */ - public createWindowForFocusedMonitor(create: (mon: number, scope: Scope) => GObject.Object|Astal.Window): (() => Astal.Window) { + public createWindowForFocusedMonitor(create: (mon: number, scope: ReturnType) => GObject.Object|Astal.Window): (() => Astal.Window) { const focusedMonitor = this.getFocusedMonitorId(); if(focusedMonitor == null)