Initial ThinkPad Hyprland dotfiles

This commit is contained in:
Pascal
2026-04-28 03:59:07 +02:00
commit 6eb922c417
56 changed files with 6587 additions and 0 deletions

312
config/hypr/ags/homelab.css Normal file
View File

@@ -0,0 +1,312 @@
* {
all: unset;
font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif;
font-size: 13px;
}
.homelab-window {
background: transparent;
}
.login-panel,
.shell {
border: 1px solid rgba(205, 214, 244, 0.16);
border-radius: 16px;
background: rgba(20, 20, 30, 0.97);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
color: #cdd6f4;
}
.login-panel {
min-width: 520px;
padding: 18px;
}
.shell {
min-width: 0;
min-height: 0;
}
.sidebar {
min-width: 218px;
padding: 14px 10px;
border-right: 1px solid rgba(205, 214, 244, 0.10);
background: rgba(14, 16, 24, 0.72);
}
.brand {
padding: 8px 8px 14px;
}
.brand-title {
color: #00ff9c;
font-size: 19px;
font-weight: 800;
}
.brand-subtitle,
.subtitle,
.row-subtitle {
color: #a6adc8;
font-size: 12px;
}
.nav-button {
min-height: 34px;
padding: 7px 8px;
border-radius: 8px;
color: #cdd6f4;
}
.nav-button:hover,
.nav-button:focus {
background: rgba(0, 255, 156, 0.14);
}
.nav-button.active {
background: rgba(0, 255, 156, 0.22);
color: #00ff9c;
}
.nav-icon {
min-width: 24px;
}
.nav-label {
font-size: 12px;
}
.content {
padding: 18px;
}
.page-scroll {
min-width: 0;
min-height: 0;
}
.header {
min-height: 38px;
}
.title {
font-size: 20px;
font-weight: 750;
color: #00ff9c;
}
.subtitle {
margin-top: 3px;
}
entry {
padding: 10px 12px;
border: 1px solid rgba(205, 214, 244, 0.14);
border-radius: 10px;
background: rgba(40, 40, 55, 0.82);
color: #cdd6f4;
}
entry:focus {
border-color: rgba(0, 255, 156, 0.78);
}
.grid,
.live-strip,
.service-grid {
min-height: 92px;
}
.card,
.chart-card,
.service-card,
.table-row {
border: 1px solid rgba(205, 214, 244, 0.10);
border-radius: 8px;
background: rgba(40, 40, 55, 0.62);
}
.card {
min-width: 220px;
padding: 12px;
}
.wide-card {
min-width: 0;
min-height: 220px;
}
.log-card {
min-width: 0;
min-height: 560px;
}
.stat-card {
min-height: 80px;
}
.chart-card {
min-width: 260px;
min-height: 98px;
padding: 12px;
}
.chart-card.cpu {
border-color: rgba(0, 255, 156, 0.22);
}
.chart-card.memory {
border-color: rgba(137, 180, 250, 0.26);
}
.chart-card.load {
border-color: rgba(249, 226, 175, 0.24);
}
.chart-card.docker,
.chart-card.network {
border-color: rgba(116, 199, 236, 0.24);
}
.card-title {
color: #00cc88;
font-size: 12px;
font-weight: 750;
}
.card-value {
color: #cdd6f4;
}
.compact,
.row-subtitle {
font-size: 12px;
}
.percent-label,
.chart-value {
color: #f9e2af;
font-size: 12px;
font-weight: 750;
}
.sparkline {
color: #00ff9c;
font-size: 25px;
letter-spacing: 0;
}
.meter {
min-height: 8px;
border-radius: 6px;
background: rgba(205, 214, 244, 0.10);
}
.meter-fill {
min-height: 8px;
border-radius: 6px;
background: linear-gradient(to right, #00ff9c, #89b4fa);
}
.alerts {
min-height: 32px;
}
.alert,
.pill {
padding: 4px 8px;
border-radius: 999px;
background: rgba(205, 214, 244, 0.10);
color: #cdd6f4;
font-size: 11px;
}
.alert.ok,
.pill.ok {
background: rgba(0, 255, 156, 0.16);
color: #00ff9c;
}
.alert.warn,
.pill.warn {
background: rgba(249, 226, 175, 0.16);
color: #f9e2af;
}
.table-row {
min-height: 72px;
padding: 10px;
}
.container-main {
min-width: 260px;
}
.container-stats {
min-width: 112px;
}
.row-actions {
min-width: 250px;
}
.row-title {
color: #cdd6f4;
font-weight: 750;
}
.service-card {
min-width: 250px;
min-height: 112px;
padding: 12px;
}
.actions {
margin-top: 2px;
}
.button,
.mini-button,
.icon-button {
padding: 8px 10px;
border-radius: 8px;
background: rgba(40, 40, 55, 0.82);
color: #cdd6f4;
}
.mini-button {
padding: 6px 8px;
font-size: 12px;
}
.button:hover,
.button:focus,
.mini-button:hover,
.mini-button:focus,
.icon-button:hover,
.icon-button:focus {
background: rgba(0, 255, 156, 0.22);
}
.primary {
background: rgba(0, 255, 156, 0.24);
color: #00ff9c;
}
.danger {
color: #f38ba8;
}
.close {
color: #f38ba8;
}
.icon-button {
min-width: 34px;
min-height: 34px;
padding: 0;
}
.error {
color: #f38ba8;
}

1124
config/hypr/ags/homelab.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
* {
all: unset;
font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif;
font-size: 14px;
}
.switcher-window {
background: transparent;
}
.switcher {
min-width: 680px;
padding: 18px;
border: 1px solid alpha(@ags_fg, 0.18);
border-radius: 16px;
background: @ags_bg;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
color: @ags_fg;
}
.header {
min-height: 34px;
}
.title {
font-size: 20px;
font-weight: 700;
}
.list-scroll {
min-height: 130px;
}
.item {
padding: 10px;
border: 1px solid alpha(@ags_fg, 0.10);
border-radius: 10px;
background: alpha(@ags_panel, 0.54);
}
.item-row {
min-height: 78px;
}
.item:hover,
.item:focus {
border-color: alpha(@ags_accent, 0.72);
background: alpha(@ags_panel, 0.82);
}
.item.active {
border-color: alpha(@ags_accent_2, 0.85);
}
.preview {
min-width: 96px;
min-height: 56px;
border-radius: 8px;
background-color: alpha(@ags_panel, 0.80);
background-size: cover;
background-position: center;
}
.preview-empty {
color: @ags_accent_2;
}
.item-title {
font-size: 15px;
font-weight: 700;
}
.item-subtitle {
margin-top: 4px;
color: @ags_muted;
font-size: 12px;
}
.swatches {
min-width: 86px;
}
.swatch {
min-width: 18px;
min-height: 18px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.16);
}
.icon-button {
min-width: 34px;
min-height: 34px;
border-radius: 8px;
color: @ags_fg;
background: alpha(@ags_panel, 0.62);
}
.preview-button {
min-width: 44px;
min-height: 78px;
}
.icon-button:hover,
.icon-button:focus {
background: alpha(@ags_accent, 0.28);
}
.close {
color: @ags_accent;
}
.empty {
min-height: 150px;
color: @ags_muted;
}

View 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);
},
});