Initial ThinkPad Hyprland dotfiles
This commit is contained in:
321
config/hypr/ags/switcher.tsx
Normal file
321
config/hypr/ags/switcher.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import app from "ags/gtk4/app";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { readFile } from "ags/file";
|
||||
import { execAsync } from "ags/process";
|
||||
import css from "./switcher.css";
|
||||
import GLib from "gi://GLib";
|
||||
|
||||
type SwitcherItem = {
|
||||
type: "theme" | "wallpaper";
|
||||
path: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
wallpaper: string;
|
||||
active?: boolean;
|
||||
accent?: string;
|
||||
accent2?: string;
|
||||
muted?: string;
|
||||
};
|
||||
|
||||
const HYPR_DIR = GLib.getenv("HYPR_DIR") || "/home/pascal/.config/hypr";
|
||||
const SCRIPT_DIR = `${HYPR_DIR}/Scripts`;
|
||||
const THEME_DIR = GLib.getenv("HYPR_SWITCHER_THEME_DIR") || `${HYPR_DIR}/Themes`;
|
||||
const WALLPAPER_DIR = GLib.getenv("HYPR_SWITCHER_WALLPAPER_DIR")
|
||||
|| GLib.getenv("WALLPAPER_DIR")
|
||||
|| `${GLib.get_home_dir()}/Bilder/Wallpaper`;
|
||||
const CURRENT_WALLPAPER = `${HYPR_DIR}/current-wallpaper`;
|
||||
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
|
||||
|
||||
type UiTheme = {
|
||||
accent: string;
|
||||
accent2: string;
|
||||
background: string;
|
||||
backgroundSoft: string;
|
||||
foreground: string;
|
||||
muted: string;
|
||||
panelHex: string;
|
||||
};
|
||||
|
||||
function notify(message: string) {
|
||||
execAsync(["notify-send", "AGS Switcher", message]).catch(console.error);
|
||||
}
|
||||
|
||||
function fileExists(path: string) {
|
||||
return GLib.file_test(path, GLib.FileTest.EXISTS);
|
||||
}
|
||||
|
||||
function readText(path: string) {
|
||||
try {
|
||||
return readFile(path);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function listFiles(dir: string, predicate: (path: string, name: string) => boolean) {
|
||||
try {
|
||||
const directory = GLib.Dir.open(dir, 0);
|
||||
const files: string[] = [];
|
||||
let name = directory.read_name();
|
||||
|
||||
while (name !== null) {
|
||||
const path = `${dir}/${name}`;
|
||||
if (predicate(path, name)) {
|
||||
files.push(path);
|
||||
}
|
||||
name = directory.read_name();
|
||||
}
|
||||
|
||||
return files.sort((a, b) => a.localeCompare(b));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function shellValue(contents: string, key: string) {
|
||||
const regex = new RegExp(`^${key}=(["']?)(.*?)\\1$`, "m");
|
||||
return contents.match(regex)?.[2] || "";
|
||||
}
|
||||
|
||||
function activeTheme(): UiTheme {
|
||||
const activeWallpaper = currentWallpaper();
|
||||
const fallback = {
|
||||
accent: "#00ff9c",
|
||||
accent2: "#00cc88",
|
||||
background: "rgba(20, 20, 30, 0.95)",
|
||||
backgroundSoft: "rgba(40, 40, 55, 0.8)",
|
||||
foreground: "#cdd6f4",
|
||||
muted: "#cccccc",
|
||||
panelHex: "#282837",
|
||||
};
|
||||
|
||||
const themeFile = listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme"))
|
||||
.find(path => shellValue(readText(path), "WALLPAPER") === activeWallpaper);
|
||||
|
||||
if (!themeFile) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const contents = readText(themeFile);
|
||||
return {
|
||||
accent: shellValue(contents, "ACCENT") || fallback.accent,
|
||||
accent2: shellValue(contents, "ACCENT_2") || fallback.accent2,
|
||||
background: shellValue(contents, "BACKGROUND") || fallback.background,
|
||||
backgroundSoft: shellValue(contents, "BACKGROUND_SOFT") || fallback.backgroundSoft,
|
||||
foreground: shellValue(contents, "FOREGROUND") || fallback.foreground,
|
||||
muted: shellValue(contents, "MUTED") || fallback.muted,
|
||||
panelHex: shellValue(contents, "PANEL_HEX") || fallback.panelHex,
|
||||
};
|
||||
}
|
||||
|
||||
function themeCss(theme: UiTheme) {
|
||||
return [
|
||||
`@define-color ags_accent ${theme.accent};`,
|
||||
`@define-color ags_accent_2 ${theme.accent2};`,
|
||||
`@define-color ags_bg ${theme.background};`,
|
||||
`@define-color ags_bg_soft ${theme.backgroundSoft};`,
|
||||
`@define-color ags_fg ${theme.foreground};`,
|
||||
`@define-color ags_muted ${theme.muted};`,
|
||||
`@define-color ags_panel ${theme.panelHex};`,
|
||||
css,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function basename(path: string) {
|
||||
return GLib.path_get_basename(path);
|
||||
}
|
||||
|
||||
function currentWallpaper() {
|
||||
return readText(CURRENT_WALLPAPER).trim();
|
||||
}
|
||||
|
||||
function loadThemes(): SwitcherItem[] {
|
||||
return listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme"))
|
||||
.map(path => {
|
||||
const contents = readText(path);
|
||||
return {
|
||||
type: "theme",
|
||||
path,
|
||||
name: shellValue(contents, "NAME") || basename(path).replace(/\.theme$/, ""),
|
||||
icon: shellValue(contents, "ICON") || "",
|
||||
wallpaper: shellValue(contents, "WALLPAPER"),
|
||||
accent: shellValue(contents, "ACCENT") || "#f38ba8",
|
||||
accent2: shellValue(contents, "ACCENT_2") || "#cba6f7",
|
||||
muted: shellValue(contents, "MUTED") || "#cdd6f4",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function loadWallpapers(): SwitcherItem[] {
|
||||
const active = currentWallpaper();
|
||||
|
||||
return listFiles(WALLPAPER_DIR, (_path, name) =>
|
||||
IMAGE_EXTENSIONS.some(ext => name.toLowerCase().endsWith(ext)),
|
||||
).map(path => ({
|
||||
type: "wallpaper",
|
||||
path,
|
||||
name: basename(path),
|
||||
wallpaper: path,
|
||||
active: path === active,
|
||||
}));
|
||||
}
|
||||
|
||||
function applyItem(item: SwitcherItem) {
|
||||
const command = item.type === "theme"
|
||||
? [`${SCRIPT_DIR}/theme-menu.sh`, "--apply", item.path]
|
||||
: [`${SCRIPT_DIR}/wallpaper-menu.sh`, "--apply", item.path];
|
||||
|
||||
execAsync(command)
|
||||
.then(() => app.quit())
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
notify(`${item.name} konnte nicht angewendet werden.`);
|
||||
});
|
||||
}
|
||||
|
||||
function previewWallpaper(item: SwitcherItem) {
|
||||
execAsync([`${SCRIPT_DIR}/wallpaper-menu.sh`, "--preview", item.path]).catch(error => {
|
||||
console.error(error);
|
||||
notify(`${item.name} konnte nicht geoeffnet werden.`);
|
||||
});
|
||||
}
|
||||
|
||||
function Preview({ item }: { item: SwitcherItem }) {
|
||||
const hasImage = item.wallpaper && fileExists(item.wallpaper);
|
||||
|
||||
return (
|
||||
<box class={hasImage ? "preview" : "preview preview-empty"}>
|
||||
{hasImage
|
||||
? <image class="preview-image" file={item.wallpaper} pixelSize={92} />
|
||||
: <label label="" />}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function Swatches({ item }: { item: SwitcherItem }) {
|
||||
return (
|
||||
<box class="swatches" spacing={6}>
|
||||
{[item.accent, item.accent2, item.muted].map(color => (
|
||||
<box class="swatch" css={`background: ${color};`} />
|
||||
))}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemButton({ item }: { item: SwitcherItem }) {
|
||||
const content = (
|
||||
<box spacing={14}>
|
||||
<Preview item={item} />
|
||||
<box
|
||||
orientation={Gtk.Orientation.VERTICAL}
|
||||
hexpand
|
||||
valign={Gtk.Align.CENTER}
|
||||
>
|
||||
<label
|
||||
class="item-title"
|
||||
xalign={0}
|
||||
ellipsize={3}
|
||||
label={`${item.icon || ""} ${item.name}`}
|
||||
/>
|
||||
<label
|
||||
class="item-subtitle"
|
||||
xalign={0}
|
||||
ellipsize={3}
|
||||
label={item.type === "theme"
|
||||
? basename(item.path)
|
||||
: item.active ? "Aktuelles Wallpaper" : item.path}
|
||||
/>
|
||||
</box>
|
||||
{item.type === "theme" ? <Swatches item={item} /> : <box />}
|
||||
</box>
|
||||
);
|
||||
|
||||
const applyButton = (
|
||||
<button
|
||||
class={`item ${item.active ? "active" : ""}`}
|
||||
hexpand
|
||||
onClicked={() => applyItem(item)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (item.type === "theme") {
|
||||
return applyButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<box class="item-row" spacing={8}>
|
||||
{applyButton}
|
||||
<button
|
||||
class="icon-button preview-button"
|
||||
tooltipText="Vorschau"
|
||||
onClicked={() => previewWallpaper(item)}
|
||||
>
|
||||
<label label="" />
|
||||
</button>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function SwitcherWindow(mode: string) {
|
||||
const isTheme = mode === "theme" || mode === "themes";
|
||||
const items = isTheme ? loadThemes() : loadWallpapers();
|
||||
const title = isTheme ? "Theme wechseln" : "Wallpaper wechseln";
|
||||
const empty = isTheme ? `Keine Themes in ${THEME_DIR}` : `Keine Bilder in ${WALLPAPER_DIR}`;
|
||||
|
||||
return (
|
||||
<window
|
||||
name="ags-hypr-switcher"
|
||||
namespace="ags-hypr-switcher"
|
||||
class="switcher-window"
|
||||
visible
|
||||
keymode={Astal.Keymode.EXCLUSIVE}
|
||||
anchor={Astal.WindowAnchor.TOP}
|
||||
application={app}
|
||||
>
|
||||
<box
|
||||
class="switcher"
|
||||
orientation={Gtk.Orientation.VERTICAL}
|
||||
spacing={16}
|
||||
marginTop={70}
|
||||
>
|
||||
<box class="header">
|
||||
<label class="title" hexpand xalign={0} label={title} />
|
||||
<button
|
||||
class="icon-button close"
|
||||
tooltipText="Schliessen"
|
||||
onClicked={() => app.quit()}
|
||||
>
|
||||
<label label="" />
|
||||
</button>
|
||||
</box>
|
||||
{items.length > 0
|
||||
? (
|
||||
<scrolledwindow
|
||||
class="list-scroll"
|
||||
hscrollbarPolicy={Gtk.PolicyType.NEVER}
|
||||
maxContentHeight={600}
|
||||
>
|
||||
<box orientation={Gtk.Orientation.VERTICAL} spacing={10}>
|
||||
{items.map(item => <ItemButton item={item} />)}
|
||||
</box>
|
||||
</scrolledwindow>
|
||||
)
|
||||
: (
|
||||
<box class="empty">
|
||||
<label label={empty} />
|
||||
</box>
|
||||
)}
|
||||
</box>
|
||||
</window>
|
||||
);
|
||||
}
|
||||
|
||||
app.start({
|
||||
css: themeCss(activeTheme()),
|
||||
instanceName: "hypr-switcher",
|
||||
main(mode = "wallpaper") {
|
||||
SwitcherWindow(mode);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user