Initial commit: SkyOS source code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-23 12:14:34 -04:00
commit e641720a3c
30 files changed
+5836

No files matched your search

+11
View File
@@ -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/
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
*.iso
*.o
deskbar/skydeskbar
installer/skyinstall
installer/skyinstall-gui
launcher/skylauncher
wm/skywm
+55
View File
@@ -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
+169
View File
@@ -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
Executable
+94
View File
@@ -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-<date>.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 ""
+26
View File
@@ -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)
+155
View File
@@ -0,0 +1,155 @@
#ifndef DESKBAR_H
#define DESKBAR_H
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/Xft/Xft.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/*
* 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
+963
View File
@@ -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;
}
+11
View File
@@ -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
File diff suppressed because it is too large. Load diff
+167
View File
@@ -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 <<EOF
127.0.0.1 localhost
::1 localhost
127.0.1.1 skyos.localdomain skyos
EOF
# timezone
ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime
hwclock --systohc
# default user
useradd -m -G wheel,audio,video,storage -s /bin/bash sky
echo "sky:skyos" | chpasswd
echo "%wheel ALL=(ALL) ALL" >> /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" <<EOF
include "/usr/share/themes/SkyOS/gtk-2.0/gtkrc"
EOF
mkdir -p "$HOME/.config/gtk-3.0"
cat > "$HOME/.config/gtk-3.0/settings.ini" <<EOF
[Settings]
gtk-theme-name=SkyOS
gtk-font-name=Sans 10
gtk-icon-theme-name=Adwaita
gtk-cursor-theme-name=DMZ-White
gtk-button-images=false
gtk-menu-images=false
EOF
# Compositor (subtle shadows, fast fade)
picom --config /etc/skyos/picom.conf &
# Alt+F1 global hotkey for launcher (via xbindkeys)
xbindkeys &
# Launch Deskbar
skydeskbar &
# Launch window manager (last, takes over the session)
exec skywm
XINITRC
chmod 755 "$WORK/home/sky/.xinitrc"
arch-chroot "$WORK" chown sky:sky /home/sky/.xinitrc
echo ">>> 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)"
+57
View File
@@ -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"]
+685
View File
@@ -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" <<EOF
[Settings]
gtk-theme-name=SkyOS
gtk-font-name=Sans 10
gtk-icon-theme-name=Adwaita
gtk-cursor-theme-name=Adwaita
EOF
cat > "$HOME/.gtkrc-2.0" <<EOF
include "/usr/share/themes/SkyOS/gtk-2.0/gtkrc"
EOF
# Audio session (pipewire + wireplumber)
pipewire &
pipewire-pulse &
wireplumber &
# Privilege escalation agent (needed for Thunar mounts, package managers, etc.)
/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 &
# Notifications
dunst &
# Network manager tray applet
nm-applet &
# Compositor
picom --config /etc/skyos/picom.conf &
# Clipboard sync: keep PRIMARY and CLIPBOARD selections in sync
if command -v xclip &>/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'
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="font">
<edit name="antialias" mode="assign"><bool>true</bool></edit>
<edit name="hinting" mode="assign"><bool>true</bool></edit>
<edit name="hintstyle" mode="assign"><const>hintslight</const></edit>
<edit name="rgba" mode="assign"><const>rgb</const></edit>
<edit name="lcdfilter" mode="assign"><const>lcddefault</const></edit>
</match>
<!-- Prefer noto for emoji -->
<alias>
<family>serif</family>
<prefer><family>DejaVu Serif</family><family>Noto Color Emoji</family></prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer><family>DejaVu Sans</family><family>Noto Color Emoji</family></prefer>
</alias>
<alias>
<family>monospace</family>
<prefer><family>DejaVu Sans Mono</family></prefer>
</alias>
</fontconfig>
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 = "<b>%s</b>\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 "════════════════════════════════════════"
+79
View File
@@ -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 |
+23
View File
@@ -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)
+407
View File
@@ -0,0 +1,407 @@
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/Xft/Xft.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/* ── 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;
}
+72
View File
@@ -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
+229
View File
@@ -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; }
+12
View File
@@ -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
+63
View File
@@ -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; };
};
+64
View File
@@ -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
+27
View File
@@ -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"
+30
View File
@@ -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)
+180
View File
@@ -0,0 +1,180 @@
#ifndef WM_H
#define WM_H
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/Xft/Xft.h>
#include <X11/cursorfont.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/* ── 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
+315
View File
@@ -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);
}
+20
View File
@@ -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);
}
+162
View File
@@ -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);
}
+302
View File
@@ -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);
}
+160
View File
@@ -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 */
}
+166
View File
@@ -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;
}