✨ chore: compile styles via gresources, gicon support
started using gicon in application for custom icons support
This commit is contained in:
+46
-6
@@ -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<Runner.Plugin> = [
|
||||
@@ -41,7 +42,7 @@ const runnerPlugins: Array<Runner.Plugin> = [
|
||||
PluginClipboard
|
||||
];
|
||||
|
||||
const defaultWindows: Array<string> = [];
|
||||
const defaultWindows: Array<string> = [ "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<string, Gio.BytesIcon> = {};
|
||||
|
||||
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();
|
||||
|
||||
+83
-54
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string> = 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, string>): 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string|undefined> {
|
||||
return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => {
|
||||
const loaded: (string|undefined) = stdout.split('=')[1]?.trim();
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -58,7 +58,7 @@ class PlayerWidget extends Gtk.Box {
|
||||
|
||||
this.append(
|
||||
<Gtk.Revealer hexpand={false} revealChild={
|
||||
createBinding(player, "artUrl").as(Boolean)
|
||||
createBinding(player, "coverArt").as(Boolean)
|
||||
} transitionType={Gtk.RevealerTransitionType.SLIDE_LEFT} transitionDuration={300}>
|
||||
|
||||
<Gtk.Box class={"image"} css={createBinding(player, "artUrl").as((art) =>
|
||||
|
||||
+3
-4
@@ -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 <Astal.Window namespace={"top-bar"} layer={Astal.Layer.TOP}
|
||||
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE} heightRequest={46} monitor={mon}
|
||||
visible={true} canFocus={false}>
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE} heightRequest={46} monitor={mon}
|
||||
canFocus={false}>
|
||||
|
||||
<Gtk.Box class={"bar-container"}>
|
||||
<Gtk.CenterBox class={"bar-centerbox"} hexpand>
|
||||
<Gtk.Box class={"widgets-left"} homogeneous={false}
|
||||
@@ -29,7 +29,6 @@ export const Bar = (mon: number) => {
|
||||
$type="center">
|
||||
|
||||
<Clock />
|
||||
<Media />
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"widgets-right"} homogeneous={false}
|
||||
spacing={widgetSpacing} halign={Gtk.Align.END}
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import { FloatingNotifications } from "./window/FloatingNotifications";
|
||||
import { CenterWindow } from "./window/CenterWindow";
|
||||
import { LogoutMenu } from "./window/LogoutMenu";
|
||||
import { AppsWindow } from "./window/AppsWindow";
|
||||
import { createRoot, getScope, onCleanup, Scope } from "/usr/share/ags/js/gnim/src/jsx/scope";
|
||||
import { createRoot, getScope, onCleanup } from "ags";
|
||||
import { Shell } from "./app";
|
||||
import GObject, { getter, register, signal } from "ags/gobject";
|
||||
|
||||
@@ -182,7 +182,7 @@ class Windows extends GObject.Object {
|
||||
* @returns a function that when called, returns Array<Astal.Window>
|
||||
* @throws Error if there are no monitors connected
|
||||
*/
|
||||
public createWindowForMonitors(create: (mon: number, scope: Scope) => GObject.Object|Astal.Window): (() => Array<Astal.Window>) {
|
||||
public createWindowForMonitors(create: (mon: number, scope: ReturnType<typeof getScope>) => GObject.Object|Astal.Window): (() => Array<Astal.Window>) {
|
||||
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<typeof getScope>) => GObject.Object|Astal.Window): (() => Astal.Window) {
|
||||
const focusedMonitor = this.getFocusedMonitorId();
|
||||
|
||||
if(focusedMonitor == null)
|
||||
|
||||
Reference in New Issue
Block a user