huge changes

This commit is contained in:
Supertiger
2025-08-01 18:04:20 +01:00
parent 441836bdd3
commit 382f61a615
25 changed files with 9699 additions and 17 deletions

View File

@@ -1,10 +1,9 @@
import { createRootPage } from "./routes/root";
import { createWaybarPage } from "./routes/waybar";
if (location.pathname === "/waybar") {
createWaybarPage();
import("./routes/waybar/waybar").then(({ createWaybarPage }) =>
createWaybarPage()
);
}
if (location.pathname === "/") {
createRootPage();
import("./routes/root/root").then(({ createRootPage }) => createRootPage());
}

View File

@@ -1,5 +0,0 @@
const appElement = document.getElementById("app")!;
export const createRootPage = async () => {
appElement.innerHTML = "test";
};

14
src/routes/root/root.css Normal file
View File

@@ -0,0 +1,14 @@
body {
background: url(/bg.avif) no-repeat center center fixed;
background-size: cover;
}
html,
body {
height: 100%;
padding: 0;
margin: 0;
}
#waybar-iframe {
width: 100vw;
border: none;
}

8
src/routes/root/root.ts Normal file
View File

@@ -0,0 +1,8 @@
import "./root.css";
const appElement = document.getElementById("app")!;
export const createRootPage = async () => {
appElement.innerHTML = `
<iframe id="waybar-iframe" src="/waybar"></iframe>
`;
};

View File

@@ -1,5 +0,0 @@
const appElement = document.getElementById("app")!;
export const createWaybarPage = async () => {
appElement.innerHTML = "test";
};

View File

@@ -0,0 +1,61 @@
import { clamp, replaceTextWithUnicode } from "./utils";
interface LabelOpts {
config?: {
[key: string]: any;
format?: string;
"format-icons"?: string[];
};
interval?: number;
update: () => void;
}
export const createLabel = (opts: LabelOpts) => {
let format = "";
const getMarkup = () => {
if (!opts.config) return undefined;
if (!format) {
return opts.config?.format || undefined;
}
return opts.config["format-" + format] || undefined;
};
const element = document.createElement("span");
if (opts.interval && opts.update) {
setInterval(opts.update, opts.interval);
}
const set = (data: Record<string, any>) => {
const markup = getMarkup() as string | undefined;
if (!markup) {
console.error("No format found");
return "N/A";
}
const result = markup.replace(/\{(\w+)\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match;
});
element.innerHTML = replaceTextWithUnicode(result);
};
const getIcon = (percentage: number, icon?: string, max: number = 0) => {
const icons = opts.config?.["format-icons"];
const iconCount = icons?.length || 0;
const index = clamp(
percentage / ((max == 0 ? 100 : max) / iconCount),
0,
iconCount - 1
);
const resIcon = icons?.[Math.round(index)];
if (!resIcon) return "";
return resIcon;
};
return {
element,
set,
getIcon,
};
};

View File

