commit e641720a3c8e5f064aa615a9fec652c9b2bb9f41 Author: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Thu Jul 23 12:14:34 2026 -0400 Initial commit: SkyOS source code Co-Authored-By: Claude Sonnet 4.6 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d4230c1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# Don't send pre-built Mac binaries — the container builds its own +wm/skywm +wm/src/*.o +deskbar/skydeskbar +deskbar/src/*.o +launcher/skylauncher + +# macOS junk +.DS_Store +**/.DS_Store +dist/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e55ccea --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +*.iso +*.o +deskbar/skydeskbar +installer/skyinstall +installer/skyinstall-gui +launcher/skylauncher +wm/skywm diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..37056a3 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +# SkyOS — top-level build +# Usage: +# make — build all components +# make install — install binaries + theme to /usr/local +# make clean — remove build artifacts +# make iso — build the bootable ISO (requires Arch Linux + archiso) + +.PHONY: all wm deskbar launcher installer clean install iso + +all: wm deskbar launcher installer + +wm: + $(MAKE) -C wm + +deskbar: + $(MAKE) -C deskbar + +launcher: + $(MAKE) -C launcher + +installer: + $(MAKE) -C installer + +clean: + $(MAKE) -C wm clean + $(MAKE) -C deskbar clean + $(MAKE) -C launcher clean + $(MAKE) -C installer clean + +install: all + # binaries + install -Dm755 wm/skywm /usr/local/bin/skywm + install -Dm755 deskbar/skydeskbar /usr/local/bin/skydeskbar + install -Dm755 launcher/skylauncher /usr/local/bin/skylauncher + install -Dm755 themes/skywallpaper /usr/local/bin/skywallpaper + # GTK theme + mkdir -p /usr/share/themes + cp -r themes/SkyOS /usr/share/themes/ + # picom config + install -Dm644 themes/picom.conf /etc/skyos/picom.conf + # wallpaper + mkdir -p /usr/share/skyos/wallpapers + bash themes/wallpapers/generate-default.sh || true + cp -f themes/wallpapers/skyos-default.png /usr/share/skyos/wallpapers/ 2>/dev/null || true + @echo "" + @echo "SkyOS installed. Add this to ~/.xinitrc to start:" + @echo "" + @echo " skywallpaper" + @echo " picom --config /etc/skyos/picom.conf &" + @echo " skydeskbar &" + @echo " exec skywm" + +iso: + chmod +x build-iso.sh + ./build-iso.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f89afd --- /dev/null +++ b/README.md @@ -0,0 +1,169 @@ +# SkyOS + +A BeOS-inspired Linux desktop built from scratch in C. Custom window manager, deskbar, launcher, GTK theme, and graphical installer — all running on Arch Linux with a bootable ISO. + +![SkyOS Desktop](docs/screenshot.png) + +--- + +## What it is + +SkyOS is a complete desktop environment built on X11/Xlib with a distinctive dark blue aesthetic. It ships as a bootable ISO you can try live or install to disk. + +**Components** + +| Component | Description | +|-----------|-------------| +| `skywm` | Stacking window manager — titlebars, 3-button close/maximize/minimize, snap-to-edge, double-click maximize | +| `skydeskbar` | Sidebar panel — clock, date, task list, CPU/RAM/battery status | +| `skylauncher` | App launcher — keyboard-driven grid of application shortcuts | +| `skyinstall-gui` | Graphical installer wizard — disk selection, user setup, live progress | +| `SkyOS GTK theme` | Matching dark theme for GTK 2/3 apps | + +--- + +## Building the ISO + +**Requirements:** Docker Desktop (Mac/Windows/Linux) + +```bash +./build-iso.sh +``` + +Output: `dist/skyos-YYYYMMDD.iso` + +The build runs entirely inside a Docker container — no Arch Linux install required on the host. First build downloads ~600 MB of packages and takes 10–20 minutes. Subsequent builds are fast thanks to Docker layer caching. + +### What the build does + +1. Starts from the official `archlinux:latest` Docker image +2. Installs `archiso` and build tools +3. Builds the SkyOS binaries (`skywm`, `skydeskbar`, `skylauncher`, `skyinstall-gui`) +4. Assembles an archiso profile with the full package set +5. Produces a hybrid ISO that boots on both UEFI and legacy BIOS hardware + +--- + +## Flashing to USB + +**Balena Etcher (recommended):** https://etcher.balena.io — drag the ISO, pick the USB, flash. + +**Command line (Mac/Linux):** +```bash +# Find your USB drive first +diskutil list # macOS +lsblk # Linux + +# Flash (replace diskN / sdX) +sudo dd if=dist/skyos-*.iso of=/dev/diskN bs=4m # macOS +sudo dd if=dist/skyos-*.iso of=/dev/sdX bs=4M # Linux +``` + +--- + +## Booting + +Boot from the USB. SkyOS starts the live desktop automatically — no login required (user: `sky`, password: `skyos`). + +**Intel Mac notes:** +- Hold `Option` at startup to select the USB boot device +- WiFi uses the `broadcom-wl` driver (loaded automatically) +- Trackpad: tap-to-click and two-finger scroll are enabled by default +- Audio uses PipeWire + SOF firmware for Intel HDA + +--- + +## Installing to disk + +Click **Install SkyOS** in the launcher, or run `sudo skyinstall-gui` from a terminal. + +The graphical installer walks through: + +1. Disk selection (shows all disks ≥ 4 GB with model and size) +2. Hostname, username, and password +3. Review screen with erase warning +4. Live installation with progress bar and log +5. Reboot + +The installer auto-detects UEFI vs legacy BIOS and sets up the appropriate bootloader (`systemd-boot` for UEFI, `GRUB` for BIOS). + +--- + +## Included software + +| Category | Applications | +|----------|-------------| +| Browser | Firefox | +| Office | OnlyOffice | +| Files | Thunar | +| Terminal | XTerm | +| Video | mpv | +| Images | Viewnior | +| PDF | Evince | +| Text editor | Mousepad | +| Audio | PipeWire, pavucontrol | +| Network | NetworkManager, nm-applet | +| Bluetooth | bluez, blueman | +| Compositor | picom (GLX, xrender fallback) | +| Notifications | dunst | + +--- + +## Building from source (without Docker) + +Requires an Arch Linux system with `libx11`, `libxft`, `fontconfig` development headers. + +```bash +make # build all components +make install # install to /usr/local (run as root) +``` + +Individual components: + +```bash +make -C wm # skywm +make -C deskbar # skydeskbar +make -C launcher # skylauncher +make -C installer # skyinstall-gui +``` + +--- + +## Project layout + +``` +OS/ +├── wm/ skywm window manager (C/Xlib) +├── deskbar/ skydeskbar panel (C/Xlib/Xft) +├── launcher/ skylauncher app grid (C/Xlib/Xft) +├── installer/ skyinstall-gui + skyinstall backend +├── themes/ +│ ├── SkyOS/ GTK 2/3 theme +│ ├── picom.conf compositor config +│ └── skywallpaper wallpaper setter +├── iso/ +│ └── docker/ Dockerfile + entrypoint.sh (ISO builder) +├── build-iso.sh one-command ISO build script +└── Makefile +``` + +--- + +## Keyboard shortcuts + +| Shortcut | Action | +|----------|--------| +| `Alt + F1` | Open launcher | +| `Alt + F4` | Close window | +| `Alt + F10` | Toggle maximize | +| `Super + ←` | Snap window left | +| `Super + →` | Snap window right | +| `Alt + drag` | Move window | +| `XF86MonBrightness↑↓` | Screen brightness | +| `XF86Audio*` | Volume / mute | + +--- + +## License + +MIT diff --git a/build-iso.sh b/build-iso.sh new file mode 100755 index 0000000..43b2a62 --- /dev/null +++ b/build-iso.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# SkyOS ISO builder — run this on your Mac to produce a bootable ISO. +# Requires: Docker Desktop (https://docker.com/products/docker-desktop) +# +# Usage: ./build-iso.sh +# Output: dist/skyos-.iso +set -euo pipefail + +REPO="$(cd "$(dirname "$0")" && pwd)" +DIST="$REPO/dist" +IMAGE="skyos-builder" +ISO_TAG="skyos-$(date +%Y%m%d)" + +# ── preflight ───────────────────────────────────────────────────────── +if ! command -v docker &>/dev/null; then + echo "" + echo " Docker is not installed." + echo "" + echo " Install Docker Desktop for Mac:" + echo " https://www.docker.com/products/docker-desktop/" + echo "" + echo " After installing, open Docker Desktop once to let it start," + echo " then re-run this script." + exit 1 +fi + +if ! docker info &>/dev/null; then + echo "" + echo " Docker is installed but not running." + echo " Open Docker Desktop and wait for it to start, then re-run." + exit 1 +fi + +echo "" +echo "┌─────────────────────────────────────────────┐" +echo "│ SkyOS ISO Builder │" +echo "│ Building a BeOS-inspired Linux desktop... │" +echo "└─────────────────────────────────────────────┘" +echo "" + +# ── build the builder image ─────────────────────────────────────────── +echo "Step 1/3: Building Docker image (downloads ~600MB Arch base, once)" +echo " This will be fast on subsequent runs." +echo "" +docker build \ + --platform linux/amd64 \ + -t "$IMAGE" \ + -f "$REPO/iso/docker/Dockerfile" \ + "$REPO" + +# ── run the ISO build inside the container ──────────────────────────── +echo "" +echo "Step 2/3: Building ISO inside Arch Linux container..." +echo " (5–15 min depending on internet speed and CPU)" +echo "" +mkdir -p "$DIST" +docker run \ + --rm \ + --privileged \ + --platform linux/amd64 \ + --security-opt seccomp=unconfined \ + -v "$DIST:/dist" \ + -e DIST=/dist \ + "$IMAGE" + +# ── rename with date tag ────────────────────────────────────────────── +echo "" +echo "Step 3/3: Finalizing..." +ISO="$(find "$DIST" -name "skyos*.iso" | head -1)" +if [[ -z "$ISO" ]]; then + echo "ERROR: ISO not found in $DIST — check the build output above." + exit 1 +fi + +FINAL="$DIST/$ISO_TAG.iso" +mv "$ISO" "$FINAL" + +SIZE="$(du -sh "$FINAL" | cut -f1)" + +echo "" +echo "┌─────────────────────────────────────────────┐" +echo "│ Done! │" +echo "│ │" +echo "│ ISO: dist/$ISO_TAG.iso" +echo "│ Size: $SIZE" +echo "│ │" +echo "│ Flash to USB: │" +echo "│ sudo dd if=$FINAL of=/dev/diskN bs=4m │" +echo "│ (replace /dev/diskN with your USB drive) │" +echo "│ │" +echo "│ Or use Balena Etcher (GUI): │" +echo "│ https://etcher.balena.io │" +echo "└─────────────────────────────────────────────┘" +echo "" diff --git a/deskbar/Makefile b/deskbar/Makefile new file mode 100644 index 0000000..f8b8b2e --- /dev/null +++ b/deskbar/Makefile @@ -0,0 +1,26 @@ +CC = gcc + +PKG_XFT_C := $(shell pkg-config --cflags xft x11 2>/dev/null) +PKG_XFT_L := $(shell pkg-config --libs xft x11 2>/dev/null) +ifeq ($(PKG_XFT_C),) + PKG_XFT_C := -I/opt/homebrew/include + PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig +endif + +CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C) +LDFLAGS = $(PKG_XFT_L) + +SRC = src/deskbar.c +OBJ = $(SRC:.c=.o) +BIN = skydeskbar + +.PHONY: all clean install +all: $(BIN) +$(BIN): $(OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) +clean: + rm -f $(OBJ) $(BIN) diff --git a/deskbar/include/deskbar.h b/deskbar/include/deskbar.h new file mode 100644 index 0000000..100dba6 --- /dev/null +++ b/deskbar/include/deskbar.h @@ -0,0 +1,155 @@ +#ifndef DESKBAR_H +#define DESKBAR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * SkyDeskbar — modern dark panel, docked top-right. + * + * ┌──────────────────────────┐ + * │ [Sky ▾] │ 12:00 PM │ <- top strip: menu + clock + * │ │ Wed Jul 23 │ <- date line + * ├──────────────────────────┤ + * │ [F] Firefox ▸ │ <- app entries (letter icon + name) + * │ [T] Thunar ▸ │ + * │ [X] [xterm] ▸ │ <- minimized: bracketed + dimmed + * ├──────────────────────────┤ + * │ [1] [2] [3] [4] │ <- workspace strip + * ├──────────────────────────┤ + * │ CPU 12% MEM 45% │ <- status bar + * └──────────────────────────┘ + */ + +/* ── layout ──────────────────────────────────────────────────────────── */ +#define DB_W 200 /* deskbar total width */ +#define DB_TOP_H 52 /* top strip height (menu + clock + date) */ +#define DB_MENU_W 68 /* width of the Sky menu button */ +#define DB_ITEM_H 26 /* app entry row height */ +#define DB_ICON_SZ 18 /* app icon square size */ +#define DB_WS_H 28 /* workspace strip height */ +#define DB_STATUS_H 22 /* status bar height */ +#define DB_MAX_APPS 32 + +/* ── fly-out menu ────────────────────────────────────────────────────── */ +#define DB_FLYOUT_W 196 /* fly-out menu panel width */ +#define DB_FLYOUT_IH 28 /* item row height */ +#define DB_FLYOUT_SH 8 /* separator height */ +#define DB_FLYOUT_PAD 4 /* top/bottom outer padding */ +#define DB_FLYOUT_LPD 13 /* left label padding */ + +/* ── modern dark palette ──────────────────────────────────────────────── */ +#define DB_COL_BG 0x111520 /* dark navy body */ +#define DB_COL_MENU_BG 0x0E1A32 /* menu button bg */ +#define DB_COL_LOGO_TEXT 0x90BBEE /* "Sky" text */ +#define DB_COL_ITEM_BG 0x141828 /* app row bg */ +#define DB_COL_ITEM_ALT 0x111520 /* alternate row */ +#define DB_COL_ITEM_HOV 0x1E4878 /* hover — blue */ +#define DB_COL_ITEM_FOCUS 0x183860 /* focused window row */ +#define DB_COL_ITEM_TEXT 0xC0D0E8 /* normal app name */ +#define DB_COL_ITEM_HTXT 0xF0F6FF /* text on hover/focus */ +#define DB_COL_ITEM_MINI 0x485868 /* dimmed minimized text */ +#define DB_COL_DIVIDER 0x1C2030 /* row separator */ +#define DB_COL_WS_ACTIVE 0x1E58A8 /* active workspace */ +#define DB_COL_WS_INACT 0x202838 /* inactive workspace */ +#define DB_COL_WS_TEXT 0x8AB0D0 /* workspace number */ +#define DB_COL_CLOCK_TXT 0xD8E8FF /* clock text */ +#define DB_COL_DATE_TXT 0x5878A0 /* date text */ +#define DB_COL_STATUS_TXT 0x486080 /* status text */ +#define DB_COL_BORDER_L 0x223050 /* bevel light */ +#define DB_COL_BORDER_D 0x080C14 /* bevel dark */ +#define DB_COL_ACCENT 0x1E58A8 /* accent stripe */ + +/* ── fonts ───────────────────────────────────────────────────────────── */ +#define DB_FONT_UI "Sans:size=9" +#define DB_FONT_MENU "Sans:size=10:weight=bold" +#define DB_FONT_CLOCK "Sans:size=11:weight=bold" +#define DB_FONT_DATE "Sans:size=8" +#define DB_FONT_ICON "Sans:size=8:weight=bold" + +/* ── app icon color palette (cycled by name hash) ────────────────────── */ +static const unsigned long DB_ICON_COLORS[] = { + 0x2060A0, /* blue */ + 0x206040, /* forest green */ + 0x804020, /* burnt orange */ + 0x602080, /* purple */ + 0x208060, /* teal */ + 0x802040, /* crimson */ + 0x406020, /* olive */ + 0x204060, /* dark blue */ +}; +#define DB_ICON_NCOLORS 8 + +typedef struct AppEntry { + Window win; + char title[64]; + int focused; + int minimized; +} AppEntry; + +typedef struct Deskbar { + Display *dpy; + int screen; + Window root; + Window win; + GC gc; + Colormap cmap; + Visual *visual; + int depth; + + XftFont *font_ui; + XftFont *font_menu; + XftFont *font_clock; + XftFont *font_date; + XftFont *font_icon; + + int bar_h; + int hovered; + int cur_ws; + int num_ws; + + AppEntry apps[DB_MAX_APPS]; + int napps; + + Atom net_client_list; + Atom net_active_window; + Atom net_current_desktop; + Atom net_number_of_desktops; + Atom net_wm_name; + Atom utf8_string; + + /* fly-out menu state */ + Window menu_win; /* main fly-out, None when closed */ + Window sub_win; /* sub-menu panel, None when closed */ + int menu_hov; /* hovered item index in main menu, -1 = none */ + int sub_active; /* which sub-menu is open: 0=Apps, 1=Settings, -1=none */ + int sub_hov; /* hovered item in sub-menu */ + int sw_cache; /* cached screen width for menu positioning */ +} Deskbar; + +void db_init(Deskbar *db); +void db_run(Deskbar *db); +void db_draw(Deskbar *db); +void db_update_apps(Deskbar *db); +void db_update_workspace(Deskbar *db); +void db_resize(Deskbar *db); +void db_click(Deskbar *db, int x, int y); +void db_hover(Deskbar *db, int x, int y); +void db_menu_open(Deskbar *db); +void db_menu_close(Deskbar *db); + +unsigned long db_color(Deskbar *db, unsigned long rgb); +void db_xft_color(Deskbar *db, unsigned long rgb, XftColor *out); + +int db_apps_y(void); +int db_ws_y(Deskbar *db); + +#endif diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c new file mode 100644 index 0000000..0a6be61 --- /dev/null +++ b/deskbar/src/deskbar.c @@ -0,0 +1,963 @@ +#include "deskbar.h" + +/* ── layout ───────────────────────────────────────────────────────────── */ + +int db_apps_y(void) { return DB_TOP_H + 1; } + +int db_ws_y(Deskbar *db) { + return db_apps_y() + db->napps * DB_ITEM_H + 1; +} + +static int db_status_y(Deskbar *db) { + return db_ws_y(db) + DB_WS_H + 1; +} + +void db_resize(Deskbar *db) { + db->bar_h = db_status_y(db) + DB_STATUS_H + 2; + int sw = DisplayWidth(db->dpy, db->screen); + XMoveResizeWindow(db->dpy, db->win, sw - DB_W, 0, DB_W, db->bar_h); +} + +/* ── colors ───────────────────────────────────────────────────────────── */ + +unsigned long db_color(Deskbar *db, unsigned long rgb) { + XColor c; + c.red = ((rgb >> 16) & 0xFF) * 257; + c.green = ((rgb >> 8) & 0xFF) * 257; + c.blue = ((rgb) & 0xFF) * 257; + c.flags = DoRed | DoGreen | DoBlue; + XAllocColor(db->dpy, db->cmap, &c); + return c.pixel; +} + +void db_xft_color(Deskbar *db, unsigned long rgb, XftColor *out) { + XRenderColor rc; + rc.red = ((rgb >> 16) & 0xFF) * 257; + rc.green = ((rgb >> 8) & 0xFF) * 257; + rc.blue = ((rgb) & 0xFF) * 257; + rc.alpha = 0xFFFF; + XftColorAllocValue(db->dpy, db->visual, db->cmap, &rc, out); +} + +static unsigned long db_app_icon_color(const char *title) { + unsigned int h = 0; + for (const char *p = title; *p; p++) h = h * 31 + (unsigned char)*p; + return DB_ICON_COLORS[h % DB_ICON_NCOLORS]; +} + +/* ── EWMH helpers ─────────────────────────────────────────────────────── */ + +static long ewmh_get_long(Deskbar *db, Window win, Atom prop) { + Atom type; int fmt; unsigned long n, after; + unsigned char *data = NULL; + long val = -1; + if (XGetWindowProperty(db->dpy, win, prop, 0, 1, False, + XA_CARDINAL, &type, &fmt, &n, &after, &data) + == Success && data) { + val = *(long *)data; + XFree(data); + } + return val; +} + +static void ewmh_get_title(Deskbar *db, Window win, char *buf, int bufsz) { + Atom type; int fmt; unsigned long n, after; + unsigned char *data = NULL; + buf[0] = '\0'; + if (XGetWindowProperty(db->dpy, win, db->net_wm_name, 0, 256, False, + db->utf8_string, &type, &fmt, &n, &after, &data) + == Success && data && n > 0) { + snprintf(buf, bufsz, "%.*s", (int)n, data); + XFree(data); + return; + } + if (data) XFree(data); + char *name = NULL; + if (XFetchName(db->dpy, win, &name) && name) { + snprintf(buf, bufsz, "%s", name); + XFree(name); + } +} + +void db_update_apps(Deskbar *db) { + Atom type; int fmt; unsigned long n, after; + unsigned char *data = NULL; + db->napps = 0; + + if (XGetWindowProperty(db->dpy, db->root, db->net_client_list, + 0, DB_MAX_APPS, False, XA_WINDOW, + &type, &fmt, &n, &after, &data) + != Success || !data) return; + + Window *wins = (Window *)data; + long active_win = -1; + { + unsigned char *aw = NULL; + unsigned long an, aa; Atom at; int af; + if (XGetWindowProperty(db->dpy, db->root, db->net_active_window, + 0, 1, False, XA_WINDOW, &at, &af, &an, &aa, &aw) + == Success && aw) { + active_win = *(long *)aw; + XFree(aw); + } + } + + Atom net_wm_state = XInternAtom(db->dpy, "_NET_WM_STATE", False); + Atom net_wm_state_hidden = XInternAtom(db->dpy, "_NET_WM_STATE_HIDDEN", False); + + for (unsigned long i = 0; i < n && db->napps < DB_MAX_APPS; i++) { + db->apps[db->napps].win = wins[i]; + db->apps[db->napps].focused = (wins[i] == (Window)active_win); + db->apps[db->napps].minimized = 0; + + { + Atom atype; int afmt; unsigned long an, aa; + unsigned char *adata = NULL; + if (XGetWindowProperty(db->dpy, wins[i], net_wm_state, + 0, 32, False, XA_ATOM, + &atype, &afmt, &an, &aa, &adata) + == Success && adata) { + Atom *atoms = (Atom *)adata; + for (unsigned long j = 0; j < an; j++) + if (atoms[j] == net_wm_state_hidden) + db->apps[db->napps].minimized = 1; + XFree(adata); + } + } + + ewmh_get_title(db, wins[i], + db->apps[db->napps].title, + sizeof(db->apps[db->napps].title)); + if (db->apps[db->napps].title[0] != '\0') + db->napps++; + } + XFree(data); +} + +void db_update_workspace(Deskbar *db) { + long cws = ewmh_get_long(db, db->root, db->net_current_desktop); + long nws = ewmh_get_long(db, db->root, db->net_number_of_desktops); + db->cur_ws = (cws >= 0) ? (int)cws : 0; + db->num_ws = (nws > 0) ? (int)nws : 4; +} + +/* ── system status ────────────────────────────────────────────────────── */ + +static int read_cpu_percent(void) { + static long prev_idle = 0, prev_total = 0; + FILE *f = fopen("/proc/stat", "r"); + if (!f) return -1; + long u, n, s, idle, iow, irq, sirq; + int ok = fscanf(f, "cpu %ld %ld %ld %ld %ld %ld %ld", + &u, &n, &s, &idle, &iow, &irq, &sirq); + fclose(f); + if (ok < 7) return -1; + long total = u + n + s + idle + iow + irq + sirq; + long d_idle = idle - prev_idle; + long d_total = total - prev_total; + prev_idle = idle; + prev_total = total; + if (d_total == 0) return 0; + return (int)(100 * (d_total - d_idle) / d_total); +} + +static int read_battery_percent(void) { + FILE *f; + char path[80]; + for (int i = 0; i <= 1; i++) { + snprintf(path, sizeof(path), + "/sys/class/power_supply/BAT%d/capacity", i); + f = fopen(path, "r"); + if (f) { int v = -1; fscanf(f, "%d", &v); fclose(f); return v; } + } + return -1; +} + +static char read_battery_status(void) { + FILE *f; + char path[80], status[16]; + for (int i = 0; i <= 1; i++) { + snprintf(path, sizeof(path), + "/sys/class/power_supply/BAT%d/status", i); + f = fopen(path, "r"); + if (f) { + status[0] = '\0'; + fscanf(f, "%15s", status); + fclose(f); + return status[0]; /* 'C'harging, 'D'ischarging, 'F'ull */ + } + } + return 0; +} + +static int read_mem_percent(void) { + FILE *f = fopen("/proc/meminfo", "r"); + if (!f) return -1; + long total = 0, available = 0; + char line[128]; + while (fgets(line, sizeof(line), f)) { + long v; + if (sscanf(line, "MemTotal: %ld", &v) == 1) total = v; + if (sscanf(line, "MemAvailable: %ld", &v) == 1) available = v; + if (total && available) break; + } + fclose(f); + if (!total) return -1; + return (int)(100 * (total - available) / total); +} + +/* ── drawing ──────────────────────────────────────────────────────────── */ + +static void draw_str(Deskbar *db, XftDraw *xd, XftFont *font, + unsigned long rgb, int x, int y, + const char *str, int clip_x, int clip_w) { + XftColor col; + db_xft_color(db, rgb, &col); + if (clip_w > 0) { + XRectangle r = { (short)clip_x, 0, + (unsigned short)clip_w, + (unsigned short)db->bar_h }; + XftDrawSetClipRectangles(xd, 0, 0, &r, 1); + } + XGlyphInfo ext; + XftTextExtentsUtf8(db->dpy, font, (const FcChar8 *)str, strlen(str), &ext); + XftDrawStringUtf8(xd, &col, font, x, y + ext.height - ext.y, + (const FcChar8 *)str, strlen(str)); + if (clip_w > 0) XftDrawSetClip(xd, None); + XftColorFree(db->dpy, db->visual, db->cmap, &col); +} + +static void draw_str_centered(Deskbar *db, XftDraw *xd, XftFont *font, + unsigned long rgb, + int strip_x, int strip_y, int strip_w, int strip_h, + const char *str) { + XGlyphInfo ext; + XftTextExtentsUtf8(db->dpy, font, (const FcChar8 *)str, strlen(str), &ext); + int tx = strip_x + (strip_w - (int)ext.width) / 2; + int ty = strip_y + (strip_h - (int)ext.height) / 2; + draw_str(db, xd, font, rgb, tx, ty, str, 0, 0); +} + +/* Draw a letter icon (first char of title) */ +static void draw_letter_icon(Deskbar *db, XftDraw *xd, + int ix, int iy, const char *title, + unsigned long bg, int mini) { + /* background square */ + unsigned long icon_col = mini ? db_color(db, 0x2A2E3A) + : db_color(db, bg); + XSetForeground(db->dpy, db->gc, icon_col); + XFillRectangle(db->dpy, db->win, db->gc, ix, iy, DB_ICON_SZ, DB_ICON_SZ); + + /* subtle border */ + unsigned long border_col = mini ? db_color(db, 0x383C4A) : db_color(db, bg + 0x101010); + XSetForeground(db->dpy, db->gc, border_col); + XDrawRectangle(db->dpy, db->win, db->gc, ix, iy, DB_ICON_SZ - 1, DB_ICON_SZ - 1); + + /* letter */ + char letter[2] = { title[0] ? (char)((title[0] & 0x5F) | 0x40) : '?', '\0' }; + /* convert to uppercase */ + if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; + unsigned long txt_col = mini ? DB_COL_ITEM_MINI : DB_COL_ITEM_HTXT; + XGlyphInfo ext; + XftTextExtentsUtf8(db->dpy, db->font_icon, (const FcChar8 *)letter, 1, &ext); + int lx = ix + (DB_ICON_SZ - ext.width) / 2; + int ly = iy + (DB_ICON_SZ - ext.height) / 2; + draw_str(db, xd, db->font_icon, txt_col, lx, ly, letter, 0, 0); +} + +void db_draw(Deskbar *db) { + XftDraw *xd = XftDrawCreate(db->dpy, db->win, db->visual, db->cmap); + + /* ── TOP STRIP ── */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); + XFillRectangle(db->dpy, db->win, db->gc, 0, 0, DB_W, DB_TOP_H); + + /* accent line at very top */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); + XFillRectangle(db->dpy, db->win, db->gc, 0, 0, DB_W, 2); + + /* Sky menu button — highlight when menu is open */ + unsigned long sky_bg = db->menu_win ? 0x1A3868 : DB_COL_MENU_BG; + XSetForeground(db->dpy, db->gc, db_color(db, sky_bg)); + XFillRectangle(db->dpy, db->win, db->gc, 0, 2, DB_MENU_W, DB_TOP_H - 2); + /* bottom indicator line when open */ + if (db->menu_win) { + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); + XFillRectangle(db->dpy, db->win, db->gc, 0, DB_TOP_H - 2, DB_MENU_W, 2); + } + /* "Sky \xe2\x96\xbe" = "Sky ▾" */ + draw_str_centered(db, xd, db->font_menu, DB_COL_LOGO_TEXT, + 0, 2, DB_MENU_W, DB_TOP_H - 2, "Sky \xe2\x96\xbe"); + + /* vertical divider */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); + XDrawLine(db->dpy, db->win, db->gc, DB_MENU_W, 2, DB_MENU_W, DB_TOP_H - 2); + + /* clock */ + { + time_t t = time(NULL); + struct tm *tm_info = localtime(&t); + char tstr[16], dstr[24]; + strftime(tstr, sizeof(tstr), "%I:%M %p", tm_info); + strftime(dstr, sizeof(dstr), "%a %b %d", tm_info); + + int cw = DB_W - DB_MENU_W - 1; + int cx = DB_MENU_W + 1; + + /* time on upper part */ + draw_str_centered(db, xd, db->font_clock, DB_COL_CLOCK_TXT, + cx, 4, cw, DB_TOP_H / 2 - 2, tstr); + /* date on lower part */ + draw_str_centered(db, xd, db->font_date, DB_COL_DATE_TXT, + cx, DB_TOP_H / 2 + 2, cw, DB_TOP_H / 2 - 4, dstr); + } + + /* separator below top strip */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); + XDrawLine(db->dpy, db->win, db->gc, 0, DB_TOP_H, DB_W, DB_TOP_H); + + /* ── APP LIST ── */ + int ay = db_apps_y(); + + for (int i = 0; i < db->napps; i++) { + int iy = ay + i * DB_ITEM_H; + int hover = (db->hovered == i); + int focus = db->apps[i].focused; + int mini = db->apps[i].minimized; + + /* row background — alternate for readability */ + unsigned long row_bg = + hover ? db_color(db, DB_COL_ITEM_HOV) : + focus ? db_color(db, DB_COL_ITEM_FOCUS) : + (i % 2 == 0) ? db_color(db, DB_COL_ITEM_BG) + : db_color(db, DB_COL_ITEM_ALT); + XSetForeground(db->dpy, db->gc, row_bg); + XFillRectangle(db->dpy, db->win, db->gc, 0, iy, DB_W, DB_ITEM_H); + + /* letter icon */ + unsigned long icon_bg = db_app_icon_color(db->apps[i].title); + int icon_x = 5; + int icon_y = iy + (DB_ITEM_H - DB_ICON_SZ) / 2; + draw_letter_icon(db, xd, icon_x, icon_y, + db->apps[i].title, icon_bg, mini); + + /* app name */ + int name_x = icon_x + DB_ICON_SZ + 6; + int name_maxw = DB_W - name_x - 14; + int name_y = iy + (DB_ITEM_H - 10) / 2; + + unsigned long tc = (hover || focus) ? DB_COL_ITEM_HTXT + : mini ? DB_COL_ITEM_MINI + : DB_COL_ITEM_TEXT; + + if (mini && !hover) { + char bracketed[70]; + snprintf(bracketed, sizeof(bracketed), "[%s]", db->apps[i].title); + draw_str(db, xd, db->font_ui, tc, name_x, name_y, + bracketed, name_x, name_maxw); + } else { + draw_str(db, xd, db->font_ui, tc, name_x, name_y, + db->apps[i].title, name_x, name_maxw); + } + + /* expand arrow ▸ */ + unsigned long arrow_col = (hover || focus) ? DB_COL_ITEM_HTXT : 0x303848; + draw_str(db, xd, db->font_ui, arrow_col, + DB_W - 13, name_y, "\xe2\x96\xb8", 0, 0); + + /* row divider */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_DIVIDER)); + XDrawLine(db->dpy, db->win, db->gc, + 0, iy + DB_ITEM_H - 1, DB_W, iy + DB_ITEM_H - 1); + } + + /* separator below app list */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); + XDrawLine(db->dpy, db->win, db->gc, + 0, ay + db->napps * DB_ITEM_H, DB_W, ay + db->napps * DB_ITEM_H); + + /* ── WORKSPACE STRIP ── */ + int wy = db_ws_y(db); + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); + XFillRectangle(db->dpy, db->win, db->gc, 0, wy, DB_W, DB_WS_H); + + int total = db->num_ws < 1 ? 4 : (db->num_ws > 8 ? 8 : db->num_ws); + int btnw = (DB_W - 8) / total; + int bth = DB_WS_H - 6; + + for (int i = 0; i < total; i++) { + int bx = 4 + i * btnw; + int by = wy + 3; + int active = (i == db->cur_ws); + + XSetForeground(db->dpy, db->gc, + db_color(db, active ? DB_COL_WS_ACTIVE : DB_COL_WS_INACT)); + XFillRectangle(db->dpy, db->win, db->gc, bx, by, btnw - 2, bth); + + /* active indicator: accent top edge */ + if (active) { + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); + XFillRectangle(db->dpy, db->win, db->gc, bx, by, btnw - 2, 2); + } + + char label[4]; + snprintf(label, sizeof(label), "%d", i + 1); + draw_str_centered(db, xd, db->font_ui, + active ? DB_COL_ITEM_HTXT : DB_COL_WS_TEXT, + bx, by, btnw - 2, bth, label); + } + + /* separator below workspace strip */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); + XDrawLine(db->dpy, db->win, db->gc, + 0, wy + DB_WS_H, DB_W, wy + DB_WS_H); + + /* ── STATUS BAR ── */ + int sy = db_status_y(db); + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); + XFillRectangle(db->dpy, db->win, db->gc, 0, sy, DB_W, DB_STATUS_H); + + { + int cpu = read_cpu_percent(); + int mem = read_mem_percent(); + int bat = read_battery_percent(); + char bst = read_battery_status(); + char status[64]; + int pos = 0; + if (cpu >= 0) + pos += snprintf(status + pos, sizeof(status) - pos, + "CPU %d%%", cpu); + if (mem >= 0) + pos += snprintf(status + pos, sizeof(status) - pos, + " MEM %d%%", mem); + if (bat >= 0) { + const char *arrow = (bst == 'C') ? "↑" : (bst == 'F') ? "=" : "↓"; + pos += snprintf(status + pos, sizeof(status) - pos, + " BAT %d%%%s", bat, arrow); + } + if (pos == 0) snprintf(status, sizeof(status), "SkyOS"); + draw_str_centered(db, xd, db->font_date, DB_COL_STATUS_TXT, + 0, sy, DB_W, DB_STATUS_H, status); + } + + XftDrawDestroy(xd); +} + +/* ── BeOS-style fly-out menu ─────────────────────────────────────────── */ + +typedef struct { const char *lbl; const char *cmd; int sep; int has_sub; int sub_i; } MI; + +/* sub-menu 0: Applications */ +static const char *S0_LBL[] = { + "Firefox","Terminal","Files","Text Editor", + "Media Player","Image Viewer","OnlyOffice","PDF Viewer" +}; +static const char *S0_CMD[] = { + "firefox &","xterm &","thunar &","mousepad &", + "mpv --player-operation-mode=pseudo-gui &","viewnior &", + "onlyoffice-desktopeditors &","evince &" +}; +#define NS0 8 + +/* sub-menu 1: Settings */ +static const char *S1_LBL[] = { + "Display","Audio","Network","Bluetooth","Power" +}; +static const char *S1_CMD[] = { + "arandr &","pavucontrol &", + "nm-connection-editor &","blueman-manager &", + "xfce4-power-manager-settings &" +}; +#define NS1 5 + +static const MI MENU[] = { + { "Install SkyOS...", "xterm -e 'sudo skyinstall; exec bash' &", 0, 0,-1 }, + { NULL, NULL, 1, 0,-1 }, + { "Applications", NULL, 0, 1, 0 }, + { "Settings", NULL, 0, 1, 1 }, + { NULL, NULL, 1, 0,-1 }, + { "About SkyOS", "xterm -e 'fastfetch; read' &", 0, 0,-1 }, + { "Lock Screen", "xscreensaver-command -lock &", 0, 0,-1 }, + { NULL, NULL, 1, 0,-1 }, + { "Restart", "systemctl reboot", 0, 0,-1 }, + { "Shut Down", "systemctl poweroff", 0, 0,-1 }, +}; +#define N_MENU ((int)(sizeof(MENU)/sizeof(MENU[0]))) + +static int mi_y(int i) { + int y = DB_FLYOUT_PAD; + for (int j = 0; j < i; j++) + y += MENU[j].sep ? DB_FLYOUT_SH : DB_FLYOUT_IH; + return y; +} +static int menu_win_h(void) { return mi_y(N_MENU) + DB_FLYOUT_PAD; } +static int sub_win_h(int n) { return n * DB_FLYOUT_IH + DB_FLYOUT_PAD * 2; } + +static int menu_item_at(int y) { + for (int i = 0; i < N_MENU; i++) { + if (MENU[i].sep) continue; + int iy = mi_y(i); + if (y >= iy && y < iy + DB_FLYOUT_IH) return i; + } + return -1; +} + +static int sub_item_at(int y, int n) { + int iy = DB_FLYOUT_PAD; + for (int i = 0; i < n; i++) { + if (y >= iy && y < iy + DB_FLYOUT_IH) return i; + iy += DB_FLYOUT_IH; + } + return -1; +} + +static void menu_draw_panel(Deskbar *db, Window win, int is_sub, + const char **lbls, int n, int hov) { + int ww = DB_FLYOUT_W; + int wh = is_sub ? sub_win_h(n) : menu_win_h(); + XftDraw *xd = XftDrawCreate(db->dpy, win, db->visual, db->cmap); + + /* background */ + XSetForeground(db->dpy, db->gc, db_color(db, 0x0C1120)); + XFillRectangle(db->dpy, win, db->gc, 0, 0, ww, wh); + + /* left accent bar (BeOS-style colored stripe) */ + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); + XFillRectangle(db->dpy, win, db->gc, 0, 0, 3, wh); + + /* subtle border */ + XSetForeground(db->dpy, db->gc, db_color(db, 0x1A2A48)); + XDrawRectangle(db->dpy, win, db->gc, 0, 0, ww - 1, wh - 1); + + if (is_sub) { + for (int i = 0; i < n; i++) { + int iy = DB_FLYOUT_PAD + i * DB_FLYOUT_IH; + if (i == hov) { + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ITEM_HOV)); + XFillRectangle(db->dpy, win, db->gc, 3, iy, ww - 4, DB_FLYOUT_IH); + } + unsigned long tc = (i == hov) ? DB_COL_ITEM_HTXT : DB_COL_ITEM_TEXT; + draw_str(db, xd, db->font_ui, tc, + DB_FLYOUT_LPD, iy + (DB_FLYOUT_IH - 10) / 2, + lbls[i], 0, 0); + } + } else { + for (int i = 0; i < N_MENU; i++) { + if (MENU[i].sep) { + int sy = mi_y(i) + DB_FLYOUT_SH / 2; + XSetForeground(db->dpy, db->gc, db_color(db, 0x1C2A44)); + XDrawLine(db->dpy, win, db->gc, 8, sy, ww - 9, sy); + continue; + } + int iy = mi_y(i); + if (i == hov) { + XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ITEM_HOV)); + XFillRectangle(db->dpy, win, db->gc, 3, iy, ww - 4, DB_FLYOUT_IH); + } + unsigned long tc = (i == hov) ? DB_COL_ITEM_HTXT : DB_COL_ITEM_TEXT; + draw_str(db, xd, db->font_ui, tc, + DB_FLYOUT_LPD, iy + (DB_FLYOUT_IH - 10) / 2, + MENU[i].lbl, 0, 0); + if (MENU[i].has_sub) { + unsigned long ac = (i == hov) ? DB_COL_ITEM_HTXT : 0x30486A; + /* ▶ U+25B6 */ + draw_str(db, xd, db->font_ui, ac, + ww - 15, iy + (DB_FLYOUT_IH - 10) / 2, + "\xe2\x96\xb6", 0, 0); + } + } + } + XftDrawDestroy(xd); + XFlush(db->dpy); +} + +static void db_sub_close(Deskbar *db) { + if (!db->sub_win) return; + XDestroyWindow(db->dpy, db->sub_win); + db->sub_win = None; + db->sub_active = -1; + db->sub_hov = -1; +} + +static void db_sub_open(Deskbar *db, int sub_i) { + if (db->sub_active == sub_i && db->sub_win) return; + db_sub_close(db); + + const char **lbls = (sub_i == 0) ? S0_LBL : S1_LBL; + int n = (sub_i == 0) ? NS0 : NS1; + int sx = db->sw_cache - DB_W - DB_FLYOUT_W - DB_FLYOUT_W; + int sy = mi_y(db->menu_hov); + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = db_color(db, 0x0C1120); + swa.event_mask = ExposureMask; + db->sub_win = XCreateWindow( + db->dpy, db->root, sx, sy, DB_FLYOUT_W, sub_win_h(n), 0, + db->depth, InputOutput, db->visual, + CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + db->sub_active = sub_i; + db->sub_hov = -1; + XMapRaised(db->dpy, db->sub_win); + menu_draw_panel(db, db->sub_win, 1, lbls, n, -1); +} + +void db_menu_open(Deskbar *db) { + if (db->menu_win) return; + db->sw_cache = DisplayWidth(db->dpy, db->screen); + int wx = db->sw_cache - DB_W - DB_FLYOUT_W; + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = db_color(db, 0x0C1120); + swa.event_mask = ExposureMask; + db->menu_win = XCreateWindow( + db->dpy, db->root, wx, 0, DB_FLYOUT_W, menu_win_h(), 0, + db->depth, InputOutput, db->visual, + CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + db->menu_hov = -1; + XMapRaised(db->dpy, db->menu_win); + menu_draw_panel(db, db->menu_win, 0, NULL, 0, -1); + + /* grab pointer so clicks outside are detected */ + XGrabPointer(db->dpy, db->menu_win, True, + ButtonPressMask | PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + + /* redraw deskbar to show Sky button as active */ + db_draw(db); +} + +void db_menu_close(Deskbar *db) { + if (!db->menu_win) return; + XUngrabPointer(db->dpy, CurrentTime); + db_sub_close(db); + XDestroyWindow(db->dpy, db->menu_win); + db->menu_win = None; + db->menu_hov = -1; + db_draw(db); +} + +static void handle_menu_motion(Deskbar *db, int rx, int ry) { + /* is pointer in sub-menu? */ + if (db->sub_win) { + int sx, sy; unsigned sw, sh, bw, dep; Window sr; + XGetGeometry(db->dpy, db->sub_win, &sr, &sx, &sy, &sw, &sh, &bw, &dep); + if (rx >= sx && ry >= sy && rx < sx + (int)sw && ry < sy + (int)sh) { + int n = (db->sub_active == 0) ? NS0 : NS1; + const char **lbls = (db->sub_active == 0) ? S0_LBL : S1_LBL; + int idx = sub_item_at(ry - sy, n); + if (idx != db->sub_hov) { + db->sub_hov = idx; + menu_draw_panel(db, db->sub_win, 1, lbls, n, idx); + } + return; + } + } + + /* is pointer in main menu? */ + if (db->menu_win) { + int mx, my; unsigned mw, mh, bw, dep; Window mr; + XGetGeometry(db->dpy, db->menu_win, &mr, &mx, &my, &mw, &mh, &bw, &dep); + if (rx >= mx && ry >= my && rx < mx + (int)mw && ry < my + (int)mh) { + int idx = menu_item_at(ry - my); + if (idx != db->menu_hov) { + db->menu_hov = idx; + menu_draw_panel(db, db->menu_win, 0, NULL, 0, idx); + if (idx >= 0 && MENU[idx].has_sub) + db_sub_open(db, MENU[idx].sub_i); + else + db_sub_close(db); + } + return; + } + } +} + +static void handle_menu_press(Deskbar *db, int rx, int ry) { + /* click in sub-menu? */ + if (db->sub_win) { + int sx, sy; unsigned sw, sh, bw, dep; Window sr; + XGetGeometry(db->dpy, db->sub_win, &sr, &sx, &sy, &sw, &sh, &bw, &dep); + if (rx >= sx && ry >= sy && rx < sx + (int)sw && ry < sy + (int)sh) { + int n = (db->sub_active == 0) ? NS0 : NS1; + const char **cmds = (db->sub_active == 0) ? S0_CMD : S1_CMD; + int idx = sub_item_at(ry - sy, n); + if (idx >= 0) system(cmds[idx]); + db_menu_close(db); + return; + } + } + + /* click in main menu? */ + if (db->menu_win) { + int mx, my; unsigned mw, mh, bw, dep; Window mr; + XGetGeometry(db->dpy, db->menu_win, &mr, &mx, &my, &mw, &mh, &bw, &dep); + if (rx >= mx && ry >= my && rx < mx + (int)mw && ry < my + (int)mh) { + int idx = menu_item_at(ry - my); + if (idx >= 0 && !MENU[idx].has_sub && MENU[idx].cmd) + system(MENU[idx].cmd); + if (idx >= 0 && !MENU[idx].has_sub) + db_menu_close(db); + /* clicking a has_sub item just keeps the sub-menu open */ + return; + } + } + + /* click outside all menu windows → close */ + db_menu_close(db); +} + +/* ── input ────────────────────────────────────────────────────────────── */ + +void db_hover(Deskbar *db, int x, int y) { + (void)x; + int prev = db->hovered; + int ay = db_apps_y(); + db->hovered = -1; + if (y >= ay && y < ay + db->napps * DB_ITEM_H) + db->hovered = (y - ay) / DB_ITEM_H; + if (db->hovered != prev) db_draw(db); +} + +void db_click(Deskbar *db, int x, int y) { + int ay = db_apps_y(); + int wy = db_ws_y(db); + + /* Sky menu button → toggle fly-out menu */ + if (y < DB_TOP_H && x < DB_MENU_W) { + if (db->menu_win) db_menu_close(db); + else db_menu_open(db); + return; + } + + /* app row click → focus or restore */ + if (y >= ay && y < ay + db->napps * DB_ITEM_H) { + int idx = (y - ay) / DB_ITEM_H; + if (idx < db->napps) { + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.window = db->apps[idx].win; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); + ev.xclient.format = 32; + ev.xclient.data.l[0] = 2; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); + XRaiseWindow(db->dpy, db->apps[idx].win); + } + return; + } + + /* workspace button click → switch desktop */ + if (y >= wy && y < wy + DB_WS_H) { + int total = db->num_ws < 1 ? 4 : (db->num_ws > 8 ? 8 : db->num_ws); + int btnw = (DB_W - 8) / total; + int idx = (x - 4) / btnw; + if (idx >= 0 && idx < db->num_ws) { + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.window = db->root; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CURRENT_DESKTOP", False); + ev.xclient.format = 32; + ev.xclient.data.l[0] = idx; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); + } + } +} + +/* ── init & run ───────────────────────────────────────────────────────── */ + +void db_init(Deskbar *db) { + db->dpy = XOpenDisplay(NULL); + if (!db->dpy) { fprintf(stderr, "skydeskbar: cannot open display\n"); exit(1); } + + db->screen = DefaultScreen(db->dpy); + db->root = RootWindow(db->dpy, db->screen); + db->cmap = DefaultColormap(db->dpy, db->screen); + db->visual = DefaultVisual(db->dpy, db->screen); + db->depth = DefaultDepth(db->dpy, db->screen); + db->hovered = -1; + db->menu_win = None; + db->sub_win = None; + db->menu_hov = -1; + db->sub_active = -1; + db->sub_hov = -1; + db->sw_cache = 0; + + db->net_client_list = XInternAtom(db->dpy, "_NET_CLIENT_LIST", False); + db->net_active_window = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); + db->net_current_desktop = XInternAtom(db->dpy, "_NET_CURRENT_DESKTOP", False); + db->net_number_of_desktops = XInternAtom(db->dpy, "_NET_NUMBER_OF_DESKTOPS", False); + db->net_wm_name = XInternAtom(db->dpy, "_NET_WM_NAME", False); + db->utf8_string = XInternAtom(db->dpy, "UTF8_STRING", False); + + db->font_ui = XftFontOpenName(db->dpy, db->screen, DB_FONT_UI); + db->font_menu = XftFontOpenName(db->dpy, db->screen, DB_FONT_MENU); + db->font_clock = XftFontOpenName(db->dpy, db->screen, DB_FONT_CLOCK); + db->font_date = XftFontOpenName(db->dpy, db->screen, DB_FONT_DATE); + db->font_icon = XftFontOpenName(db->dpy, db->screen, DB_FONT_ICON); + if (!db->font_ui) db->font_ui = XftFontOpenName(db->dpy, db->screen, "fixed:size=9"); + if (!db->font_menu) db->font_menu = db->font_ui; + if (!db->font_clock) db->font_clock = db->font_ui; + if (!db->font_date) db->font_date = db->font_ui; + if (!db->font_icon) db->font_icon = db->font_ui; + + db_update_apps(db); + db_update_workspace(db); + + db->bar_h = DB_TOP_H + 1 + db->napps * DB_ITEM_H + 1 + DB_WS_H + 1 + DB_STATUS_H + 2; + + int sw = DisplayWidth(db->dpy, db->screen); + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = db_color(db, DB_COL_BG); + swa.event_mask = ExposureMask | ButtonPressMask + | PointerMotionMask | LeaveWindowMask + | VisibilityChangeMask; + + db->win = XCreateWindow( + db->dpy, db->root, + sw - DB_W, 0, DB_W, db->bar_h, 0, + db->depth, InputOutput, db->visual, + CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + db->gc = XCreateGC(db->dpy, db->win, 0, NULL); + + Atom net_wm_window_type = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE", False); + Atom net_wm_type_dock = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE_DOCK", False); + XChangeProperty(db->dpy, db->win, net_wm_window_type, XA_ATOM, 32, + PropModeReplace, (unsigned char *)&net_wm_type_dock, 1); + + /* strut: reserve right edge for deskbar */ + Atom net_strut_partial = XInternAtom(db->dpy, "_NET_WM_STRUT_PARTIAL", False); + long strut[12] = { 0, DB_W, 0, 0, 0,0, 0,0, 0,0, 0,0 }; + /* right strut: strut[1]=DB_W, right_start[8]=0, right_end[9]=screen_height */ + int sh = DisplayHeight(db->dpy, db->screen); + strut[9] = sh; + XChangeProperty(db->dpy, db->win, net_strut_partial, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)strut, 12); + + XSelectInput(db->dpy, db->root, PropertyChangeMask); + + db_resize(db); + XMapRaised(db->dpy, db->win); + XSync(db->dpy, False); +} + +void db_run(Deskbar *db) { + time_t last_tick = 0; + int status_tick = 0; + + for (;;) { + while (XPending(db->dpy)) { + XEvent e; + XNextEvent(db->dpy, &e); + + /* While the fly-out menu is open, the pointer is grabbed. + Route all motion and button events through the menu handlers + (they use root-relative coords to determine which panel was hit). */ + if (db->menu_win) { + if (e.type == MotionNotify) { + handle_menu_motion(db, e.xmotion.x_root, e.xmotion.y_root); + continue; + } + if (e.type == ButtonPress) { + handle_menu_press(db, e.xbutton.x_root, e.xbutton.y_root); + continue; + } + if (e.type == ButtonRelease) + continue; + if (e.type == Expose && e.xexpose.count == 0) { + if (e.xany.window == db->menu_win) + menu_draw_panel(db, db->menu_win, 0, NULL, 0, db->menu_hov); + else if (db->sub_win && e.xany.window == db->sub_win) { + int n = (db->sub_active == 0) ? NS0 : NS1; + const char **lbls = (db->sub_active == 0) ? S0_LBL : S1_LBL; + menu_draw_panel(db, db->sub_win, 1, lbls, n, db->sub_hov); + } else if (e.xany.window == db->win) + db_draw(db); + continue; + } + } + + switch (e.type) { + case Expose: + if (e.xexpose.count == 0) db_draw(db); + break; + case ButtonPress: + if (e.xany.window == db->win) + db_click(db, e.xbutton.x, e.xbutton.y); + break; + case MotionNotify: + if (e.xany.window == db->win) + db_hover(db, e.xmotion.x, e.xmotion.y); + break; + case LeaveNotify: + if (e.xany.window == db->win && db->hovered != -1) { + db->hovered = -1; db_draw(db); + } + break; + case VisibilityNotify: + /* Another window covered us — fight back to the top */ + if (e.xany.window == db->win && + e.xvisibility.state != VisibilityUnobscured) + XRaiseWindow(db->dpy, db->win); + break; + case PropertyNotify: + if (e.xproperty.atom == db->net_client_list) { + /* Window list changed — full refresh + resize */ + db_update_apps(db); + db_resize(db); + db_draw(db); + } else if (e.xproperty.atom == db->net_active_window) { + /* Focus changed — only update focused flag, no resize */ + Atom at; int af; unsigned long an, aa; + unsigned char *aw = NULL; + long active_win = -1; + if (XGetWindowProperty(db->dpy, db->root, + db->net_active_window, 0, 1, False, + XA_WINDOW, &at, &af, &an, &aa, &aw) + == Success && aw) { + active_win = *(long *)aw; + XFree(aw); + } + for (int i = 0; i < db->napps; i++) + db->apps[i].focused = + (db->apps[i].win == (Window)active_win); + db_draw(db); + } else if (e.xproperty.atom == db->net_current_desktop) { + db_update_workspace(db); + db_draw(db); + } + break; + default: break; + } + } + + time_t now = time(NULL); + if (now != last_tick) { + last_tick = now; + status_tick++; + if (status_tick >= 5) { + status_tick = 0; + /* Safety net: re-raise deskbar every 5s in case + VisibilityNotify was missed while menu was open */ + if (!db->menu_win) + XRaiseWindow(db->dpy, db->win); + } + db_draw(db); + } + + usleep(100000); + } +} + +int main(void) { + Deskbar db = {0}; + db_init(&db); + db_run(&db); + return 0; +} diff --git a/installer/Makefile b/installer/Makefile new file mode 100644 index 0000000..ade6c0c --- /dev/null +++ b/installer/Makefile @@ -0,0 +1,11 @@ +CC = gcc +CFLAGS = -O2 -Wall -Wextra $(shell pkg-config --cflags x11 xft fontconfig) +LDFLAGS = $(shell pkg-config --libs x11 xft fontconfig) + +all: skyinstall-gui + +skyinstall-gui: skyinstall-gui.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +clean: + rm -f skyinstall-gui diff --git a/installer/skyinstall-gui.c b/installer/skyinstall-gui.c new file mode 100644 index 0000000..6d2bcd3 --- /dev/null +++ b/installer/skyinstall-gui.c @@ -0,0 +1,1124 @@ +/* + * SkyOS GUI Installer — Xlib/Xft wizard + * + * Steps: + * 0 Welcome + * 1 Select disk + * 2 User & hostname + * 3 Review + * 4 Installing (live log + progress bar) + * 5 Done / reboot + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── window geometry ──────────────────────────────────────────────── */ +#define WIN_W 660 +#define WIN_H 520 +#define HDR_H 70 /* title header height */ +#define FOOT_H 56 /* button footer height */ +#define BODY_Y HDR_H +#define BODY_H (WIN_H - HDR_H - FOOT_H) + +/* ── colors ──────────────────────────────────────────────────────── */ +#define C_BG 0x12161F /* window background */ +#define C_HDR 0x0E1828 /* header panel */ +#define C_HDR_ACCENT 0x1C4A80 /* header accent stripe */ +#define C_BODY 0x161A26 /* body background */ +#define C_FOOT 0x0E1220 /* footer */ +#define C_PANEL 0x1A2030 /* card / panel bg */ +#define C_INPUT_BG 0x0C1018 /* text field bg */ +#define C_INPUT_BD 0x263040 /* text field border */ +#define C_INPUT_ACT 0x2060A0 /* active field border */ +#define C_BTN 0x1C3A60 /* button fill */ +#define C_BTN_HOV 0x2060A0 /* button hover */ +#define C_BTN_PRI 0x2060A0 /* primary button */ +#define C_BTN_PRI_H 0x2878C0 /* primary button hover */ +#define C_BTN_DNG 0x6A1818 /* danger button */ +#define C_BTN_DNG_H 0x9A2020 /* danger hover */ +#define C_RADIO 0x1C3A60 /* radio fill */ +#define C_RADIO_SEL 0x2060A0 /* radio selected */ +#define C_SEP 0x1E2840 /* separator */ +#define C_TXT 0xC8D4E8 /* primary text */ +#define C_TXT_DIM 0x5A7090 /* dim text */ +#define C_TXT_INP 0xD8E4F8 /* input text */ +#define C_TXT_HDR 0xE8F0FF /* header text */ +#define C_TXT_LOGO 0x88AADD /* "SkyOS" accent */ +#define C_WARN 0xFFCC44 /* warning text */ +#define C_ERR 0xFF5555 /* error text */ +#define C_OK 0x44CC77 /* success text */ +#define C_BAR_BG 0x1A2030 /* progress bar bg */ +#define C_BAR_FG 0x2060A0 /* progress bar fill */ +#define C_BAR_SHINE 0x2878C0 /* bar highlight */ +#define C_BORDER 0x1E2840 /* outer border */ + +/* ── fonts ───────────────────────────────────────────────────────── */ +#define F_LOGO "Sans:size=18:weight=bold" +#define F_HEAD "Sans:size=11:weight=bold" +#define F_BODY "Sans:size=10" +#define F_SMALL "Sans:size=9" +#define F_MONO "DejaVu Sans Mono:size=9" +#define F_BTN "Sans:size=10:weight=bold" + +/* ── limits ──────────────────────────────────────────────────────── */ +#define MAX_DISKS 8 +#define MAX_LOG 200 +#define LOG_COLS 80 +#define TF_MAX 128 + +/* ── steps ───────────────────────────────────────────────────────── */ +typedef enum { + STEP_WELCOME = 0, + STEP_DISK, + STEP_USER, + STEP_REVIEW, + STEP_INSTALL, + STEP_DONE, +} Step; + +/* ── disk info ───────────────────────────────────────────────────── */ +typedef struct { + char name[32]; /* sda, nvme0n1, … */ + char path[40]; /* /dev/sda */ + char model[64]; + char size_str[16]; /* "256 GB" */ + long long size_mb; +} Disk; + +/* ── text field ──────────────────────────────────────────────────── */ +typedef struct { + char buf[TF_MAX]; + int len; + int password; /* show bullets */ + int focused; +} TF; + +/* ── log buffer ──────────────────────────────────────────────────── */ +typedef struct { + char lines[MAX_LOG][LOG_COLS + 1]; + int head; /* next write index (circular) */ + int count; /* total lines written */ +} LogBuf; + +static void log_push(LogBuf *lb, const char *line) { + snprintf(lb->lines[lb->head], LOG_COLS, "%s", line); + lb->head = (lb->head + 1) % MAX_LOG; + if (lb->count < MAX_LOG) lb->count++; +} + +/* ── main state ──────────────────────────────────────────────────── */ +typedef struct { + Display *dpy; + int scr; + Window win; + GC gc; + Colormap cmap; + Visual *vis; + int depth; + + XftFont *f_logo, *f_head, *f_body, *f_small, *f_mono, *f_btn; + + Step step; + int prev_step; + + /* disk selection */ + Disk disks[MAX_DISKS]; + int ndisks; + int sel_disk; + + /* user fields */ + TF tf_host, tf_user, tf_pass, tf_pass2; + TF *focused_tf; + + /* button hover */ + int hov_back, hov_next; + + /* install progress */ + int progress; /* 0–100 */ + char prog_msg[128]; + LogBuf log; + int install_fd; /* pipe fd to installer subprocess */ + int install_done; + char install_err[256]; + + /* error display */ + char err_msg[256]; +} Ins; + +/* ══════════════════════════════════════════════════════════════════ */ +/* X helpers */ +/* ══════════════════════════════════════════════════════════════════ */ + +static unsigned long mkcolor(Ins *s, unsigned long rgb) { + XColor c; + c.red = ((rgb >> 16) & 0xFF) * 257; + c.green = ((rgb >> 8) & 0xFF) * 257; + c.blue = ((rgb) & 0xFF) * 257; + c.flags = DoRed | DoGreen | DoBlue; + XAllocColor(s->dpy, s->cmap, &c); + return c.pixel; +} + +static void xftcol(Ins *s, unsigned long rgb, XftColor *out) { + XRenderColor rc = { + ((rgb >> 16) & 0xFF) * 257, + ((rgb >> 8) & 0xFF) * 257, + ((rgb) & 0xFF) * 257, + 0xFFFF + }; + XftColorAllocValue(s->dpy, s->vis, s->cmap, &rc, out); +} + +static void fill_rect(Ins *s, int x, int y, int w, int h, unsigned long rgb) { + XSetForeground(s->dpy, s->gc, mkcolor(s, rgb)); + XFillRectangle(s->dpy, s->win, s->gc, x, y, w, h); +} + +static void draw_line(Ins *s, int x1, int y1, int x2, int y2, unsigned long rgb) { + XSetForeground(s->dpy, s->gc, mkcolor(s, rgb)); + XDrawLine(s->dpy, s->win, s->gc, x1, y1, x2, y2); +} + +/* Draw text and return pixel width */ +static int xft_text(Ins *s, XftDraw *xd, XftFont *f, unsigned long rgb, + int x, int y, const char *str) { + if (!str || !str[0]) return 0; + XftColor col; xftcol(s, rgb, &col); + XGlyphInfo ext; + XftTextExtentsUtf8(s->dpy, f, (const FcChar8 *)str, strlen(str), &ext); + XftDrawStringUtf8(xd, &col, f, x, y + ext.height - ext.y, + (const FcChar8 *)str, strlen(str)); + XftColorFree(s->dpy, s->vis, s->cmap, &col); + return ext.width; +} + +static int text_w(Ins *s, XftFont *f, const char *str) { + XGlyphInfo e; + XftTextExtentsUtf8(s->dpy, f, (const FcChar8 *)str, strlen(str), &e); + return e.width; +} +static int text_h(Ins *s, XftFont *f) { + XGlyphInfo e; + XftTextExtentsUtf8(s->dpy, f, (const FcChar8 *)"Ag", 2, &e); + return e.height; +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Widget drawing */ +/* ══════════════════════════════════════════════════════════════════ */ + +/* Button: returns 1 if (mx,my) is inside */ +static void draw_button(Ins *s, XftDraw *xd, int x, int y, int w, int h, + const char *label, unsigned long bg, int hov) { + unsigned long face = hov ? (bg + 0x101828) : bg; + /* clamp */ + if ((face & 0xFF) > 0xFF) face = (face & 0xFFFF00) | 0xFF; + + fill_rect(s, x, y, w, h, face); + + /* top highlight */ + unsigned long hi = (face < 0xEFEFEF) ? face + 0x101010 : 0xFFFFFF; + draw_line(s, x, y, x+w-1, y, hi); + draw_line(s, x, y, x, y+h-1, hi); + /* bottom shadow */ + draw_line(s, x, y+h-1, x+w-1, y+h-1, C_BORDER); + draw_line(s, x+w-1, y, x+w-1, y+h-1, C_BORDER); + + int tw = text_w(s, s->f_btn, label); + int th = text_h(s, s->f_btn); + xft_text(s, xd, s->f_btn, C_TXT_HDR, + x + (w - tw) / 2, + y + (h - th) / 2, + label); +} + +static void draw_textfield(Ins *s, XftDraw *xd, int x, int y, int w, + TF *tf, const char *placeholder) { + int focused = tf->focused; + unsigned long bord = focused ? C_INPUT_ACT : C_INPUT_BD; + + fill_rect(s, x, y, w, 32, C_INPUT_BG); + /* border */ + XSetForeground(s->dpy, s->gc, mkcolor(s, bord)); + XDrawRectangle(s->dpy, s->win, s->gc, x, y, w - 1, 31); + + /* content */ + if (tf->len == 0 && !focused && placeholder) { + XRectangle clip = { (short)(x+6), (short)(y+2), + (unsigned short)(w-12), 28 }; + XftDraw *xd2 = xd; + XftDrawSetClipRectangles(xd2, 0, 0, &clip, 1); + xft_text(s, xd, s->f_body, C_TXT_DIM, x+6, y+6, placeholder); + XftDrawSetClip(xd2, None); + } else { + /* build display string */ + char display[TF_MAX * 3 + 4]; + if (tf->password) { + int i; for (i = 0; i < tf->len; i++) { + /* UTF-8 bullet U+2022 */ + display[i*3+0] = (char)0xE2; + display[i*3+1] = (char)0x80; + display[i*3+2] = (char)0xA2; + } + display[i*3] = '\0'; + } else { + memcpy(display, tf->buf, tf->len); + display[tf->len] = '\0'; + } + + XRectangle clip = { (short)(x+6), (short)(y+2), + (unsigned short)(w-12), 28 }; + XftDrawSetClipRectangles(xd, 0, 0, &clip, 1); + xft_text(s, xd, s->f_body, C_TXT_INP, x+6, y+6, display); + XftDrawSetClip(xd, None); + + /* cursor */ + if (focused) { + int cw = text_w(s, s->f_body, display); + int cx = x + 6 + cw; + if (cx > x + w - 8) cx = x + w - 8; + fill_rect(s, cx, y + 6, 2, 20, C_INPUT_ACT); + } + } +} + +/* Radio button row */ +static void draw_radio(Ins *s, XftDraw *xd, int x, int y, int w, + const char *label, const char *sub, + int selected, int hovered) { + unsigned long bg = selected ? C_RADIO_SEL : + hovered ? C_RADIO + 0x080808 : C_RADIO; + fill_rect(s, x, y, w, 44, bg); + + /* top border */ + draw_line(s, x, y, x+w-1, y, + selected ? C_BTN_PRI : C_SEP); + + /* radio circle */ + int cx = x + 20, cy = y + 22; + XSetForeground(s->dpy, s->gc, mkcolor(s, C_SEP)); + XDrawArc(s->dpy, s->win, s->gc, cx-8, cy-8, 16, 16, 0, 360*64); + if (selected) { + XSetForeground(s->dpy, s->gc, mkcolor(s, C_BTN_PRI)); + XFillArc(s->dpy, s->win, s->gc, cx-5, cy-5, 10, 10, 0, 360*64); + } + + xft_text(s, xd, s->f_body, C_TXT, x+38, y+8, label); + xft_text(s, xd, s->f_small, C_TXT_DIM, x+38, y+24, sub); +} + +/* Progress bar */ +static void draw_progress(Ins *s, int x, int y, int w, int h, int pct) { + fill_rect(s, x, y, w, h, C_BAR_BG); + XSetForeground(s->dpy, s->gc, mkcolor(s, C_BORDER)); + XDrawRectangle(s->dpy, s->win, s->gc, x, y, w-1, h-1); + + if (pct > 0) { + int fw = (w - 2) * pct / 100; + if (fw > 0) { + fill_rect(s, x+1, y+1, fw, h-2, C_BAR_FG); + /* shine on top half */ + fill_rect(s, x+1, y+1, fw, (h-2)/2, C_BAR_SHINE); + } + } +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Disk detection */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void detect_disks(Ins *s) { + s->ndisks = 0; + DIR *d = opendir("/sys/block"); + if (!d) return; + struct dirent *de; + while ((de = readdir(d)) && s->ndisks < MAX_DISKS) { + const char *n = de->d_name; + if (n[0] == '.') continue; + /* skip loop, ram, sr devices */ + if (strncmp(n, "loop", 4) == 0) continue; + if (strncmp(n, "ram", 3) == 0) continue; + if (strncmp(n, "sr", 2) == 0) continue; + if (strncmp(n, "zram", 4) == 0) continue; + + Disk *dk = &s->disks[s->ndisks]; + snprintf(dk->name, sizeof(dk->name), "%s", n); + snprintf(dk->path, sizeof(dk->path), "/dev/%s", n); + + /* size from /sys/block//size (512-byte sectors) */ + char spath[128]; + snprintf(spath, sizeof(spath), "/sys/block/%s/size", n); + FILE *f = fopen(spath, "r"); + long long sectors = 0; + if (f) { fscanf(f, "%lld", §ors); fclose(f); } + dk->size_mb = sectors / 2048; /* sectors → MB */ + if (dk->size_mb < 4000) continue; /* skip tiny devices <4GB */ + + if (dk->size_mb >= 1024*1024) + snprintf(dk->size_str, sizeof(dk->size_str), + "%.1f TB", dk->size_mb / 1024.0 / 1024.0); + else + snprintf(dk->size_str, sizeof(dk->size_str), + "%lld GB", dk->size_mb / 1024); + + /* model */ + snprintf(spath, sizeof(spath), "/sys/block/%s/device/model", n); + f = fopen(spath, "r"); + dk->model[0] = '\0'; + if (f) { + fgets(dk->model, sizeof(dk->model), f); + fclose(f); + /* trim trailing newline/spaces */ + int l = strlen(dk->model); + while (l > 0 && (dk->model[l-1] == '\n' || + dk->model[l-1] == ' ')) + dk->model[--l] = '\0'; + } + if (!dk->model[0]) { + /* NVMe — try nvme/model */ + snprintf(spath, sizeof(spath), "/sys/block/%s/device/model", n); + snprintf(dk->model, sizeof(dk->model), "Disk"); + } + + s->ndisks++; + } + closedir(d); +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Layout helpers */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void draw_header(Ins *s, XftDraw *xd, const char *title) { + fill_rect(s, 0, 0, WIN_W, HDR_H, C_HDR); + fill_rect(s, 0, 0, WIN_W, 3, C_HDR_ACCENT); + draw_line(s, 0, HDR_H-1, WIN_W, HDR_H-1, C_BORDER); + + xft_text(s, xd, s->f_logo, C_TXT_LOGO, 20, 12, "SkyOS"); + int lw = text_w(s, s->f_logo, "SkyOS"); + draw_line(s, 20 + lw + 12, HDR_H/2, WIN_W - 20, HDR_H/2, C_SEP); + xft_text(s, xd, s->f_head, C_TXT_HDR, 20 + lw + 20, 18, title); +} + +static void draw_footer(Ins *s, XftDraw *xd, + const char *back_lbl, const char *next_lbl, + unsigned long next_col) { + int fy = WIN_H - FOOT_H; + fill_rect(s, 0, fy, WIN_W, FOOT_H, C_FOOT); + draw_line(s, 0, fy, WIN_W, fy, C_BORDER); + + /* step dots */ + int ndots = 5; /* steps 0-4 shown as dots (5 is done) */ + int dot_step = (int)s->step < ndots ? (int)s->step : ndots-1; + for (int i = 0; i < ndots; i++) { + int dx = WIN_W/2 - (ndots*14)/2 + i*14 + 7; + int dy = fy + FOOT_H/2; + unsigned long dc = (i == dot_step) ? C_BTN_PRI : C_SEP; + XSetForeground(s->dpy, s->gc, mkcolor(s, dc)); + XFillArc(s->dpy, s->win, s->gc, dx-4, dy-4, 8, 8, 0, 360*64); + } + + int bw = 120, bh = 36, by = fy + (FOOT_H - bh) / 2; + + if (back_lbl) + draw_button(s, xd, 20, by, bw, bh, back_lbl, + C_BTN, s->hov_back); + if (next_lbl) + draw_button(s, xd, WIN_W - bw - 20, by, bw, bh, next_lbl, + next_col, s->hov_next); +} + +static int in_rect(int mx, int my, int x, int y, int w, int h) { + return mx>=x && mx=y && myerr_msg[0] = '\0'; + switch (s->step) { + case STEP_DISK: + if (s->sel_disk < 0) { + snprintf(s->err_msg, sizeof(s->err_msg), "Please select a disk."); + return 0; + } + break; + case STEP_USER: + if (s->tf_host.len < 1) { + snprintf(s->err_msg, sizeof(s->err_msg), "Hostname cannot be empty."); + return 0; + } + if (s->tf_user.len < 1) { + snprintf(s->err_msg, sizeof(s->err_msg), "Username cannot be empty."); + return 0; + } + if (s->tf_pass.len < 6) { + snprintf(s->err_msg, sizeof(s->err_msg), + "Password must be at least 6 characters."); + return 0; + } + if (strcmp(s->tf_pass.buf, s->tf_pass2.buf) != 0) { + snprintf(s->err_msg, sizeof(s->err_msg), "Passwords do not match."); + return 0; + } + break; + default: break; + } + return 1; +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Step rendering */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void draw_welcome(Ins *s, XftDraw *xd) { + draw_header(s, xd, "Welcome"); + fill_rect(s, 0, BODY_Y, WIN_W, BODY_H, C_BODY); + + int y = BODY_Y + 30; + xft_text(s, xd, s->f_head, C_TXT_HDR, 40, y, + "Welcome to the SkyOS Installer"); + y += 36; + + const char *lines[] = { + "This wizard will guide you through installing SkyOS", + "onto your computer.", + "", + "What you will need:", + " • A target disk of at least 16 GB", + " • About 10–20 minutes", + "", + "The target disk will be completely erased.", + "Make sure you have backed up any important data", + "before continuing.", + }; + for (int i = 0; i < (int)(sizeof(lines)/sizeof(lines[0])); i++) { + unsigned long col = (lines[i][0] == '\0') ? C_TXT : + (lines[i][0] == ' ') ? C_TXT : + C_TXT; + xft_text(s, xd, s->f_body, col, 40, y, lines[i]); + y += 22; + } + + /* EFI/BIOS detection note */ + y += 10; + int uefi = (access("/sys/firmware/efi", F_OK) == 0); + char boot_note[80]; + snprintf(boot_note, sizeof(boot_note), + "Boot mode detected: %s", uefi ? "UEFI" : "Legacy BIOS"); + xft_text(s, xd, s->f_small, C_TXT_DIM, 40, y, boot_note); + + draw_footer(s, xd, NULL, "Next →", C_BTN_PRI); +} + +static void draw_disk(Ins *s, XftDraw *xd) { + draw_header(s, xd, "Select Installation Disk"); + fill_rect(s, 0, BODY_Y, WIN_W, BODY_H, C_BODY); + + xft_text(s, xd, s->f_body, C_TXT_DIM, 30, BODY_Y + 16, + "Choose the disk where SkyOS will be installed. It will be erased."); + + int y = BODY_Y + 42; + int dw = WIN_W - 60; + + if (s->ndisks == 0) { + xft_text(s, xd, s->f_body, C_ERR, 30, y, + "No suitable disks found (≥4 GB)."); + } else { + for (int i = 0; i < s->ndisks; i++) { + char label[80], sub[80]; + snprintf(label, sizeof(label), "/dev/%s — %s", + s->disks[i].name, s->disks[i].size_str); + snprintf(sub, sizeof(sub), "%s", s->disks[i].model[0] + ? s->disks[i].model : "Unknown model"); + draw_radio(s, xd, 30, y, dw, label, sub, + s->sel_disk == i, 0); + y += 46; + } + } + + if (s->err_msg[0]) + xft_text(s, xd, s->f_body, C_ERR, 30, WIN_H - FOOT_H - 24, + s->err_msg); + + draw_footer(s, xd, "← Back", "Next →", C_BTN_PRI); +} + +static void draw_user(Ins *s, XftDraw *xd) { + draw_header(s, xd, "Create Your Account"); + fill_rect(s, 0, BODY_Y, WIN_W, BODY_H, C_BODY); + + int lx = 30, fx = 200, fw = WIN_W - fx - 30; + int y = BODY_Y + 24; + int fh = 32, gap = 48; + + struct { const char *label; TF *tf; const char *ph; } fields[] = { + { "Hostname:", &s->tf_host, "skyos" }, + { "Username:", &s->tf_user, "sky" }, + { "Password:", &s->tf_pass, "••••••••" }, + { "Confirm password:", &s->tf_pass2, "••••••••" }, + }; + + for (int i = 0; i < 4; i++) { + xft_text(s, xd, s->f_body, C_TXT_DIM, lx, + y + (fh - text_h(s, s->f_body)) / 2, fields[i].label); + draw_textfield(s, xd, fx, y, fw, fields[i].tf, fields[i].ph); + y += gap; + } + + /* hints */ + xft_text(s, xd, s->f_small, C_TXT_DIM, lx, y + 8, + "Password must be at least 6 characters."); + xft_text(s, xd, s->f_small, C_TXT_DIM, lx, y + 24, + "You can change these settings after installation."); + + if (s->err_msg[0]) + xft_text(s, xd, s->f_body, C_ERR, lx, WIN_H - FOOT_H - 24, + s->err_msg); + + draw_footer(s, xd, "← Back", "Next →", C_BTN_PRI); +} + +static void draw_review(Ins *s, XftDraw *xd) { + draw_header(s, xd, "Ready to Install"); + fill_rect(s, 0, BODY_Y, WIN_W, BODY_H, C_BODY); + + /* summary card */ + int cx = 30, cy = BODY_Y + 20, cw = WIN_W - 60, ch = 150; + fill_rect(s, cx, cy, cw, ch, C_PANEL); + draw_line(s, cx, cy, cx+cw, cy, C_SEP); + + int lx = cx + 24, rx = cx + 180; + int y = cy + 18; + int lh = 26; + + xft_text(s, xd, s->f_small, C_TXT_DIM, lx, y, "Disk:"); + { + char v[80]; + snprintf(v, sizeof(v), "/dev/%s (%s — %s)", + s->disks[s->sel_disk].name, + s->disks[s->sel_disk].size_str, + s->disks[s->sel_disk].model[0] + ? s->disks[s->sel_disk].model : "?"); + xft_text(s, xd, s->f_body, C_TXT, rx, y, v); + } + y += lh; + xft_text(s, xd, s->f_small, C_TXT_DIM, lx, y, "Hostname:"); + xft_text(s, xd, s->f_body, C_TXT, rx, y, s->tf_host.buf); + y += lh; + xft_text(s, xd, s->f_small, C_TXT_DIM, lx, y, "Username:"); + xft_text(s, xd, s->f_body, C_TXT, rx, y, s->tf_user.buf); + y += lh; + xft_text(s, xd, s->f_small, C_TXT_DIM, lx, y, "Boot mode:"); + xft_text(s, xd, s->f_body, C_TXT, rx, y, + (access("/sys/firmware/efi", F_OK) == 0) ? "UEFI (systemd-boot)" + : "BIOS (GRUB)"); + + /* warning */ + int wy = cy + ch + 20; + fill_rect(s, cx, wy, cw, 54, 0x2A1010); + draw_line(s, cx, wy, cx+cw, wy, 0x6A1818); + xft_text(s, xd, s->f_head, C_WARN, cx+16, wy+8, + "⚠ This will erase all data on the selected disk!"); + char wline[128]; + snprintf(wline, sizeof(wline), + " /dev/%s (%s) will be permanently reformatted.", + s->disks[s->sel_disk].name, + s->disks[s->sel_disk].size_str); + xft_text(s, xd, s->f_body, C_ERR, cx+16, wy+30, wline); + + draw_footer(s, xd, "← Back", "Install", C_BTN_DNG); +} + +/* ── install step ────────────────────────────────────────────────── */ +#define LOG_LINES_VISIBLE 12 +#define LOG_LINE_H 17 + +static void draw_install(Ins *s, XftDraw *xd) { + draw_header(s, xd, s->install_done ? "Installation Complete" : + "Installing SkyOS…"); + fill_rect(s, 0, BODY_Y, WIN_W, BODY_H, C_BODY); + + int y = BODY_Y + 16; + + /* progress bar */ + draw_progress(s, 30, y, WIN_W - 60, 26, s->progress); + y += 30; + + /* progress percentage + message */ + char pct_str[16]; + snprintf(pct_str, sizeof(pct_str), "%d%%", s->progress); + xft_text(s, xd, s->f_head, C_TXT, 30, y, pct_str); + xft_text(s, xd, s->f_body, C_TXT_DIM, + 30 + text_w(s, s->f_head, pct_str) + 12, y, s->prog_msg); + y += 28; + + /* log panel */ + fill_rect(s, 30, y, WIN_W - 60, + LOG_LINES_VISIBLE * LOG_LINE_H + 8, C_PANEL); + draw_line(s, 30, y, WIN_W-30, y, C_BORDER); + + int logy = y + 6; + int start = (s->log.count > LOG_LINES_VISIBLE) + ? (s->log.head - LOG_LINES_VISIBLE + MAX_LOG) % MAX_LOG + : (s->log.head - s->log.count + MAX_LOG) % MAX_LOG; + + int show = s->log.count < LOG_LINES_VISIBLE ? s->log.count : LOG_LINES_VISIBLE; + for (int i = 0; i < show; i++) { + int idx = (start + i) % MAX_LOG; + const char *line = s->log.lines[idx]; + unsigned long col = C_TXT_DIM; + if (strncmp(line, ">>>", 3) == 0) col = C_TXT; + if (strncmp(line, " ✓", 5) == 0) col = C_OK; + if (strncmp(line, "ERROR", 5) == 0 || + strncmp(line, "error", 5) == 0) col = C_ERR; + + XRectangle clip = { 32, (short)logy, + (unsigned short)(WIN_W - 64), + (unsigned short)LOG_LINE_H }; + XftDrawSetClipRectangles(xd, 0, 0, &clip, 1); + xft_text(s, xd, s->f_mono, col, 36, logy, line); + XftDrawSetClip(xd, None); + logy += LOG_LINE_H; + } + + if (s->install_done) { + if (s->install_err[0]) { + xft_text(s, xd, s->f_body, C_ERR, 30, WIN_H - FOOT_H - 24, + s->install_err); + } + draw_footer(s, xd, NULL, "Reboot", C_OK); + } + /* else: no buttons while installing */ +} + +static void draw_done(Ins *s, XftDraw *xd) { + draw_header(s, xd, "Installation Complete"); + fill_rect(s, 0, BODY_Y, WIN_W, BODY_H, C_BODY); + + int cy = BODY_Y + BODY_H / 2 - 60; + xft_text(s, xd, s->f_logo, C_OK, + WIN_W/2 - text_w(s, s->f_logo, "✓ SkyOS Installed!")/2, + cy, "✓ SkyOS Installed!"); + cy += 48; + const char *l1 = "SkyOS has been installed successfully."; + const char *l2 = "Remove the USB drive and restart your computer."; + xft_text(s, xd, s->f_body, C_TXT, + WIN_W/2 - text_w(s, s->f_body, l1)/2, cy, l1); + cy += 26; + xft_text(s, xd, s->f_body, C_TXT_DIM, + WIN_W/2 - text_w(s, s->f_body, l2)/2, cy, l2); + + draw_footer(s, xd, NULL, "Reboot Now", C_BTN_PRI); +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Main draw dispatch */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void redraw(Ins *s) { + XftDraw *xd = XftDrawCreate(s->dpy, s->win, s->vis, s->cmap); + fill_rect(s, 0, 0, WIN_W, WIN_H, C_BG); + + switch (s->step) { + case STEP_WELCOME: draw_welcome(s, xd); break; + case STEP_DISK: draw_disk(s, xd); break; + case STEP_USER: draw_user(s, xd); break; + case STEP_REVIEW: draw_review(s, xd); break; + case STEP_INSTALL: draw_install(s, xd); break; + case STEP_DONE: draw_done(s, xd); break; + } + + /* outer border */ + XSetForeground(s->dpy, s->gc, mkcolor(s, C_BORDER)); + XDrawRectangle(s->dpy, s->win, s->gc, 0, 0, WIN_W-1, WIN_H-1); + + XftDrawDestroy(xd); + XFlush(s->dpy); +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Installation runner */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void start_install(Ins *s) { + int pipefd[2]; + if (pipe(pipefd) < 0) { + snprintf(s->install_err, sizeof(s->install_err), + "pipe() failed: %s", strerror(errno)); + s->install_done = 1; + return; + } + + pid_t pid = fork(); + if (pid < 0) { + snprintf(s->install_err, sizeof(s->install_err), + "fork() failed: %s", strerror(errno)); + s->install_done = 1; + return; + } + + if (pid == 0) { + /* child */ + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + execl("/usr/local/bin/skyinstall", "skyinstall", + "--gui", + s->disks[s->sel_disk].path, + s->tf_host.buf, + s->tf_user.buf, + s->tf_pass.buf, + NULL); + fprintf(stderr, "exec skyinstall failed: %s\n", strerror(errno)); + _exit(1); + } + + /* parent */ + close(pipefd[1]); + s->install_fd = pipefd[0]; + /* non-blocking reads */ + fcntl(s->install_fd, F_SETFL, O_NONBLOCK); +} + +static void poll_install(Ins *s) { + if (s->install_fd < 0) return; + + char buf[512]; + static char linebuf[LOG_COLS + 4]; + static int linelen = 0; + + ssize_t n = read(s->install_fd, buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + for (int i = 0; i < (int)n; i++) { + char c = buf[i]; + if (c == '\n' || c == '\r') { + linebuf[linelen] = '\0'; + if (linelen > 0) { + /* parse PROGRESS:pct:msg */ + if (strncmp(linebuf, "PROGRESS:", 9) == 0) { + int pct = 0; + char msg[128] = {0}; + sscanf(linebuf + 9, "%d:%127[^\n]", &pct, msg); + s->progress = pct; + snprintf(s->prog_msg, sizeof(s->prog_msg), "%s", msg); + if (pct == 100) { + s->install_done = 1; + s->step = STEP_DONE; + } + } else { + log_push(&s->log, linebuf); + } + redraw(s); + } + linelen = 0; + } else { + if (linelen < LOG_COLS) + linebuf[linelen++] = c; + } + } + } else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) { + /* EOF */ + close(s->install_fd); + s->install_fd = -1; + if (!s->install_done) { + snprintf(s->install_err, sizeof(s->install_err), + "Installer exited unexpectedly."); + s->install_done = 1; + } + redraw(s); + } +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Text field input handling */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void tf_key(TF *tf, XKeyEvent *e) { + KeySym sym; char buf[8]; int len; + len = XLookupString(e, buf, sizeof(buf)-1, &sym, NULL); + buf[len] = '\0'; + + if (sym == XK_BackSpace) { + if (tf->len > 0) tf->buf[--tf->len] = '\0'; + } else if (len > 0 && buf[0] >= 0x20 && buf[0] < 0x7F && + tf->len < TF_MAX - 1) { + tf->buf[tf->len++] = buf[0]; + tf->buf[tf->len] = '\0'; + } +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Event handling */ +/* ══════════════════════════════════════════════════════════════════ */ + +static TF *tf_at(Ins *s, int x, int y) { + int fx = 200, fw = WIN_W - fx - 30, lh = 48; + int ys[] = { BODY_Y+24, BODY_Y+72, BODY_Y+120, BODY_Y+168 }; + TF *tfs[] = { &s->tf_host, &s->tf_user, &s->tf_pass, &s->tf_pass2 }; + for (int i = 0; i < 4; i++) + if (in_rect(x, y, fx, ys[i], fw, 32)) return tfs[i]; + (void)lh; + return NULL; +} + +static void handle_click(Ins *s, int x, int y) { + int bx, by, bw, bh; + + /* next button */ + next_rect(&bx, &by, &bw, &bh); + if (in_rect(x, y, bx, by, bw, bh)) { + if (!validate_step(s)) { redraw(s); return; } + switch (s->step) { + case STEP_WELCOME: + s->step = STEP_DISK; + if (s->ndisks == 0) detect_disks(s); + break; + case STEP_DISK: s->step = STEP_USER; break; + case STEP_USER: s->step = STEP_REVIEW; break; + case STEP_REVIEW: + s->step = STEP_INSTALL; + redraw(s); + start_install(s); + return; + case STEP_INSTALL: + if (s->install_done) { + system("reboot"); + } + break; + case STEP_DONE: + system("reboot"); + break; + } + redraw(s); + return; + } + + /* back button */ + back_rect(&bx, &by, &bw, &bh); + if (in_rect(x, y, bx, by, bw, bh) && + s->step > STEP_WELCOME && s->step < STEP_INSTALL) { + s->step = (Step)((int)s->step - 1); + s->err_msg[0] = '\0'; + redraw(s); + return; + } + + /* disk list */ + if (s->step == STEP_DISK) { + int dy = BODY_Y + 42; + for (int i = 0; i < s->ndisks; i++) { + if (in_rect(x, y, 30, dy, WIN_W-60, 44)) { + s->sel_disk = i; + redraw(s); + return; + } + dy += 46; + } + } + + /* text fields */ + if (s->step == STEP_USER) { + TF *hit = tf_at(s, x, y); + if (s->focused_tf) s->focused_tf->focused = 0; + s->focused_tf = hit; + if (hit) hit->focused = 1; + redraw(s); + } +} + +static void handle_key(Ins *s, XKeyEvent *e) { + KeySym sym = XLookupKeysym(e, 0); + + /* Tab cycles text fields */ + if (sym == XK_Tab && s->step == STEP_USER) { + TF *order[] = { &s->tf_host, &s->tf_user, &s->tf_pass, &s->tf_pass2 }; + int cur = -1; + for (int i = 0; i < 4; i++) + if (order[i] == s->focused_tf) { cur = i; break; } + if (s->focused_tf) s->focused_tf->focused = 0; + cur = (cur + 1) % 4; + s->focused_tf = order[cur]; + s->focused_tf->focused = 1; + redraw(s); + return; + } + + /* Enter advances */ + if (sym == XK_Return) { + int bx, by, bw, bh; + next_rect(&bx, &by, &bw, &bh); + handle_click(s, bx + 1, by + 1); + return; + } + + if (s->focused_tf) { + tf_key(s->focused_tf, e); + redraw(s); + } +} + +static void handle_motion(Ins *s, int x, int y) { + int bx, by, bw, bh; + int prev_back = s->hov_back, prev_next = s->hov_next; + + next_rect(&bx, &by, &bw, &bh); + s->hov_next = in_rect(x, y, bx, by, bw, bh); + + back_rect(&bx, &by, &bw, &bh); + s->hov_back = in_rect(x, y, bx, by, bw, bh) && + s->step > STEP_WELCOME && s->step < STEP_INSTALL; + + if (s->hov_back != prev_back || s->hov_next != prev_next) + redraw(s); +} + +/* ══════════════════════════════════════════════════════════════════ */ +/* Init & main loop */ +/* ══════════════════════════════════════════════════════════════════ */ + +static void init(Ins *s) { + s->dpy = XOpenDisplay(NULL); + if (!s->dpy) { fputs("Cannot open display\n", stderr); exit(1); } + + s->scr = DefaultScreen(s->dpy); + s->vis = DefaultVisual(s->dpy, s->scr); + s->cmap = DefaultColormap(s->dpy, s->scr); + s->depth = DefaultDepth(s->dpy, s->scr); + + int sw = DisplayWidth(s->dpy, s->scr); + int sh = DisplayHeight(s->dpy, s->scr); + + XSetWindowAttributes swa; + swa.background_pixel = mkcolor(s, C_BG); + swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask + | KeyPressMask | StructureNotifyMask; + + s->win = XCreateWindow( + s->dpy, RootWindow(s->dpy, s->scr), + (sw - WIN_W)/2, (sh - WIN_H)/2, WIN_W, WIN_H, + 0, s->depth, InputOutput, s->vis, + CWBackPixel | CWEventMask, &swa); + s->gc = XCreateGC(s->dpy, s->win, 0, NULL); + + /* window title + WM hints */ + XStoreName(s->dpy, s->win, "SkyOS Installer"); + Atom wm_del = XInternAtom(s->dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(s->dpy, s->win, &wm_del, 1); + /* fixed size */ + XSizeHints *sh2 = XAllocSizeHints(); + sh2->flags = PMinSize | PMaxSize; + sh2->min_width = sh2->max_width = WIN_W; + sh2->min_height = sh2->max_height = WIN_H; + XSetWMNormalHints(s->dpy, s->win, sh2); + XFree(sh2); + + /* fonts */ + s->f_logo = XftFontOpenName(s->dpy, s->scr, F_LOGO); + s->f_head = XftFontOpenName(s->dpy, s->scr, F_HEAD); + s->f_body = XftFontOpenName(s->dpy, s->scr, F_BODY); + s->f_small = XftFontOpenName(s->dpy, s->scr, F_SMALL); + s->f_mono = XftFontOpenName(s->dpy, s->scr, F_MONO); + s->f_btn = XftFontOpenName(s->dpy, s->scr, F_BTN); + XftFont **ff[] = {&s->f_logo,&s->f_head,&s->f_body, + &s->f_small,&s->f_mono,&s->f_btn}; + for (int i = 0; i < 6; i++) + if (!*ff[i]) + *ff[i] = XftFontOpenName(s->dpy, s->scr, "fixed:size=10"); + + /* defaults */ + s->step = STEP_WELCOME; + s->sel_disk = -1; + s->install_fd = -1; + s->progress = 0; + snprintf(s->prog_msg, sizeof(s->prog_msg), "Starting…"); + snprintf(s->tf_host.buf, TF_MAX, "skyos"); + s->tf_host.len = strlen("skyos"); + snprintf(s->tf_user.buf, TF_MAX, "sky"); + s->tf_user.len = strlen("sky"); + s->tf_pass.password = 1; + s->tf_pass2.password = 1; + + detect_disks(s); + if (s->ndisks == 1) s->sel_disk = 0; + + XMapRaised(s->dpy, s->win); + XSync(s->dpy, False); +} + +int main(void) { + Ins s = {0}; + init(&s); + redraw(&s); + + Atom wm_del = XInternAtom(s.dpy, "WM_DELETE_WINDOW", False); + + for (;;) { + /* poll for X events or installer output */ + int xfd = ConnectionNumber(s.dpy); + struct pollfd pfds[2]; + pfds[0].fd = xfd; pfds[0].events = POLLIN; + pfds[1].fd = s.install_fd; + pfds[1].events = (s.install_fd >= 0) ? POLLIN : 0; + + int r = poll(pfds, (s.install_fd >= 0) ? 2 : 1, 100); + (void)r; + + /* drain installer output */ + if (s.install_fd >= 0 && (pfds[1].revents & (POLLIN|POLLHUP))) + poll_install(&s); + /* also poll on timeout to catch buffered data */ + if (s.install_fd >= 0 && r == 0) + poll_install(&s); + + /* X events */ + while (XPending(s.dpy)) { + XEvent e; + XNextEvent(s.dpy, &e); + switch (e.type) { + case Expose: + if (e.xexpose.count == 0) redraw(&s); + break; + case ButtonPress: + handle_click(&s, e.xbutton.x, e.xbutton.y); + break; + case MotionNotify: + handle_motion(&s, e.xmotion.x, e.xmotion.y); + break; + case KeyPress: + handle_key(&s, &e.xkey); + break; + case ClientMessage: + if ((Atom)e.xclient.data.l[0] == wm_del) return 0; + break; + default: break; + } + } + } + return 0; +} diff --git a/iso/build-base.sh b/iso/build-base.sh new file mode 100755 index 0000000..f145b57 --- /dev/null +++ b/iso/build-base.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# SkyOS base system builder +# Builds a bootable ISO from an Arch Linux base with the SkyWM desktop. +# Run this on an existing Arch Linux install or in a Docker container. +# +# Usage: sudo ./build-base.sh +# Output: skyos.iso + +set -euo pipefail + +WORK=/tmp/skyos-build +ISO_NAME=skyos.iso +SKYOS_PKGS=( + # base + base linux linux-firmware + # bootloader (EFI + BIOS — covers Intel Macs via rEFInd) + grub efibootmgr refind + # X11 stack (no display manager, just startx) + xorg-server xorg-xinit xorg-xrandr xorg-xsetroot + xterm + # fonts + ttf-dejavu ttf-liberation noto-fonts + # hardware support for Intel Macs + broadcom-wl-dkms # WiFi + xf86-video-intel # GPU (older iGPU Macs) + xf86-input-libinput + alsa-utils # audio + # tools + thunar # file manager + mousepad # text editor (GTK, lightweight) + feh # desktop wallpaper setter + picom # compositor (optional shadows) + networkmanager # networking + sudo git nano +) + +echo ">>> Creating chroot at $WORK" +mkdir -p "$WORK" + +echo ">>> Bootstrapping Arch base" +pacstrap "$WORK" "${SKYOS_PKGS[@]}" + +echo ">>> Configuring system" +arch-chroot "$WORK" /bin/bash -s <<'CHROOT' +# locale +echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen +locale-gen +echo "LANG=en_US.UTF-8" > /etc/locale.conf + +# hostname +echo "skyos" > /etc/hostname +cat > /etc/hosts <> /etc/sudoers + +# enable services +systemctl enable NetworkManager + +# initramfs +mkinitcpio -P +CHROOT + +echo ">>> Installing SkyWM & Deskbar binaries" +cp "$(dirname "$0")/../wm/skywm" "$WORK/usr/local/bin/skywm" +cp "$(dirname "$0")/../deskbar/skydeskbar" "$WORK/usr/local/bin/skydeskbar" +chmod 755 "$WORK/usr/local/bin/skywm" "$WORK/usr/local/bin/skydeskbar" + +echo ">>> Writing xinitrc" +cat > "$WORK/home/sky/.xinitrc" <<'XINITRC' +#!/bin/sh +# Set wallpaper (restores saved choice, or uses default) +skywallpaper + +# Apply GTK theme +export GTK_THEME=SkyOS +export GTK2_RC_FILES="$HOME/.gtkrc-2.0" +cat > "$HOME/.gtkrc-2.0" < "$HOME/.config/gtk-3.0/settings.ini" <>> Writing xbindkeys config (Alt+F1 = launcher)" +cat > "$WORK/home/sky/.xbindkeysrc" <<'XBK' +"skylauncher" + Mod1 + F1 +XBK +arch-chroot "$WORK" chown sky:sky /home/sky/.xbindkeysrc + +echo ">>> Installing picom config" +mkdir -p "$WORK/etc/skyos" +cp "$(dirname "$0")/../themes/picom.conf" "$WORK/etc/skyos/picom.conf" + +echo ">>> Installing GTK theme" +mkdir -p "$WORK/usr/share/themes" +cp -r "$(dirname "$0")/../themes/SkyOS" "$WORK/usr/share/themes/" + +echo ">>> Installing skywallpaper helper" +cp "$(dirname "$0")/../themes/skywallpaper" "$WORK/usr/local/bin/skywallpaper" +chmod 755 "$WORK/usr/local/bin/skywallpaper" + +echo ">>> Generating default wallpaper" +mkdir -p "$WORK/usr/share/skyos/wallpapers" +(cd "$WORK/usr/share/skyos/wallpapers" && bash "$(dirname "$0")/../themes/wallpapers/generate-default.sh") || true + +echo ">>> Installing rEFInd bootloader config for Intel Macs" +mkdir -p "$WORK/boot/EFI/refind" +cat > "$WORK/boot/EFI/refind/refind.conf" <<'REFIND' +timeout 5 +use_graphics_for linux +scanfor manual +menuentry "SkyOS" { + icon /EFI/refind/icons/os_linux.png + volume "SkyOS" + loader /boot/vmlinuz-linux + initrd /boot/initramfs-linux.img + options "root=LABEL=SkyOS rw quiet splash" +} +REFIND + +echo ">>> Creating ISO" +# mkarchiso requires archiso package +if command -v mkarchiso &>/dev/null; then + # Minimal archiso profile pointing to our chroot + mkarchiso -v -w /tmp/skyos-iso-work -o . "$WORK" + mv *.iso "$ISO_NAME" +else + echo "WARNING: archiso not installed. To build the ISO:" + echo " pacman -S archiso && rerun this script" + echo " OR: use the chroot at $WORK to install manually." +fi + +echo ">>> Done. Output: $ISO_NAME (or chroot at $WORK)" diff --git a/iso/docker/Dockerfile b/iso/docker/Dockerfile new file mode 100644 index 0000000..5ec9a0a --- /dev/null +++ b/iso/docker/Dockerfile @@ -0,0 +1,57 @@ +# SkyOS ISO builder +# Uses official Arch Linux as the base — provides pacman, pacstrap, archiso. +# Build context: the repo root (OS/) +# +# Build: docker build -t skyos-builder -f iso/docker/Dockerfile . +# Run: docker run --privileged -v $(pwd)/dist:/dist skyos-builder +# Output: dist/skyos.iso + +FROM archlinux:latest + +# ── disable pacman seccomp sandbox (breaks under QEMU on Apple Silicon) ── +RUN echo "DisableSandbox" >> /etc/pacman.conf + +# ── install build tools ──────────────────────────────────────────────── +RUN pacman -Syu --noconfirm && \ + pacman -S --noconfirm \ + archiso \ + base-devel \ + libx11 \ + libxft \ + fontconfig \ + freetype2 \ + imagemagick \ + fakeroot \ + git \ + sudo \ + && pacman -Scc --noconfirm + +# ── build OnlyOffice from AUR (as a non-root build user) ───────────── +RUN useradd -m builder && \ + echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder +RUN su builder -c " \ + git clone --depth=1 https://aur.archlinux.org/onlyoffice-bin.git /tmp/onlyoffice && \ + cd /tmp/onlyoffice && \ + makepkg -s --noconfirm 2>&1 | tail -5 \ + " && \ + find /tmp/onlyoffice -name '*.pkg.tar.zst' -exec cp {} /onlyoffice.pkg.tar.zst \; || \ + echo "OnlyOffice AUR build failed — will be skipped in ISO" + +# ── show which mkinitcpio hooks archiso installed (helps debugging) ─── +RUN echo "=== archiso initcpio hooks ===" && \ + ls /usr/lib/initcpio/hooks/ 2>/dev/null || echo "(none found)" + +# ── copy the entire SkyOS source tree ──────────────────────────────── +WORKDIR /skyos +COPY . . + +# ── build the SkyOS binaries inside the container ──────────────────── +# pkg-config finds X11/Xft headers automatically on Arch — no overrides needed +RUN make + +# ── generate the default wallpaper ─────────────────────────────────── +RUN bash themes/wallpapers/generate-default.sh || true + +# ── run the ISO build script ────────────────────────────────────────── +# --privileged is required for loop device mounts inside the container +ENTRYPOINT ["/skyos/iso/docker/entrypoint.sh"] diff --git a/iso/docker/entrypoint.sh b/iso/docker/entrypoint.sh new file mode 100755 index 0000000..0fa9e1d --- /dev/null +++ b/iso/docker/entrypoint.sh @@ -0,0 +1,685 @@ +#!/usr/bin/env bash +# Runs inside the Docker container. +# Produces /dist/skyos.iso using archiso's "releng" profile as a base. +set -euo pipefail + +DIST="${DIST:-/dist}" +WORK=/tmp/skyos-work +PROFILE=/tmp/skyos-profile +SRC=/skyos + +echo "════════════════════════════════════════" +echo " SkyOS ISO Builder" +echo "════════════════════════════════════════" + +# ── 1. Start from archiso's releng profile ──────────────────────────── +echo ">>> Copying archiso releng profile..." +cp -r /usr/share/archiso/configs/releng "$PROFILE" + +# ── 2. Package list — trim releng defaults, add what we need ───────── +cat > "$PROFILE/packages.x86_64" <<'PKGS' +base +linux +linux-firmware +mkinitcpio +archiso +grub +efibootmgr +syslinux +xorg-server +xorg-xinit +xorg-xrandr +xorg-xset +xorg-xsetroot +xterm +ttf-dejavu +ttf-liberation +noto-fonts +libxft +fontconfig +xf86-video-intel +xf86-video-vesa +mesa +xf86-input-libinput +alsa-utils +alsa-firmware +sof-firmware +pipewire +pipewire-alsa +pipewire-pulse +wireplumber +networkmanager +networkmanager-openvpn +broadcom-wl +thunar +gvfs +gvfs-smb +tumbler +ffmpegthumbnailer +file-roller +mousepad +feh +picom +xbindkeys +arandr +pavucontrol +brightnessctl +dunst +network-manager-applet +polkit-gnome +xdg-user-dirs +xdg-utils +shared-mime-info +desktop-file-utils +adwaita-icon-theme +gnome-themes-extra +hicolor-icon-theme +firefox +mpv +evince +viewnior +xscreensaver +xfce4-power-manager +bluez +blueman +xclip +zip +unzip +p7zip +wget +curl +man-db +noto-fonts-emoji +playerctl +xorg-xkill +rsync +arch-install-scripts +htop +nano +git +sudo +bash-completion +fastfetch +PKGS + +# ── 3. airootfs overlay — files that land in the live system ───────── +AIROOTFS="$PROFILE/airootfs" + +# ── Ship our own archiso mkinitcpio hooks ──────────────────────────── +# Archiso v78+ removed the mkinitcpio hook binaries from the package. +# We provide them ourselves so the live initramfs can mount the squashfs. +mkdir -p "$AIROOTFS/usr/lib/initcpio/hooks" +mkdir -p "$AIROOTFS/usr/lib/initcpio/install" + +# -- archiso hook (runtime: mounts squashfs and sets up overlay) ------ +cat > "$AIROOTFS/usr/lib/initcpio/hooks/archiso" <<'HOOK' +run_hook() { + export mount_handler="archiso_mount_handler" +} + +archiso_mount_handler() { + local _newroot="${1}" + local _dev _sfs _i + + [ -z "${archisobasedir}" ] && archisobasedir="arch" + if [ -z "${archisolabel}" ]; then + echo "ERROR: archisolabel not set" + return 1 + fi + + # Wait up to 30 s for the USB/CD device to appear + _i=0 + while [ "${_i}" -lt 30 ]; do + _dev=$(blkid --match-token LABEL="${archisolabel}" --output device 2>/dev/null | head -1) + [ -n "${_dev}" ] && break + sleep 1 + _i=$((_i + 1)) + done + if [ -z "${_dev}" ]; then + echo "ERROR: archiso: cannot find device with label '${archisolabel}'" + return 1 + fi + + # Mount the boot medium (ISO/USB) + mkdir -p /run/archiso/bootmnt + mount -r "${_dev}" /run/archiso/bootmnt + + # Mount the squashfs + _sfs="/run/archiso/bootmnt/${archisobasedir}/x86_64/airootfs.sfs" + mkdir -p /run/archiso/airootfs + mount -r -t squashfs "${_sfs}" /run/archiso/airootfs + + # Writable tmpfs overlay (so the live session can write) + mkdir -p /run/archiso/cowspace + mount -t tmpfs -o "size=75%,mode=0755" tmpfs /run/archiso/cowspace + mkdir -p /run/archiso/cowspace/upper /run/archiso/cowspace/work + + # Overlay squashfs (ro) + tmpfs (rw) → new root + mount -t overlay overlay \ + -o "lowerdir=/run/archiso/airootfs,upperdir=/run/archiso/cowspace/upper,workdir=/run/archiso/cowspace/work" \ + "${_newroot}" +} +HOOK + +# -- archiso install script (build-time: adds binaries/modules to initramfs) +cat > "$AIROOTFS/usr/lib/initcpio/install/archiso" <<'INSTALL' +build() { + add_runscript + add_binary blkid + add_binary sleep + add_module squashfs + add_module overlay + add_module iso9660 + add_module vfat + add_dir /run/archiso/bootmnt + add_dir /run/archiso/airootfs + add_dir /run/archiso/cowspace +} +help() { + echo "Mounts the archiso squashfs for live boot." +} +INSTALL + +# Override the releng archiso.conf — drop memdisk/pxe hooks (memdiskfind +# is no longer shipped by syslinux), keep only what we actually provide. +mkdir -p "$AIROOTFS/etc/mkinitcpio.conf.d" +cat > "$AIROOTFS/etc/mkinitcpio.conf.d/archiso.conf" <<'MKCONF' +MODULES=() +BINARIES=() +FILES=() +HOOKS=(base udev microcode archiso block filesystems keyboard) +MKCONF +echo ">>> Live initramfs HOOKS: base udev microcode archiso block filesystems keyboard" + +# passwd / shadow / group +cat > "$AIROOTFS/etc/hostname" <<'EOF' +skyos +EOF + +# Create 'sky' user in the live environment +mkdir -p "$AIROOTFS/etc/sudoers.d" +echo "%wheel ALL=(ALL) ALL" > "$AIROOTFS/etc/sudoers.d/wheel" + +# ── 3b. OnlyOffice — create a local pacman repo so mkarchiso can find it +if [[ -f /onlyoffice.pkg.tar.zst ]]; then + echo ">>> Setting up local OnlyOffice repo..." + mkdir -p /skyos-local-repo + cp /onlyoffice.pkg.tar.zst /skyos-local-repo/ + OO_PKG="$(find /skyos-local-repo -name '*.pkg.tar.zst' | head -1)" + repo-add /skyos-local-repo/skyos-local.db.tar.gz "$OO_PKG" + + # Prepend [skyos-local] to the profile's pacman.conf + sed -i 's/^\[core\]/[skyos-local]\nSigLevel = Never\nServer = file:\/\/\/skyos-local-repo\n\n[core]/' \ + "$PROFILE/pacman.conf" + + echo "onlyoffice-bin" >> "$PROFILE/packages.x86_64" + echo ">>> OnlyOffice repo ready." +else + echo ">>> OnlyOffice build not available — skipping." +fi + +# ── 4. Install SkyOS binaries ───────────────────────────────────────── +echo ">>> Installing SkyOS binaries..." +mkdir -p "$AIROOTFS/usr/local/bin" +install -m755 "$SRC/wm/skywm" "$AIROOTFS/usr/local/bin/skywm" +install -m755 "$SRC/deskbar/skydeskbar" "$AIROOTFS/usr/local/bin/skydeskbar" +install -m755 "$SRC/launcher/skylauncher" "$AIROOTFS/usr/local/bin/skylauncher" +install -m755 "$SRC/themes/skywallpaper" "$AIROOTFS/usr/local/bin/skywallpaper" +install -m755 "$SRC/installer/skyinstall" "$AIROOTFS/usr/local/bin/skyinstall" +# GUI installer (compiled binary; falls back gracefully if not built) +if [[ -f "$SRC/installer/skyinstall-gui" ]]; then + install -m755 "$SRC/installer/skyinstall-gui" "$AIROOTFS/usr/local/bin/skyinstall-gui" +fi + +# ── 5. GTK theme ────────────────────────────────────────────────────── +echo ">>> Installing GTK theme..." +mkdir -p "$AIROOTFS/usr/share/themes" +cp -r "$SRC/themes/SkyOS" "$AIROOTFS/usr/share/themes/" + +# ── 6. picom config ─────────────────────────────────────────────────── +mkdir -p "$AIROOTFS/etc/skyos" +install -m644 "$SRC/themes/picom.conf" "$AIROOTFS/etc/skyos/picom.conf" + +# ── 7. Default wallpaper ────────────────────────────────────────────── +echo ">>> Generating default wallpaper..." +mkdir -p "$AIROOTFS/usr/share/skyos/wallpapers" +if convert -size 1920x1080 gradient:"#1E3A5A-#0E1A28" \ + "$AIROOTFS/usr/share/skyos/wallpapers/skyos-default.png" 2>/dev/null; then + echo " Wallpaper generated." +else + echo " (ImageMagick unavailable in container — wallpaper skipped, solid color will be used)" +fi + +# ── 8. User skeleton (.xinitrc, GTK config, xbindkeys) ─────────────── +echo ">>> Writing user skeleton..." +SKEL="$AIROOTFS/etc/skel" +mkdir -p "$SKEL/.config/gtk-3.0" + +cat > "$SKEL/.xinitrc" <<'XINITRC' +#!/bin/sh +# SkyOS session startup + +# Merge X resources if present +[ -f "$HOME/.Xresources" ] && xrdb -merge "$HOME/.Xresources" + +# Key repeat: 250ms delay, 30/s rate — comfortable for typing +xset r rate 250 30 + +# HiDPI hint for GTK/Qt (96 DPI default; bump to 144 for HiDPI Macs) +xrandr --dpi 96 + +# Wallpaper +skywallpaper + +# Create standard user directories (Desktop, Downloads, Pictures…) +xdg-user-dirs-update 2>/dev/null || true + +# GTK theme +export GTK_THEME=SkyOS +mkdir -p "$HOME/.config/gtk-3.0" +cat > "$HOME/.config/gtk-3.0/settings.ini" < "$HOME/.gtkrc-2.0" </dev/null; then + while true; do + xclip -o -sel primary 2>/dev/null | xclip -i -sel clipboard 2>/dev/null || true + sleep 1 + done & +fi + +# Screen locker daemon (locks after idle timeout) +xscreensaver -no-splash & + +# Power management (handles suspend, battery alerts, lid close) +xfce4-power-manager & + +# Global hotkeys +xbindkeys & + +# Deskbar +skydeskbar & + +# Window manager (must be last — controls the session) +exec skywm +XINITRC +chmod 755 "$SKEL/.xinitrc" + +cat > "$SKEL/.xbindkeysrc" <<'XBK' +# App launcher +"skylauncher" + Mod1 + F1 + +# Brightness (Mac Fn+F1/F2, also mapped as XF86 keys) +"brightnessctl set 10%-" + XF86MonBrightnessDown +"brightnessctl set +10%" + XF86MonBrightnessUp + +# Volume (Mac Fn+F10/F11/F12) +"pactl set-sink-mute @DEFAULT_SINK@ toggle" + XF86AudioMute +"pactl set-sink-volume @DEFAULT_SINK@ -5%" + XF86AudioLowerVolume +"pactl set-sink-volume @DEFAULT_SINK@ +5%" + XF86AudioRaiseVolume +XBK + +# ── Font antialiasing (system-wide) ────────────────────────────────── +mkdir -p "$AIROOTFS/etc/fonts" +cat > "$AIROOTFS/etc/fonts/local.conf" <<'FONTS' + + + + + true + true + hintslight + rgb + lcddefault + + + + serif + DejaVu SerifNoto Color Emoji + + + sans-serif + DejaVu SansNoto Color Emoji + + + monospace + DejaVu Sans Mono + + +FONTS + +# ── .Xresources — nice dark xterm ──────────────────────────────────── +cat > "$SKEL/.Xresources" <<'XRES' +! SkyOS dark terminal theme +XTerm*faceName: DejaVu Sans Mono +XTerm*faceSize: 11 +XTerm*renderFont: true +XTerm*antialias: true +XTerm*background: #0E1520 +XTerm*foreground: #C8D4E8 +XTerm*cursorColor: #4A90D9 +XTerm*pointerColor: #4A90D9 +XTerm*selectBackground: #2060A0 +XTerm*selectForeground: #F0F4FF +XTerm*scrollBar: false +XTerm*saveLines: 4096 +XTerm*bellIsUrgent: true +XTerm*metaSendsEscape: true +XTerm*utf8: 2 +! Colors: dark background with readable palette +XTerm*color0: #1A1E2A +XTerm*color1: #CC4444 +XTerm*color2: #44AA66 +XTerm*color3: #BBAA22 +XTerm*color4: #4488CC +XTerm*color5: #8844CC +XTerm*color6: #44AACC +XTerm*color7: #C8D4E8 +XTerm*color8: #3A4050 +XTerm*color9: #FF6666 +XTerm*color10: #66CC88 +XTerm*color11: #DDCC44 +XTerm*color12: #66AAFF +XTerm*color13: #AA66FF +XTerm*color14: #66CCEE +XTerm*color15: #EEF4FF +! Urxvt (same palette, in case rxvt-unicode is used) +URxvt*font: xft:DejaVu Sans Mono:size=11 +URxvt*background: #0E1520 +URxvt*foreground: #C8D4E8 +XRES + +mkdir -p "$SKEL/.config/dunst" +cat > "$SKEL/.config/dunst/dunstrc" <<'DUNST' +[global] + font = Sans 10 + geometry = "300x5-20+46" + transparency = 0 + frame_color = "#2A3A5A" + frame_width = 1 + separator_color = "#2A3A5A" + sort = yes + indicate_hidden = yes + shrink = no + line_height = 0 + markup = full + format = "%s\n%b" + alignment = left + show_age_threshold = 60 + word_wrap = yes + ignore_newline = no + stack_duplicates = true + hide_duplicate_count = false + padding = 10 + horizontal_padding = 10 + max_icon_size = 32 + sticky_history = yes + history_length = 20 + dmenu = /usr/bin/dmenu -p dunst + browser = firefox + always_run_script = true + title = Dunst + class = Dunst + +[urgency_low] + background = "#1C2230" + foreground = "#A0B8D0" + timeout = 5 + +[urgency_normal] + background = "#1E3050" + foreground = "#D0E0F0" + timeout = 8 + +[urgency_critical] + background = "#5A1010" + foreground = "#FFD0D0" + frame_color = "#AA2020" + timeout = 0 +DUNST + +cat > "$SKEL/.config/gtk-3.0/settings.ini" <<'GTK3' +[Settings] +gtk-theme-name=SkyOS +gtk-font-name=Sans 10 +gtk-icon-theme-name=Adwaita +gtk-cursor-theme-name=DMZ-White +GTK3 + +# ── Mac hardware: libinput touchpad config ──────────────────────────── +mkdir -p "$AIROOTFS/etc/X11/xorg.conf.d" +cat > "$AIROOTFS/etc/X11/xorg.conf.d/40-libinput.conf" <<'TOUCHPAD' +Section "InputClass" + Identifier "Apple Trackpad" + MatchIsTouchpad "on" + Driver "libinput" + Option "Tapping" "on" + Option "TappingButtonMap" "lrm" + Option "NaturalScrolling" "true" + Option "ScrollMethod" "twofinger" + Option "DisableWhileTyping" "true" + Option "ClickMethod" "clickfinger" + Option "AccelProfile" "adaptive" + Option "AccelSpeed" "0.3" +EndSection +TOUCHPAD + +# ── Keyboard: US layout, Ctrl+Alt+Backspace to kill X ──────────────── +cat > "$AIROOTFS/etc/X11/xorg.conf.d/10-keyboard.conf" <<'KEYBOARD' +Section "InputClass" + Identifier "All Keyboards" + MatchIsKeyboard "on" + Driver "libinput" + Option "XkbLayout" "us" + Option "XkbOptions" "terminate:ctrl_alt_bksp" +EndSection +KEYBOARD + +# ── Mac: load broadcom-wl at boot ───────────────────────────────────── +mkdir -p "$AIROOTFS/etc/modules-load.d" +echo "wl" > "$AIROOTFS/etc/modules-load.d/broadcom-wl.conf" + +# Blacklist conflicting Broadcom modules +mkdir -p "$AIROOTFS/etc/modprobe.d" +cat > "$AIROOTFS/etc/modprobe.d/broadcom-wl.conf" <<'MODPROBE' +blacklist b43 +blacklist b43legacy +blacklist ssb +blacklist bcm43xx +blacklist brcm80211 +blacklist brcmfmac +blacklist brcmsmac +blacklist bcma +MODPROBE + +# Let X auto-detect the GPU via the modesetting driver (kernel DRM/KMS). +# Forcing a specific driver (intel/amdgpu/etc.) breaks VMs and cross-hardware boots. + +# ── 9. profiledef.sh — archiso metadata ────────────────────────────── +cat > "$PROFILE/profiledef.sh" <<'PROFILEDEF' +#!/usr/bin/env bash +iso_name="skyos" +iso_label="SKYOS_$(date +%Y%m)" +iso_publisher="SkyOS Project" +iso_application="SkyOS — BeOS-inspired Linux Desktop" +iso_version="0.2" +install_dir="arch" +buildmodes=('iso') +# Support both UEFI (systemd-boot) and legacy BIOS (syslinux) hardware +bootmodes=( + 'bios.syslinux' + 'uefi.systemd-boot' +) +arch="x86_64" +pacman_conf="pacman.conf" +airootfs_image_type="squashfs" +airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '5' '-b' '1M') +bootstrap_tarball_compression=('zstd' '-c' '-T0' '--auto-threads=logical' '--long' '-19') +file_permissions=( + ["/etc/shadow"]="0:0:400" + ["/etc/skel/.xinitrc"]="0:0:755" + ["/root/customize_airootfs.sh"]="0:0:755" + ["/usr/local/bin/skywm"]="0:0:755" + ["/usr/local/bin/skydeskbar"]="0:0:755" + ["/usr/local/bin/skylauncher"]="0:0:755" + ["/usr/local/bin/skywallpaper"]="0:0:755" + ["/usr/local/bin/skyinstall"]="0:0:755" +) +PROFILEDEF + +# ── 10. Boot loader configs ─────────────────────────────────────────── + +# systemd-boot entry (UEFI) +mkdir -p "$PROFILE/efiboot/loader/entries" +cat > "$PROFILE/efiboot/loader/loader.conf" <<'LOADER' +default skyos.conf +timeout 5 +editor no +LOADER + +cat > "$PROFILE/efiboot/loader/entries/skyos.conf" <<'ENTRY' +title SkyOS +linux /arch/boot/x86_64/vmlinuz-linux +initrd /arch/boot/x86_64/initramfs-linux.img +options archisobasedir=arch archisolabel=%ARCHISO_LABEL% quiet loglevel=3 +ENTRY + + +# ── 10. Auto-login to tty1 and startx ──────────────────────────────── +mkdir -p "$AIROOTFS/etc/systemd/system/getty@tty1.service.d" +cat > "$AIROOTFS/etc/systemd/system/getty@tty1.service.d/autologin.conf" <<'AUTOLOGIN' +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin sky --noclear %I $TERM +AUTOLOGIN + +# bash_profile: startx automatically on tty1 (no exec — keep shell on failure) +cat > "$SKEL/.bash_profile" <<'BASH_PROFILE' +if [[ -z $DISPLAY && $XDG_VTNR -eq 1 ]]; then + startx 2>&1 | tee /tmp/x-session.log +fi +BASH_PROFILE + +# ── 11. customize_airootfs.sh — runs inside the chroot ─────────────── +cat > "$PROFILE/airootfs/root/customize_airootfs.sh" <<'CUSTOMIZE' +#!/usr/bin/env bash +set -euo pipefail + +# Locale +sed -i 's/#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen +locale-gen +echo "LANG=en_US.UTF-8" > /etc/locale.conf + +# Timezone +ln -sf /usr/share/zoneinfo/UTC /etc/localtime + +# Create user 'sky' (live session user) +useradd -m -G wheel,audio,video,storage,input -s /bin/bash sky 2>/dev/null || true +echo "sky:skyos" | chpasswd + +# sudoers +echo "%wheel ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/wheel +chmod 440 /etc/sudoers.d/wheel + +# Enable services +systemctl enable NetworkManager +systemctl enable bluetooth 2>/dev/null || true +systemctl enable fstrim.timer 2>/dev/null || true + +# MIME / default app associations +if command -v xdg-mime &>/dev/null; then + # Run as the sky user (xdg-mime needs $HOME) + sudo -u sky HOME=/home/sky XDG_DATA_HOME=/home/sky/.local/share \ + xdg-mime default firefox.desktop x-scheme-handler/http \ + x-scheme-handler/https \ + text/html 2>/dev/null || true + sudo -u sky HOME=/home/sky XDG_DATA_HOME=/home/sky/.local/share \ + xdg-mime default evince.desktop application/pdf 2>/dev/null || true + sudo -u sky HOME=/home/sky XDG_DATA_HOME=/home/sky/.local/share \ + xdg-mime default viewnior.desktop image/jpeg image/png \ + image/gif image/webp 2>/dev/null || true + sudo -u sky HOME=/home/sky XDG_DATA_HOME=/home/sky/.local/share \ + xdg-mime default mpv.desktop video/mp4 video/mkv \ + audio/mpeg audio/flac 2>/dev/null || true + sudo -u sky HOME=/home/sky XDG_DATA_HOME=/home/sky/.local/share \ + xdg-mime default mousepad.desktop text/plain 2>/dev/null || true +fi +update-desktop-database 2>/dev/null || true + +# Fontconfig cache +fc-cache -f 2>/dev/null || true + +# xscreensaver config (screen lock) +mkdir -p /etc/skel +cat > /etc/skel/.xscreensaver <<'SS' +mode: blank +timeout: 0:10:00 +lock: True +lockTimeout: 0:00:00 +dpmsEnabled: True +dpmsStandby: 0:10:00 +dpmsSuspend: 0:20:00 +dpmsOff: 0:30:00 +SS + +# Copy skeleton to sky's home (ensure xinitrc is present) +cp -r /etc/skel/. /home/sky/ 2>/dev/null || true +chown -R sky:sky /home/sky + +# Ensure SkyOS binaries are executable (permissions can be lost through squashfs build) +chmod +x /usr/local/bin/skywm \ + /usr/local/bin/skydeskbar \ + /usr/local/bin/skylauncher \ + /usr/local/bin/skywallpaper \ + /usr/local/bin/skyinstall 2>/dev/null || true +[ -f /usr/local/bin/skyinstall-gui ] && chmod +x /usr/local/bin/skyinstall-gui + +echo "SkyOS customization complete." +CUSTOMIZE +chmod 755 "$PROFILE/airootfs/root/customize_airootfs.sh" + +# ── 12. Build the ISO ───────────────────────────────────────────────── +echo ">>> Building ISO (this takes 5-15 minutes)..." +mkdir -p "$DIST" "$WORK" + +mkarchiso -v \ + -w "$WORK" \ + -o "$DIST" \ + "$PROFILE" + +echo "" +echo "════════════════════════════════════════" +echo " Build complete!" +find "$DIST" -name "*.iso" -exec echo " ISO: {}" \; +echo "════════════════════════════════════════" diff --git a/iso/mac-install-notes.md b/iso/mac-install-notes.md new file mode 100644 index 0000000..8fa9ff9 --- /dev/null +++ b/iso/mac-install-notes.md @@ -0,0 +1,79 @@ +# Installing SkyOS on an Intel Mac + +## Before you start + +- Intel Mac (any MacBook/MacBook Pro with Intel CPU — 2006–2020) +- USB drive ≥ 4 GB +- The SkyOS ISO + +## Step 1 — Boot from USB + +Intel Macs use EFI + rEFInd (included in the ISO) to boot Linux. + +1. Hold **Option (⌥)** at startup to see the boot picker. +2. Select the SkyOS USB drive (shows as "EFI Boot"). + +## Step 2 — Partition the disk + +In the live environment, run `cfdisk /dev/sda` (replace with your disk): + +| Partition | Size | Type | Mount | +|-----------|-------------|------------------|-----------| +| sda1 | 512 MB | EFI System | /boot/efi | +| sda2 | 4 GB | Linux swap | swap | +| sda3 | remainder | Linux filesystem | / | + +Format: +```bash +mkfs.fat -F32 /dev/sda1 +mkswap /dev/sda2 +mkfs.ext4 -L SkyOS /dev/sda3 +``` + +## Step 3 — Install + +```bash +mount /dev/sda3 /mnt +mkdir -p /mnt/boot/efi +mount /dev/sda1 /mnt/boot/efi +swapon /dev/sda2 + +pacstrap /mnt base linux linux-firmware ... # (handled by build-base.sh) +genfstab -U /mnt >> /mnt/etc/fstab +``` + +## Step 4 — rEFInd for Mac EFI + +Intel Macs need rEFInd instead of GRUB for reliable EFI booting: + +```bash +arch-chroot /mnt +refind-install +``` + +rEFInd auto-detects the kernel; our config at `/boot/EFI/refind/refind.conf` +points it at the right initramfs. + +## Step 5 — First boot + +Log in as `sky` (password: `skyos` — change it!), then: + +```bash +startx +``` + +The SkyWM desktop launches with the Deskbar in the top-right corner. + +## Intel Mac hardware notes + +| Hardware | Status | Notes | +|-----------------|----------|----------------------------------------| +| WiFi (Broadcom) | Works | `broadcom-wl-dkms` DKMS driver | +| Intel iGPU | Works | `xf86-video-intel` or modesetting | +| Discrete GPU | Partial | AMD: works; Nvidia: use `nouveau` | +| Trackpad | Works | `xf86-input-libinput` with gestures | +| Audio | Works | `alsa-utils` + `snd_hda_intel` | +| Suspend/Resume | Mostly | `echo mem > /sys/power/state` | +| Keyboard backlit| Works | `/sys/class/leds/` brightness control | +| Webcam | Works | UVC driver built into kernel | +| Thunderbolt | Partial | TB1/2: limited; TB3+: better | diff --git a/launcher/Makefile b/launcher/Makefile new file mode 100644 index 0000000..ac0303f --- /dev/null +++ b/launcher/Makefile @@ -0,0 +1,23 @@ +CC = gcc + +PKG_XFT_C := $(shell pkg-config --cflags xft x11 2>/dev/null) +PKG_XFT_L := $(shell pkg-config --libs xft x11 2>/dev/null) +ifeq ($(PKG_XFT_C),) + PKG_XFT_C := -I/opt/homebrew/include + PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_XFT_C) +LDFLAGS = $(PKG_XFT_L) + +SRC = src/launcher.c +BIN = skylauncher + +.PHONY: all clean install +all: $(BIN) +$(BIN): $(SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) +clean: + rm -f $(BIN) diff --git a/launcher/src/launcher.c b/launcher/src/launcher.c new file mode 100644 index 0000000..ecf60b5 --- /dev/null +++ b/launcher/src/launcher.c @@ -0,0 +1,407 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── dimensions ─────────────────────────────────────────────────────── */ +#define WIN_W 580 +#define WIN_H 420 +#define HDR_H 50 /* search header */ +#define TAB_H 34 /* category tab strip */ +#define CONTENT_Y (HDR_H + TAB_H) +#define PAD 12 /* grid outer margin */ +#define CARD_W 96 +#define CARD_H 84 +#define CARD_GAP 8 +#define COLS 5 +#define ICON_SZ 38 + +/* grid left edge */ +#define GX0 ((WIN_W - (COLS * CARD_W + (COLS-1) * CARD_GAP)) / 2) +#define GY0 (CONTENT_Y + PAD) + +/* ── palette ─────────────────────────────────────────────────────────── */ +#define C_BG 0x0E1220 +#define C_HDR 0x090C18 +#define C_BORDER 0x192236 +#define C_ACCENT 0x2258B0 +#define C_TAB_SEL 0x141E30 +#define C_CARD_HOV 0x131C2E +#define C_CARD_BD 0x1E2E48 +#define C_SEARCH 0x131A28 +#define C_SEARCH_BD 0x1C2C44 +#define C_TEXT 0x8AAAC8 +#define C_TEXT_HOV 0xE8F0FF +#define C_TEXT_DIM 0x344A62 +#define C_TAB_TXT 0x486070 +#define C_TAB_TSEL 0xA8C4E8 + +static const unsigned long ICON_BG[] = { + 0x1A4D88, 0x166C3C, 0x6C3018, + 0x4A1A80, 0x126070, 0x6C1A38, + 0x365218, 0x183460, +}; +#define NICON 8 + +#define FONT_UI "Sans:size=9" +#define FONT_LOGO "Sans:size=11:weight=bold" +#define FONT_ICON "Sans:size=13:weight=bold" +#define MAX_FLT 64 + +typedef struct { const char *name, *cmd, *cat; } App; +static const App APPS[] = { + { "Firefox", "firefox &", "Internet" }, + { "Email", "firefox https://mail.google.com &", "Internet" }, + { "Media Player", "mpv --player-operation-mode=pseudo-gui &", "Media" }, + { "Images", "viewnior &", "Media" }, + { "OnlyOffice", "onlyoffice-desktopeditors &", "Office" }, + { "PDF Viewer", "evince &", "Office" }, + { "Terminal", "xterm &", "System" }, + { "Files", "thunar &", "System" }, + { "Text Editor", "mousepad &", "System" }, + { "System Info", "xterm -e 'fastfetch; read' &", "System" }, + { "Screen Lock", "xscreensaver-command -lock &", "System" }, + { "Install", "xterm -e 'sudo skyinstall' &", "System" }, + { "Display", "arandr &", "Settings" }, + { "Audio", "pavucontrol &", "Settings" }, + { "Network", "nm-connection-editor &", "Settings" }, + { "Bluetooth", "blueman-manager &", "Settings" }, +}; +#define NAPP ((int)(sizeof(APPS)/sizeof(APPS[0]))) + +static const char *CATS[] = { + "All", "Internet", "Media", "Office", "System", "Settings" +}; +#define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0]))) + +typedef struct { + Display *dpy; int screen; + Window root, win; GC gc; + Colormap cmap; Visual *visual; + XftFont *font_ui, *font_logo, *font_icon; + int sel_cat, hov; + char filter[MAX_FLT]; + int filter_len; +} L; + +/* ── helpers ─────────────────────────────────────────────────────────── */ +static unsigned long mkc(L *l, unsigned long rgb) { + XColor c; + c.red = ((rgb>>16)&0xFF)*257; + c.green = ((rgb>> 8)&0xFF)*257; + c.blue = ((rgb) &0xFF)*257; + c.flags = DoRed|DoGreen|DoBlue; + XAllocColor(l->dpy, l->cmap, &c); + return c.pixel; +} +static void xftc(L *l, unsigned long rgb, XftColor *out) { + XRenderColor r = { + ((rgb>>16)&0xFF)*257, ((rgb>>8)&0xFF)*257, + ((rgb) &0xFF)*257, 0xFFFF + }; + XftColorAllocValue(l->dpy, l->visual, l->cmap, &r, out); +} +static void ds(L *l, XftDraw *xd, XftFont *f, unsigned long rgb, + int x, int y, const char *s) { + XftColor c; xftc(l, rgb, &c); + XGlyphInfo e; + XftTextExtentsUtf8(l->dpy, f, (FcChar8*)s, strlen(s), &e); + XftDrawStringUtf8(xd, &c, f, x, y + e.height - e.y, (FcChar8*)s, strlen(s)); + XftColorFree(l->dpy, l->visual, l->cmap, &c); +} +static void ds_ctr(L *l, XftDraw *xd, XftFont *f, unsigned long rgb, + int bx, int by, int bw, int bh, const char *s) { + XGlyphInfo e; + XftTextExtentsUtf8(l->dpy, f, (FcChar8*)s, strlen(s), &e); + int tx = bx + (bw - (int)e.width) / 2; + int ty = by + (bh - (int)e.height) / 2; + ds(l, xd, f, rgb, tx, ty, s); +} + +/* ── query ───────────────────────────────────────────────────────────── */ +static int nvis(L *l, int out[NAPP]) { + int n = 0; + const char *cat = CATS[l->sel_cat]; + for (int i = 0; i < NAPP; i++) { + if (strcmp(cat,"All")!=0 && strcmp(APPS[i].cat,cat)!=0) continue; + if (l->filter_len > 0) { + char nm[64], fl[MAX_FLT]; + snprintf(nm, sizeof(nm), "%s", APPS[i].name); + snprintf(fl, sizeof(fl), "%s", l->filter); + for (char *p=nm; *p; p++) if (*p>='A'&&*p<='Z') *p+=32; + for (char *p=fl; *p; p++) if (*p>='A'&&*p<='Z') *p+=32; + if (!strstr(nm, fl)) continue; + } + out[n++] = i; + } + return n; +} + +/* tab widths — measured once per draw via font metrics */ +static void tab_layout(L *l, int widths[NCAT], int *total) { + *total = 0; + for (int i = 0; i < NCAT; i++) { + XGlyphInfo e; + XftTextExtentsUtf8(l->dpy, l->font_ui, (FcChar8*)CATS[i], strlen(CATS[i]), &e); + widths[i] = (int)e.width + 22; + *total += widths[i]; + } +} + +/* hit: which category tab contains point (x,y)? -1 = none */ +static int tab_at(L *l, int x, int y) { + if (y < HDR_H || y >= CONTENT_Y) return -1; + int w[NCAT], total; tab_layout(l, w, &total); + int tx = (WIN_W - total) / 2; + for (int i = 0; i < NCAT; i++) { + if (x >= tx && x < tx + w[i]) return i; + tx += w[i]; + } + return -1; +} + +/* hit: which card index (into vis[]) is at point? -1 = none */ +static int card_at(int x, int y) { + if (y < CONTENT_Y) return -1; + int lx = x - GX0; + int ly = y - GY0; + if (lx < 0 || ly < 0) return -1; + int col = lx / (CARD_W + CARD_GAP); + int row = ly / (CARD_H + CARD_GAP); + if (col < 0 || col >= COLS || row < 0) return -1; + /* reject click in gap between cards */ + int cx = col * (CARD_W + CARD_GAP); + int cy = row * (CARD_H + CARD_GAP); + if (lx - cx >= CARD_W || ly - cy >= CARD_H) return -1; + return row * COLS + col; +} + +/* ── draw ─────────────────────────────────────────────────────────────── */ +static void draw(L *l) { + XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap); + + /* background */ + XSetForeground(l->dpy, l->gc, mkc(l, C_BG)); + XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, WIN_H); + + /* header */ + XSetForeground(l->dpy, l->gc, mkc(l, C_HDR)); + XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, HDR_H); + + /* accent top stripe */ + XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT)); + XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, 2); + + /* logo */ + ds(l, xd, l->font_logo, C_ACCENT, 14, 13, "SkyOS"); + + /* search box */ + { + int sx = 86, sy = 10, sw = WIN_W - 86 - 14, sh = 30; + XSetForeground(l->dpy, l->gc, mkc(l, C_SEARCH)); + XFillRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh); + XSetForeground(l->dpy, l->gc, mkc(l, C_SEARCH_BD)); + XDrawRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh); + + if (l->filter_len == 0) { + ds(l, xd, l->font_ui, C_TEXT_DIM, sx+8, sy+7, "Search..."); + } else { + char buf[MAX_FLT+2]; + snprintf(buf, sizeof(buf), "%s|", l->filter); + ds(l, xd, l->font_ui, C_TEXT_HOV, sx+8, sy+7, buf); + } + } + + /* separator below header */ + XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER)); + XDrawLine(l->dpy, l->win, l->gc, 0, HDR_H, WIN_W, HDR_H); + + /* category tabs */ + int tw[NCAT], ttotal; tab_layout(l, tw, &ttotal); + int tx = (WIN_W - ttotal) / 2; + for (int i = 0; i < NCAT; i++) { + if (i == l->sel_cat) { + XSetForeground(l->dpy, l->gc, mkc(l, C_TAB_SEL)); + XFillRectangle(l->dpy, l->win, l->gc, tx, HDR_H+1, tw[i], TAB_H-1); + /* accent underline */ + XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT)); + XFillRectangle(l->dpy, l->win, l->gc, tx+5, HDR_H+TAB_H-2, tw[i]-10, 2); + } + unsigned long tc = (i == l->sel_cat) ? C_TAB_TSEL : C_TAB_TXT; + ds_ctr(l, xd, l->font_ui, tc, tx, HDR_H, tw[i], TAB_H, CATS[i]); + tx += tw[i]; + } + + /* separator below tabs */ + XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER)); + XDrawLine(l->dpy, l->win, l->gc, 0, CONTENT_Y, WIN_W, CONTENT_Y); + + /* app grid */ + int vis[NAPP]; + int nv = nvis(l, vis); + + if (nv == 0) { + ds_ctr(l, xd, l->font_ui, C_TEXT_DIM, + 0, CONTENT_Y, WIN_W, WIN_H - CONTENT_Y, "No apps found"); + } else { + for (int i = 0; i < nv; i++) { + int col = i % COLS; + int row = i / COLS; + int cx = GX0 + col * (CARD_W + CARD_GAP); + int cy = GY0 + row * (CARD_H + CARD_GAP); + if (cy + CARD_H > WIN_H) break; /* clip to window */ + int hover = (i == l->hov); + + /* card hover highlight */ + if (hover) { + XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_HOV)); + XFillRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W, CARD_H); + XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_BD)); + XDrawRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W-1, CARD_H-1); + } + + /* icon square */ + int app_i = vis[i]; + unsigned int h = 0; + for (const char *p = APPS[app_i].name; *p; p++) h = h*31 + (unsigned char)*p; + unsigned long ibg = ICON_BG[h % NICON]; + int ix = cx + (CARD_W - ICON_SZ) / 2; + int iy = cy + 10; + XSetForeground(l->dpy, l->gc, mkc(l, ibg)); + XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ); + + /* icon letter */ + char letter[2] = { APPS[app_i].name[0], '\0' }; + if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; + ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter); + + /* label */ + char lbl[14]; + snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name); + unsigned long tc = hover ? C_TEXT_HOV : C_TEXT; + ds_ctr(l, xd, l->font_ui, tc, + cx, iy + ICON_SZ + 4, CARD_W, CARD_H - ICON_SZ - 18, lbl); + } + } + + /* outer border */ + XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER)); + XDrawRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W-1, WIN_H-1); + + XftDrawDestroy(xd); +} + +/* ── event handlers ──────────────────────────────────────────────────── */ +static void launch(L *l, int hov) { + int vis[NAPP]; + int nv = nvis(l, vis); + if (hov >= 0 && hov < nv) { + system(APPS[vis[hov]].cmd); + XDestroyWindow(l->dpy, l->win); + XCloseDisplay(l->dpy); + exit(0); + } +} + +static void on_key(L *l, XKeyEvent *e) { + KeySym sym = XLookupKeysym(e, 0); + if (sym == XK_Escape) { + XDestroyWindow(l->dpy, l->win); + XCloseDisplay(l->dpy); + exit(0); + } + if (sym == XK_Return) { launch(l, l->hov); return; } + if (sym == XK_BackSpace && l->filter_len > 0) { + l->filter[--l->filter_len] = '\0'; + l->hov = -1; draw(l); return; + } + char buf[8] = {0}; + int len = XLookupString(e, buf, sizeof(buf)-1, &sym, NULL); + if (len > 0 && buf[0] >= 0x20 && buf[0] < 0x7F && l->filter_len < MAX_FLT-1) { + l->filter[l->filter_len++] = buf[0]; + l->filter[l->filter_len] = '\0'; + l->sel_cat = 0; l->hov = -1; + draw(l); + } +} + +/* ── main ─────────────────────────────────────────────────────────────── */ +int main(void) { + L l = {0}; l.sel_cat = 0; l.hov = -1; + + l.dpy = XOpenDisplay(NULL); + if (!l.dpy) { fprintf(stderr, "skylauncher: cannot open display\n"); return 1; } + l.screen = DefaultScreen(l.dpy); + l.root = RootWindow(l.dpy, l.screen); + l.cmap = DefaultColormap(l.dpy, l.screen); + l.visual = DefaultVisual(l.dpy, l.screen); + + l.font_ui = XftFontOpenName(l.dpy, l.screen, FONT_UI); + l.font_logo = XftFontOpenName(l.dpy, l.screen, FONT_LOGO); + l.font_icon = XftFontOpenName(l.dpy, l.screen, FONT_ICON); + if (!l.font_ui) l.font_ui = XftFontOpenName(l.dpy, l.screen, "fixed:size=9"); + if (!l.font_logo) l.font_logo = l.font_ui; + if (!l.font_icon) l.font_icon = l.font_ui; + + int sw = DisplayWidth(l.dpy, l.screen); + int sh = DisplayHeight(l.dpy, l.screen); + + XSetWindowAttributes swa; + swa.background_pixel = mkc(&l, C_BG); + swa.override_redirect = True; + swa.event_mask = ExposureMask | ButtonPressMask + | PointerMotionMask | LeaveWindowMask | KeyPressMask; + + l.win = XCreateWindow( + l.dpy, l.root, + (sw - WIN_W) / 2, (sh - WIN_H) / 2, WIN_W, WIN_H, 0, + DefaultDepth(l.dpy, l.screen), InputOutput, l.visual, + CWBackPixel | CWOverrideRedirect | CWEventMask, &swa); + l.gc = XCreateGC(l.dpy, l.win, 0, NULL); + + XMapRaised(l.dpy, l.win); + XGrabKeyboard(l.dpy, l.win, True, GrabModeAsync, GrabModeAsync, CurrentTime); + XSync(l.dpy, False); + + for (;;) { + XEvent e; + XNextEvent(l.dpy, &e); + switch (e.type) { + case Expose: + if (e.xexpose.count == 0) draw(&l); + break; + case ButtonPress: + { + int t = tab_at(&l, e.xbutton.x, e.xbutton.y); + if (t >= 0) { + l.sel_cat = t; l.hov = -1; draw(&l); + } else { + int idx = card_at(e.xbutton.x, e.xbutton.y); + launch(&l, idx); + } + } + break; + case MotionNotify: + { + int idx = card_at(e.xmotion.x, e.xmotion.y); + int vis[NAPP]; int nv = nvis(&l, vis); + int prev = l.hov; + l.hov = (idx >= 0 && idx < nv) ? idx : -1; + if (l.hov != prev) draw(&l); + } + break; + case LeaveNotify: + if (l.hov != -1) { l.hov = -1; draw(&l); } + break; + case KeyPress: + on_key(&l, &e.xkey); + break; + } + } + return 0; +} diff --git a/themes/SkyOS/gtk-2.0/gtkrc b/themes/SkyOS/gtk-2.0/gtkrc new file mode 100644 index 0000000..74c0023 --- /dev/null +++ b/themes/SkyOS/gtk-2.0/gtkrc @@ -0,0 +1,72 @@ +# SkyOS GTK2 Theme — BeOS/SkyOS tan palette +# Covers legacy GTK2 apps (GIMP, older system tools) + +gtk-color-scheme = "bg_color:#D8CEAA\nfg_color:#100800\nbase_color:#F5F0E0\ntext_color:#100800\nselected_bg_color:#4A6888\nselected_fg_color:#FFFFFF\ntooltip_bg_color:#FFFCE0\ntooltip_fg_color:#100800" + +style "default" { + bg[NORMAL] = @bg_color + bg[PRELIGHT] = "#E8DEC0" + bg[ACTIVE] = "#B0A888" + bg[SELECTED] = @selected_bg_color + bg[INSENSITIVE] = @bg_color + + fg[NORMAL] = @fg_color + fg[PRELIGHT] = @fg_color + fg[ACTIVE] = @fg_color + fg[SELECTED] = @selected_fg_color + fg[INSENSITIVE] = "#7A7060" + + base[NORMAL] = @base_color + base[PRELIGHT] = @base_color + base[ACTIVE] = "#D0C8A8" + base[SELECTED] = @selected_bg_color + base[INSENSITIVE] = @bg_color + + text[NORMAL] = @text_color + text[PRELIGHT] = @text_color + text[ACTIVE] = @text_color + text[SELECTED] = @selected_fg_color + text[INSENSITIVE] = "#7A7060" + + GtkWidget::focus-line-width = 1 + GtkButton::default-border = { 1, 1, 1, 1 } + GtkScrollbar::min-slider-length = 20 +} +class "GtkWidget" style "default" + +style "menubar" { + bg[NORMAL] = "#C8BE9A" + bg[PRELIGHT] = "#4A6888" + fg[PRELIGHT] = "#FFFFFF" +} +class "GtkMenuBar" style "menubar" + +style "menu" { + bg[NORMAL] = "#D8CEAA" + bg[PRELIGHT] = "#4A6888" + fg[NORMAL] = "#100800" + fg[PRELIGHT] = "#FFFFFF" +} +class "GtkMenu" style "menu" +class "GtkMenuItem" style "menu" + +style "tooltip" { + bg[NORMAL] = @tooltip_bg_color + fg[NORMAL] = @tooltip_fg_color +} +class "GtkTooltip" style "tooltip" + +style "entry" { + bg[NORMAL] = @base_color + bg[SELECTED] = @selected_bg_color + fg[NORMAL] = @text_color +} +class "GtkEntry" style "entry" + +gtk-theme-name = "SkyOS" +gtk-icon-theme-name = "Adwaita" +gtk-font-name = "Sans 10" +gtk-cursor-theme-name = "DMZ-White" +gtk-button-images = 0 +gtk-menu-images = 0 +gtk-toolbar-style = GTK_TOOLBAR_ICONS diff --git a/themes/SkyOS/gtk-3.0/gtk.css b/themes/SkyOS/gtk-3.0/gtk.css new file mode 100644 index 0000000..5b6fc12 --- /dev/null +++ b/themes/SkyOS/gtk-3.0/gtk.css @@ -0,0 +1,229 @@ +/* SkyOS GTK3 Theme — BeOS/SkyOS tan palette */ + +/* ── variables ───────────────────────────────────────────────────────── */ +@define-color bg_color #D8CEAA; +@define-color bg_dark #C8BE9A; +@define-color bg_darker #B0A888; +@define-color bg_light #E8DEC0; +@define-color border_light #F0E8D0; +@define-color border_dark #6B5F50; +@define-color text_color #100800; +@define-color text_disabled #7A7060; +@define-color selected_bg #4A6888; +@define-color selected_fg #FFFFFF; +@define-color accent #4A6888; +@define-color button_bg #C8BE9A; +@define-color button_hover #D8D0B8; +@define-color button_active #A89E88; +@define-color entry_bg #F5F0E0; +@define-color tooltip_bg #FFFCE0; +@define-color link_color #2244AA; + +/* ── window / base ───────────────────────────────────────────────────── */ +* { + font-family: "Sans"; + font-size: 10pt; + color: @text_color; + outline: none; + -gtk-icon-style: regular; +} + +window, .background { + background-color: @bg_color; + color: @text_color; +} + +/* ── header bars ─────────────────────────────────────────────────────── */ +headerbar, .titlebar { + background: linear-gradient(to bottom, @bg_light, @bg_dark); + border-bottom: 1px solid @border_dark; + color: @text_color; + padding: 4px 8px; + min-height: 28px; +} +headerbar .title { font-weight: bold; } + +/* ── buttons ─────────────────────────────────────────────────────────── */ +button { + background: linear-gradient(to bottom, @border_light, @button_bg); + border: 1px solid @border_dark; + border-radius: 2px; + padding: 3px 10px; + color: @text_color; + box-shadow: inset 1px 1px 0 @border_light, inset -1px -1px 0 @border_dark; +} +button:hover { + background: linear-gradient(to bottom, #F8F0D8, @button_hover); +} +button:active, button:checked { + background: linear-gradient(to bottom, @button_active, @bg_darker); + box-shadow: inset 1px 1px 0 @border_dark, inset -1px -1px 0 @border_light; +} +button:disabled { + color: @text_disabled; + background: @bg_color; +} +button.suggested-action { + background: linear-gradient(to bottom, #5A7898, @accent); + color: white; + border-color: #3A5878; +} +button.destructive-action { + background: linear-gradient(to bottom, #DD5555, #BB3333); + color: white; + border-color: #993333; +} + +/* ── menus ───────────────────────────────────────────────────────────── */ +menu, .menu, .context-menu { + background-color: @bg_color; + border: 1px solid @border_dark; + padding: 2px 0; +} +menuitem { + padding: 4px 12px; + color: @text_color; +} +menuitem:hover, menuitem:selected { + background-color: @selected_bg; + color: @selected_fg; +} +menuitem:disabled { color: @text_disabled; } + +separator, menuitem > separator { + background-color: @border_dark; + min-height: 1px; + margin: 3px 6px; +} + +/* ── entries (text input) ────────────────────────────────────────────── */ +entry { + background-color: @entry_bg; + border: 1px solid @border_dark; + border-radius: 1px; + padding: 3px 6px; + color: @text_color; + box-shadow: inset 1px 1px 2px rgba(0,0,0,0.12); +} +entry:focus { + border-color: @accent; + box-shadow: inset 1px 1px 2px rgba(0,0,0,0.12), 0 0 0 2px rgba(74,104,136,0.25); +} +entry:disabled { background-color: @bg_color; color: @text_disabled; } + +/* ── lists & trees ───────────────────────────────────────────────────── */ +treeview, listview, .view { + background-color: @entry_bg; + color: @text_color; +} +treeview:selected, listview row:selected, .view:selected { + background-color: @selected_bg; + color: @selected_fg; +} +treeview header button { + background: linear-gradient(to bottom, @bg_light, @bg_dark); + border-right: 1px solid @border_dark; + border-bottom: 1px solid @border_dark; + border-radius: 0; + box-shadow: none; +} + +/* ── notebooks (tabs) ────────────────────────────────────────────────── */ +notebook > header { + background-color: @bg_darker; + border-bottom: 1px solid @border_dark; +} +notebook > header tab { + background-color: @bg_dark; + border: 1px solid @border_dark; + border-bottom: none; + padding: 4px 12px; + margin-right: 2px; + border-radius: 2px 2px 0 0; +} +notebook > header tab:checked { + background-color: @bg_color; + border-bottom: 1px solid @bg_color; + margin-bottom: -1px; +} +notebook > stack { background-color: @bg_color; } + +/* ── scrollbars ──────────────────────────────────────────────────────── */ +scrollbar { + background-color: @bg_darker; + border: 1px solid @border_dark; +} +scrollbar slider { + background-color: @button_bg; + border: 1px solid @border_dark; + border-radius: 2px; + min-width: 12px; + min-height: 12px; +} +scrollbar slider:hover { background-color: @button_hover; } +scrollbar button { background-color: @bg_dark; } + +/* ── toolbars ────────────────────────────────────────────────────────── */ +toolbar, .toolbar { + background: linear-gradient(to bottom, @bg_light, @bg_dark); + border-bottom: 1px solid @border_dark; + padding: 2px; +} + +/* ── statusbar ───────────────────────────────────────────────────────── */ +statusbar { + background-color: @bg_darker; + border-top: 1px solid @border_dark; + padding: 2px 6px; + font-size: 9pt; +} + +/* ── tooltips ────────────────────────────────────────────────────────── */ +tooltip { + background-color: @tooltip_bg; + border: 1px solid @border_dark; + color: @text_color; + padding: 4px 8px; + border-radius: 0; +} + +/* ── progress bars ───────────────────────────────────────────────────── */ +progressbar trough { + background-color: @bg_darker; + border: 1px solid @border_dark; + border-radius: 1px; +} +progressbar progress { + background-color: @accent; + border-radius: 1px; +} + +/* ── checkboxes & radio buttons ──────────────────────────────────────── */ +checkbutton check, radiobutton radio { + background-color: @entry_bg; + border: 1px solid @border_dark; + min-width: 14px; + min-height: 14px; +} +checkbutton check:checked { + background-color: @accent; + color: white; +} +radiobutton radio:checked { background-color: @accent; } + +/* ── spin buttons ────────────────────────────────────────────────────── */ +spinbutton { background-color: @entry_bg; border: 1px solid @border_dark; } +spinbutton button { border-radius: 0; padding: 0 4px; } + +/* ── combo boxes ─────────────────────────────────────────────────────── */ +combobox button { + background: linear-gradient(to bottom, @border_light, @button_bg); +} + +/* ── file chooser ────────────────────────────────────────────────────── */ +.sidebar { background-color: @bg_darker; } +.sidebar row:selected { background-color: @selected_bg; color: @selected_fg; } + +/* ── links ───────────────────────────────────────────────────────────── */ +*:link { color: @link_color; } +*:visited { color: #6644AA; } diff --git a/themes/SkyOS/index.theme b/themes/SkyOS/index.theme new file mode 100644 index 0000000..856d025 --- /dev/null +++ b/themes/SkyOS/index.theme @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=X-GNOME-Metatheme +Name=SkyOS +Comment=BeOS/SkyOS-inspired warm tan desktop theme +Encoding=UTF-8 + +[X-GNOME-Metatheme] +GtkTheme=SkyOS +MetacityTheme=SkyOS +IconTheme=Adwaita +CursorTheme=DMZ-White +ButtonLayout=close:minimize,maximize diff --git a/themes/picom.conf b/themes/picom.conf new file mode 100644 index 0000000..7871205 --- /dev/null +++ b/themes/picom.conf @@ -0,0 +1,63 @@ +# SkyOS picom compositor config +# Goal: subtle window shadows, no transparency, fast rendering. +# BeOS/SkyOS didn't have transparency — we keep it clean. + +backend = "xrender"; # xrender works on all hardware; switch to glx for performance +vsync = false; + +# ── shadows ─────────────────────────────────────────────────────────────── +shadow = true; +shadow-radius = 8; +shadow-offset-x = 4; +shadow-offset-y = 4; +shadow-opacity = 0.30; +shadow-color = "#000000"; + +# Don't shadow the deskbar, menus, or tooltips +shadow-exclude = [ + "class_g = 'skydeskbar'", + "class_g = 'skylauncher'", + "_NET_WM_WINDOW_TYPE@:a *= 'DOCK'", + "_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'", + "_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'", + "_NET_WM_WINDOW_TYPE@:a *= 'POPUP_MENU'", + "_NET_WM_WINDOW_TYPE@:a *= 'DROPDOWN_MENU'", + "override_redirect = 1 && !WM_CLASS@:s" +]; + +# ── fading ──────────────────────────────────────────────────────────────── +fading = true; +fade-in-step = 0.06; +fade-out-step = 0.06; +fade-delta = 8; # ms between steps — snappy, not sluggish +no-fading-openclose = false; + +# ── opacity ─────────────────────────────────────────────────────────────── +# No transparency — BeOS aesthetic is solid, crisp windows. +inactive-opacity = 1.0; +active-opacity = 1.0; +frame-opacity = 1.0; +inactive-opacity-override = false; + +# ── blur ───────────────────────────────────────────────────────────────── +# Disabled — keep it clean. +blur-method = "none"; + +# ── focus & refresh ─────────────────────────────────────────────────────── +detect-client-opacity = true; +detect-transient = true; +detect-client-leader = true; +use-ewmh-active-win = true; +mark-wmwin-focused = true; +mark-ovredir-focused = false; + +# ── window type rules ───────────────────────────────────────────────────── +wintypes: +{ + tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; }; + dock = { shadow = false; clip-shadow-above = true; }; + dnd = { shadow = false; }; + popup_menu = { opacity = 1.0; shadow = false; }; + dropdown_menu = { opacity = 1.0; shadow = false; }; + notification = { shadow = false; }; +}; diff --git a/themes/skywallpaper b/themes/skywallpaper new file mode 100644 index 0000000..4eb5b7a --- /dev/null +++ b/themes/skywallpaper @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# skywallpaper — set or restore the SkyOS desktop wallpaper +# +# Usage: +# skywallpaper # restore saved wallpaper (called from xinitrc) +# skywallpaper /path/to/image.jpg # set new wallpaper and save it +# skywallpaper --color "#6B8FB5" # set solid color +# skywallpaper --reset # reset to default blue-gray solid color +# +# Wallpaper preference is stored in ~/.config/skyos/wallpaper + +CONF_DIR="$HOME/.config/skyos" +CONF_FILE="$CONF_DIR/wallpaper" +DEFAULT_COLOR="#1A2840" +WALLPAPER_DIR="/usr/share/skyos/wallpapers" + +mkdir -p "$CONF_DIR" + +case "${1:-}" in + --reset) + echo "color:$DEFAULT_COLOR" > "$CONF_FILE" + xsetroot -solid "$DEFAULT_COLOR" + echo "Wallpaper reset to default color." + ;; + --color) + COLOR="${2:-$DEFAULT_COLOR}" + echo "color:$COLOR" > "$CONF_FILE" + xsetroot -solid "$COLOR" + echo "Wallpaper set to solid $COLOR." + ;; + "") + # Restore from saved config + if [[ -f "$CONF_FILE" ]]; then + SAVED="$(cat "$CONF_FILE")" + if [[ "$SAVED" == color:* ]]; then + xsetroot -solid "${SAVED#color:}" + elif [[ -f "$SAVED" ]]; then + feh --no-fehbg --bg-fill "$SAVED" + else + xsetroot -solid "$DEFAULT_COLOR" + fi + else + # First run: use default wallpaper if it exists, else solid color + if [[ -f "$WALLPAPER_DIR/skyos-default.png" ]]; then + feh --no-fehbg --bg-fill "$WALLPAPER_DIR/skyos-default.png" + echo "$WALLPAPER_DIR/skyos-default.png" > "$CONF_FILE" + else + xsetroot -solid "$DEFAULT_COLOR" + echo "color:$DEFAULT_COLOR" > "$CONF_FILE" + fi + fi + ;; + *) + # Set specific image file + IMG="$1" + if [[ ! -f "$IMG" ]]; then + echo "Error: file not found: $IMG" >&2 + exit 1 + fi + echo "$IMG" > "$CONF_FILE" + feh --no-fehbg --bg-fill "$IMG" + echo "Wallpaper set to: $IMG" + ;; +esac diff --git a/themes/wallpapers/generate-default.sh b/themes/wallpapers/generate-default.sh new file mode 100644 index 0000000..de0544d --- /dev/null +++ b/themes/wallpapers/generate-default.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Generates the default SkyOS wallpaper as a PNG using ImageMagick. +# On the live system: sudo pacman -S imagemagick +# Output: skyos-default.png (1920x1080 — scales to any resolution via feh) + +OUT="$(dirname "$0")/skyos-default.png" + +if ! command -v convert &>/dev/null; then + echo "ImageMagick not found. Install it, or place any PNG at:" + echo " $OUT" + exit 0 +fi + +convert -size 1920x1080 \ + gradient:"#4A6888-#2A3848" \ + \( -size 1920x1080 xc:none \ + -fill 'rgba(255,255,255,0.04)' \ + -draw "roundrectangle 200,180 760,540 8,8" \ + -draw "roundrectangle 840,220 1400,520 8,8" \ + -draw "roundrectangle 1480,160 1860,580 8,8" \ + \) \ + -composite \ + \( -size 1920x2 xc:"#6B8FB5" -resize 1920x1080\! \) \ + -compose Overlay -composite \ + "$OUT" + +echo "Written: $OUT" diff --git a/wm/Makefile b/wm/Makefile new file mode 100644 index 0000000..71eb493 --- /dev/null +++ b/wm/Makefile @@ -0,0 +1,30 @@ +CC = gcc + +# pkg-config gives correct flags on both Linux and macOS/Homebrew. +# Fallback covers systems where pkg-config isn't available. +PKG_XFT_C := $(shell pkg-config --cflags xft x11 2>/dev/null) +PKG_XFT_L := $(shell pkg-config --libs xft x11 2>/dev/null) + +ifeq ($(PKG_XFT_C),) + # macOS Homebrew fallback + PKG_XFT_C := -I/opt/homebrew/include + PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig +endif + +CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C) +LDFLAGS = $(PKG_XFT_L) + +SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c +OBJ = $(SRC:.c=.o) +BIN = skywm + +.PHONY: all clean install +all: $(BIN) +$(BIN): $(OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) +clean: + rm -f $(OBJ) $(BIN) diff --git a/wm/include/wm.h b/wm/include/wm.h new file mode 100644 index 0000000..0b1abfd --- /dev/null +++ b/wm/include/wm.h @@ -0,0 +1,180 @@ +#ifndef WM_H +#define WM_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── palette (modern dark) ───────────────────────────────────────────── */ +#define COLOR_TITLE_ACTIVE 0x1E4D78 /* deep blue active title bar */ +#define COLOR_TITLE_INACTIVE 0x252830 /* dark grey inactive */ +#define COLOR_TITLE_TEXT_ACT 0xE8EEF8 /* near-white active text */ +#define COLOR_TITLE_TEXT_INACT 0x5C6070 /* dimmed inactive text */ +#define COLOR_BORDER_LIGHT 0x3A6898 /* blue-grey highlight */ +#define COLOR_BORDER_DARK 0x0C1018 /* near-black shadow */ +#define COLOR_CLOSE_BTN 0xCC3333 /* red close */ +#define COLOR_MAXIMIZE_BTN 0x2E8B44 /* green maximize */ +#define COLOR_MINIMIZE_BTN 0xBB8820 /* amber minimize */ +#define COLOR_CLOSE_BTN_HOV 0xFF5050 +#define COLOR_MAXIMIZE_BTN_HOV 0x44CC66 +#define COLOR_MINIMIZE_BTN_HOV 0xFFBB30 +#define COLOR_BG 0x1A2840 /* desktop background */ +#define COLOR_SNAP_OVERLAY 0x3A6898 /* snap highlight (unused, placeholder) */ + +/* ── geometry ────────────────────────────────────────────────────────── */ +#define TITLE_HEIGHT 28 /* taller for modern look */ +#define TITLE_TAB_W 200 +#define BORDER_W 3 +#define BTN_SIZE 14 +#define BTN_MARGIN 5 +#define RESIZE_GRIP 12 +#define MIN_WIN_W 120 +#define MIN_WIN_H 60 +#define SNAP_DIST 24 /* px from edge to trigger snap */ +#define DBL_CLICK_MS 400 /* double-click threshold */ + +/* + * Three buttons on right side (left to right): minimize, maximize, close + * close = fw - BTN_SIZE - BTN_MARGIN - BORDER_W + * max = close - BTN_SIZE - BTN_MARGIN - 2 + * min = max - BTN_SIZE - BTN_MARGIN - 2 + */ +#define BTN_CLOSE_X(fw) ((fw) - BTN_SIZE - BTN_MARGIN - BORDER_W) +#define BTN_MAX_X(fw) ((fw) - BTN_SIZE*2 - BTN_MARGIN*2 - BORDER_W - 2) +#define BTN_MIN_X(fw) ((fw) - BTN_SIZE*3 - BTN_MARGIN*3 - BORDER_W - 4) +#define BTN_Y (((TITLE_HEIGHT) - BTN_SIZE) / 2) + +/* ── fonts ───────────────────────────────────────────────────────────── */ +#define FONT_TITLE "Sans:size=9:weight=bold" +#define FONT_MENU "Sans:size=10" + +/* ── drag mode ───────────────────────────────────────────────────────── */ +typedef enum { DRAG_NONE, DRAG_MOVE, DRAG_RESIZE } DragMode; + +/* WM_STATE values (ICCCM) */ +#define WM_STATE_NORMAL 1 +#define WM_STATE_ICONIC 3 +#define WM_STATE_WITHDRAWN 0 + +typedef struct Client { + Window win; + Window frame; + int x, y; + unsigned int w, h; + int focused; + int minimized; + int maximized; + int saved_x, saved_y; /* pre-maximize position */ + unsigned saved_w, saved_h; /* pre-maximize size */ + Time last_click_time; /* for double-click detection */ + char title[256]; + struct Client *next; +} Client; + +typedef struct MenuItem { + const char *label; + const char *cmd; +} MenuItem; + +typedef struct WM { + Display *dpy; + int screen; + Window root; + GC gc; + Colormap cmap; + Visual *visual; + int depth; + + XftFont *font_title; + XftFont *font_menu; + + Cursor cur_normal; + Cursor cur_move; + Cursor cur_resize; + + Client *clients; + Client *focused; + + Atom wm_delete_window; + Atom wm_protocols; + Atom wm_state; + Atom net_wm_name; + Atom net_wm_state; + Atom net_wm_state_hidden; + Atom net_wm_state_maximized_vert; + Atom net_wm_state_maximized_horz; + + DragMode drag_mode; + int drag_start_x, drag_start_y; + int drag_win_x, drag_win_y; + unsigned drag_win_w, drag_win_h; + Window drag_frame; + + Window menu_win; + int menu_visible; + int menu_x, menu_y; + int menu_hovered; +} WM; + +/* core */ +void wm_init(WM *wm); +void wm_run(WM *wm); +void wm_cleanup(WM *wm); + +/* client */ +Client *client_add(WM *wm, Window win); +void client_remove(WM *wm, Window win); +Client *client_find_by_frame(WM *wm, Window frame); +Client *client_find_by_win(WM *wm, Window win); +void client_focus(WM *wm, Client *c); +void client_raise(WM *wm, Client *c); +void client_minimize(WM *wm, Client *c); +void client_restore(WM *wm, Client *c); +void client_maximize(WM *wm, Client *c); +void client_unmaximize(WM *wm, Client *c); +void client_toggle_maximize(WM *wm, Client *c); +void client_snap_left(WM *wm, Client *c); +void client_snap_right(WM *wm, Client *c); +void client_set_wm_state(WM *wm, Client *c, long state); + +/* drawing */ +void frame_draw(WM *wm, Client *c); +void frame_draw_buttons(WM *wm, Client *c); +unsigned long color_alloc(WM *wm, unsigned long rgb); +void xft_color(WM *wm, unsigned long rgb, XftColor *out); + +/* hit testing */ +int frame_hit_resize(Client *c, int x, int y); +int frame_hit_close(Client *c, int x, int y); +int frame_hit_minimize(Client *c, int x, int y); +int frame_hit_maximize(Client *c, int x, int y); + +/* menu */ +void menu_show(WM *wm, int x, int y); +void menu_hide(WM *wm); +void menu_draw(WM *wm); +void menu_click(WM *wm, int px, int py); +int menu_item_at(int py); + +/* events */ +void on_map_request(WM *wm, XMapRequestEvent *e); +void on_unmap_notify(WM *wm, XUnmapEvent *e); +void on_destroy_notify(WM *wm, XDestroyWindowEvent *e); +void on_expose(WM *wm, XExposeEvent *e); +void on_button_press(WM *wm, XButtonEvent *e); +void on_button_release(WM *wm, XButtonEvent *e); +void on_motion_notify(WM *wm, XMotionEvent *e); +void on_configure_request(WM *wm, XConfigureRequestEvent *e); +void on_key_press(WM *wm, XKeyEvent *e); +void on_enter_notify(WM *wm, XCrossingEvent *e); +void on_client_message(WM *wm, XClientMessageEvent *e); +void on_property_notify(WM *wm, XPropertyEvent *e); + +#endif diff --git a/wm/src/client.c b/wm/src/client.c new file mode 100644 index 0000000..5f50b34 --- /dev/null +++ b/wm/src/client.c @@ -0,0 +1,315 @@ +#include "wm.h" + +/* ── WM_STATE ICCCM helper ───────────────────────────────────────────── */ + +void client_set_wm_state(WM *wm, Client *c, long state) { + long data[2] = { state, None }; + XChangeProperty(wm->dpy, c->win, wm->wm_state, wm->wm_state, + 32, PropModeReplace, (unsigned char *)data, 2); + + if (state == WM_STATE_ICONIC) { + XChangeProperty(wm->dpy, c->win, wm->net_wm_state, XA_ATOM, + 32, PropModeReplace, + (unsigned char *)&wm->net_wm_state_hidden, 1); + } else { + XDeleteProperty(wm->dpy, c->win, wm->net_wm_state); + } +} + +static void update_net_client_list(WM *wm) { + Atom net_client_list = XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False); + int count = 0; + for (Client *c = wm->clients; c; c = c->next) count++; + Window *wins = calloc(count, sizeof(Window)); + int i = 0; + for (Client *c = wm->clients; c; c = c->next) wins[i++] = c->win; + XChangeProperty(wm->dpy, wm->root, net_client_list, XA_WINDOW, 32, + PropModeReplace, (unsigned char *)wins, count); + free(wins); +} + +/* ── add / remove ────────────────────────────────────────────────────── */ + +/* smart placement: cascade from top-left, offset each window */ +static void smart_place(WM *wm, Client *c) { + static int cascade_x = 40, cascade_y = 40; + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + + /* reset cascade if it would go off-screen */ + if (cascade_x + (int)c->w > sw - 40 || cascade_y + (int)c->h > sh - 40) { + cascade_x = 40; + cascade_y = 40; + } + c->x = cascade_x; + c->y = cascade_y; + cascade_x += 30; + cascade_y += 30; +} + +Client *client_add(WM *wm, Window win) { + XWindowAttributes wa; + if (!XGetWindowAttributes(wm->dpy, win, &wa)) return NULL; + if (wa.override_redirect) return NULL; + if (client_find_by_win(wm, win)) return NULL; + + Client *c = calloc(1, sizeof(Client)); + c->win = win; + c->w = wa.width; + c->h = wa.height; + + /* use application-requested position if non-zero, otherwise smart place */ + if (wa.x == 0 && wa.y == 0) { + smart_place(wm, c); + } else { + c->x = wa.x; + c->y = wa.y; + } + /* never land behind deskbar (top-right panel ~180px wide) */ + int sw = DisplayWidth(wm->dpy, wm->screen); + if (c->x + (int)c->w > sw - 185 && c->y < 200) + c->y = 40; + + /* fetch title */ + char *name = NULL; + Atom utf8 = XInternAtom(wm->dpy, "UTF8_STRING", False); + Atom net_wm = XInternAtom(wm->dpy, "_NET_WM_NAME", False); + { + Atom type; int fmt; unsigned long n, after; + unsigned char *data = NULL; + if (XGetWindowProperty(wm->dpy, win, net_wm, 0, 256, False, + utf8, &type, &fmt, &n, &after, &data) + == Success && data && n > 0) { + snprintf(c->title, 255, "%.*s", (int)n, data); + XFree(data); + } else { + if (data) XFree(data); + if (XFetchName(wm->dpy, win, &name) && name) { + strncpy(c->title, name, 255); + XFree(name); + } else { + strncpy(c->title, "Untitled", 255); + } + } + } + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + + XSetWindowAttributes swa; + swa.background_pixel = color_alloc(wm, COLOR_TITLE_INACTIVE); + swa.event_mask = SubstructureRedirectMask | SubstructureNotifyMask + | ButtonPressMask | ButtonReleaseMask + | PointerMotionMask | ExposureMask + | EnterWindowMask; + swa.override_redirect = False; + swa.cursor = wm->cur_normal; + + c->frame = XCreateWindow( + wm->dpy, wm->root, + c->x, c->y, fw, fh, 0, + CopyFromParent, InputOutput, CopyFromParent, + CWBackPixel | CWEventMask | CWOverrideRedirect | CWCursor, + &swa + ); + + XReparentWindow(wm->dpy, win, c->frame, BORDER_W, TITLE_HEIGHT); + XSelectInput(wm->dpy, win, PropertyChangeMask | FocusChangeMask); + + c->next = wm->clients; + wm->clients = c; + + XMapWindow(wm->dpy, c->frame); + XMapWindow(wm->dpy, win); + client_set_wm_state(wm, c, WM_STATE_NORMAL); + update_net_client_list(wm); + client_focus(wm, c); + return c; +} + +void client_remove(WM *wm, Window win) { + Client *prev = NULL, *c = wm->clients; + while (c) { + if (c->win == win || c->frame == win) { + if (prev) prev->next = c->next; + else wm->clients = c->next; + + if (wm->focused == c) { + wm->focused = wm->clients; + if (wm->focused) client_focus(wm, wm->focused); + else wm->focused = NULL; + } + XDestroyWindow(wm->dpy, c->frame); + free(c); + update_net_client_list(wm); + return; + } + prev = c; c = c->next; + } +} + +/* ── lookup ──────────────────────────────────────────────────────────── */ + +Client *client_find_by_frame(WM *wm, Window frame) { + for (Client *c = wm->clients; c; c = c->next) + if (c->frame == frame) return c; + return NULL; +} + +Client *client_find_by_win(WM *wm, Window win) { + for (Client *c = wm->clients; c; c = c->next) + if (c->win == win) return c; + return NULL; +} + +/* ── focus / raise ───────────────────────────────────────────────────── */ + +void client_focus(WM *wm, Client *c) { + if (!c || c->minimized) return; + if (wm->focused && wm->focused != c) { + wm->focused->focused = 0; + frame_draw(wm, wm->focused); + } + wm->focused = c; + c->focused = 1; + XSetInputFocus(wm->dpy, c->win, RevertToPointerRoot, CurrentTime); + XRaiseWindow(wm->dpy, c->frame); + frame_draw(wm, c); + + /* advertise active window */ + Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); + XChangeProperty(wm->dpy, wm->root, net_active, XA_WINDOW, 32, + PropModeReplace, (unsigned char *)&c->win, 1); +} + +void client_raise(WM *wm, Client *c) { + XRaiseWindow(wm->dpy, c->frame); +} + +/* ── minimize / restore ──────────────────────────────────────────────── */ + +void client_minimize(WM *wm, Client *c) { + if (c->minimized) return; + if (c->maximized) client_unmaximize(wm, c); + c->minimized = 1; + c->focused = 0; + XUnmapWindow(wm->dpy, c->frame); + client_set_wm_state(wm, c, WM_STATE_ICONIC); + + if (wm->focused == c) { + wm->focused = NULL; + for (Client *n = wm->clients; n; n = n->next) { + if (!n->minimized) { client_focus(wm, n); break; } + } + } +} + +void client_restore(WM *wm, Client *c) { + if (!c->minimized) { client_focus(wm, c); return; } + c->minimized = 0; + XMapWindow(wm->dpy, c->frame); + client_set_wm_state(wm, c, WM_STATE_NORMAL); + client_focus(wm, c); +} + +/* ── maximize / unmaximize ───────────────────────────────────────────── */ + +void client_maximize(WM *wm, Client *c) { + if (c->maximized || c->minimized) return; + + /* save current geometry */ + c->saved_x = c->x; + c->saved_y = c->y; + c->saved_w = c->w; + c->saved_h = c->h; + c->maximized = 1; + + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + + /* leave room for deskbar on the right */ + int deskbar_w = 185; + int new_w = sw - deskbar_w - BORDER_W * 2; + int new_h = sh - TITLE_HEIGHT - BORDER_W; + + c->x = 0; c->y = 0; + c->w = (unsigned)new_w; + c->h = (unsigned)new_h; + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + + /* advertise maximized state via EWMH */ + Atom states[2] = { wm->net_wm_state_maximized_vert, + wm->net_wm_state_maximized_horz }; + XChangeProperty(wm->dpy, c->win, wm->net_wm_state, XA_ATOM, 32, + PropModeReplace, (unsigned char *)states, 2); + + client_focus(wm, c); + frame_draw(wm, c); +} + +void client_unmaximize(WM *wm, Client *c) { + if (!c->maximized) return; + c->maximized = 0; + c->x = c->saved_x; + c->y = c->saved_y; + c->w = c->saved_w; + c->h = c->saved_h; + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + XDeleteProperty(wm->dpy, c->win, wm->net_wm_state); + + client_focus(wm, c); + frame_draw(wm, c); +} + +void client_toggle_maximize(WM *wm, Client *c) { + if (c->maximized) client_unmaximize(wm, c); + else client_maximize(wm, c); +} + +/* ── edge snap ───────────────────────────────────────────────────────── */ + +void client_snap_left(WM *wm, Client *c) { + if (c->maximized) client_unmaximize(wm, c); + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + int deskbar_w = 185; + int half_w = (sw - deskbar_w) / 2; + + c->x = 0; + c->y = 0; + c->w = (unsigned)(half_w - BORDER_W * 2); + c->h = (unsigned)(sh - TITLE_HEIGHT - BORDER_W); + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); +} + +void client_snap_right(WM *wm, Client *c) { + if (c->maximized) client_unmaximize(wm, c); + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + int deskbar_w = 185; + int half_w = (sw - deskbar_w) / 2; + + c->x = half_w; + c->y = 0; + c->w = (unsigned)(sw - deskbar_w - half_w - BORDER_W * 2); + c->h = (unsigned)(sh - TITLE_HEIGHT - BORDER_W); + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); +} diff --git a/wm/src/color.c b/wm/src/color.c new file mode 100644 index 0000000..a6209d3 --- /dev/null +++ b/wm/src/color.c @@ -0,0 +1,20 @@ +#include "wm.h" + +unsigned long color_alloc(WM *wm, unsigned long rgb) { + XColor c; + c.red = ((rgb >> 16) & 0xFF) * 257; + c.green = ((rgb >> 8) & 0xFF) * 257; + c.blue = ((rgb) & 0xFF) * 257; + c.flags = DoRed | DoGreen | DoBlue; + XAllocColor(wm->dpy, wm->cmap, &c); + return c.pixel; +} + +void xft_color(WM *wm, unsigned long rgb, XftColor *out) { + XRenderColor rc; + rc.red = ((rgb >> 16) & 0xFF) * 257; + rc.green = ((rgb >> 8) & 0xFF) * 257; + rc.blue = ((rgb) & 0xFF) * 257; + rc.alpha = 0xFFFF; + XftColorAllocValue(wm->dpy, wm->visual, wm->cmap, &rc, out); +} diff --git a/wm/src/draw.c b/wm/src/draw.c new file mode 100644 index 0000000..ca7f04e --- /dev/null +++ b/wm/src/draw.c @@ -0,0 +1,162 @@ +#include "wm.h" + +/* + * Frame layout (3 buttons on right): + * + * ┌─[Title______________]──────[_][□][X]─┐ <- TITLE_HEIGHT + * │ content │ + * │ [grip]────┘ + */ + +/* ── hit tests ───────────────────────────────────────────────────────── */ + +int frame_hit_close(Client *c, int x, int y) { + int fw = (int)c->w + BORDER_W * 2; + int bx = BTN_CLOSE_X(fw); + return (x >= bx && x <= bx + BTN_SIZE && + y >= BTN_Y && y <= BTN_Y + BTN_SIZE); +} + +int frame_hit_maximize(Client *c, int x, int y) { + int fw = (int)c->w + BORDER_W * 2; + int bx = BTN_MAX_X(fw); + return (x >= bx && x <= bx + BTN_SIZE && + y >= BTN_Y && y <= BTN_Y + BTN_SIZE); +} + +int frame_hit_minimize(Client *c, int x, int y) { + int fw = (int)c->w + BORDER_W * 2; + int bx = BTN_MIN_X(fw); + return (x >= bx && x <= bx + BTN_SIZE && + y >= BTN_Y && y <= BTN_Y + BTN_SIZE); +} + +int frame_hit_resize(Client *c, int x, int y) { + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + return (x >= fw - RESIZE_GRIP && y >= fh - RESIZE_GRIP); +} + +/* ── button drawing ──────────────────────────────────────────────────── */ + +static void draw_circle_btn(WM *wm, Drawable d, int bx, int by, + unsigned long face, char sym) { + int r = BTN_SIZE / 2; + int cx = bx + r; + int cy = by + r; + + /* filled circle */ + XSetForeground(wm->dpy, wm->gc, face); + XFillArc(wm->dpy, d, wm->gc, bx, by, BTN_SIZE, BTN_SIZE, 0, 360*64); + + /* subtle dark ring */ + unsigned long darker = (face > 0x303030) ? face - 0x202020 : 0; + XSetForeground(wm->dpy, wm->gc, darker); + XDrawArc(wm->dpy, d, wm->gc, bx, by, BTN_SIZE, BTN_SIZE, 0, 360*64); + + /* symbol in white */ + XSetForeground(wm->dpy, wm->gc, 0xFFFFFF); + if (sym == 'X') { + XDrawLine(wm->dpy, d, wm->gc, cx-3, cy-3, cx+3, cy+3); + XDrawLine(wm->dpy, d, wm->gc, cx+3, cy-3, cx-3, cy+3); + XDrawLine(wm->dpy, d, wm->gc, cx-3, cy-3, cx+3, cy+3); /* double for boldness */ + } else if (sym == '_') { + XDrawLine(wm->dpy, d, wm->gc, cx-3, cy+2, cx+3, cy+2); + XDrawLine(wm->dpy, d, wm->gc, cx-3, cy+3, cx+3, cy+3); + } else if (sym == 'O') { + /* maximize: small hollow square */ + XDrawRectangle(wm->dpy, d, wm->gc, cx-3, cy-3, 6, 6); + } +} + +void frame_draw_buttons(WM *wm, Client *c) { + int fw = (int)c->w + BORDER_W * 2; + draw_circle_btn(wm, c->frame, BTN_CLOSE_X(fw), BTN_Y, + color_alloc(wm, COLOR_CLOSE_BTN), 'X'); + draw_circle_btn(wm, c->frame, BTN_MAX_X(fw), BTN_Y, + color_alloc(wm, COLOR_MAXIMIZE_BTN), 'O'); + draw_circle_btn(wm, c->frame, BTN_MIN_X(fw), BTN_Y, + color_alloc(wm, COLOR_MINIMIZE_BTN), '_'); +} + +/* ── resize grip ─────────────────────────────────────────────────────── */ + +static void draw_resize_grip(WM *wm, Client *c) { + if (c->maximized) return; /* no resize grip when maximized */ + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + for (int i = 0; i < 4; i++) { + int ox = fw - 4 - i * 3; + int oy = fh - 4 - i * 3; + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, COLOR_BORDER_LIGHT)); + XDrawPoint(wm->dpy, c->frame, wm->gc, ox, oy); + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, COLOR_BORDER_DARK)); + XDrawPoint(wm->dpy, c->frame, wm->gc, ox+1, oy+1); + } +} + +/* ── main frame draw ─────────────────────────────────────────────────── */ + +void frame_draw(WM *wm, Client *c) { + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + + unsigned long col_light = color_alloc(wm, COLOR_BORDER_LIGHT); + unsigned long col_dark = color_alloc(wm, COLOR_BORDER_DARK); + + /* title bar background */ + unsigned long col_title = color_alloc(wm, + c->focused ? COLOR_TITLE_ACTIVE : COLOR_TITLE_INACTIVE); + XSetForeground(wm->dpy, wm->gc, col_title); + XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, TITLE_HEIGHT); + + /* subtle gradient effect: lighter top strip (2px) */ + if (c->focused) { + unsigned long lighter = color_alloc(wm, 0x2A6098); + XSetForeground(wm->dpy, wm->gc, lighter); + XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, 2); + } + + /* client area background strip (below title) */ + XSetForeground(wm->dpy, wm->gc, col_dark); + XFillRectangle(wm->dpy, c->frame, wm->gc, 0, TITLE_HEIGHT, BORDER_W, fh - TITLE_HEIGHT); + XFillRectangle(wm->dpy, c->frame, wm->gc, fw - BORDER_W, TITLE_HEIGHT, BORDER_W, fh - TITLE_HEIGHT); + XFillRectangle(wm->dpy, c->frame, wm->gc, 0, fh - BORDER_W, fw, BORDER_W); + + /* outer bevel */ + XSetForeground(wm->dpy, wm->gc, col_light); + XDrawLine(wm->dpy, c->frame, wm->gc, 0, 0, fw - 1, 0); + XDrawLine(wm->dpy, c->frame, wm->gc, 0, 0, 0, fh - 1); + XSetForeground(wm->dpy, wm->gc, col_dark); + XDrawLine(wm->dpy, c->frame, wm->gc, fw-1, 0, fw-1, fh-1); + XDrawLine(wm->dpy, c->frame, wm->gc, 0, fh-1, fw-1, fh-1); + + /* thin separator line under title */ + XSetForeground(wm->dpy, wm->gc, col_dark); + XDrawLine(wm->dpy, c->frame, wm->gc, 0, TITLE_HEIGHT - 1, fw - 1, TITLE_HEIGHT - 1); + + /* title text */ + int btn_area = BTN_SIZE * 3 + BTN_MARGIN * 3 + BORDER_W + 8; + int text_max_w = fw - BORDER_W * 2 - btn_area - 8; + if (text_max_w < 10) text_max_w = 10; + + XftDraw *xd = XftDrawCreate(wm->dpy, c->frame, wm->visual, wm->cmap); + if (xd) { + XftColor col_text; + xft_color(wm, c->focused ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &col_text); + XRectangle clip = { BORDER_W + 4, 1, (unsigned short)text_max_w, TITLE_HEIGHT - 2 }; + XftDrawSetClipRectangles(xd, 0, 0, &clip, 1); + XGlyphInfo ext; + XftTextExtentsUtf8(wm->dpy, wm->font_title, + (const FcChar8 *)c->title, strlen(c->title), &ext); + int ty = (TITLE_HEIGHT - ext.height) / 2 + ext.height - ext.y; + XftDrawStringUtf8(xd, &col_text, wm->font_title, + BORDER_W + 6, ty, + (const FcChar8 *)c->title, strlen(c->title)); + XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_text); + XftDrawDestroy(xd); + } + + frame_draw_buttons(wm, c); + draw_resize_grip(wm, c); +} diff --git a/wm/src/events.c b/wm/src/events.c new file mode 100644 index 0000000..56271a4 --- /dev/null +++ b/wm/src/events.c @@ -0,0 +1,302 @@ +#include "wm.h" + +/* ── helpers ─────────────────────────────────────────────────────────── */ + +static void send_delete(WM *wm, Window win) { + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.window = win; + ev.xclient.message_type = wm->wm_protocols; + ev.xclient.format = 32; + ev.xclient.data.l[0] = wm->wm_delete_window; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(wm->dpy, win, False, NoEventMask, &ev); +} + +static void set_cursor(WM *wm, Client *c, int x, int y) { + Cursor cur; + if (!c->maximized && frame_hit_resize(c, x, y)) cur = wm->cur_resize; + else if (y < TITLE_HEIGHT) cur = wm->cur_move; + else cur = wm->cur_normal; + XDefineCursor(wm->dpy, c->frame, cur); +} + +/* ── snap on drag release ────────────────────────────────────────────── */ + +static void check_snap(WM *wm, Client *c) { + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + + if (c->x <= SNAP_DIST) { + if (c->y <= SNAP_DIST) { + client_maximize(wm, c); + } else { + client_snap_left(wm, c); + } + } else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - 185 - SNAP_DIST) { + client_snap_right(wm, c); + } else if (c->y <= SNAP_DIST) { + client_maximize(wm, c); + } else { + (void)sh; + } +} + +/* ── event handlers ──────────────────────────────────────────────────── */ + +void on_map_request(WM *wm, XMapRequestEvent *e) { + client_add(wm, e->window); +} + +void on_unmap_notify(WM *wm, XUnmapEvent *e) { + if (e->send_event) return; + Client *c = client_find_by_win(wm, e->window); + if (c && !c->minimized) client_remove(wm, e->window); +} + +void on_destroy_notify(WM *wm, XDestroyWindowEvent *e) { + Client *c = client_find_by_win(wm, e->window); + if (!c) c = client_find_by_frame(wm, e->window); + if (c) client_remove(wm, c->win); +} + +void on_expose(WM *wm, XExposeEvent *e) { + if (e->count != 0) return; + if (wm->menu_visible && e->window == wm->menu_win) { + menu_draw(wm); + return; + } + Client *c = client_find_by_frame(wm, e->window); + if (c && !c->minimized) frame_draw(wm, c); +} + +void on_configure_request(WM *wm, XConfigureRequestEvent *e) { + Client *c = client_find_by_win(wm, e->window); + XWindowChanges wc; + wc.x = e->x; wc.y = e->y; + wc.width = e->width; wc.height = e->height; + wc.border_width = e->border_width; + wc.sibling = e->above; wc.stack_mode = e->detail; + if (c && !c->maximized) { + c->w = e->width; c->h = e->height; + XResizeWindow(wm->dpy, c->frame, + c->w + BORDER_W*2, c->h + TITLE_HEIGHT + BORDER_W); + } + XConfigureWindow(wm->dpy, e->window, e->value_mask, &wc); +} + +void on_enter_notify(WM *wm, XCrossingEvent *e) { + Client *c = client_find_by_frame(wm, e->window); + if (c && !c->minimized) set_cursor(wm, c, e->x, e->y); +} + +void on_client_message(WM *wm, XClientMessageEvent *e) { + Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); + if (e->message_type == net_active) { + Client *c = client_find_by_win(wm, e->window); + if (c) client_restore(wm, c); + } +} + +/* Called when a managed window changes a property (title, etc.) */ +void on_property_notify(WM *wm, XPropertyEvent *e) { + Client *c = client_find_by_win(wm, e->window); + if (!c || c->minimized) return; + + Atom wm_name = XInternAtom(wm->dpy, "WM_NAME", False); + Atom net_wm_name = XInternAtom(wm->dpy, "_NET_WM_NAME", False); + Atom utf8 = XInternAtom(wm->dpy, "UTF8_STRING", False); + + if (e->atom != wm_name && e->atom != net_wm_name) return; + + /* re-fetch title */ + char new_title[256] = {0}; + { + Atom type; int fmt; unsigned long n, after; + unsigned char *data = NULL; + if (XGetWindowProperty(wm->dpy, c->win, net_wm_name, 0, 256, False, + utf8, &type, &fmt, &n, &after, &data) + == Success && data && n > 0) { + snprintf(new_title, 255, "%.*s", (int)n, data); + XFree(data); + } else { + if (data) XFree(data); + char *name = NULL; + if (XFetchName(wm->dpy, c->win, &name) && name) { + strncpy(new_title, name, 255); + XFree(name); + } + } + } + + if (new_title[0] && strcmp(new_title, c->title) != 0) { + strncpy(c->title, new_title, 255); + frame_draw(wm, c); + } +} + +void on_button_press(WM *wm, XButtonEvent *e) { + /* menu window */ + if (wm->menu_visible && e->window == wm->menu_win) { + if (e->button == Button1) menu_click(wm, e->x, e->y); + else menu_hide(wm); + return; + } + /* right-click root → menu */ + if (e->window == wm->root && e->button == Button3) { + menu_show(wm, e->x_root, e->y_root); + return; + } + if (wm->menu_visible) { menu_hide(wm); return; } + + Client *c = client_find_by_frame(wm, e->window); + if (!c || c->minimized) return; + + client_focus(wm, c); + if (e->button != Button1) return; + + if (frame_hit_close(c, e->x, e->y)) { + send_delete(wm, c->win); + return; + } + if (frame_hit_maximize(c, e->x, e->y)) { + client_toggle_maximize(wm, c); + return; + } + if (frame_hit_minimize(c, e->x, e->y)) { + client_minimize(wm, c); + return; + } + if (!c->maximized && frame_hit_resize(c, e->x, e->y)) { + wm->drag_mode = DRAG_RESIZE; + wm->drag_frame = c->frame; + wm->drag_start_x = e->x_root; + wm->drag_start_y = e->y_root; + wm->drag_win_w = c->w; + wm->drag_win_h = c->h; + XGrabPointer(wm->dpy, c->frame, False, + ButtonReleaseMask | PointerMotionMask, + GrabModeAsync, GrabModeAsync, + None, wm->cur_resize, CurrentTime); + return; + } + if (e->y < TITLE_HEIGHT) { + /* double-click title bar → maximize toggle */ + if (e->time - c->last_click_time < DBL_CLICK_MS) { + client_toggle_maximize(wm, c); + c->last_click_time = 0; + return; + } + c->last_click_time = e->time; + + if (c->maximized) return; /* don't drag a maximized window */ + + wm->drag_mode = DRAG_MOVE; + wm->drag_frame = c->frame; + wm->drag_start_x = e->x_root; + wm->drag_start_y = e->y_root; + wm->drag_win_x = c->x; + wm->drag_win_y = c->y; + XGrabPointer(wm->dpy, c->frame, False, + ButtonReleaseMask | PointerMotionMask, + GrabModeAsync, GrabModeAsync, + None, wm->cur_move, CurrentTime); + } +} + +void on_button_release(WM *wm, XButtonEvent *e) { + (void)e; + if (wm->drag_mode == DRAG_MOVE) { + /* snap check after move */ + Client *c = client_find_by_frame(wm, wm->drag_frame); + if (c) check_snap(wm, c); + } + if (wm->drag_mode != DRAG_NONE) { + wm->drag_mode = DRAG_NONE; + XUngrabPointer(wm->dpy, CurrentTime); + } +} + +void on_motion_notify(WM *wm, XMotionEvent *e) { + if (wm->menu_visible && e->window == wm->menu_win) { + int idx = menu_item_at(e->y); + if (idx != wm->menu_hovered) { + wm->menu_hovered = idx; + menu_draw(wm); + } + return; + } + if (wm->drag_mode == DRAG_NONE) { + Client *c = client_find_by_frame(wm, e->window); + if (c) set_cursor(wm, c, e->x, e->y); + return; + } + Client *c = client_find_by_frame(wm, wm->drag_frame); + if (!c) return; + int dx = e->x_root - wm->drag_start_x; + int dy = e->y_root - wm->drag_start_y; + if (wm->drag_mode == DRAG_MOVE) { + c->x = wm->drag_win_x + dx; + c->y = wm->drag_win_y + dy; + if (c->y < 0) c->y = 0; /* don't drag above screen top */ + XMoveWindow(wm->dpy, c->frame, c->x, c->y); + } else { + int nw = (int)wm->drag_win_w + dx; + int nh = (int)wm->drag_win_h + dy; + if (nw < MIN_WIN_W) nw = MIN_WIN_W; + if (nh < MIN_WIN_H) nh = MIN_WIN_H; + c->w = (unsigned)nw; c->h = (unsigned)nh; + XResizeWindow(wm->dpy, c->frame, c->w+BORDER_W*2, c->h+TITLE_HEIGHT+BORDER_W); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); + } +} + +void on_key_press(WM *wm, XKeyEvent *e) { + KeySym sym = XLookupKeysym(e, 0); + + /* Alt+F4 — close focused window */ + if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused) + send_delete(wm, wm->focused->win); + + /* Alt+Tab — cycle windows */ + if ((e->state & Mod1Mask) && sym == XK_Tab && wm->clients) { + Client *start = wm->focused ? wm->focused->next : wm->clients; + Client *n = start; + while (n && n->minimized) n = n->next; + if (!n) { n = wm->clients; while (n && n->minimized) n = n->next; } + if (n && n != wm->focused) client_focus(wm, n); + } + + /* Alt+F9 — minimize */ + if ((e->state & Mod1Mask) && sym == XK_F9 && wm->focused) + client_minimize(wm, wm->focused); + + /* Alt+F10 — maximize toggle */ + if ((e->state & Mod1Mask) && sym == XK_F10 && wm->focused) + client_toggle_maximize(wm, wm->focused); + + /* Alt+F1 — launcher */ + if ((e->state & Mod1Mask) && sym == XK_F1) + system("skylauncher &"); + + /* Super+Left — snap left */ + if ((e->state & Mod4Mask) && sym == XK_Left && wm->focused) + client_snap_left(wm, wm->focused); + + /* Super+Right — snap right */ + if ((e->state & Mod4Mask) && sym == XK_Right && wm->focused) + client_snap_right(wm, wm->focused); + + /* Super+Up — maximize */ + if ((e->state & Mod4Mask) && sym == XK_Up && wm->focused) + client_maximize(wm, wm->focused); + + /* Super+Down — unmaximize / restore */ + if ((e->state & Mod4Mask) && sym == XK_Down && wm->focused) + client_unmaximize(wm, wm->focused); + + /* Escape — hide menu */ + if (sym == XK_Escape && wm->menu_visible) + menu_hide(wm); +} diff --git a/wm/src/menu.c b/wm/src/menu.c new file mode 100644 index 0000000..d402cce --- /dev/null +++ b/wm/src/menu.c @@ -0,0 +1,160 @@ +#include "wm.h" + +#define MENU_ITEM_H 24 +#define MENU_W 220 +#define MENU_PADDING 5 + +static const MenuItem MENU_ITEMS[] = { + { "App Launcher", "skylauncher &" }, + { "Terminal", "xterm &" }, + { "File Manager", "thunar &" }, + { "Text Editor", "mousepad &" }, + { "──────────────────", NULL }, + { "Web Browser", "firefox &" }, + { "System Monitor", "xterm -T 'System Monitor' -e htop &" }, + { "Volume Control", "pavucontrol &" }, + { "Display Settings", "arandr &" }, + { "──────────────────", NULL }, + { "Change Wallpaper", "skywallpaper --reset &" }, + { "──────────────────", NULL }, + { "Restart SkyWM", "pkill skywm; sleep 0.2; skywm &" }, + { "Quit SkyOS", "pkill skywm" }, +}; +#define MENU_COUNT ((int)(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]))) + +static int is_separator(int i) { + return MENU_ITEMS[i].cmd == NULL; +} + +static int menu_height(void) { + int h = MENU_PADDING; + for (int i = 0; i < MENU_COUNT; i++) + h += is_separator(i) ? 8 : MENU_ITEM_H; + return h + MENU_PADDING; +} + +void menu_show(WM *wm, int x, int y) { + if (wm->menu_visible) menu_hide(wm); + + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + int mh = menu_height(); + if (x + MENU_W > sw) x = sw - MENU_W - 4; + if (y + mh > sh) y = sh - mh - 4; + + wm->menu_x = x; + wm->menu_y = y; + wm->menu_hovered = -1; + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = color_alloc(wm, 0x1C2030); /* dark menu bg */ + swa.event_mask = ExposureMask | ButtonPressMask + | ButtonReleaseMask | PointerMotionMask + | LeaveWindowMask; + swa.save_under = True; + + wm->menu_win = XCreateWindow( + wm->dpy, wm->root, + x, y, MENU_W, mh, 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWEventMask | CWSaveUnder, + &swa + ); + + /* 1px accent border */ + XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, COLOR_BORDER_LIGHT)); + + XMapRaised(wm->dpy, wm->menu_win); + wm->menu_visible = 1; + + /* Grab pointer so clicks outside hide the menu */ + XGrabPointer(wm->dpy, wm->menu_win, False, + ButtonPressMask | ButtonReleaseMask | PointerMotionMask, + GrabModeAsync, GrabModeAsync, + None, None, CurrentTime); +} + +void menu_hide(WM *wm) { + if (!wm->menu_visible) return; + XUngrabPointer(wm->dpy, CurrentTime); + XDestroyWindow(wm->dpy, wm->menu_win); + wm->menu_win = 0; + wm->menu_visible = 0; +} + +static int item_y(int idx) { + int y = MENU_PADDING; + for (int i = 0; i < idx; i++) + y += is_separator(i) ? 8 : MENU_ITEM_H; + return y; +} + +int menu_item_at(int py) { + int y = MENU_PADDING; + for (int i = 0; i < MENU_COUNT; i++) { + int h = is_separator(i) ? 8 : MENU_ITEM_H; + if (py >= y && py < y + h) return i; + y += h; + } + return -1; +} + +void menu_draw(WM *wm) { + if (!wm->menu_visible) return; + + XftDraw *xd = XftDrawCreate(wm->dpy, wm->menu_win, wm->visual, wm->cmap); + if (!xd) return; + + XftColor col_normal, col_hover_bg, col_hover_text, col_sep; + xft_color(wm, 0xC8D4E8, &col_normal); /* light text on dark bg */ + xft_color(wm, 0x2060A0, &col_hover_bg); /* blue highlight */ + xft_color(wm, 0xF0F4FF, &col_hover_text); + xft_color(wm, 0x303848, &col_sep); + + /* clear background */ + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x1C2030)); + XFillRectangle(wm->dpy, wm->menu_win, wm->gc, 0, 0, MENU_W, menu_height()); + + for (int i = 0; i < MENU_COUNT; i++) { + int iy = item_y(i); + + if (is_separator(i)) { + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x2A3040)); + XDrawLine(wm->dpy, wm->menu_win, wm->gc, 8, iy + 4, MENU_W - 8, iy + 4); + continue; + } + + /* hover highlight */ + if (i == wm->menu_hovered && wm->menu_hovered >= 0) { + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x4A6888)); + XFillRectangle(wm->dpy, wm->menu_win, wm->gc, + 2, iy, MENU_W - 4, MENU_ITEM_H); + } + + XftColor *tc = (i == wm->menu_hovered) ? &col_hover_text : &col_normal; + XGlyphInfo ext; + XftTextExtentsUtf8(wm->dpy, wm->font_menu, + (const FcChar8 *)MENU_ITEMS[i].label, + strlen(MENU_ITEMS[i].label), &ext); + int ty = iy + (MENU_ITEM_H - ext.height) / 2 + ext.height - ext.y; + XftDrawStringUtf8(xd, tc, wm->font_menu, + MENU_PADDING + 4, ty, + (const FcChar8 *)MENU_ITEMS[i].label, + strlen(MENU_ITEMS[i].label)); + } + + XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_normal); + XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_hover_bg); + XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_hover_text); + XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_sep); + XftDrawDestroy(xd); +} + +void menu_click(WM *wm, int px, int py) { (void)px; + int i = menu_item_at(py); + if (i < 0 || is_separator(i)) { menu_hide(wm); return; } + menu_hide(wm); + const char *cmd = MENU_ITEMS[i].cmd; + if (cmd) { system(cmd); } /* intentional: user-defined launch commands */ +} diff --git a/wm/src/wm.c b/wm/src/wm.c new file mode 100644 index 0000000..b54c132 --- /dev/null +++ b/wm/src/wm.c @@ -0,0 +1,166 @@ +#include "wm.h" + +static int x_error_handler(Display *dpy, XErrorEvent *e) { + if (e->error_code == BadWindow || e->error_code == BadAccess) + return 0; + char buf[256]; + XGetErrorText(dpy, e->error_code, buf, sizeof(buf)); + fprintf(stderr, "skywm: X error: %s (request %d)\n", buf, e->request_code); + return 0; +} + +static int x_error_start(Display *dpy, XErrorEvent *e) { + (void)dpy; (void)e; + fprintf(stderr, "skywm: another window manager is already running\n"); + exit(1); +} + +void wm_init(WM *wm) { + wm->dpy = XOpenDisplay(NULL); + if (!wm->dpy) { fprintf(stderr, "skywm: cannot open display\n"); exit(1); } + + wm->screen = DefaultScreen(wm->dpy); + wm->root = RootWindow(wm->dpy, wm->screen); + wm->cmap = DefaultColormap(wm->dpy, wm->screen); + wm->visual = DefaultVisual(wm->dpy, wm->screen); + wm->depth = DefaultDepth(wm->dpy, wm->screen); + wm->gc = XCreateGC(wm->dpy, wm->root, 0, NULL); + + /* fonts */ + wm->font_title = XftFontOpenName(wm->dpy, wm->screen, FONT_TITLE); + if (!wm->font_title) + wm->font_title = XftFontOpenName(wm->dpy, wm->screen, "fixed:size=9"); + wm->font_menu = XftFontOpenName(wm->dpy, wm->screen, FONT_MENU); + if (!wm->font_menu) wm->font_menu = wm->font_title; + + /* cursors */ + wm->cur_normal = XCreateFontCursor(wm->dpy, XC_left_ptr); + wm->cur_move = XCreateFontCursor(wm->dpy, XC_fleur); + wm->cur_resize = XCreateFontCursor(wm->dpy, XC_bottom_right_corner); + XDefineCursor(wm->dpy, wm->root, wm->cur_normal); + + /* atoms */ + wm->wm_protocols = XInternAtom(wm->dpy, "WM_PROTOCOLS", False); + wm->wm_delete_window = XInternAtom(wm->dpy, "WM_DELETE_WINDOW", False); + wm->wm_state = XInternAtom(wm->dpy, "WM_STATE", False); + wm->net_wm_name = XInternAtom(wm->dpy, "_NET_WM_NAME", False); + wm->net_wm_state = XInternAtom(wm->dpy, "_NET_WM_STATE", False); + wm->net_wm_state_hidden = XInternAtom(wm->dpy, "_NET_WM_STATE_HIDDEN", False); + wm->net_wm_state_maximized_vert = XInternAtom(wm->dpy, "_NET_WM_STATE_MAXIMIZED_VERT", False); + wm->net_wm_state_maximized_horz = XInternAtom(wm->dpy, "_NET_WM_STATE_MAXIMIZED_HORZ", False); + + /* advertise basic EWMH support */ + Atom net_supported = XInternAtom(wm->dpy, "_NET_SUPPORTED", False); + Atom supported[] = { + wm->net_wm_state, wm->net_wm_state_hidden, + wm->net_wm_state_maximized_vert, + wm->net_wm_state_maximized_horz, + XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False), + XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False), + XInternAtom(wm->dpy, "_NET_NUMBER_OF_DESKTOPS", False), + XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False), + XInternAtom(wm->dpy, "_NET_WM_NAME", False), + }; + XChangeProperty(wm->dpy, wm->root, net_supported, XA_ATOM, 32, + PropModeReplace, (unsigned char *)supported, + sizeof(supported)/sizeof(Atom)); + + /* set initial desktop count */ + Atom net_ndesktops = XInternAtom(wm->dpy, "_NET_NUMBER_OF_DESKTOPS", False); + long ndesktops = 4; + XChangeProperty(wm->dpy, wm->root, net_ndesktops, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)&ndesktops, 1); + Atom net_curdesk = XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False); + long curdesk = 0; + XChangeProperty(wm->dpy, wm->root, net_curdesk, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)&curdesk, 1); + + /* detect conflicting WM */ + XSetErrorHandler(x_error_start); + XSelectInput(wm->dpy, wm->root, + SubstructureRedirectMask | SubstructureNotifyMask | + KeyPressMask | ButtonPressMask); + XSync(wm->dpy, False); + XSetErrorHandler(x_error_handler); + + /* desktop background — feh will overwrite this if a wallpaper is set */ + XSetWindowBackground(wm->dpy, wm->root, color_alloc(wm, COLOR_BG)); + XClearWindow(wm->dpy, wm->root); + + /* global key grabs */ + struct { KeySym sym; unsigned mod; } grabs[] = { + { XK_Tab, Mod1Mask }, + { XK_F4, Mod1Mask }, + { XK_F9, Mod1Mask }, /* minimize */ + { XK_F10, Mod1Mask }, /* maximize */ + { XK_F1, Mod1Mask }, /* launcher */ + { XK_Left, Mod4Mask }, /* snap left */ + { XK_Right, Mod4Mask }, /* snap right */ + { XK_Up, Mod4Mask }, /* maximize */ + { XK_Down, Mod4Mask }, /* restore */ + { XK_Escape, 0 }, + }; + for (int i = 0; i < (int)(sizeof(grabs)/sizeof(grabs[0])); i++) { + KeyCode kc = XKeysymToKeycode(wm->dpy, grabs[i].sym); + XGrabKey(wm->dpy, kc, grabs[i].mod, wm->root, + True, GrabModeAsync, GrabModeAsync); + } + + /* add Alt+F1 to key handler */ + + /* adopt pre-existing windows */ + Window root_ret, parent_ret, *children; + unsigned int nchildren; + XQueryTree(wm->dpy, wm->root, &root_ret, &parent_ret, &children, &nchildren); + for (unsigned int i = 0; i < nchildren; i++) { + XWindowAttributes wa; + XGetWindowAttributes(wm->dpy, children[i], &wa); + if (!wa.override_redirect && wa.map_state == IsViewable) + client_add(wm, children[i]); + } + if (children) XFree(children); + + XSync(wm->dpy, False); + fprintf(stderr, "skywm: started\n"); +} + +void wm_run(WM *wm) { + XEvent e; + for (;;) { + XNextEvent(wm->dpy, &e); + switch (e.type) { + case MapRequest: on_map_request(wm, &e.xmaprequest); break; + case UnmapNotify: on_unmap_notify(wm, &e.xunmap); break; + case DestroyNotify: on_destroy_notify(wm, &e.xdestroywindow); break; + case ConfigureRequest: on_configure_request(wm, &e.xconfigurerequest); break; + case Expose: on_expose(wm, &e.xexpose); break; + case ButtonPress: on_button_press(wm, &e.xbutton); break; + case ButtonRelease: on_button_release(wm, &e.xbutton); break; + case MotionNotify: on_motion_notify(wm, &e.xmotion); break; + case KeyPress: on_key_press(wm, &e.xkey); break; + case EnterNotify: on_enter_notify(wm, &e.xcrossing); break; + case ClientMessage: on_client_message(wm, &e.xclient); break; + case PropertyNotify: on_property_notify(wm, &e.xproperty); break; + default: break; + } + } +} + +void wm_cleanup(WM *wm) { + if (wm->font_title) XftFontClose(wm->dpy, wm->font_title); + if (wm->font_menu && wm->font_menu != wm->font_title) + XftFontClose(wm->dpy, wm->font_menu); + XFreeCursor(wm->dpy, wm->cur_normal); + XFreeCursor(wm->dpy, wm->cur_move); + XFreeCursor(wm->dpy, wm->cur_resize); + XFreeGC(wm->dpy, wm->gc); + XCloseDisplay(wm->dpy); +} + +int main(void) { + WM wm = {0}; + wm_init(&wm); + wm_run(&wm); + wm_cleanup(&wm); + return 0; +}