Initial commit: SkyOS source code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
e641720a3c
30 files changed
+5836
No files matched your search
@@ -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/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.iso
|
||||||
|
*.o
|
||||||
|
deskbar/skydeskbar
|
||||||
|
installer/skyinstall
|
||||||
|
installer/skyinstall-gui
|
||||||
|
launcher/skylauncher
|
||||||
|
wm/skywm
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
@@ -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 ""
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
Executable
+167
@@ -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)"
|
||||||
@@ -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"]
|
||||||
Executable
+685
@@ -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 "════════════════════════════════════════"
|
||||||
@@ -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 |
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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; }
|
||||||
@@ -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
|
||||||
@@ -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; };
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user