@@ -0,0 +1,288 @@
import { parse } from "jsonc-parser";
/**
* A general interface for icon mappings, where a key maps to a single icon string or an array of icon strings.
*/
interface IconMap {
[key: string]: string | string[];
}
/**
* Common configuration properties for many Waybar modules.
*/
interface BaseModule {
format?: string;
"format-icons"?: IconMap;
tooltip?: boolean;
}
/**
* Defines the states and icons for a module.
*/
interface ModuleWithStates extends BaseModule {
states?: {
[state: string]: number;
};
"format-critical"?: string;
"format-alt"?: string;
}
/**
* Interface for the `sway/workspaces` module.
* Note: This module is commented out in the original JSON, but the interface is included for completeness.
*/
interface SwayWorkspacesModule extends BaseModule {
"disable-scroll"?: boolean;
"all-outputs"?: boolean;
"warp-on-scroll"?: boolean;
"format-icons"?: {
[key: string]: string;
};
}
/**
* Interface for the `keyboard-state` module.
*/
interface KeyboardStateModule extends BaseModule {
numlock?: boolean;
capslock?: boolean;
"format-icons"?: {
locked: string;
unlocked: string;
};
}
/**
* Interface for the `sway/mode` module.
*/
interface SwayModeModule {
format?: string;
}
/**
* Interface for the `sway/scratchpad` module.
*/
interface SwayScratchpadModule extends BaseModule {
"show-empty"?: boolean;
"format-icons"?: string[];
"tooltip-format"?: string;
}
/**
* Interface for the `mpd` module.
*/
interface MpdModule extends BaseModule {
"format-disconnected"?: string;
"format-stopped"?: string;
"unknown-tag"?: string;
interval?: number;
"consume-icons"?: {
on: string;
};
"random-icons"?: {
on: string;
off: string;
};
"repeat-icons"?: {
on: string;
};
"single-icons"?: {
on: string;
};
"state-icons"?: {
paused: string;
playing: string;
};
"tooltip-format"?: string;
"tooltip-format-disconnected"?: string;
}
/**
* Interface for the `idle_inhibitor` module.
*/
interface IdleInhibitorModule extends BaseModule {
"format-icons"?: {
activated: string;
deactivated: string;
};
}
/**
* Interface for the `tray` module.
*/
interface TrayModule {
"icon-size"?: number;
spacing?: number;
icons?: {
[appName: string]: string;
};
}
/**
* Interface for the `clock` module.
*/
interface ClockModule {
timezone?: string;
"tooltip-format"?: string;
"format-alt"?: string;
}
/**
* Interface for the `cpu` module.
*/
interface CpuModule extends BaseModule {
tooltip?: boolean;
}
/**
* Interface for the `memory` module.
*/
interface MemoryModule extends BaseModule {}
/**
* Interface for the `temperature` module.
*/
interface TemperatureModule extends ModuleWithStates {
"thermal-zone"?: number;
"hwmon-path"?: string;
"critical-threshold"?: number;
"format-icons"?: string[];
}
/**
* Interface for the `backlight` module.
*/
interface BacklightModule extends BaseModule {
device?: string;
"format-icons"?: string[];
}
/**
* Interface for the `battery` module.
*/
interface BatteryModule extends ModuleWithStates {
bat?: string; // Used for battery#bat2
"format-full"?: string;
"format-charging"?: string;
"format-plugged"?: string;
"format-good"?: string;
"format-icons"?: string[];
}
/**
* Interface for the `power-profiles-daemon` module.
*/
interface PowerProfilesDaemonModule extends BaseModule {
tooltip?: boolean;
"tooltip-format"?: string;
"format-icons"?: {
default: string;
performance: string;
balanced: string;
"power-saver": string;
};
}
/**
* Interface for the `network` module.
*/
interface NetworkModule {
interface?: string;
"format-wifi"?: string;
"format-ethernet"?: string;
"tooltip-format"?: string;
"format-linked"?: string;
"format-disconnected"?: string;
"format-alt"?: string;
}
/**
* Interface for the `pulseaudio` module.
*/
interface PulseaudioModule extends BaseModule {
"scroll-step"?: number;
"format-bluetooth"?: string;
"format-bluetooth-muted"?: string;
"format-muted"?: string;
"format-source"?: string;
"format-source-muted"?: string;
"format-icons"?: {
headphone: string;
"hands-free": string;
headset: string;
phone: string;
portable: string;
car: string;
default: string[];
};
"on-click"?: string;
}
/**
* Interface for the `custom/media` module.
*/
interface CustomMediaModule extends BaseModule {
"return-type"?: "json" | "text";
"max-length"?: number;
"format-icons"?: {
[key: string]: string;
};
escape?: boolean;
exec?: string;
}
/**
* Interface for the `custom/power` module.
*/
interface CustomPowerModule extends BaseModule {
menu?: string;
"menu-file"?: string;
"menu-actions"?: {
shutdown: string;
reboot: string;
suspend: string;
hibernate: string;
};
}
/**
* The main interface for the entire Waybar configuration file.
* It combines the main settings with a generic index signature for module configurations.
*/
export interface WaybarConfig {
layer?: "top" | "bottom" | "overlay";
position?: "top" | "bottom" | "left" | "right";
height?: number;
width?: number;
spacing?: number;
"modules-left"?: string[];
"modules-center"?: string[];
"modules-right"?: string[];
// Module-specific configurations
"sway/workspaces"?: SwayWorkspacesModule;
"sway/mode"?: SwayModeModule;
"sway/scratchpad"?: SwayScratchpadModule;
"keyboard-state"?: KeyboardStateModule;
mpd?: MpdModule;
idle_inhibitor?: IdleInhibitorModule;
tray?: TrayModule;
clock?: ClockModule;
cpu?: CpuModule;
memory?: MemoryModule;
temperature?: TemperatureModule;
backlight?: BacklightModule;
battery?: BatteryModule;
"battery#bat2"?: BatteryModule;
"power-profiles-daemon"?: PowerProfilesDaemonModule;
network?: NetworkModule;
pulseaudio?: PulseaudioModule;
"custom/media"?: CustomMediaModule;
"custom/power"?: CustomPowerModule;
}
export const parseConfig = async () => {
const response = await fetch("/resources/config.jsonc");
const jsoncString = await response.text();
return parse(jsoncString) as WaybarConfig;
};

View File

@@ -0,0 +1,11 @@
export type Module = ReturnType<typeof createModule>;
export const createModule = (name: string) => {
const element = document.createElement("div");
element.id = name;
element.className = "module";
return {
element,
};
};

View File

@@ -0,0 +1,13 @@
export type Modules = keyof typeof modules;
export type ModuleArray = Array<keyof typeof modules>;
export const modules = {
clock: () =>
import("./modules/clock").then(
({ createClockModule }) => createClockModule
),
battery: () =>
import("./modules/battery").then(
({ createBatteryModule }) => createBatteryModule
),
};

View File

@@ -0,0 +1,50 @@
import type { WaybarConfig } from "../configParser";
import type { Module } from "../createModule";
import { createLabel } from "../Label";
export const createBatteryModule = (
module: Module,
config: WaybarConfig["battery"]
) => {
const label = createLabel({
interval: 1000,
update: () => update(),
config: config!,
});
module.element.appendChild(label.element);
const update = async () => {
// const battery = await navigator.getBattery();
const battery = {
level: Math.random(),
charging: Math.random() > 0.5,
};
let state = "Unknown";
if (battery.charging && battery.level < 1) {
state = "Charging";
}
if (battery.charging && battery.level === 1) {
state = "Full";
}
const charging = battery.charging;
const batteryPercent = Math.round(battery.level * 100);
if (charging) {
module.element.classList.add("charging");
} else {
module.element.classList.remove("charging");
}
label.set({
capacity: batteryPercent,
icon: label.getIcon(batteryPercent),
});
};
update();
};

View File

@@ -0,0 +1,14 @@
import type { Module } from "../createModule";
export const createClockModule = (module: Module) => {
const update = () => {
const date = new Date();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
module.element.innerHTML = `${hours}:${minutes}`;
};
update();
setInterval(update, 1000);
};

View File

@@ -0,0 +1,60 @@
import { CssTypes, parse, stringify } from "@adobe/css-tools";
export const clamp = (val: number, min: number, max: number) =>
Math.min(Math.max(val, min), max);
const fetchFontAwesomeStylesheet = async () => {
const raw = await fetch("/fontawesome/css/fontawesome.min.css").then((res) =>
res.text()
);
return raw;
};
export const tryCatch = (tryTo: () => string, ifCatch: string) => {
try {
return tryTo();
} catch (e) {
return ifCatch;
}
};
const css = await fetchFontAwesomeStylesheet();
function unicodeToHexMap() {
const map = new Map<string, string>();
const parsed = parse(css);
parsed.stylesheet.rules.forEach((rule) => {
if (rule.type === CssTypes.rule) {
const declaration = rule.declarations[0];
if (declaration.type === CssTypes.declaration) {
if (declaration.property !== "--fa") return;
const selector = rule.selectors[0];
const value = declaration.value.slice(1, -1);
try {
const unicodeChar = String.fromCodePoint(
parseInt(value.slice(1), 16)
);
if (!Number.isNaN(parseInt(unicodeChar))) return;
map.set(unicodeChar, value);
} catch {}
}
}
});
return map;
}
const unicodeArray = [...unicodeToHexMap().keys()];
const unicodeRegex = new RegExp(
unicodeArray.map((x) => `\\${x}`).join("|"),
"g"
);
export const replaceTextWithUnicode = (text: string) => {
let newText = text.replace(
unicodeRegex,
(char) => `<span class="fa">${char}</span>`
);
return newText;
};

View File

@@ -0,0 +1,26 @@
#waybar {
display: flex;
}
.modules {
display: flex;
}
#modules-left {
}
#modules-center {
flex: 1;
}
#modules-right {
}
html,
body {
padding: 0;
margin: 0;
}
.module {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,56 @@
import { parseConfig } from "./configParser";
import { createModule } from "./createModule";
import { modules, type ModuleArray, type Modules } from "./modules";
import "./waybar.css";
const appElement = document.getElementById("app")!;
export const createWaybarPage = async () => {
const styleElement = document.createElement("link");
styleElement.rel = "stylesheet";
styleElement.href = "/resources/style.css";
document.head.appendChild(styleElement);
const config = await parseConfig();
appElement.innerHTML = `
<window id="waybar" style="height: ${config.height}px;">
<div class="modules" id="modules-left" style="gap: ${config.spacing}px;"></div>
<div class="modules" id="modules-center" style="gap: ${config.spacing}px;"></div>
<div class="modules" id="modules-right" style="gap: ${config.spacing}px;"></div>
</window>`;
const modulesLeftElement = document.getElementById("modules-left")!;
const modulesCenterElement = document.getElementById("modules-center")!;
const modulesRightElement = document.getElementById("modules-right")!;
const moduleLeft = (config["modules-left"] as ModuleArray) || [];
const moduleCenter = (config["modules-center"] as ModuleArray) || [];
const moduleRight = (config["modules-right"] as ModuleArray) || [];
const loadModules = async (
enabledModules: ModuleArray,
modulesElement: HTMLElement
) => {
enabledModules.forEach(async (moduleName) => {
if (moduleName === "battery") {
console.log(moduleName);
const module = createModule("battery");
modulesElement.appendChild(module.element);
const createBatteryModule = await modules.battery();
createBatteryModule(module, config.battery);
}
if (moduleName === "clock") {
console.log(moduleName);
const module = createModule("clock");
modulesElement.appendChild(module.element);
const createClockModule = await modules.clock();
createClockModule(module, config.clock);
}
});
};
loadModules(moduleLeft, modulesLeftElement);
loadModules(moduleCenter, modulesCenterElement);
loadModules(moduleRight, modulesRightElement);
};