Compare commits

..
24 Commits
Author SHA1 Message Date
nmemmertandClaude Sonnet 4.6 0fdc14e26d Wire up installer, networking, and fix UEFI boot entry
- Dockerfile: compile and install orioninstall-gui + orioninstall script
- packages: add rsync, parted, arch-install-scripts, grub, efibootmgr,
  dosfstools, amd/intel-ucode, wpa_supplicant
- customize_airootfs.sh: enable NetworkManager + wpa_supplicant on boot
- orioninstall: fix UEFI (ESP at /boot, not /boot/efi); pass TIMEZONE
  and KEYBOARD into chroot heredoc; conditionalize microcode initrd lines
- menu: launch orioninstall-gui via pkexec (runs as root)
- profiledef: add file_permissions for installer binaries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 13:47:25 -04:00
nmemmertandClaude Sonnet 4.6 333eb6bc3e Add mirrorlist and pacman.conf to live airootfs
Without these the live system has no mirrors configured and pacman
refuses to sync. Uses geo.mirror.pkgbuild.com as primary with
kernel.org, rackspace, and MIT as fallbacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 13:40:07 -04:00
nmemmertandClaude Sonnet 4.6 4854ab85aa Redesign launcher: modern dark UI, 4-col grid, pill filters, footer
- 640x490 window, larger cards (132x106), 4 columns
- Full-width search bar in header with active-state indicator
- Category filters as pill chips with gaps
- Cards show surface bg + accent top bar on hover
- Footer bar with app count and keyboard hints
- Fix menu: pavucontrol -> alsamixer in xterm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 13:35:23 -04:00
nmemmertandClaude Sonnet 4.6 daf9cb4f96 Fix menu/launcher to use installed apps; add pcmanfm, mousepad, arandr
Menu and launcher fallback pointed to alacritty/firefox/rigel/quill
which are not in the live ISO. Switch to xterm (already installed),
pcmanfm and mousepad (now added to packages). Also add arandr for
display settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 13:29:45 -04:00
nmemmertandClaude Sonnet 4.6 1fa8f212b1 Auto-start X on boot: autologin root on TTY1, startx from .bash_profile
Also add all OrionOS binaries to file_permissions so archiso sets 755
on them in the squashfs (fixes Permission denied from xinitrc).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 13:20:03 -04:00
nmemmertandClaude Sonnet 4.6 689249cf5e Add customize_airootfs.sh to remove root password on live system
Without this, pacstrap locks root's password by default and the live
ISO is unbootable/unloggable-into.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 12:55:53 -04:00
nmemmertandClaude Sonnet 4.6 5de7cfec78 archiso hook: replace getarg with direct /proc/cmdline parsing
getarg's trailing-= convention varies by mkinitcpio version and was
returning empty even when archisosearchuuid was present in cmdline.
Parse /proc/cmdline directly in ash — no external tools, no version
sensitivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 12:39:06 -04:00
nmemmertandClaude Sonnet 4.6 685b725dbb archiso hook: support archisolabel= fallback for syslinux boots
Syslinux passes archisolabel= not archisosearchuuid= (UUID is only in
systemd-boot entries). Add blkid label lookup as fallback so both boot
paths work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 12:32:27 -04:00
nmemmertandClaude Sonnet 4.6 9159626cff Fix archiso initcpio hook: use run_hook+mount_handler override
run_latehook fires after mkinitcpio already attempts to mount root= (empty),
which fails. Switch to run_hook() setting mount_handler=archiso_mount_handler
so mkinitcpio calls our handler instead of the default block-device mount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 12:05:41 -04:00
nmemmertandClaude Sonnet 4.6 ca0e9f7c72 Bundle archiso initcpio hook directly in repo
Current archiso package does not install initcpio hook files (confirmed:
find /usr -path */initcpio/hooks/archiso returns nothing). Write the hook
and its mkinitcpio install script directly into the profile airootfs so
COPY carries them into the image without any package dependency.

Hook finds ISO by archisosearchuuid= kernel param, waits up to 30s for
the device to appear via blkid, mounts iso9660→squashfs→overlay onto
/new_root. mkinitcpio detects /new_root is already mounted and succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 11:34:08 -04:00
nmemmertandClaude Sonnet 4.6 a3beffae15 Find archiso initcpio hooks by path rather than hardcoded location
Current archiso package version may place hooks at a non-standard path.
Use find to locate hooks/archiso and install/archiso anywhere under /usr
and copy to the canonical initcpio location in airootfs.
Prints found paths so build output shows where they came from.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 11:14:05 -04:00
nmemmertandClaude Sonnet 4.6 ebb61beaac Copy archiso initcpio hooks directly into airootfs at build time
The hook package install inside the pacstrap chroot may silently skip
the initcpio install step under QEMU emulation, leaving the archiso hook
absent from the initrd (confirmed: no 'running late hook [archiso]' in
boot output despite UUID match on device).

Explicitly copy /usr/lib/initcpio/hooks/archiso and install/archiso from
the build host into the airootfs before mkarchiso runs mkinitcpio, so
the hook is present regardless of how the chroot package install behaves.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 10:52:05 -04:00
nmemmertandClaude Sonnet 4.6 525bb49787 Fix live boot: custom syslinux.cfg and explicit squashfs/overlay modules
The releng syslinux.cfg references UI .c32 modules that may not load,
causing syslinux to fall back without archisolabel= in the kernel cmdline
so the archiso initrd hook scans for an empty device label and sets root=''.

- Write our own minimal syslinux.cfg using %INSTALL_DIR%/%ARCHISO_LABEL%
  placeholders; Dockerfile now only copies .c32 binaries from releng
- Add squashfs and overlay to mkinitcpio MODULES explicitly
- Remove microcode hook (no intel-ucode/amd-ucode packages installed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 09:36:45 -04:00
nmemmertandClaude Sonnet 4.6 d6f2cbb2b8 Add archiso package to live ISO for working initrd hook
mkinitcpio runs inside the airootfs chroot, so the archiso hook files
(/usr/lib/initcpio/hooks/archiso) must be installed there. Without this
the hook has no device-scanning logic and boots to an empty root mount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 09:11:12 -04:00
nmemmertandClaude Sonnet 4.6 18d3675368 Fix ISO build pipeline for Docker on macOS
- Disable pacman alpm sandbox in Dockerfile (seccomp blocks it in BuildKit)
- Move mkarchiso from RUN to CMD so it runs with --privileged at docker run time
- Fix output path: mkarchiso now writes to /dist (matches volume mount)
- Add syslinux + imlib2 to Docker build packages and live ISO package list
- Copy syslinux/ and efiboot/ boot configs from archiso releng into profile
- Switch deprecated UEFI grub bootmodes to uefi-x64.systemd-boot
- Fix wrong package names: libpam→pam, light→brightnessctl, remove pkexec
- Set SigLevel=Never and hardcode reliable mirrors in profile pacman.conf
- Add mkinitcpio.conf with archiso hook so initrd can mount the live squashfs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 08:27:16 -04:00
nmemmert e2e8c49a0a Merge pull request #1 from nmemmert/claude/elemetryos-macos-fork-mmkshh
OrionOS macOS-style fork — full feature build
2026-08-30 17:13:50 -04:00
Claude 6b1d705d4f Add dual-path ISO build scripts (Arch + Debian) with README
- iso/build.sh: Debian/Ubuntu host builder using debootstrap + xorriso
  Bootstraps Debian bookworm, compiles OrionOS in chroot, produces
  bootable hybrid ISO with GRUB (BIOS + EFI), auto-login root on tty1
- iso/build-arch.sh: Arch Linux host builder using mkarchiso
  Compiles OrionOS, installs into archiso profile, runs mkarchiso
- iso/README.md: instructions for all three paths (Docker/Debian/Arch)
  including dd and Ventoy USB flashing
- Makefile: iso target auto-detects available tool (docker/debootstrap/mkarchiso)
2026-08-30 20:57:40 +00:00
Claude 454f925891 Rewrite orionpkg into full GUI package manager
Previously only an update manager. Now has three tabs:
- Search: pacman -Ss query, shows results with installed badge, Install button
- Installed: pacman -Q list with scrollable rows, Remove button
- Updates: checkupdates list, Update All runs pkexec pacman -Syu

Features:
- Editable search input field with cursor, Enter to search
- Row selection with highlight, double-click toggles
- Scrollable package lists with scrollbar
- Live log view + progress bar during install/remove/upgrade
- Spinner animation during async operations
- Cancel button during running operations
- Error/success status display
- Same dark-navy palette as rest of OrionOS
2026-08-30 20:55:05 +00:00
Claude a5b2c5aee3 Complete archiso profile for OrionOS ISO build
- iso/profile/profiledef.sh: archiso profile with BIOS+UEFI bootmodes
- iso/profile/packages.x86_64: full package list (X11, audio, OrionOS deps)
- iso/profile/pacman.conf: pacman config for archiso
- iso/profile/airootfs/: live overlay (xinitrc, hostname, skel)
- iso/docker/Dockerfile: builds OrionOS from source, installs into airootfs, runs mkarchiso
2026-08-30 20:49:45 +00:00
Claude a2092fd989 Add hot corners, media keys, orionshot, orionclip, notification history, autostart, GTK accent update, wallpaper slideshow, ISO build
- wm: hot corner polling via select()-based wm_run loop; TL=Exposé, TR=show desktop, BL=launcher, BR=lock
- wm: XF86 media key grabs (volume, brightness, playback) + Super+V for clipboard
- wm: autostart reads ~/.config/orionos/autostart on startup
- events: media key dispatch (amixer/brightnessctl/playerctl); Super+V → orionclip --show; Print → orionshot
- orionshot: new X11 screenshot tool, full-screen and rubber-band region modes, saves PNG to ~/Pictures/Screenshots/
- orionclip: new clipboard manager daemon; polls clipboard, stores 20 entries, popup via Super+V IPC
- orionnotify: append-only notification history log at ~/.local/share/orionos/notif-history.log
- themes: GTK accent updated from #3A6898 to #1E58A8 to match WM/deskbar
- themes: orionwallpaper gains --slideshow DIR [SECS] mode using feh + sleep loop
- iso: Docker-based archiso builder, live-session xinitrc, profiledef.sh
- Makefile: add orionshot and orionclip to all/clean/install; Docker-based iso target
2026-08-30 20:47:47 +00:00
Claude 2a42b826fb Add 6 desktop features: workspace shortcuts, Exposé, deskbar icons, notifications, screen lock, move-to-desktop
- Super+1-4: switch virtual desktop; Super+Shift+1-4: move focused window to desktop N
- Super+Space: Exposé window overview (tiled grid), click to select, Escape to cancel
- wm/src/expose.c: expose_show/expose_hide using ceil(sqrt) grid layout with saved/restored geometry
- client_move_to_desktop(): moves window between desktops, fires _NET_WM_DESKTOP update
- on_client_message: handles _NET_WM_DESKTOP to move windows from external requests
- deskbar: optional imlib2 app icon rendering (Papirus-Dark → Papirus → hicolor → pixmaps fallback)
- deskbar: XGetClassHint populates cls field for better icon lookup
- deskbar: context menu extended with separator + "→ Desktop 1-4" items via _NET_WM_DESKTOP
- orionnotify/: freedesktop.org notification daemon (libdbus-1), stacks up to 5 popups top-right
- orionlock/: PAM-authenticated fullscreen screen lock with clock, date, password dots
- Lock Screen menu item wired to orionlock
- Root Makefile includes orionnotify and orionlock targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHiBQKPfUoaJgCKuA9op1H
2026-08-30 20:14:45 +00:00
Claude b0f64d9b2e Fixes #7–#11: launcher Other tab, ICCCM size hints, icon loading, multi-desktop
Fix #7: Add "Other" catch-all category to the launcher. map_category() now
returns "Other" instead of "" for unrecognized .desktop Categories, and an
"Other" tab is added to the CATS list so those apps are discoverable instead
of silently hidden.

Fix #8: Respect ICCCM WM_NORMAL_HINTS. client_add() reads PMinSize,
PResizeInc, and PBaseSize from the window's size hints. The resize drag
handler in events.c clamps to min_w/min_h and snaps to the
width_inc/height_inc grid, fixing jank in character-cell terminal emulators
(xterm, Alacritty).

Fix #10: Optional freedesktop icon loading via imlib2. The launcher now
parses the Icon= field from .desktop files and attempts to render a PNG icon
from Papirus-Dark (or hicolor fallback). When imlib2 is not present the
existing letter-square is shown unchanged. launcher/Makefile auto-detects
imlib2 via pkg-config and sets -DHAVE_IMLIB2.

Fix #11: True 4-desktop support in the WM. Each Client carries a desktop
field (assigned at map time from wm->cur_desktop). Clicking a workspace
button in the deskbar sends _NET_CURRENT_DESKTOP; the WM now handles it via
wm_switch_desktop() which unmaps frames on the leaving desktop, maps frames
on the arriving desktop, and re-focuses. _NET_WM_DESKTOP is set per window
so the deskbar can filter db_update_apps() to show only current-desktop
windows (sticky windows with desktop 0xFFFFFFFF are always shown).
2026-08-30 20:00:10 +00:00
Claude 36641e77e2 macOS-style polish: fix seam, bidirectional Alt+Tab, minimized windows in switcher, rounded corners, deskbar context menu
Fix #1: Replace hardcoded 185px deskbar-offset in client.c and events.c with
DESKBAR_W constant (200) to eliminate the 15px gap between the WM frame and
deskbar edge.

Fix #2: Add Shift+Alt+Tab reverse-direction support — grab Mod1+Shift+XK_Tab
in wm.c, detect ShiftMask in events.c, and implement switcher_prev() in
switcher.c.

Fix #3: Include minimized windows in the Alt+Tab switcher. swl_populate() no
longer skips minimized clients; swl_mini[] tracks state so switcher_draw()
can dim them with bracketed titles and darker card backgrounds.

Fix #4: Rewrite themes/picom.conf with macOS-inspired settings — 10px corner
radius on normal windows, 8px on menus, soft downward shadow (radius 18,
offset-y 8, opacity 0.35), smooth fading (step 0.08), no dock/tray rounding.

Fix #5: Add per-app right-click context menu to the deskbar. Right-clicking
an app row opens a popup with Raise / Minimize / Close actions (Close shown
in red). Actions are sent as EWMH client messages (_NET_ACTIVE_WINDOW,
WM_CHANGE_STATE+IconicState, _NET_CLOSE_WINDOW) with matching handlers added
to the WM's on_client_message. Pointer grab closes the menu on any outside
click.
2026-08-30 19:51:54 +00:00
Claude 0d874617dd Fork ElemetryOS with macOS-style window decorations
- Move traffic-light buttons to the LEFT side of titlebars (close/minimize/maximize)
  in macOS order, using fixed left-pad positions instead of right-edge offsets
- Adopt macOS dark-mode color palette: #3C3C3C active titlebar, #2C2C2C inactive,
  traffic-light colors #FF5F57/#FEBC2E/#28C840, near-black #1C1C1C desktop
- Center window title text in the titlebar (macOS style), clamped to avoid
  overlapping the left button cluster
- Thin 1px window borders replacing the original 3px beveled border
- macOS blue (#4A90D9) selection highlight in the context menu
- Neutral dark context menu (#2C2C2C) with subtle separators and text indent
- Update README to document the ElemetryOS macOS fork identity and design changes
2026-08-30 19:40:11 +00:00
54 changed files with 3880 additions and 778 deletions

No files matched your search

+40 -5
View File
@@ -5,9 +5,9 @@
# make clean — remove build artifacts
# make iso — build the bootable ISO (requires Arch Linux + archiso)
.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens clean install iso
.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock orionshot orionclip clean install iso
all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens
all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock orionshot orionclip
wm:
$(MAKE) -C wm
@@ -39,6 +39,18 @@ quill:
lens:
$(MAKE) -C lens
orionnotify:
$(MAKE) -C orionnotify
orionlock:
$(MAKE) -C orionlock
orionshot:
$(MAKE) -C orionshot
orionclip:
$(MAKE) -C orionclip
clean:
$(MAKE) -C wm clean
$(MAKE) -C deskbar clean
@@ -49,7 +61,11 @@ clean:
$(MAKE) -C skykit clean
$(MAKE) -C rigel clean
$(MAKE) -C quill clean
$(MAKE) -C lens clean
$(MAKE) -C lens clean
$(MAKE) -C orionnotify clean
$(MAKE) -C orionlock clean
$(MAKE) -C orionshot clean
$(MAKE) -C orionclip clean
install: all
# binaries
@@ -62,6 +78,10 @@ install: all
install -Dm755 rigel/rigel /usr/local/bin/rigel
install -Dm755 quill/quill /usr/local/bin/quill
install -Dm755 lens/lens /usr/local/bin/lens
install -Dm755 orionnotify/orionnotify /usr/local/bin/orionnotify
install -Dm755 orionlock/orionlock /usr/local/bin/orionlock
install -Dm755 orionshot/orionshot /usr/local/bin/orionshot
install -Dm755 orionclip/orionclip /usr/local/bin/orionclip
# GTK theme
mkdir -p /usr/share/themes
cp -r themes/OrionOS /usr/share/themes/
@@ -77,8 +97,23 @@ install: all
@echo " orionwallpaper"
@echo " picom --config /etc/orionos/picom.conf &"
@echo " oriondeskbar &"
@echo " orionnotify &"
@echo " orionclip &"
@echo " exec orionwm"
iso:
chmod +x build-iso.sh
./build-iso.sh
@if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then \
echo "Building OrionOS ISO via Docker (Arch)..."; \
docker build -t orionos-iso-builder -f iso/docker/Dockerfile .; \
mkdir -p iso/output; \
docker run --rm --privileged -v "$$(pwd)/iso/output:/output" orionos-iso-builder; \
elif command -v debootstrap >/dev/null 2>&1; then \
echo "Building OrionOS ISO via debootstrap (Debian/Ubuntu)..."; \
sudo bash iso/build.sh; \
elif command -v mkarchiso >/dev/null 2>&1; then \
echo "Building OrionOS ISO via mkarchiso (Arch native)..."; \
sudo bash iso/build-arch.sh; \
else \
echo "No ISO build tool found. See iso/README.md for options."; \
exit 1; \
fi
+13 -3
View File
@@ -1,12 +1,22 @@
# OrionOS
# OrionOS — ElemetryOS macOS Fork
A BeOS-inspired Linux desktop built from scratch in C. Custom window manager, panel, app launcher, settings panel, package manager, and graphical installer — all running on Arch Linux, shipped as a bootable ISO.
A macOS-inspired Linux desktop forked from ElemetryOS, built from scratch in C. Custom window manager, panel, app launcher, settings panel, package manager, and graphical installer — all running on Arch Linux, shipped as a bootable ISO.
---
## What it is
OrionOS is a complete desktop environment built on X11/Xlib with a dark navy aesthetic. It ships as a bootable ISO you can try live or install to disk. Every shell component is written in native C with no toolkit dependency — just Xlib and Xft.
OrionOS is a complete desktop environment built on X11/Xlib with a macOS dark-mode aesthetic. It ships as a bootable ISO you can try live or install to disk. Every shell component is written in native C with no toolkit dependency — just Xlib and Xft.
### macOS design language
This fork re-skins the window manager with macOS design conventions:
- **Traffic-light buttons on the left** — close (red), minimize (yellow), maximize (green), just like macOS
- **macOS dark-mode palette** — near-black desktop, `#3C3C3C` active titlebars, `#2C2C2C` inactive
- **Centered window titles** — title text centered in the titlebar, macOS-style
- **Thin 1 px borders** — minimal window chrome, no thick bevels
- **macOS blue selection** (`#4A90D9`) in context menus and snap overlays
### Components
+1 -1
View File
@@ -66,7 +66,7 @@ docker run \
# ── rename with date tag ──────────────────────────────────────────────
echo ""
echo "Step 3/3: Finalizing..."
ISO="$(find "$DIST" -name "orionos*.iso" | head -1)"
ISO="$(find "$DIST" -iname "*.iso" | head -1)"
if [[ -z "$ISO" ]]; then
echo "ERROR: ISO not found in $DIST — check the build output above."
exit 1
+8
View File
@@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),)
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
endif
# optional imlib2 for app icon rendering
PKG_IL_C := $(shell pkg-config --cflags imlib2 2>/dev/null)
PKG_IL_L := $(shell pkg-config --libs imlib2 2>/dev/null)
ifneq ($(PKG_IL_C),)
PKG_XFT_C += $(PKG_IL_C) -DHAVE_IMLIB2
PKG_XFT_L += $(PKG_IL_L)
endif
CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C)
LDFLAGS = $(PKG_XFT_L)
+15
View File
@@ -44,6 +44,14 @@
#define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */
#define DB_MAX_APPS 32
/* ── per-app context menu ────────────────────────────────────────────── */
#define DB_CTX_W 160 /* context menu width */
#define DB_CTX_ITEM_H 26 /* item row height */
#define DB_CTX_PAD 4 /* top/bottom padding */
#define DB_CTX_N 3 /* base items: Raise, Minimize, Close */
#define DB_CTX_NDESK 4 /* number of "→ Desktop N" items */
#define DB_CTX_SEP_H 6 /* separator line height */
/* ── fly-out menu ────────────────────────────────────────────────────── */
#define DB_FLYOUT_W 196 /* fly-out menu panel width */
#define DB_FLYOUT_IH 28 /* item row height */
@@ -96,8 +104,10 @@ static const unsigned long DB_ICON_COLORS[] = {
typedef struct AppEntry {
Window win;
char title[64];
char cls[64]; /* WM_CLASS instance for icon lookup */
int focused;
int minimized;
int desktop; /* which desktop this window is on */
} AppEntry;
typedef struct Deskbar {
@@ -139,6 +149,11 @@ typedef struct Deskbar {
int sub_hov; /* hovered item in sub-menu */
int sw_cache; /* cached screen width for menu positioning */
/* per-app right-click context menu */
Window ctx_win; /* None when closed */
int ctx_app_idx; /* which app row the ctx menu is for */
int ctx_hov; /* hovered item index, -1 = none */
/* system tray (freedesktop.org XEmbed tray protocol) */
Window tray_icons[DB_TRAY_MAX];
int n_tray_icons;
+323 -5
View File
@@ -1,5 +1,9 @@
#include "deskbar.h"
#ifdef HAVE_IMLIB2
#include <Imlib2.h>
#endif
#define TRAY_REQUEST_DOCK 0
#define XEMBED_EMBEDDED_NOTIFY 0
#define XEMBED_VERSION 0
@@ -111,8 +115,14 @@ void db_update_apps(Deskbar *db) {
Atom net_wm_state = XInternAtom(db->dpy, "_NET_WM_STATE", False);
Atom net_wm_state_hidden = XInternAtom(db->dpy, "_NET_WM_STATE_HIDDEN", False);
Atom net_wm_desktop = XInternAtom(db->dpy, "_NET_WM_DESKTOP", False);
for (unsigned long i = 0; i < n && db->napps < DB_MAX_APPS; i++) {
/* only show windows on the current desktop (or sticky: desktop == 0xFFFFFFFF) */
long win_desk = ewmh_get_long(db, wins[i], net_wm_desktop);
if (win_desk >= 0 && win_desk != (long)db->cur_ws && win_desk != (long)0xFFFFFFFF)
continue;
db->apps[db->napps].win = wins[i];
db->apps[db->napps].focused = (wins[i] == (Window)active_win);
db->apps[db->napps].minimized = 0;
@@ -135,6 +145,24 @@ void db_update_apps(Deskbar *db) {
ewmh_get_title(db, wins[i],
db->apps[db->napps].title,
sizeof(db->apps[db->napps].title));
/* WM_CLASS instance for icon lookup */
db->apps[db->napps].cls[0] = '\0';
{
XClassHint ch = {NULL, NULL};
if (XGetClassHint(db->dpy, wins[i], &ch)) {
if (ch.res_name) {
strncpy(db->apps[db->napps].cls, ch.res_name,
sizeof(db->apps[db->napps].cls) - 1);
XFree(ch.res_name);
}
if (ch.res_class) XFree(ch.res_class);
}
}
/* store which desktop this window is on */
db->apps[db->napps].desktop = (int)win_desk;
if (db->apps[db->napps].title[0] != '\0')
db->napps++;
}
@@ -420,12 +448,52 @@ void db_draw(Deskbar *db) {
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);
/* icon: imlib2 if available, else letter-square */
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);
int icon_drawn = 0;
#ifdef HAVE_IMLIB2
{
static const char *dirs[] = {
"/usr/share/icons/Papirus-Dark/22x22/apps",
"/usr/share/icons/Papirus/22x22/apps",
"/usr/share/icons/hicolor/22x22/apps",
"/usr/share/icons/hicolor/32x32/apps",
"/usr/share/pixmaps",
NULL
};
const char *names[2] = { db->apps[i].cls, db->apps[i].title };
for (int ni = 0; ni < 2 && !icon_drawn; ni++) {
if (!names[ni][0]) continue;
for (int di = 0; dirs[di] && !icon_drawn; di++) {
char path[512];
const char *exts[] = { "png", "svg", "xpm", NULL };
for (int ei = 0; exts[ei] && !icon_drawn; ei++) {
snprintf(path, sizeof(path), "%s/%s.%s",
dirs[di], names[ni], exts[ei]);
imlib_context_set_display(db->dpy);
imlib_context_set_visual(db->visual);
imlib_context_set_colormap(db->cmap);
imlib_context_set_drawable(db->win);
Imlib_Image img = imlib_load_image_immediately(path);
if (img) {
imlib_context_set_image(img);
imlib_context_set_blend(1);
imlib_render_image_on_drawable_at_size(
icon_x, icon_y, DB_ICON_SZ, DB_ICON_SZ);
imlib_free_image();
icon_drawn = 1;
}
}
}
}
}
#endif
if (!icon_drawn) {
unsigned long icon_bg = db_app_icon_color(db->apps[i].title);
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;
@@ -578,7 +646,7 @@ static const MI MENU[] = {
{ "Settings", NULL, 0, 1, 1 },
{ NULL, NULL, 1, 0,-1 },
{ "About OrionOS", "alacritty -e bash -c 'fastfetch; read' &", 0, 0,-1 },
{ "Lock Screen", "xscreensaver-command -lock &", 0, 0,-1 },
{ "Lock Screen", "orionlock &", 0, 0,-1 },
{ NULL, NULL, 1, 0,-1 },
{ "Restart", "systemctl reboot", 0, 0,-1 },
{ "Shut Down", "systemctl poweroff", 0, 0,-1 },
@@ -809,6 +877,222 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) {
db_menu_close(db);
}
/* ── per-app context menu ─────────────────────────────────────────────── */
static int ctx_total_h(void) {
return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H
+ DB_CTX_SEP_H + DB_CTX_NDESK * DB_CTX_ITEM_H
+ DB_CTX_PAD;
}
/* y offset of a context-menu item (base items, then sep, then desk items) */
static int ctx_item_y(int idx) {
if (idx < DB_CTX_N)
return DB_CTX_PAD + idx * DB_CTX_ITEM_H;
/* desktop items come after separator */
return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H
+ DB_CTX_SEP_H + (idx - DB_CTX_N) * DB_CTX_ITEM_H;
}
static int ctx_item_at(int lx) {
/* lx = ry - ctx_win_y */
int total = DB_CTX_N + DB_CTX_NDESK;
for (int i = 0; i < total; i++) {
int iy = ctx_item_y(i);
if (lx >= iy && lx < iy + DB_CTX_ITEM_H) return i;
}
return -1;
}
static void db_ctx_draw(Deskbar *db) {
if (!db->ctx_win) return;
int ww = DB_CTX_W;
int wh = ctx_total_h();
int mini = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps)
? db->apps[db->ctx_app_idx].minimized : 0;
int cur_desk = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps)
? db->apps[db->ctx_app_idx].desktop : -1;
static const char *labels_normal[] = { "Raise", "Minimize", "Close" };
static const char *labels_mini[] = { "Restore", "Restore", "Close" };
const char **labels = mini ? labels_mini : labels_normal;
XftDraw *xd = XftDrawCreate(db->dpy, db->ctx_win, db->visual, db->cmap);
/* background */
XSetForeground(db->dpy, db->gc, db_color(db, 0x2C2C2C));
XFillRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww, wh);
/* 1px border */
XSetForeground(db->dpy, db->gc, db_color(db, 0x505050));
XDrawRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww - 1, wh - 1);
/* base items */
for (int i = 0; i < DB_CTX_N; i++) {
int iy = ctx_item_y(i);
if (i == db->ctx_hov) {
XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9));
XFillRectangle(db->dpy, db->ctx_win, db->gc,
2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2);
}
unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF
: (i == DB_CTX_N - 1) ? 0xFF6B6B
: 0xE8E8E8;
draw_str(db, xd, db->font_ui, tc,
14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H),
labels[i], 0, 0);
}
/* separator */
{
int sep_y = DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_SEP_H / 2;
XSetForeground(db->dpy, db->gc, db_color(db, 0x505050));
XDrawLine(db->dpy, db->ctx_win, db->gc, 8, sep_y, ww - 9, sep_y);
}
/* "→ Desktop N" items */
for (int d = 0; d < DB_CTX_NDESK; d++) {
int item_idx = DB_CTX_N + d;
int iy = ctx_item_y(item_idx);
if (item_idx == db->ctx_hov) {
XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9));
XFillRectangle(db->dpy, db->ctx_win, db->gc,
2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2);
}
char label[32];
snprintf(label, sizeof(label), "\xe2\x86\x92 Desktop %d", d + 1);
unsigned long tc = (item_idx == db->ctx_hov) ? 0xFFFFFF
: (d == cur_desk) ? 0x88AACC /* current = dimmed */
: 0xE8E8E8;
draw_str(db, xd, db->font_ui, tc,
14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H),
label, 0, 0);
}
XftDrawDestroy(xd);
XFlush(db->dpy);
}
static void db_ctx_close(Deskbar *db) {
if (!db->ctx_win) return;
XUngrabPointer(db->dpy, CurrentTime);
XDestroyWindow(db->dpy, db->ctx_win);
db->ctx_win = None;
db->ctx_app_idx = -1;
db->ctx_hov = -1;
}
static void db_ctx_open(Deskbar *db, int app_idx, int rx, int ry) {
db_ctx_close(db);
int sw = DisplayWidth(db->dpy, db->screen);
int sh = DisplayHeight(db->dpy, db->screen);
int wh = ctx_total_h();
int wx = sw - DB_W - DB_CTX_W - 2;
int wy = ry;
if (wy + wh > sh) wy = sh - wh - 2;
if (wy < 0) wy = 0;
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = db_color(db, 0x2C2C2C);
swa.event_mask = ExposureMask;
db->ctx_win = XCreateWindow(
db->dpy, db->root, wx, wy, DB_CTX_W, wh, 1,
db->depth, InputOutput, db->visual,
CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
XSetWindowBorder(db->dpy, db->ctx_win, db_color(db, 0x505050));
db->ctx_app_idx = app_idx;
db->ctx_hov = -1;
XMapRaised(db->dpy, db->ctx_win);
db_ctx_draw(db);
XGrabPointer(db->dpy, db->ctx_win, True,
ButtonPressMask | PointerMotionMask | ButtonReleaseMask,
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
}
static void db_ctx_handle_motion(Deskbar *db, int rx, int ry) {
if (!db->ctx_win) return;
int cx, cy; unsigned cw, ch, bw, dep; Window cr;
XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep);
if (rx < cx || ry < cy || rx >= cx + (int)cw || ry >= cy + (int)ch) {
if (db->ctx_hov != -1) { db->ctx_hov = -1; db_ctx_draw(db); }
return;
}
int idx = ctx_item_at(ry - cy);
if (idx != db->ctx_hov) { db->ctx_hov = idx; db_ctx_draw(db); }
}
static void db_ctx_handle_press(Deskbar *db, int rx, int ry) {
if (!db->ctx_win) return;
int cx, cy; unsigned cw, ch, bw, dep; Window cr;
XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep);
int app_idx = db->ctx_app_idx;
int in_menu = (rx >= cx && ry >= cy && rx < cx + (int)cw && ry < cy + (int)ch);
if (!in_menu || app_idx < 0 || app_idx >= db->napps) {
db_ctx_close(db);
return;
}
int idx = ctx_item_at(ry - cy);
Window target = db->apps[app_idx].win;
int mini = db->apps[app_idx].minimized;
db_ctx_close(db);
if (idx < 0) return;
XEvent ev = {0};
ev.xclient.type = ClientMessage;
ev.xclient.format = 32;
if (idx == 0) {
/* Raise / Restore */
ev.xclient.window = target;
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False);
ev.xclient.data.l[0] = 2;
ev.xclient.data.l[1] = CurrentTime;
XSendEvent(db->dpy, db->root, False,
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
} else if (idx == 1) {
if (mini) {
ev.xclient.window = target;
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False);
ev.xclient.data.l[0] = 2;
ev.xclient.data.l[1] = CurrentTime;
} else {
ev.xclient.window = target;
ev.xclient.message_type = XInternAtom(db->dpy, "WM_CHANGE_STATE", False);
ev.xclient.data.l[0] = 3; /* IconicState */
}
XSendEvent(db->dpy, db->root, False,
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
} else if (idx == 2) {
ev.xclient.window = target;
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CLOSE_WINDOW", False);
ev.xclient.data.l[0] = CurrentTime;
ev.xclient.data.l[1] = 2;
XSendEvent(db->dpy, db->root, False,
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
} else if (idx >= DB_CTX_N && idx < DB_CTX_N + DB_CTX_NDESK) {
/* Move to Desktop N via _NET_WM_DESKTOP */
int new_desk = idx - DB_CTX_N;
ev.xclient.window = target;
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_WM_DESKTOP", False);
ev.xclient.data.l[0] = new_desk;
ev.xclient.data.l[1] = 2;
XSendEvent(db->dpy, db->root, False,
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
}
}
/* ── input ────────────────────────────────────────────────────────────── */
void db_hover(Deskbar *db, int x, int y) {
@@ -981,6 +1265,26 @@ void db_run(Deskbar *db) {
XEvent e;
XNextEvent(db->dpy, &e);
/* While the per-app context menu is open, the pointer is grabbed.
Route motion and button events through the ctx handlers. */
if (db->ctx_win) {
if (e.type == MotionNotify) {
db_ctx_handle_motion(db, e.xmotion.x_root, e.xmotion.y_root);
continue;
}
if (e.type == ButtonPress) {
db_ctx_handle_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->ctx_win) db_ctx_draw(db);
else if (e.xany.window == db->win) db_draw(db);
continue;
}
}
/* 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). */
@@ -1026,6 +1330,18 @@ void db_run(Deskbar *db) {
system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &");
break;
}
if (e.xbutton.button == Button3) {
/* right-click on an app row → open context menu */
int ay = db_apps_y();
int by = e.xbutton.y;
if (by >= ay && by < ay + db->napps * DB_ITEM_H) {
int idx = (by - ay) / DB_ITEM_H;
if (idx >= 0 && idx < db->napps)
db_ctx_open(db, idx,
e.xbutton.x_root, e.xbutton.y_root);
}
break;
}
if (e.xbutton.button == Button1 && e.xbutton.y >= sy)
system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &");
else
@@ -1091,6 +1407,8 @@ void db_run(Deskbar *db) {
db_draw(db);
} else if (e.xproperty.atom == db->net_current_desktop) {
db_update_workspace(db);
db_update_apps(db); /* re-filter by new desktop */
db_resize(db);
db_draw(db);
}
break;
+15 -15
View File
@@ -152,8 +152,8 @@ progress 14 "Mounting partitions…"
info "Mounting..."
mount "$ROOT_PART" /mnt
if [[ "$BOOT_MODE" == "uefi" ]]; then
mkdir -p /mnt/boot/efi
mount "$EFI_PART" /mnt/boot/efi
mkdir -p /mnt/boot
mount "$EFI_PART" /mnt/boot
fi
ok "Mounted."
@@ -197,9 +197,11 @@ info "Configuring system..."
arch-chroot /mnt /bin/bash -s "$HOSTNAME" "$USERNAME" "$PASSWORD" \
"$BOOT_MODE" "$ROOT_PART" \
"${TIMEZONE:-UTC}" "${KEYBOARD:-us}" \
<<'CHROOT'
HOSTNAME="$1"; USERNAME="$2"; PASSWORD="$3"
BOOT_MODE="$4"; ROOT_PART="$5"
TIMEZONE="${6:-UTC}"; KEYBOARD="${7:-us}"
# Hostname
echo "$HOSTNAME" > /etc/hostname
@@ -268,22 +270,20 @@ mkinitcpio -P
# Bootloader
if [[ "$BOOT_MODE" == "uefi" ]]; then
bootctl install 2>/dev/null || true
bootctl --esp-path=/boot install 2>/dev/null || true
ROOT_UUID="$(blkid -s UUID -o value "$ROOT_PART")"
mkdir -p /boot/efi/loader/entries
cat > /boot/efi/loader/loader.conf <<'LC'
default skyos.conf
mkdir -p /boot/loader/entries
cat > /boot/loader/loader.conf <<'LC'
default orionos.conf
timeout 5
editor no
LC
cat > /boot/efi/loader/entries/skyos.conf <<ENTRY
title OrionOS
linux /vmlinuz-linux
initrd /amd-ucode.img
initrd /intel-ucode.img
initrd /initramfs-linux.img
options root=UUID=${ROOT_UUID} rw quiet loglevel=3
ENTRY
# Build initrd lines — only include microcode if the image exists
INITRD_LINES="initrd /initramfs-linux.img"
[[ -f /boot/amd-ucode.img ]] && INITRD_LINES="initrd /amd-ucode.img\n${INITRD_LINES}"
[[ -f /boot/intel-ucode.img ]] && INITRD_LINES="initrd /intel-ucode.img\n${INITRD_LINES}"
printf "title OrionOS\nlinux /vmlinuz-linux\n%b\noptions root=UUID=%s rw quiet loglevel=3\n" \
"$INITRD_LINES" "$ROOT_UUID" > /boot/loader/entries/orionos.conf
else
# Find the whole disk (strip trailing partition number)
DISK="${ROOT_PART%[0-9]}"
@@ -305,7 +305,7 @@ ok "System configured."
progress 97 "Unmounting…"
info "Unmounting..."
sync
[[ "$BOOT_MODE" == "uefi" ]] && umount /mnt/boot/efi 2>/dev/null || true
[[ "$BOOT_MODE" == "uefi" ]] && umount /mnt/boot 2>/dev/null || true
umount /mnt
ok "Done."
+51
View File
@@ -0,0 +1,51 @@
# OrionOS ISO Builder
Two build paths are provided. Pick the one that matches your host.
## Path A — Arch Linux host (native, recommended)
Requires `archiso` and `pacman-contrib`.
```bash
sudo pacman -S archiso
sudo make iso # runs iso/build-arch.sh
```
## Path B — Debian / Ubuntu host
Requires `xorriso`, `squashfs-tools`, `debootstrap`, `grub-pc-bin`, `grub-efi-amd64-bin`.
```bash
sudo apt-get install -y xorriso squashfs-tools debootstrap \
grub-pc-bin grub-efi-amd64-bin mtools
sudo bash iso/build.sh
```
Output lands in `iso/output/OrionOS-YYYYMMDD.iso`.
## Path C — Docker (any host with Docker)
```bash
make iso
```
The Dockerfile (`iso/docker/Dockerfile`) builds inside `archlinux:latest`,
compiles OrionOS, and runs `mkarchiso`. Output is written to `iso/output/`.
## Writing the ISO to a USB drive
```bash
# Find your USB device (e.g. /dev/sdb) — be careful
lsblk
sudo dd if=iso/output/OrionOS-*.iso of=/dev/sdX bs=4M status=progress oflag=sync
```
Or use [Ventoy](https://ventoy.net) — just copy the `.iso` onto the Ventoy USB.
## First boot
- Logs in automatically as root on tty1 and starts X / OrionOS.
- Super+F1 or bottom-left hot corner → launcher
- Super+V → clipboard manager
- Print → screenshot (Shift+Print → region)
- Top-left hot corner → Exposé overview
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# OrionOS live session autostart — sourced by ~/.xinitrc on the ISO
# Compositor
picom --config /etc/orionos/picom.conf &
# Set wallpaper
orionwallpaper &
# Notification daemon
orionnotify &
# Clipboard manager
orionclip &
# Deskbar
oriondeskbar &
# Window manager (blocking)
exec orionwm
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
source /etc/orionos/xinitrc
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# OrionOS ISO builder — Arch Linux host using mkarchiso
# Run as root from the repo root: sudo bash iso/build-arch.sh
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
OUT="$REPO/iso/output"
echo "==> Building OrionOS binaries..."
cd "$REPO"
make all
echo "==> Installing OrionOS binaries into profile airootfs..."
install -Dm755 wm/orionwm iso/profile/airootfs/usr/local/bin/orionwm
install -Dm755 deskbar/oriondeskbar iso/profile/airootfs/usr/local/bin/oriondeskbar
install -Dm755 launcher/orionlauncher iso/profile/airootfs/usr/local/bin/orionlauncher
install -Dm755 orionnotify/orionnotify iso/profile/airootfs/usr/local/bin/orionnotify
install -Dm755 orionlock/orionlock iso/profile/airootfs/usr/local/bin/orionlock
install -Dm755 orionshot/orionshot iso/profile/airootfs/usr/local/bin/orionshot
install -Dm755 orionclip/orionclip iso/profile/airootfs/usr/local/bin/orionclip
install -Dm755 orionpkg/orionpkg iso/profile/airootfs/usr/local/bin/orionpkg
install -Dm755 themes/orionwallpaper iso/profile/airootfs/usr/local/bin/orionwallpaper
mkdir -p iso/profile/airootfs/usr/share/themes
cp -r themes/OrionOS iso/profile/airootfs/usr/share/themes/
install -Dm644 themes/picom.conf iso/profile/airootfs/etc/orionos/picom.conf
mkdir -p iso/profile/airootfs/usr/share/orionos/wallpapers
bash themes/wallpapers/generate-default.sh 2>/dev/null || true
cp -f themes/wallpapers/orionos-default.png \
iso/profile/airootfs/usr/share/orionos/wallpapers/ 2>/dev/null || true
echo "==> Running mkarchiso..."
mkdir -p "$OUT"
mkarchiso -v -w /tmp/orionos-arch-work -o "$OUT" iso/profile
echo ""
echo "✓ ISO built in $OUT"
ls -lh "$OUT"/*.iso 2>/dev/null || true
Executable
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# OrionOS ISO builder (Debian-based live system)
# Run as root from the repo root: sudo bash iso/build.sh
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
WORK="/tmp/orionos-build"
OUT="$REPO/iso/output"
FS="$WORK/rootfs"
ISO_STAGE="$WORK/iso"
echo "==> OrionOS ISO builder"
echo " repo: $REPO"
echo " work: $WORK"
echo " output: $OUT"
mkdir -p "$FS" "$ISO_STAGE/live" "$ISO_STAGE/boot/grub" "$OUT"
# ── 1. Bootstrap minimal Debian base ────────────────────────────────────
echo "==> [1/7] Bootstrapping Debian bookworm..."
if [[ ! -f "$FS/bin/bash" ]]; then
debootstrap --arch=amd64 \
--include=linux-image-amd64,live-boot,systemd-sysv,sudo,bash,\
coreutils,util-linux,procps,grep,gawk,sed,tar,gzip,less,vim,git,\
xorg,xinit,xterm,feh,picom,alsa-utils,libx11-6,libxft2,libxfixes3,\
libxext6,libpng16-16,dbus,libpam0g,fontconfig,fonts-dejavu,\
fonts-noto,libdbus-1-3,notify-send,bash-completion,network-manager \
bookworm "$FS" 2>&1 | grep -E '(I:|W:|E:|error|warn)' || true
else
echo " (rootfs already exists, skipping bootstrap)"
fi
# ── 2. Compile OrionOS inside chroot ────────────────────────────────────
echo "==> [2/7] Compiling OrionOS components..."
mkdir -p "$FS/build"
cp -r "$REPO"/{wm,deskbar,launcher,orionnotify,orionlock,orionshot,orionclip,orionpkg,themes} "$FS/build/" 2>/dev/null || true
chroot "$FS" /bin/bash -c '
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --no-install-recommends \
gcc make pkg-config \
libx11-dev libxft-dev libxfixes-dev libxext-dev \
libpng-dev libdbus-1-dev libpam0g-dev \
2>&1 | tail -3
cd /build
for dir in wm deskbar launcher orionnotify orionlock orionshot orionclip orionpkg; do
if [[ -d "$dir" ]]; then
echo " building $dir..."
make -C "$dir" 2>&1 | tail -2 || echo " WARNING: $dir build failed"
fi
done
' 2>&1
# ── 3. Install OrionOS binaries ──────────────────────────────────────────
echo "==> [3/7] Installing OrionOS binaries..."
chroot "$FS" /bin/bash -c '
set -e
install_if() { [[ -f "/build/$1" ]] && install -Dm755 "/build/$1" "/usr/local/bin/$2"; }
install_if wm/orionwm orionwm
install_if deskbar/oriondeskbar oriondeskbar
install_if launcher/orionlauncher orionlauncher
install_if orionnotify/orionnotify orionnotify
install_if orionlock/orionlock orionlock
install_if orionshot/orionshot orionshot
install_if orionclip/orionclip orionclip
install_if orionpkg/orionpkg orionpkg
install -Dm755 /build/themes/orionwallpaper /usr/local/bin/orionwallpaper
mkdir -p /usr/share/themes
cp -r /build/themes/OrionOS /usr/share/themes/ 2>/dev/null || true
mkdir -p /etc/orionos
cp /build/themes/picom.conf /etc/orionos/ 2>/dev/null || true
mkdir -p /usr/share/orionos/wallpapers
bash /build/themes/wallpapers/generate-default.sh 2>/dev/null || true
cp -f /build/themes/wallpapers/orionos-default.png /usr/share/orionos/wallpapers/ 2>/dev/null || true
' 2>&1
# ── 4. Configure live session ────────────────────────────────────────────
echo "==> [4/7] Configuring live session..."
cat > "$FS/etc/orionos/xinitrc" <<'XINITRC'
#!/bin/bash
picom --config /etc/orionos/picom.conf &
orionwallpaper &
orionnotify &
orionclip &
oriondeskbar &
exec orionwm
XINITRC
chmod 755 "$FS/etc/orionos/xinitrc"
cat > "$FS/root/.xinitrc" <<'XINIRC'
#!/bin/bash
source /etc/orionos/xinitrc
XINIRC
chmod 755 "$FS/root/.xinitrc"
# Auto-login and startx on tty1
mkdir -p "$FS/etc/systemd/system/getty@tty1.service.d"
cat > "$FS/etc/systemd/system/getty@tty1.service.d/autologin.conf" <<'UNIT'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM
UNIT
cat > "$FS/root/.bash_profile" <<'PROFILE'
[[ -z "$DISPLAY" && "$XDG_VTNR" == 1 ]] && exec startx
PROFILE
# hostname
echo "orionos" > "$FS/etc/hostname"
# Clean up build artifacts to save space
chroot "$FS" /bin/bash -c '
apt-get remove -y gcc make pkg-config \
libx11-dev libxft-dev libxfixes-dev libxext-dev \
libpng-dev libdbus-1-dev libpam0g-dev 2>&1 | tail -2 || true
apt-get autoremove -y 2>&1 | tail -2 || true
apt-get clean
rm -rf /build /var/cache/apt /tmp/*
' 2>&1
# ── 5. Build squashfs ────────────────────────────────────────────────────
echo "==> [5/7] Building squashfs filesystem..."
mksquashfs "$FS" "$ISO_STAGE/live/filesystem.squashfs" \
-comp zstd -Xcompression-level 6 \
-e boot -noappend 2>&1 | tail -3
# ── 6. Copy kernel + initrd ──────────────────────────────────────────────
echo "==> [6/7] Copying kernel and initrd..."
VMLINUZ="$(ls "$FS/boot/vmlinuz-"* 2>/dev/null | head -1)"
INITRD="$(ls "$FS/boot/initrd.img-"* 2>/dev/null | head -1)"
if [[ -z "$VMLINUZ" || -z "$INITRD" ]]; then
echo "ERROR: kernel or initrd not found in $FS/boot/"
ls "$FS/boot/" || true
exit 1
fi
cp "$VMLINUZ" "$ISO_STAGE/boot/vmlinuz"
cp "$INITRD" "$ISO_STAGE/boot/initrd.img"
# ── 7. GRUB bootloader + ISO ─────────────────────────────────────────────
echo "==> [7/7] Building bootable ISO with GRUB..."
cat > "$ISO_STAGE/boot/grub/grub.cfg" <<'GRUB'
set default=0
set timeout=5
menuentry "OrionOS Live" {
linux /boot/vmlinuz boot=live quiet splash
initrd /boot/initrd.img
}
menuentry "OrionOS Live (safe mode)" {
linux /boot/vmlinuz boot=live nomodeset
initrd /boot/initrd.img
}
GRUB
ISOFILE="$OUT/OrionOS-$(date +%Y%m%d).iso"
grub-mkrescue -o "$ISOFILE" "$ISO_STAGE" \
--modules="normal iso9660 linux ext2 squash4 biosdisk" \
2>&1 | grep -v "^$" | tail -10
SIZE="$(du -sh "$ISOFILE" | cut -f1)"
echo ""
echo "✓ ISO built: $ISOFILE ($SIZE)"
+67 -40
View File
@@ -1,58 +1,85 @@
# OrionOS ISO builder
# Uses official Arch Linux as the base — provides pacman, pacstrap, archiso.
# Build context: the repo root (OS/)
#
# Build: docker build -t orionos-builder -f iso/docker/Dockerfile .
# Run: docker run --privileged -v $(pwd)/dist:/dist orionos-builder
# Output: dist/orionos.iso
FROM archlinux:latest
# ── disable pacman seccomp sandbox (breaks under QEMU on Apple Silicon) ──
# Disable pacman's alpm sandbox (seccomp-incompatible inside Docker/BuildKit)
RUN echo "DisableSandbox" >> /etc/pacman.conf
# ── install build tools ────────────────────────────────────────────────
# Update and install archiso + OrionOS build dependencies
RUN pacman -Syu --noconfirm && \
pacman -S --noconfirm \
archiso \
syslinux \
base-devel \
git \
libx11 \
libxft \
libxfixes \
libxext \
libpng \
imlib2 \
dbus \
pam \
fontconfig \
freetype2 \
imlib2 \
imagemagick \
fakeroot \
git \
sudo \
&& pacman -Scc --noconfirm
xorg-server \
xorg-xinit \
feh \
picom \
alsa-utils \
ttf-dejavu \
noto-fonts \
pkgconf && \
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"
WORKDIR /build
# ── 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 OrionOS source
COPY . /build/orionos
# ── copy the entire OrionOS source tree ──────────────────────────────
WORKDIR /orionos
COPY . .
# Bootstrap boot loader configs from the installed archiso releng profile.
# Copy only the .c32 binary modules — our repo provides syslinux.cfg.
# efiboot/ is needed for systemd-boot UEFI; fall back to a minimal loader.conf.
# ── build the OrionOS binaries inside the container ───────────────────
# pkg-config finds X11/Xft headers automatically on Arch — no overrides needed
RUN make
RUN mkdir -p /build/orionos/iso/profile/syslinux && \
cp /usr/share/archiso/configs/releng/syslinux/*.c32 \
/build/orionos/iso/profile/syslinux/ 2>/dev/null || true && \
if [ -d /usr/share/archiso/configs/releng/efiboot ]; then \
cp -r /usr/share/archiso/configs/releng/efiboot \
/build/orionos/iso/profile/; \
else \
mkdir -p /build/orionos/iso/profile/efiboot/loader/entries && \
printf 'timeout 4\ndefault arch-*\n' \
> /build/orionos/iso/profile/efiboot/loader/loader.conf; \
fi
# ── generate the default wallpaper ───────────────────────────────────
RUN bash themes/wallpapers/generate-default.sh || true
# Build OrionOS components (full output — no tail so errors are visible)
RUN cd /build/orionos && make
# ── run the ISO build script ──────────────────────────────────────────
# --privileged is required for loop device mounts inside the container
ENTRYPOINT ["/orionos/iso/docker/entrypoint.sh"]
# Install binaries into the staging root that archiso will include
RUN cd /build/orionos && \
install -Dm755 wm/orionwm /build/orionos/iso/profile/airootfs/usr/local/bin/orionwm && \
install -Dm755 deskbar/oriondeskbar /build/orionos/iso/profile/airootfs/usr/local/bin/oriondeskbar && \
install -Dm755 launcher/orionlauncher /build/orionos/iso/profile/airootfs/usr/local/bin/orionlauncher && \
install -Dm755 themes/orionwallpaper /build/orionos/iso/profile/airootfs/usr/local/bin/orionwallpaper && \
install -Dm755 orionnotify/orionnotify /build/orionos/iso/profile/airootfs/usr/local/bin/orionnotify && \
install -Dm755 orionlock/orionlock /build/orionos/iso/profile/airootfs/usr/local/bin/orionlock && \
install -Dm755 orionshot/orionshot /build/orionos/iso/profile/airootfs/usr/local/bin/orionshot && \
install -Dm755 orionclip/orionclip /build/orionos/iso/profile/airootfs/usr/local/bin/orionclip && \
install -Dm755 orionpkg/orionpkg /build/orionos/iso/profile/airootfs/usr/local/bin/orionpkg && \
install -Dm755 installer/orioninstall-gui /build/orionos/iso/profile/airootfs/usr/local/bin/orioninstall-gui && \
install -Dm755 installer/orioninstall /build/orionos/iso/profile/airootfs/usr/local/bin/orioninstall && \
mkdir -p /build/orionos/iso/profile/airootfs/usr/share/themes/ && \
cp -r themes/OrionOS /build/orionos/iso/profile/airootfs/usr/share/themes/ && \
install -Dm644 themes/picom.conf /build/orionos/iso/profile/airootfs/etc/orionos/picom.conf
# Generate default wallpaper
RUN mkdir -p /build/orionos/iso/profile/airootfs/usr/share/orionos/wallpapers && \
bash /build/orionos/themes/wallpapers/generate-default.sh 2>/dev/null && \
cp -f /build/orionos/themes/wallpapers/orionos-default.png \
/build/orionos/iso/profile/airootfs/usr/share/orionos/wallpapers/ 2>/dev/null || true
# Build the ISO at container runtime — mkarchiso needs --privileged (from docker run)
# Output goes to /dist which build-iso.sh mounts from the host.
CMD mkarchiso -v \
-w /tmp/iso-work \
-o /dist \
/build/orionos/iso/profile
+1
View File
@@ -0,0 +1 @@
orionos
+4
View File
@@ -0,0 +1,4 @@
MODULES=(squashfs overlay)
BINARIES=()
FILES=()
HOOKS=(base udev archiso block filesystems keyboard)
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# OrionOS live session autostart
picom --config /etc/orionos/picom.conf &
orionwallpaper &
orionnotify &
orionclip &
oriondeskbar &
exec orionwm
+16
View File
@@ -0,0 +1,16 @@
[options]
HoldPkg = pacman glibc
Architecture = auto
CheckSpace
ParallelDownloads = 5
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
[core]
Include = /etc/pacman.d/mirrorlist
[extra]
Include = /etc/pacman.d/mirrorlist
[multilib]
Include = /etc/pacman.d/mirrorlist
@@ -0,0 +1,4 @@
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
Server = https://mirrors.mit.edu/archlinux/$repo/os/$arch
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
source /etc/orionos/xinitrc
@@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM
+3
View File
@@ -0,0 +1,3 @@
if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
exec startx
fi
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
source /etc/orionos/xinitrc
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e -u
# Allow passwordless root login on the live system
passwd -d root
# Enable networking on the live system
systemctl enable NetworkManager
systemctl enable wpa_supplicant
@@ -0,0 +1,65 @@
#!/usr/bin/ash
archiso_mount_handler() {
local _new_root="$1"
local _uuid _label _basedir _dev _img _param
for _param in $(cat /proc/cmdline); do
case "$_param" in
archisosearchuuid=*) _uuid="${_param#archisosearchuuid=}" ;;
archisolabel=*) _label="${_param#archisolabel=}" ;;
archisobasedir=*) _basedir="${_param#archisobasedir=}" ;;
esac
done
: "${_basedir:=arch}"
if [ -z "${_uuid}" ] && [ -z "${_label}" ]; then
echo "archiso: neither archisosearchuuid nor archisolabel in kernel cmdline"
return 1
fi
local _waited=0
while [ "${_waited}" -lt 30 ]; do
if [ -n "${_uuid}" ]; then
_dev="$(blkid -l -o device -t UUID="${_uuid}" 2>/dev/null)"
else
_dev="$(blkid -l -o device -t LABEL="${_label}" 2>/dev/null)"
fi
[ -n "${_dev}" ] && break
sleep 1
_waited=$(( _waited + 1 ))
done
if [ -z "${_dev}" ]; then
echo "archiso: could not find boot medium (uuid=${_uuid} label=${_label})"
return 1
fi
echo "archiso: found boot medium ${_dev}"
mkdir -p /run/archiso/bootmnt
mount -r "${_dev}" /run/archiso/bootmnt || { echo "archiso: mount bootmnt failed"; return 1; }
_img="/run/archiso/bootmnt/${_basedir}/x86_64/airootfs.sfs"
if [ ! -e "${_img}" ]; then
echo "archiso: squashfs not found at ${_img}"
return 1
fi
mkdir -p /run/archiso/airootfs
mount -r -t squashfs "${_img}" /run/archiso/airootfs || { echo "archiso: squashfs mount failed"; return 1; }
mkdir -p /run/archiso/overlay
mount -t tmpfs tmpfs -o mode=0755 /run/archiso/overlay
mkdir -p /run/archiso/overlay/upper /run/archiso/overlay/work
mount -t overlay overlay \
-o lowerdir=/run/archiso/airootfs,upperdir=/run/archiso/overlay/upper,workdir=/run/archiso/overlay/work \
"${_new_root}" || { echo "archiso: overlay mount failed"; return 1; }
echo "archiso: live root mounted on ${_new_root}"
}
run_hook() {
mount_handler="archiso_mount_handler"
}
@@ -0,0 +1,19 @@
#!/bin/bash
# mkinitcpio install script for the archiso live-boot hook.
build() {
add_runscript
add_binary blkid
add_binary mount
add_binary sleep
add_module squashfs
add_module overlay
add_module isofs
}
help() {
cat <<HELPEOF
Locates the live ISO by UUID, mounts the squashfs airootfs, and sets up
a RAM-backed overlay as the writable root filesystem.
HELPEOF
}
+88
View File
@@ -0,0 +1,88 @@
# OrionOS live system packages
# Base system
base
linux
linux-firmware
mkinitcpio
# X11
xorg-server
xorg-xinit
xorg-xrandr
xorg-xsetroot
xorg-xev
# Fonts
ttf-dejavu
noto-fonts
ttf-liberation
# Dependencies for OrionOS components
libx11
libxft
libxfixes
libxext
libpng
imlib2
dbus
pam
fontconfig
freetype2
syslinux
# Desktop utilities
feh
picom
xterm
libnotify
pcmanfm
mousepad
arandr
# Audio
alsa-utils
pulseaudio
# Brightness
brightnessctl
# Media playback control
playerctl
# Installer dependencies
rsync
parted
arch-install-scripts
grub
efibootmgr
dosfstools
amd-ucode
intel-ucode
# Arch tools
archiso
pacman-contrib
polkit
# System tools
bash
coreutils
util-linux
procps-ng
grep
gawk
sed
tar
gzip
# Network
networkmanager
wpa_supplicant
dhcpcd
# Misc
git
vim
less
which
+21
View File
@@ -0,0 +1,21 @@
[options]
HoldPkg = pacman glibc
Architecture = auto
CheckSpace
ParallelDownloads = 5
SigLevel = Never
[core]
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
[extra]
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
[multilib]
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# archiso profile definition for OrionOS
iso_name="OrionOS"
iso_label="ORIONOS_$(date +%Y%m)"
iso_publisher="OrionOS Project"
iso_application="OrionOS Live"
iso_version="$(date +%Y.%m)"
install_dir="arch"
buildmodes=('iso')
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito'
'uefi-x64.systemd-boot.esp' 'uefi-x64.systemd-boot.eltorito')
arch="x86_64"
pacman_conf="pacman.conf"
airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '15')
file_permissions=(
["/etc/orionos/xinitrc"]="0:0:755"
["/root/.xinitrc"]="0:0:755"
["/root/.bash_profile"]="0:0:644"
["/etc/skel/.xinitrc"]="0:0:755"
["/root/customize_airootfs.sh"]="0:0:755"
["/usr/local/bin/orionwm"]="0:0:755"
["/usr/local/bin/oriondeskbar"]="0:0:755"
["/usr/local/bin/orionlauncher"]="0:0:755"
["/usr/local/bin/orionwallpaper"]="0:0:755"
["/usr/local/bin/orionnotify"]="0:0:755"
["/usr/local/bin/orionlock"]="0:0:755"
["/usr/local/bin/orionshot"]="0:0:755"
["/usr/local/bin/orionclip"]="0:0:755"
["/usr/local/bin/orionpkg"]="0:0:755"
["/usr/local/bin/orioninstall"]="0:0:755"
["/usr/local/bin/orioninstall-gui"]="0:0:755"
)
+8
View File
@@ -0,0 +1,8 @@
DEFAULT arch64
TIMEOUT 50
PROMPT 0
LABEL arch64
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% rw
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# archiso profile definition for OrionOS
iso_name="OrionOS"
iso_label="ORIONOS_$(date +%Y%m)"
iso_publisher="OrionOS Project"
iso_application="OrionOS Live"
iso_version="$(date +%Y.%m)"
install_dir="arch"
buildmodes=('iso')
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito'
'uefi-ia32.grub.esp' 'uefi-x64.grub.esp'
'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito')
arch="x86_64"
pacman_conf="pacman.conf"
airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '15')
file_permissions=(
["/etc/orionos/xinitrc"]="0:0:755"
["/root/.xinitrc"]="0:0:755"
)
+8
View File
@@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),)
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
endif
# Optional: imlib2 for freedesktop icon loading
PKG_IL_C := $(shell pkg-config --cflags imlib2 2>/dev/null)
PKG_IL_L := $(shell pkg-config --libs imlib2 2>/dev/null)
ifneq ($(PKG_IL_C),)
PKG_XFT_C += $(PKG_IL_C) -DHAVE_IMLIB2
PKG_XFT_L += $(PKG_IL_L)
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_XFT_C)
LDFLAGS = $(PKG_XFT_L)
+194 -113
View File
@@ -11,60 +11,64 @@
#include <unistd.h>
/* ── dimensions ─────────────────────────────────────────────────────── */
#define WIN_W 580
#define WIN_H 420
#define HDR_H 50
#define TAB_H 34
#define CONTENT_Y (HDR_H + TAB_H)
#define PAD 12
#define CARD_W 96
#define CARD_H 84
#define CARD_GAP 8
#define COLS 5
#define ICON_SZ 38
#define WIN_W 640
#define WIN_H 490
#define HDR_H 58
#define PILLS_H 42
#define CONTENT_Y (HDR_H + PILLS_H)
#define FOOTER_H 28
#define CARD_W 132
#define CARD_H 106
#define CARD_GAP 10
#define COLS 4
#define ICON_SZ 46
#define PILL_GAP 8
#define ROWS_VIS 3
#define GX0 ((WIN_W - (COLS * CARD_W + (COLS-1) * CARD_GAP)) / 2)
#define GY0 (CONTENT_Y + PAD)
#define ROWS_VIS 3 /* rows visible before scrolling */
#define GY0 (CONTENT_Y + 12)
#define MAX_APPS 256
/* ── 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
#define C_SCROLL_BG 0x0C1020
#define C_SCROLL_TH 0x1E3258
#define C_BG 0x111318
#define C_SURFACE 0x1A1D28
#define C_SURFACE_HV 0x1F2335
#define C_BORDER 0x252A42
#define C_BORDER_HV 0x3A5490
#define C_ACCENT 0x4B8FF0
#define C_PILL_SEL 0x19305C
#define C_PILL_BD 0x2A4480
#define C_SRCH_BG 0x161820
#define C_SRCH_BD 0x262C48
#define C_TEXT 0xC0CFF0
#define C_TEXT_HOV 0xEEF2FF
#define C_TEXT_DIM 0x3A4068
#define C_TEXT_CAT 0x586090
#define C_TEXT_CAT_S 0x8AAEE8
#define C_FOOTER_BG 0x0C0E14
#define C_FOOTER_TXT 0x353C62
#define C_SCROLL_BG 0x161820
#define C_SCROLL_TH 0x2C3460
static const unsigned long ICON_BG[] = {
0x1A4D88, 0x166C3C, 0x6C3018,
0x4A1A80, 0x126070, 0x6C1A38,
0x365218, 0x183460,
0x1A3A88, 0x0F6040, 0x7A2820, 0x501A80,
0x106070, 0x6A1A3A, 0x384C18, 0x1A3060,
0x6A4010, 0x103A60,
};
#define NICON 8
#define NICON 10
#define FONT_UI "Sans:size=9"
#define FONT_LOGO "Sans:size=11:weight=bold"
#define FONT_ICON "Sans:size=13:weight=bold"
#define FONT_ICON "Sans:size=14:weight=bold"
#define FONT_HINT "Sans:size=8"
#define MAX_FLT 64
typedef struct { char name[64]; char cmd[256]; char cat[32]; } App;
typedef struct { char name[64]; char cmd[256]; char cat[32]; char icon[64]; } App;
static App APPS[MAX_APPS];
static int NAPP = 0;
static const char *CATS[] = {
"All", "Internet", "Media", "Office", "System", "Settings"
"All", "Internet", "Media", "Office", "System", "Settings", "Other"
};
#define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0])))
@@ -89,7 +93,7 @@ static const char *map_category(const char *cats) {
strstr(cats, "FileManager") || strstr(cats, "Utility") ||
strstr(cats, "Monitor") || strstr(cats, "Security"))
return "System";
return "";
return "Other";
}
static void clean_exec(char *dst, const char *src, size_t dstsz) {
@@ -108,7 +112,7 @@ static void load_desktop_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return;
char name[64] = {0}, exec[256] = {0}, cats[128] = {0};
char name[64] = {0}, exec[256] = {0}, cats[128] = {0}, icon[64] = {0};
int in_entry = 0, nodisplay = 0, is_app = 0;
char line[512];
@@ -126,6 +130,8 @@ static void load_desktop_file(const char *path) {
snprintf(exec, sizeof(exec), "%s", line + 5);
else if (!cats[0] && strncmp(line, "Categories=", 11) == 0)
snprintf(cats, sizeof(cats), "%s", line + 11);
else if (!icon[0] && strncmp(line, "Icon=", 5) == 0)
snprintf(icon, sizeof(icon), "%s", line + 5);
else if (strncmp(line, "NoDisplay=true", 14) == 0)
nodisplay = 1;
else if (strncmp(line, "Type=Application", 16) == 0)
@@ -137,6 +143,7 @@ static void load_desktop_file(const char *path) {
snprintf(APPS[NAPP].name, 64, "%s", name);
clean_exec(APPS[NAPP].cmd, exec, 256);
snprintf(APPS[NAPP].cat, 32, "%s", map_category(cats));
snprintf(APPS[NAPP].icon, 64, "%s", icon);
NAPP++;
}
@@ -168,18 +175,13 @@ static void load_apps(void) {
load_desktop_dir(p);
}
/* fallback if no .desktop files (dev environment, macOS, etc.) */
if (NAPP == 0) {
static const struct { const char *n, *c, *cat; } FB[] = {
{ "Firefox", "firefox &", "Internet" },
{ "Terminal", "alacritty &", "System" },
{ "Files", "thunar &", "System" },
{ "Text Editor", "mousepad &", "System" },
{ "Media", "mpv --player-operation-mode=pseudo-gui &", "Media" },
{ "Images", "viewnior &", "Media" },
{ "PDF Viewer", "evince &", "Office" },
{ "Display", "arandr &", "Settings" },
{ "Audio", "pavucontrol &", "Settings" },
{ "Terminal", "xterm &", "System" },
{ "Files", "pcmanfm &", "System" },
{ "Text Editor", "mousepad &", "System" },
{ "System Mon", "xterm -e htop &", "System" },
{ "Display", "arandr &", "Settings" },
};
for (int i = 0; i < (int)(sizeof(FB)/sizeof(FB[0])); i++) {
snprintf(APPS[i].name, 64, "%s", FB[i].n);
@@ -197,7 +199,7 @@ typedef struct {
Display *dpy; int screen;
Window root, win; GC gc;
Colormap cmap; Visual *visual;
XftFont *font_ui, *font_logo, *font_icon;
XftFont *font_ui, *font_icon, *font_hint;
int sel_cat, hov, scroll;
char filter[MAX_FLT];
int filter_len;
@@ -258,28 +260,28 @@ static int nvis(L *l, int out[MAX_APPS]) {
return n;
}
static void tab_layout(L *l, int widths[NCAT], int *total) {
static void pill_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;
widths[i] = (int)e.width + 24;
*total += widths[i];
}
*total += (NCAT - 1) * PILL_GAP;
}
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 w[NCAT], total; pill_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];
tx += w[i] + PILL_GAP;
}
return -1;
}
/* card index within visible slice (adjusted for scroll) */
static int card_at_vis(int x, int y) {
if (y < CONTENT_Y) return -1;
int lx = x - GX0, ly = y - GY0;
@@ -292,64 +294,105 @@ static int card_at_vis(int x, int y) {
return row * COLS + col;
}
/* ── icon loading (imlib2, optional) ─────────────────────────────────── */
#ifdef HAVE_IMLIB2
#include <Imlib2.h>
static const char *ICON_DIRS[] = {
"/usr/share/icons/Papirus-Dark/48x48/apps",
"/usr/share/icons/Papirus/48x48/apps",
"/usr/share/icons/hicolor/48x48/apps",
"/usr/share/pixmaps",
NULL
};
static int render_icon(L *l, const char *icon_name, int ix, int iy, int sz) {
if (!icon_name || !icon_name[0]) return 0;
char path[512];
for (int i = 0; ICON_DIRS[i]; i++) {
snprintf(path, sizeof(path), "%s/%s.png", ICON_DIRS[i], icon_name);
if (access(path, R_OK) == 0) goto found;
}
return 0;
found:;
imlib_context_set_display(l->dpy);
imlib_context_set_visual(l->visual);
imlib_context_set_colormap(l->cmap);
imlib_context_set_drawable(l->win);
imlib_context_set_blend(1);
Imlib_Image img = imlib_load_image_immediately(path);
if (!img) return 0;
imlib_context_set_image(img);
imlib_render_image_on_drawable_at_size(ix, iy, sz, sz);
imlib_free_image();
return 1;
}
#endif
/* ── 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));
/* ── header: search bar ─────────────────────────── */
XSetForeground(l->dpy, l->gc, mkc(l, C_SURFACE));
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, HDR_H);
XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT));
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, 2);
ds(l, xd, l->font_logo, C_ACCENT, 14, 13, "OrionOS");
/* search box */
/* search input box */
{
int sx = 86, sy = 10, sw = WIN_W - 86 - 14, sh = 30;
XSetForeground(l->dpy, l->gc, mkc(l, C_SEARCH));
int sx = 16, sy = 11, sw = WIN_W - 32, sh = 36;
XSetForeground(l->dpy, l->gc, mkc(l, C_SRCH_BG));
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);
XSetForeground(l->dpy, l->gc, mkc(l,
l->filter_len > 0 ? C_BORDER_HV : C_SRCH_BD));
XDrawRectangle(l->dpy, l->win, l->gc, sx, sy, sw - 1, sh - 1);
/* accent left bar when active */
if (l->filter_len > 0) {
XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT));
XFillRectangle(l->dpy, l->win, l->gc, sx, sy, 2, sh);
}
if (l->filter_len == 0) {
ds(l, xd, l->font_ui, C_TEXT_DIM, sx + 8, sy + 7, "Search apps...");
ds(l, xd, l->font_ui, C_TEXT_DIM, sx + 12, sy + 10, "Search applications...");
} 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);
snprintf(buf, sizeof(buf), "%s_", l->filter);
ds(l, xd, l->font_ui, C_TEXT_HOV, sx + 12, sy + 10, buf);
}
}
/* header bottom separator */
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
XDrawLine(l->dpy, l->win, l->gc, 0, HDR_H, WIN_W, HDR_H);
XDrawLine(l->dpy, l->win, l->gc, 0, HDR_H - 1, WIN_W, HDR_H - 1);
/* ── pills row ───────────────────────────────────── */
int pw[NCAT], ptotal; pill_layout(l, pw, &ptotal);
int px = (WIN_W - ptotal) / 2;
int py = HDR_H + (PILLS_H - 24) / 2;
/* 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);
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);
XSetForeground(l->dpy, l->gc, mkc(l, C_PILL_SEL));
XFillRectangle(l->dpy, l->win, l->gc, px, py, pw[i], 24);
XSetForeground(l->dpy, l->gc, mkc(l, C_PILL_BD));
XDrawRectangle(l->dpy, l->win, l->gc, px, py, pw[i] - 1, 23);
ds_ctr(l, xd, l->font_ui, C_TEXT_CAT_S, px, py, pw[i], 24, CATS[i]);
} else {
ds_ctr(l, xd, l->font_ui, C_TEXT_CAT, px, py, pw[i], 24, CATS[i]);
}
ds_ctr(l, xd, l->font_ui,
(i == l->sel_cat) ? C_TAB_TSEL : C_TAB_TXT,
tx, HDR_H, tw[i], TAB_H, CATS[i]);
tx += tw[i];
px += pw[i] + PILL_GAP;
}
/* pills bottom separator */
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
XDrawLine(l->dpy, l->win, l->gc, 0, CONTENT_Y, WIN_W, CONTENT_Y);
XDrawLine(l->dpy, l->win, l->gc, 0, CONTENT_Y - 1, WIN_W, CONTENT_Y - 1);
/* app grid with scroll */
/* ── app grid ────────────────────────────────────── */
int vis[MAX_APPS];
int nv = nvis(l, vis);
/* clamp scroll */
int max_scroll = (nv - ROWS_VIS * COLS + COLS - 1) / COLS;
if (max_scroll < 0) max_scroll = 0;
if (l->scroll > max_scroll) l->scroll = max_scroll;
@@ -359,7 +402,8 @@ static void draw(L *l) {
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");
0, CONTENT_Y, WIN_W, WIN_H - CONTENT_Y - FOOTER_H,
"No matching apps");
} else {
for (int i = 0; i < ROWS_VIS * COLS; i++) {
int vi = vis_start + i;
@@ -370,48 +414,87 @@ static void draw(L *l) {
int cy = GY0 + row * (CARD_H + CARD_GAP);
int hover = (i == l->hov);
/* card background */
if (hover) {
XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_HOV));
XSetForeground(l->dpy, l->gc, mkc(l, C_SURFACE_HV));
XFillRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W, CARD_H);
XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_BD));
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER_HV));
XDrawRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W - 1, CARD_H - 1);
/* accent top bar on hover */
XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT));
XFillRectangle(l->dpy, l->win, l->gc, cx + 1, cy, CARD_W - 2, 2);
} else {
XSetForeground(l->dpy, l->gc, mkc(l, C_SURFACE));
XFillRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W, CARD_H);
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
XDrawRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W - 1, CARD_H - 1);
}
int app_i = vis[vi];
unsigned int h = 0;
for (const char *p = APPS[app_i].name; *p; p++) h = h * 31 + (unsigned char)*p;
/* icon */
int ix = cx + (CARD_W - ICON_SZ) / 2;
int iy = cy + 10;
XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON]));
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
int iy = cy + 14;
int icon_rendered = 0;
#ifdef HAVE_IMLIB2
icon_rendered = render_icon(l, APPS[app_i].icon, ix, iy, ICON_SZ);
#endif
if (!icon_rendered) {
XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON]));
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
/* 1px darker border on icon box */
XSetForeground(l->dpy, l->gc, mkc(l, 0x000000));
XDrawRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ - 1, ICON_SZ - 1);
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, 0xF0F4FF,
ix, iy, ICON_SZ, ICON_SZ, 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);
char lbl[14];
snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name);
/* label */
char lbl[16];
snprintf(lbl, sizeof(lbl), "%.14s", APPS[app_i].name);
ds_ctr(l, xd, l->font_ui,
hover ? C_TEXT_HOV : C_TEXT,
cx, iy + ICON_SZ + 4, CARD_W, CARD_H - ICON_SZ - 18, lbl);
cx, iy + ICON_SZ + 6, CARD_W, CARD_H - ICON_SZ - 24, lbl);
}
}
/* scrollbar */
if (max_scroll > 0) {
int sb_x = WIN_W - 8, sb_y = CONTENT_Y + 4;
int sb_h = WIN_H - CONTENT_Y - 8;
int sb_x = WIN_W - 6;
int sb_y = CONTENT_Y + 4;
int sb_h = WIN_H - CONTENT_Y - FOOTER_H - 8;
XSetForeground(l->dpy, l->gc, mkc(l, C_SCROLL_BG));
XFillRectangle(l->dpy, l->win, l->gc, sb_x, sb_y, 6, sb_h);
XFillRectangle(l->dpy, l->win, l->gc, sb_x, sb_y, 4, sb_h);
int total_rows = (nv + COLS - 1) / COLS;
int thumb_h = sb_h * ROWS_VIS / total_rows;
if (thumb_h < 12) thumb_h = 12;
if (thumb_h < 10) thumb_h = 10;
int thumb_y = sb_y + (sb_h - thumb_h) * l->scroll / max_scroll;
XSetForeground(l->dpy, l->gc, mkc(l, C_SCROLL_TH));
XFillRectangle(l->dpy, l->win, l->gc, sb_x + 1, thumb_y, 4, thumb_h);
XFillRectangle(l->dpy, l->win, l->gc, sb_x, thumb_y, 4, thumb_h);
}
/* outer border */
/* ── footer ──────────────────────────────────────── */
int fy = WIN_H - FOOTER_H;
XSetForeground(l->dpy, l->gc, mkc(l, C_FOOTER_BG));
XFillRectangle(l->dpy, l->win, l->gc, 0, fy, WIN_W, FOOTER_H);
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
XDrawLine(l->dpy, l->win, l->gc, 0, fy, WIN_W, fy);
/* app count left, hints right */
{
char cnt[32];
snprintf(cnt, sizeof(cnt), "%d apps", nv);
ds(l, xd, l->font_hint, C_FOOTER_TXT, 14, fy + 7, cnt);
ds_ctr(l, xd, l->font_hint, C_FOOTER_TXT,
0, fy, WIN_W, FOOTER_H,
"Return: launch Esc: close Type: filter");
}
/* outer window 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);
@@ -441,7 +524,6 @@ static void on_key(L *l, XKeyEvent *e) {
l->filter[--l->filter_len] = '\0';
l->scroll = 0; l->hov = -1; draw(l); return;
}
/* page up/down for keyboard scroll */
if (sym == XK_Prior) { l->scroll -= ROWS_VIS; if (l->scroll < 0) l->scroll = 0; draw(l); return; }
if (sym == XK_Next) { l->scroll += ROWS_VIS; draw(l); return; }
@@ -469,11 +551,11 @@ int main(void) {
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);
l.font_hint = XftFontOpenName(l.dpy, l.screen, FONT_HINT);
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;
if (!l.font_hint) l.font_hint = l.font_ui;
int sw = DisplayWidth(l.dpy, l.screen);
int sh = DisplayHeight(l.dpy, l.screen);
@@ -503,14 +585,13 @@ int main(void) {
if (e.xexpose.count == 0) draw(&l);
break;
case ButtonPress:
if (e.xbutton.button == Button4) {
l.scroll -= 1; if (l.scroll < 0) l.scroll = 0; draw(&l); break;
}
if (e.xbutton.button == Button5) {
l.scroll += 1; draw(&l); break;
}
{
/* scroll wheel */
if (e.xbutton.button == Button4) {
l.scroll -= 1; if (l.scroll < 0) l.scroll = 0; draw(&l); break;
}
if (e.xbutton.button == Button5) {
l.scroll += 1; draw(&l); break;
}
int t = tab_at(&l, e.xbutton.x, e.xbutton.y);
if (t >= 0) {
l.sel_cat = t; l.scroll = 0; l.hov = -1; draw(&l);
+22
View File
@@ -0,0 +1,22 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags x11 xfixes xft 2>/dev/null)
PKG_L := $(shell pkg-config --libs x11 xfixes xft 2>/dev/null)
ifeq ($(PKG_C),)
$(error "Required: libx11-dev libxfixes-dev libxft-dev")
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L)
BIN = orionclip
.PHONY: all clean install
all: $(BIN)
$(BIN): orionclip.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
clean:
rm -f $(BIN)
+407
View File
@@ -0,0 +1,407 @@
/*
* orionclip — clipboard manager for OrionOS
* Usage:
* orionclip run as daemon, monitors clipboard
* orionclip --show toggle the history popup (sent by Super+V hotkey)
*
* Stores up to CLIP_MAX entries. Popup appears in bottom-right corner
* (left of the deskbar). Click an entry to restore it to the clipboard.
* Press Escape or click outside to dismiss.
*
* IPC: the daemon registers the _ORIONCLIP_WIN atom on the root window.
* orionclip --show sends a ClientMessage to that window.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/Xft/Xft.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define CLIP_MAX 20
#define POPUP_W 340
#define POPUP_ROW_H 28
#define POPUP_PAD 8
#define DESKBAR_W 200
#define POLL_MS 300 /* clipboard poll interval in ms */
#define COL_BG 0x2A2D36
#define COL_BORDER 0x4A6080
#define COL_TEXT 0xD8E4F4
#define COL_SEL_BG 0x1E58A8
#define COL_SEL_FG 0xF0F8FF
#define COL_ACCENT 0x1E58A8
#define COL_DIM 0x607080
static char clip_history[CLIP_MAX][512];
static int clip_count = 0;
static Display *dpy;
static int screen;
static Visual *visual;
static Colormap cmap;
static GC gc;
static XftFont *font;
static Window popup_win = None;
static int popup_hovered = -1;
static Atom atom_clip_win; /* _ORIONCLIP_WIN — IPC handle */
static Atom atom_show; /* _ORIONCLIP_SHOW — toggle message */
static Atom atom_utf8;
static Atom atom_clipboard;
static Atom atom_targets;
/* ── helpers ─────────────────────────────────────────────────────────── */
static unsigned long xcolor(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(dpy, cmap, &c);
return c.pixel;
}
static void xft_color(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(dpy, visual, cmap, &rc, out);
}
/* ── clipboard read ──────────────────────────────────────────────────── */
static char *clipboard_get(void) {
/* request clipboard content via XConvertSelection */
Window owner = XGetSelectionOwner(dpy, atom_clipboard);
if (owner == None) return NULL;
/* use a small utility window as the requestor */
static Window req_win = None;
if (req_win == None)
req_win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen),
-10, -10, 1, 1, 0, 0, 0);
Atom prop = XInternAtom(dpy, "_ORIONCLIP_PROP", False);
XConvertSelection(dpy, atom_clipboard, atom_utf8, prop, req_win, CurrentTime);
/* wait up to ~300ms for SelectionNotify */
struct timespec deadline, now;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_nsec += 300000000L;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_nsec -= 1000000000L;
deadline.tv_sec++;
}
for (;;) {
if (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == SelectionNotify && e.xselection.requestor == req_win) {
if (e.xselection.property == None) return NULL;
Atom type; int fmt; unsigned long nitems, left;
unsigned char *data = NULL;
XGetWindowProperty(dpy, req_win, prop, 0, 65536/4, True,
atom_utf8, &type, &fmt, &nitems, &left, &data);
if (!data) return NULL;
char *copy = strndup((char *)data, nitems < 511 ? nitems : 511);
XFree(data);
return copy;
}
}
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec))
break;
usleep(10000);
}
return NULL;
}
static void clipboard_set(const char *text) {
/* put text on the clipboard by becoming the selection owner */
static Window sel_win = None;
static char sel_buf[512];
if (sel_win == None)
sel_win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen),
-10, -10, 1, 1, 0, 0, 0);
strncpy(sel_buf, text, 511); sel_buf[511] = '\0';
XSetSelectionOwner(dpy, atom_clipboard, sel_win, CurrentTime);
/* SelectionRequest events are handled in the main event loop */
(void)sel_buf;
}
/* ── history management ──────────────────────────────────────────────── */
static void history_push(const char *text) {
if (!text || !*text) return;
/* deduplicate: if same text is already entry 0, skip */
if (clip_count > 0 && strcmp(clip_history[0], text) == 0) return;
/* shift down */
int top = clip_count < CLIP_MAX ? clip_count : CLIP_MAX - 1;
for (int i = top; i > 0; i--)
memcpy(clip_history[i], clip_history[i-1], sizeof(clip_history[0]));
strncpy(clip_history[0], text, 511);
clip_history[0][511] = '\0';
if (clip_count < CLIP_MAX) clip_count++;
}
/* ── popup ───────────────────────────────────────────────────────────── */
static int popup_h(void) {
return POPUP_PAD * 2 + clip_count * POPUP_ROW_H;
}
static void popup_draw(void) {
if (!popup_win) return;
int ph = popup_h();
XSetForeground(dpy, gc, xcolor(COL_BG));
XFillRectangle(dpy, popup_win, gc, 0, 0, POPUP_W, ph);
XSetForeground(dpy, gc, xcolor(COL_ACCENT));
XFillRectangle(dpy, popup_win, gc, 0, 0, 3, ph);
XSetForeground(dpy, gc, xcolor(COL_BORDER));
XDrawRectangle(dpy, popup_win, gc, 0, 0, POPUP_W - 1, ph - 1);
XftDraw *xd = XftDrawCreate(dpy, popup_win, visual, cmap);
for (int i = 0; i < clip_count; i++) {
int ry = POPUP_PAD + i * POPUP_ROW_H;
if (i == popup_hovered) {
XSetForeground(dpy, gc, xcolor(COL_SEL_BG));
XFillRectangle(dpy, popup_win, gc, 4, ry, POPUP_W - 8, POPUP_ROW_H - 2);
}
unsigned long text_col = (i == popup_hovered) ? COL_SEL_FG : COL_TEXT;
if (i > 0) text_col = COL_DIM;
XftColor col;
xft_color(text_col, &col);
/* truncate display text */
char disp[64];
strncpy(disp, clip_history[i], 63); disp[63] = '\0';
/* replace newlines with spaces */
for (char *p = disp; *p; p++) if (*p == '\n' || *p == '\r' || *p == '\t') *p = ' ';
XftDrawStringUtf8(xd, &col, font, 12, ry + POPUP_ROW_H - 7,
(const FcChar8 *)disp, strlen(disp));
XftColorFree(dpy, visual, cmap, &col);
}
XftDrawDestroy(xd);
XFlush(dpy);
}
static void popup_show(void) {
if (popup_win != None) { popup_draw(); return; }
if (clip_count == 0) return;
int ph = popup_h();
int sw = DisplayWidth(dpy, screen);
int sh = DisplayHeight(dpy, screen);
int px = sw - DESKBAR_W - POPUP_W - 12;
int py = sh - ph - 40;
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = xcolor(COL_BG);
swa.event_mask = ExposureMask | ButtonPressMask |
PointerMotionMask | LeaveWindowMask;
popup_win = XCreateWindow(dpy, RootWindow(dpy, screen),
px, py, POPUP_W, ph, 0,
DefaultDepth(dpy, screen), InputOutput,
visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
XMapRaised(dpy, popup_win);
popup_draw();
}
static void popup_hide(void) {
if (popup_win == None) return;
XDestroyWindow(dpy, popup_win);
popup_win = None;
popup_hovered = -1;
XFlush(dpy);
}
static void popup_toggle(void) {
if (popup_win != None) popup_hide();
else popup_show();
}
static int popup_row_at(int y) {
int row = (y - POPUP_PAD) / POPUP_ROW_H;
if (row < 0 || row >= clip_count) return -1;
return row;
}
/* ── --show mode: send IPC to daemon ─────────────────────────────────── */
static void send_show(void) {
Display *d = XOpenDisplay(NULL);
if (!d) { fprintf(stderr, "orionclip: cannot open display\n"); return; }
Atom a = XInternAtom(d, "_ORIONCLIP_WIN", True);
if (a == None) { XCloseDisplay(d); return; }
Atom type; int fmt; unsigned long n, left;
unsigned char *data = NULL;
XGetWindowProperty(d, RootWindow(d, DefaultScreen(d)), a, 0, 1, False,
XA_WINDOW, &type, &fmt, &n, &left, &data);
if (!data) { XCloseDisplay(d); return; }
Window target = *(Window *)data;
XFree(data);
Atom show_atom = XInternAtom(d, "_ORIONCLIP_SHOW", False);
XClientMessageEvent ev = {0};
ev.type = ClientMessage;
ev.window = target;
ev.message_type = show_atom;
ev.format = 32;
XSendEvent(d, target, False, 0, (XEvent *)&ev);
XFlush(d);
XCloseDisplay(d);
}
/* ── main daemon ─────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
if (argc > 1 && strcmp(argv[1], "--show") == 0) {
send_show();
return 0;
}
dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionclip: cannot open display\n"); return 1; }
screen = DefaultScreen(dpy);
visual = DefaultVisual(dpy, screen);
cmap = DefaultColormap(dpy, screen);
gc = XCreateGC(dpy, RootWindow(dpy, screen), 0, NULL);
font = XftFontOpenName(dpy, screen, "Sans:size=9");
if (!font) font = XftFontOpenName(dpy, screen, "fixed:size=9");
atom_utf8 = XInternAtom(dpy, "UTF8_STRING", False);
atom_clipboard = XInternAtom(dpy, "CLIPBOARD", False);
atom_targets = XInternAtom(dpy, "TARGETS", False);
atom_clip_win = XInternAtom(dpy, "_ORIONCLIP_WIN", False);
atom_show = XInternAtom(dpy, "_ORIONCLIP_SHOW", False);
/* register ourselves on the root window */
Window self = XCreateSimpleWindow(dpy, RootWindow(dpy, screen),
-10, -10, 1, 1, 0, 0, 0);
XSelectInput(dpy, self, StructureNotifyMask | PropertyChangeMask);
XChangeProperty(dpy, RootWindow(dpy, screen), atom_clip_win, XA_WINDOW, 32,
PropModeReplace, (unsigned char *)&self, 1);
XFlush(dpy);
int xfd = ConnectionNumber(dpy);
struct timespec last_poll = {0, 0};
for (;;) {
/* pump X events */
while (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
/* IPC: show/hide toggle */
if (e.type == ClientMessage &&
(Atom)e.xclient.message_type == atom_show) {
popup_toggle();
continue;
}
/* selection ownership request — serve clipboard content */
if (e.type == SelectionRequest) {
XSelectionRequestEvent *req = &e.xselectionrequest;
XSelectionEvent sev = {0};
sev.type = SelectionNotify;
sev.requestor = req->requestor;
sev.selection = req->selection;
sev.target = req->target;
sev.time = req->time;
sev.property = None;
if (clip_count > 0) {
if (req->target == atom_targets) {
Atom targets[2] = { atom_targets, atom_utf8 };
XChangeProperty(dpy, req->requestor, req->property, XA_ATOM, 32,
PropModeReplace, (unsigned char *)targets, 2);
sev.property = req->property;
} else if (req->target == atom_utf8) {
XChangeProperty(dpy, req->requestor, req->property, atom_utf8, 8,
PropModeReplace,
(unsigned char *)clip_history[0],
strlen(clip_history[0]));
sev.property = req->property;
}
}
XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&sev);
continue;
}
if (popup_win == None) continue;
if (e.type == Expose && e.xexpose.count == 0 &&
e.xany.window == popup_win)
popup_draw();
if (e.type == LeaveNotify && e.xany.window == popup_win) {
popup_hovered = -1;
popup_draw();
}
if (e.type == MotionNotify && e.xany.window == popup_win) {
int row = popup_row_at(e.xmotion.y);
if (row != popup_hovered) {
popup_hovered = row;
popup_draw();
}
}
if (e.type == ButtonPress && e.xany.window == popup_win) {
int row = popup_row_at(e.xbutton.y);
if (row >= 0 && row < clip_count) {
/* move chosen entry to top and set as clipboard */
char chosen[512];
strncpy(chosen, clip_history[row], 511); chosen[511] = '\0';
history_push(chosen);
clipboard_set(clip_history[0]);
}
popup_hide();
}
}
/* poll clipboard every POLL_MS ms */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long elapsed_ms = (now.tv_sec - last_poll.tv_sec) * 1000
+ (now.tv_nsec - last_poll.tv_nsec) / 1000000;
if (elapsed_ms >= POLL_MS) {
last_poll = now;
char *text = clipboard_get();
if (text) {
history_push(text);
free(text);
}
}
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
struct timeval tv = { 0, POLL_MS * 1000 };
select(xfd + 1, &fds, NULL, NULL, &tv);
}
return 0;
}
+24
View File
@@ -0,0 +1,24 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags xft x11 2>/dev/null)
PKG_L := $(shell pkg-config --libs xft x11 2>/dev/null)
ifeq ($(PKG_C),)
PKG_C := -I/opt/homebrew/include
PKG_L := -L/opt/homebrew/lib -lXft -lfontconfig -lX11
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L) -lpam
BIN = orionlock
.PHONY: all clean install
all: $(BIN)
$(BIN): orionlock.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
install -Dm644 orionlock.pam /etc/pam.d/login 2>/dev/null || true
clean:
rm -f $(BIN)
+271
View File
@@ -0,0 +1,271 @@
/*
* orionlock — PAM-authenticated fullscreen screen lock.
* Grabs keyboard + pointer, draws clock and password entry.
* On correct password, ungrabs and exits.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/Xft/Xft.h>
#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define COL_BG 0x000000
#define COL_CLOCK 0xFFFFFF
#define COL_DATE 0x8899AA
#define COL_DOT 0x4A90D9
#define COL_ERR 0xFF5F57
#define COL_HINT 0x555566
#define PW_MAX 256
#define DOT_R 5
#define DOT_PAD 8
static Display *dpy;
static int screen;
static Window root, lock_win;
static Visual *visual;
static Colormap cmap;
static GC gc;
static XftFont *font_clock;
static XftFont *font_date;
static XftFont *font_ui;
static char pw_buf[PW_MAX];
static int pw_len = 0;
static int auth_fail = 0;
static int auth_ok = 0;
static unsigned long alloc_color(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(dpy, cmap, &c);
return c.pixel;
}
static void xft_color(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(dpy, visual, cmap, &rc, out);
}
static void draw_lock(void) {
int sw = DisplayWidth(dpy, screen);
int sh = DisplayHeight(dpy, screen);
XSetForeground(dpy, gc, alloc_color(COL_BG));
XFillRectangle(dpy, lock_win, gc, 0, 0, sw, sh);
XftDraw *xd = XftDrawCreate(dpy, lock_win, visual, cmap);
XftColor col;
XGlyphInfo ext;
/* clock */
{
time_t t = time(NULL);
struct tm *tm = localtime(&t);
char clock_str[16], date_str[32];
strftime(clock_str, sizeof(clock_str), "%H:%M", tm);
strftime(date_str, sizeof(date_str), "%A, %B %d", tm);
xft_color(COL_CLOCK, &col);
XftTextExtentsUtf8(dpy, font_clock, (const FcChar8 *)clock_str,
strlen(clock_str), &ext);
int cx = (sw - (int)ext.width) / 2;
int cy = sh / 2 - 100;
XftDrawStringUtf8(xd, &col, font_clock, cx, cy,
(const FcChar8 *)clock_str, strlen(clock_str));
XftColorFree(dpy, visual, cmap, &col);
xft_color(COL_DATE, &col);
XftTextExtentsUtf8(dpy, font_date, (const FcChar8 *)date_str,
strlen(date_str), &ext);
cx = (sw - (int)ext.width) / 2;
XftDrawStringUtf8(xd, &col, font_date, cx, cy + 40,
(const FcChar8 *)date_str, strlen(date_str));
XftColorFree(dpy, visual, cmap, &col);
}
/* password dots */
{
int dots = pw_len;
if (dots > 20) dots = 20;
int total_w = dots * (DOT_R * 2 + DOT_PAD) - (dots > 0 ? DOT_PAD : 0);
int start_x = (sw - total_w) / 2;
int dot_y = sh / 2 + 10;
unsigned long dot_col = alloc_color(auth_fail ? COL_ERR : COL_DOT);
XSetForeground(dpy, gc, dot_col);
for (int i = 0; i < dots; i++) {
int dx = start_x + i * (DOT_R * 2 + DOT_PAD);
XFillArc(dpy, lock_win, gc,
dx, dot_y - DOT_R, DOT_R * 2, DOT_R * 2, 0, 360 * 64);
}
if (dots == 0 && pw_len == 0) {
/* hint: show lock icon placeholder */
xft_color(COL_HINT, &col);
const char *hint = "Type password to unlock";
XftTextExtentsUtf8(dpy, font_ui, (const FcChar8 *)hint,
strlen(hint), &ext);
int hx = (sw - (int)ext.width) / 2;
XftDrawStringUtf8(xd, &col, font_ui, hx, dot_y + DOT_R + 18,
(const FcChar8 *)hint, strlen(hint));
XftColorFree(dpy, visual, cmap, &col);
}
}
/* error message */
if (auth_fail) {
xft_color(COL_ERR, &col);
const char *msg = "Incorrect password";
XftTextExtentsUtf8(dpy, font_ui, (const FcChar8 *)msg,
strlen(msg), &ext);
int ex = (sw - (int)ext.width) / 2;
XftDrawStringUtf8(xd, &col, font_ui, ex, sh / 2 + 60,
(const FcChar8 *)msg, strlen(msg));
XftColorFree(dpy, visual, cmap, &col);
}
XftDrawDestroy(xd);
XFlush(dpy);
}
/* ── PAM ─────────────────────────────────────────────────────────────── */
static const char *pam_pw = NULL;
static int pam_conv_fn(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata_ptr) {
(void)appdata_ptr;
struct pam_response *r = calloc(num_msg, sizeof(struct pam_response));
if (!r) return PAM_CONV_ERR;
for (int i = 0; i < num_msg; i++) {
if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF ||
msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
r[i].resp = strdup(pam_pw ? pam_pw : "");
}
}
*resp = r;
return PAM_SUCCESS;
}
static int try_auth(const char *password) {
const char *user = getenv("USER");
if (!user) user = "root";
pam_pw = password;
struct pam_conv conv = { pam_conv_fn, NULL };
pam_handle_t *ph = NULL;
int ret = pam_start("login", user, &conv, &ph);
if (ret != PAM_SUCCESS) { pam_end(ph, ret); return 0; }
ret = pam_authenticate(ph, PAM_SILENT);
pam_end(ph, ret);
return (ret == PAM_SUCCESS);
}
int main(void) {
dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionlock: cannot open display\n"); return 1; }
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
visual = DefaultVisual(dpy, screen);
cmap = DefaultColormap(dpy, screen);
int sw = DisplayWidth(dpy, screen);
int sh = DisplayHeight(dpy, screen);
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = alloc_color(COL_BG);
swa.event_mask = ExposureMask | KeyPressMask;
lock_win = XCreateWindow(dpy, root, 0, 0, sw, sh, 0,
DefaultDepth(dpy, screen), InputOutput,
visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
gc = XCreateGC(dpy, lock_win, 0, NULL);
font_clock = XftFontOpenName(dpy, screen, "Sans:size=64:weight=bold");
font_date = XftFontOpenName(dpy, screen, "Sans:size=20");
font_ui = XftFontOpenName(dpy, screen, "Sans:size=11");
if (!font_clock) font_clock = XftFontOpenName(dpy, screen, "fixed:size=20");
if (!font_date) font_date = font_clock;
if (!font_ui) font_ui = font_clock;
XMapRaised(dpy, lock_win);
XSync(dpy, False);
/* grab keyboard and pointer */
for (int t = 0; t < 10; t++) {
int r = XGrabKeyboard(dpy, lock_win, True, GrabModeAsync, GrabModeAsync, CurrentTime);
if (r == GrabSuccess) break;
usleep(100000);
}
XGrabPointer(dpy, lock_win, True, ButtonPressMask | ButtonReleaseMask |
PointerMotionMask, GrabModeAsync, GrabModeAsync,
lock_win, None, CurrentTime);
draw_lock();
time_t last_tick = 0;
while (!auth_ok) {
/* update clock every second even with no events */
fd_set fds;
FD_ZERO(&fds);
FD_SET(ConnectionNumber(dpy), &fds);
struct timeval tv = { 1, 0 };
select(ConnectionNumber(dpy) + 1, &fds, NULL, NULL, &tv);
time_t now = time(NULL);
if (now != last_tick) { last_tick = now; draw_lock(); }
while (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == Expose && e.xexpose.count == 0) {
draw_lock();
} else if (e.type == KeyPress) {
char buf[32] = {0};
KeySym sym;
XLookupString(&e.xkey, buf, sizeof(buf) - 1, &sym, NULL);
if (sym == XK_Return || sym == XK_KP_Enter) {
pw_buf[pw_len] = '\0';
if (try_auth(pw_buf)) {
auth_ok = 1;
} else {
auth_fail = 1;
pw_len = 0;
draw_lock();
usleep(400000);
auth_fail = 0;
}
} else if (sym == XK_BackSpace || sym == XK_Delete) {
if (pw_len > 0) pw_len--;
auth_fail = 0;
} else if (sym == XK_Escape) {
pw_len = 0;
auth_fail = 0;
} else if (buf[0] >= 0x20 && pw_len < PW_MAX - 1) {
pw_buf[pw_len++] = buf[0];
auth_fail = 0;
}
draw_lock();
}
}
}
XUngrabKeyboard(dpy, CurrentTime);
XUngrabPointer(dpy, CurrentTime);
XDestroyWindow(dpy, lock_win);
XSync(dpy, False);
XCloseDisplay(dpy);
return 0;
}
+22
View File
@@ -0,0 +1,22 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags dbus-1 xft x11 2>/dev/null)
PKG_L := $(shell pkg-config --libs dbus-1 xft x11 2>/dev/null)
ifeq ($(PKG_C),)
$(error "Required: libdbus-1-dev libxft-dev libx11-dev")
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L)
BIN = orionnotify
.PHONY: all clean install
all: $(BIN)
$(BIN): orionnotify.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
clean:
rm -f $(BIN)
+314
View File
@@ -0,0 +1,314 @@
/*
* orionnotify — freedesktop.org notification daemon
* Implements org.freedesktop.Notifications via libdbus-1.
* Draws popups in the top-right corner (left of the deskbar).
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xft/Xft.h>
#include <dbus/dbus.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#define DESKBAR_W 200
#define NOTIF_W 320
#define NOTIF_H 72
#define NOTIF_PAD 10
#define NOTIF_MAX 5
#define NOTIF_GAP 6
#define NOTIF_DEFAULT_MS 4000
#define COL_BG 0x2A2D36
#define COL_BORDER 0x4A6080
#define COL_TITLE 0xE8F0FF
#define COL_BODY 0xA0B8D0
#define COL_ACCENT 0x1E58A8
typedef struct {
Window win;
char summary[128];
char body[256];
int expire_ms;
struct timespec show_time;
int active;
} Notif;
static Notif notifs[NOTIF_MAX];
static int n_notifs = 0;
static unsigned int next_id = 1;
static Display *dpy;
static int screen;
static Visual *visual;
static Colormap cmap;
static GC gc;
static XftFont *font_title;
static XftFont *font_body;
static unsigned long alloc_color(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(dpy, cmap, &c);
return c.pixel;
}
static void xft_color(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(dpy, visual, cmap, &rc, out);
}
static int notif_x(void) {
return DisplayWidth(dpy, screen) - DESKBAR_W - NOTIF_W - 12;
}
static int notif_y(int slot) {
return 12 + slot * (NOTIF_H + NOTIF_GAP);
}
static void notif_draw(int idx) {
Notif *n = &notifs[idx];
if (!n->active || !n->win) return;
Window w = n->win;
XSetForeground(dpy, gc, alloc_color(COL_BG));
XFillRectangle(dpy, w, gc, 0, 0, NOTIF_W, NOTIF_H);
XSetForeground(dpy, gc, alloc_color(COL_ACCENT));
XFillRectangle(dpy, w, gc, 0, 0, 3, NOTIF_H);
XSetForeground(dpy, gc, alloc_color(COL_BORDER));
XDrawRectangle(dpy, w, gc, 0, 0, NOTIF_W - 1, NOTIF_H - 1);
XftDraw *xd = XftDrawCreate(dpy, w, visual, cmap);
XftColor col;
xft_color(COL_TITLE, &col);
XftDrawStringUtf8(xd, &col, font_title, NOTIF_PAD + 4, 24,
(const FcChar8 *)n->summary, strlen(n->summary));
XftColorFree(dpy, visual, cmap, &col);
xft_color(COL_BODY, &col);
XftDrawStringUtf8(xd, &col, font_body, NOTIF_PAD + 4, 50,
(const FcChar8 *)n->body, strlen(n->body));
XftColorFree(dpy, visual, cmap, &col);
XftDrawDestroy(xd);
XFlush(dpy);
}
static void notif_restack(void) {
int slot = 0;
for (int i = 0; i < NOTIF_MAX; i++) {
if (!notifs[i].active) continue;
XMoveWindow(dpy, notifs[i].win, notif_x(), notif_y(slot));
slot++;
}
}
static void notif_close(int idx) {
if (!notifs[idx].active) return;
XDestroyWindow(dpy, notifs[idx].win);
notifs[idx].win = None;
notifs[idx].active = 0;
n_notifs--;
notif_restack();
}
static void notif_log(const char *summary, const char *body) {
const char *home = getenv("HOME");
if (!home) return;
char dir[512], path[560];
snprintf(dir, sizeof(dir), "%s/.local/share/orionos", home);
snprintf(path, sizeof(path), "%s/notif-history.log", dir);
mkdir(dir, 0755);
FILE *f = fopen(path, "a");
if (!f) return;
time_t t = time(NULL);
/* sanitise: replace tabs and newlines with spaces */
char sum[128], bod[256];
strncpy(sum, summary ? summary : "", 127); sum[127] = '\0';
strncpy(bod, body ? body : "", 255); bod[255] = '\0';
for (char *p = sum; *p; p++) if (*p == '\t' || *p == '\n') *p = ' ';
for (char *p = bod; *p; p++) if (*p == '\t' || *p == '\n') *p = ' ';
fprintf(f, "%ld\t%s\t%s\n", (long)t, sum, bod);
fclose(f);
}
static unsigned int notif_add(const char *summary, const char *body, int expire_ms) {
if (n_notifs >= NOTIF_MAX) {
/* evict oldest */
for (int i = 0; i < NOTIF_MAX; i++) {
if (notifs[i].active) { notif_close(i); break; }
}
}
int slot = -1;
for (int i = 0; i < NOTIF_MAX; i++) {
if (!notifs[i].active) { slot = i; break; }
}
if (slot < 0) return 0;
Notif *n = &notifs[slot];
snprintf(n->summary, sizeof(n->summary), "%s", summary ? summary : "");
snprintf(n->body, sizeof(n->body), "%s", body ? body : "");
n->expire_ms = (expire_ms <= 0) ? NOTIF_DEFAULT_MS : expire_ms;
clock_gettime(CLOCK_MONOTONIC, &n->show_time);
n->active = 1;
n_notifs++;
notif_log(summary, body);
int nx = notif_x();
int ny = notif_y(n_notifs - 1);
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = alloc_color(COL_BG);
swa.event_mask = ExposureMask | ButtonPressMask;
n->win = XCreateWindow(dpy, RootWindow(dpy, screen),
nx, ny, NOTIF_W, NOTIF_H, 0,
DefaultDepth(dpy, screen), InputOutput,
visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
XMapRaised(dpy, n->win);
notif_draw(slot);
return next_id++;
}
static void expire_check(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
for (int i = 0; i < NOTIF_MAX; i++) {
if (!notifs[i].active) continue;
long elapsed_ms = (now.tv_sec - notifs[i].show_time.tv_sec) * 1000
+ (now.tv_nsec - notifs[i].show_time.tv_nsec) / 1000000;
if (elapsed_ms >= notifs[i].expire_ms)
notif_close(i);
}
}
/* ── DBus message handling ───────────────────────────────────────────── */
static DBusHandlerResult handle_message(DBusConnection *conn,
DBusMessage *msg, void *data) {
(void)data;
if (!dbus_message_is_method_call(msg, "org.freedesktop.Notifications", "Notify"))
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
DBusMessageIter iter;
dbus_message_iter_init(msg, &iter);
/* app_name */ dbus_message_iter_next(&iter);
/* replaces_id */ dbus_message_iter_next(&iter);
/* app_icon */ dbus_message_iter_next(&iter);
const char *summary = "";
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING)
dbus_message_iter_get_basic(&iter, &summary);
dbus_message_iter_next(&iter);
const char *body = "";
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING)
dbus_message_iter_get_basic(&iter, &body);
dbus_message_iter_next(&iter); /* actions */
dbus_message_iter_next(&iter); /* hints */
dbus_int32_t expire_timeout = NOTIF_DEFAULT_MS;
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INT32)
dbus_message_iter_get_basic(&iter, &expire_timeout);
unsigned int id = notif_add(summary, body, (int)expire_timeout);
DBusMessage *reply = dbus_message_new_method_return(msg);
dbus_uint32_t uid = id;
dbus_message_append_args(reply, DBUS_TYPE_UINT32, &uid, DBUS_TYPE_INVALID);
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
return DBUS_HANDLER_RESULT_HANDLED;
}
int main(void) {
dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionnotify: cannot open display\n"); return 1; }
screen = DefaultScreen(dpy);
visual = DefaultVisual(dpy, screen);
cmap = DefaultColormap(dpy, screen);
gc = XCreateGC(dpy, RootWindow(dpy, screen), 0, NULL);
font_title = XftFontOpenName(dpy, screen, "Sans:size=10:weight=bold");
font_body = XftFontOpenName(dpy, screen, "Sans:size=9");
if (!font_title) font_title = XftFontOpenName(dpy, screen, "fixed:size=10");
if (!font_body) font_body = font_title;
DBusError err;
dbus_error_init(&err);
DBusConnection *conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (!conn) {
fprintf(stderr, "orionnotify: dbus: %s\n", err.message);
return 1;
}
int ret = dbus_bus_request_name(conn, "org.freedesktop.Notifications",
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
fprintf(stderr, "orionnotify: could not claim notification bus name\n");
return 1;
}
dbus_connection_add_filter(conn, handle_message, NULL, NULL);
int dbus_fd = -1;
dbus_connection_get_unix_fd(conn, &dbus_fd);
int xfd = ConnectionNumber(dpy);
for (;;) {
/* pump X events */
while (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == Expose && e.xexpose.count == 0) {
for (int i = 0; i < NOTIF_MAX; i++)
if (notifs[i].active && notifs[i].win == e.xany.window)
notif_draw(i);
}
if (e.type == ButtonPress) {
for (int i = 0; i < NOTIF_MAX; i++)
if (notifs[i].active && notifs[i].win == e.xany.window)
notif_close(i);
}
}
/* pump dbus */
dbus_connection_read_write(conn, 0);
DBusMessage *msg;
while ((msg = dbus_connection_pop_message(conn)) != NULL) {
handle_message(conn, msg, NULL);
dbus_message_unref(msg);
}
expire_check();
/* sleep up to 200ms, woken by X or dbus */
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
if (dbus_fd >= 0) FD_SET(dbus_fd, &fds);
int maxfd = xfd > dbus_fd ? xfd : dbus_fd;
struct timeval tv = { 0, 200000 };
select(maxfd + 1, &fds, NULL, NULL, &tv);
}
return 0;
}
+17 -10
View File
@@ -1,15 +1,22 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags x11 xft 2>/dev/null)
PKG_L := $(shell pkg-config --libs x11 xft 2>/dev/null)
ifeq ($(PKG_C),)
$(error "Required: libx11-dev libxft-dev")
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L)
BIN = orionpkg
CC = gcc
CFLAGS = -Wall -Wextra -O2 \
$(shell pkg-config --cflags x11 xft fontconfig 2>/dev/null)
LIBS = $(shell pkg-config --libs x11 xft fontconfig 2>/dev/null) \
-lX11 -lXft -lfontconfig
.PHONY: all clean install
all: $(BIN)
$(BIN): orionpkg.c
$(CC) $(CFLAGS) -o $@ $< $(LIBS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
clean:
rm -f $(BIN)
.PHONY: clean
+601 -403
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags x11 libpng 2>/dev/null)
PKG_L := $(shell pkg-config --libs x11 libpng 2>/dev/null)
ifeq ($(PKG_C),)
$(error "Required: libx11-dev libpng-dev")
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L)
BIN = orionshot
.PHONY: all clean install
all: $(BIN)
$(BIN): orionshot.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
clean:
rm -f $(BIN)
+229
View File
@@ -0,0 +1,229 @@
/*
* orionshot — X11 screenshot tool for OrionOS
* Usage: orionshot full full-screen PNG capture
* orionshot region interactive rubber-band region select
*
* Saves to ~/Pictures/Screenshots/orionos_YYYYMMDD_HHMMSS.png
* Sends a desktop notification on success.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#include <png.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif
/* ── PNG writer ──────────────────────────────────────────────────────── */
static int get_shift(unsigned long mask) {
if (!mask) return 0;
int s = 0;
while (!(mask & 1)) { mask >>= 1; s++; }
return s;
}
static int write_png(const char *path, XImage *img, int ox, int oy, int w, int h) {
FILE *fp = fopen(path, "wb");
if (!fp) { perror("orionshot: fopen"); return -1; }
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) { fclose(fp); return -1; }
png_infop info = png_create_info_struct(png);
if (!info) { png_destroy_write_struct(&png, NULL); fclose(fp); return -1; }
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
fclose(fp);
return -1;
}
png_init_io(png, fp);
png_set_IHDR(png, info, (unsigned)w, (unsigned)h, 8,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
int r_shift = get_shift(img->red_mask);
int g_shift = get_shift(img->green_mask);
int b_shift = get_shift(img->blue_mask);
int r_scale = (img->red_mask >> r_shift) < 0xFF ? 2 : 1;
int g_scale = (img->green_mask >> g_shift) < 0xFF ? 2 : 1;
int b_scale = (img->blue_mask >> b_shift) < 0xFF ? 2 : 1;
(void)r_scale; (void)g_scale; (void)b_scale;
unsigned char *row = malloc((size_t)w * 3);
if (!row) { png_destroy_write_struct(&png, &info); fclose(fp); return -1; }
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
unsigned long px = XGetPixel(img, ox + i, oy + j);
row[i*3+0] = (unsigned char)((px & img->red_mask) >> r_shift);
row[i*3+1] = (unsigned char)((px & img->green_mask) >> g_shift);
row[i*3+2] = (unsigned char)((px & img->blue_mask) >> b_shift);
}
png_write_row(png, row);
}
free(row);
png_write_end(png, NULL);
png_destroy_write_struct(&png, &info);
fclose(fp);
return 0;
}
/* ── output path ─────────────────────────────────────────────────────── */
static void make_save_path(char *buf, size_t bufsz) {
const char *home = getenv("HOME");
if (!home) home = "/tmp";
char dir[512];
snprintf(dir, sizeof(dir), "%s/Pictures/Screenshots", home);
mkdir(dir, 0755);
time_t t = time(NULL);
struct tm *tm = localtime(&t);
char ts[32];
strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", tm);
snprintf(buf, bufsz, "%s/orionos_%s.png", dir, ts);
}
/* ── region selection ────────────────────────────────────────────────── */
static int select_region(Display *dpy, Window root,
int *rx, int *ry, int *rw, int *rh) {
Cursor cross = XCreateFontCursor(dpy, XC_crosshair);
if (XGrabPointer(dpy, root, False,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync,
root, cross, CurrentTime) != GrabSuccess) {
XFreeCursor(dpy, cross);
return -1;
}
XGrabKeyboard(dpy, root, False, GrabModeAsync, GrabModeAsync, CurrentTime);
GC gc;
XGCValues gcv;
gcv.function = GXxor;
gcv.foreground = 0xFFFFFF;
gcv.subwindow_mode = IncludeInferiors;
gcv.line_width = 1;
gc = XCreateGC(dpy, root,
GCFunction | GCForeground | GCSubwindowMode | GCLineWidth, &gcv);
int sx = 0, sy = 0, ex = 0, ey = 0;
int started = 0, drawn = 0, done = 0, cancelled = 0;
while (!done) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == ButtonPress && e.xbutton.button == Button1) {
sx = e.xbutton.x_root; sy = e.xbutton.y_root;
ex = sx; ey = sy; started = 1;
} else if (e.type == MotionNotify && started) {
/* erase previous rubber band */
if (drawn) {
int dx = MIN(sx,ex), dy = MIN(sy,ey);
int dw = abs(ex-sx), dh = abs(ey-sy);
if (dw > 0 && dh > 0)
XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh);
XFlush(dpy); drawn = 0;
}
ex = e.xmotion.x_root; ey = e.xmotion.y_root;
{
int dx = MIN(sx,ex), dy = MIN(sy,ey);
int dw = abs(ex-sx), dh = abs(ey-sy);
if (dw > 0 && dh > 0) {
XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh);
XFlush(dpy); drawn = 1;
}
}
} else if (e.type == ButtonRelease && e.xbutton.button == Button1) {
if (drawn) {
int dx = MIN(sx,ex), dy = MIN(sy,ey);
int dw = abs(ex-sx), dh = abs(ey-sy);
if (dw > 0 && dh > 0)
XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh);
XFlush(dpy);
}
ex = e.xbutton.x_root; ey = e.xbutton.y_root;
done = 1;
} else if (e.type == KeyPress) {
cancelled = 1; done = 1;
}
}
XUngrabPointer(dpy, CurrentTime);
XUngrabKeyboard(dpy, CurrentTime);
XFreeGC(dpy, gc);
XFreeCursor(dpy, cross);
XSync(dpy, False);
if (cancelled) return -1;
*rx = MIN(sx,ex); *ry = MIN(sy,ey);
*rw = abs(ex-sx); *rh = abs(ey-sy);
if (*rw < 2 || *rh < 2) return -1;
return 0;
}
/* ── main ────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
int do_region = (argc > 1 && strcmp(argv[1], "region") == 0);
Display *dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionshot: cannot open display\n"); return 1; }
int screen = DefaultScreen(dpy);
Window root = RootWindow(dpy, screen);
int sw = DisplayWidth(dpy, screen);
int sh = DisplayHeight(dpy, screen);
int cx = 0, cy = 0, cw = sw, ch = sh;
if (do_region) {
if (select_region(dpy, root, &cx, &cy, &cw, &ch) < 0) {
XCloseDisplay(dpy);
return 0; /* cancelled — silently exit */
}
/* tiny pause so rubber-band XOR erasure is fully flushed */
usleep(60000);
}
XImage *img = XGetImage(dpy, root, cx, cy, (unsigned)cw, (unsigned)ch,
AllPlanes, ZPixmap);
XCloseDisplay(dpy);
if (!img) { fprintf(stderr, "orionshot: XGetImage failed\n"); return 1; }
char path[1024];
make_save_path(path, sizeof(path));
if (write_png(path, img, 0, 0, cw, ch) < 0) {
XDestroyImage(img);
fprintf(stderr, "orionshot: failed to write %s\n", path);
return 1;
}
XDestroyImage(img);
/* extract just the filename for the notification body */
const char *fname = strrchr(path, '/');
fname = fname ? fname + 1 : path;
char notif[512];
snprintf(notif, sizeof(notif),
"notify-send -t 3000 'Screenshot' '%s' 2>/dev/null &", fname);
system(notif);
fprintf(stderr, "orionshot: saved %s\n", path);
return 0;
}
+3 -3
View File
@@ -4,13 +4,13 @@
@define-color bg_dark #161C2A;
@define-color bg_darker #0E1220;
@define-color bg_light #262E42;
@define-color border_light #3A6898;
@define-color border_light #2A60A8;
@define-color border_dark #080C18;
@define-color text_color #D8E4F4;
@define-color text_disabled #48587A;
@define-color selected_bg #1E4D78;
@define-color selected_bg #1E4878;
@define-color selected_fg #F0F6FF;
@define-color accent #3A6898;
@define-color accent #1E58A8;
@define-color button_bg #202A3C;
@define-color button_hover #283650;
@define-color button_active #181E2E;
+20 -4
View File
@@ -2,10 +2,11 @@
# orionwallpaper — set or restore the OrionOS desktop wallpaper
#
# Usage:
# orionwallpaper # restore saved wallpaper (called from xinitrc)
# orionwallpaper /path/to/image.jpg # set new wallpaper and save it
# orionwallpaper --color "#6B8FB5" # set solid color
# orionwallpaper --reset # reset to default blue-gray solid color
# orionwallpaper # restore saved wallpaper (called from xinitrc)
# orionwallpaper /path/to/image.jpg # set new wallpaper and save it
# orionwallpaper --color "#6B8FB5" # set solid color
# orionwallpaper --reset # reset to default blue-gray solid color
# orionwallpaper --slideshow DIR [SECS] # rotate images in DIR every SECS seconds (default 300)
#
# Wallpaper preference is stored in ~/.config/orionos/wallpaper
@@ -17,6 +18,21 @@ WALLPAPER_DIR="/usr/share/orionos/wallpapers"
mkdir -p "$CONF_DIR"
case "${1:-}" in
--slideshow)
DIR="${2:-$WALLPAPER_DIR}"
SECS="${3:-300}"
if [[ ! -d "$DIR" ]]; then
echo "Error: directory not found: $DIR" >&2
exit 1
fi
while true; do
IMG="$(find "$DIR" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | shuf -n1)"
if [[ -n "$IMG" ]]; then
feh --no-fehbg --bg-fill "$IMG"
fi
sleep "$SECS"
done
;;
--reset)
echo "color:$DEFAULT_COLOR" > "$CONF_FILE"
xsetroot -solid "$DEFAULT_COLOR"
+25 -20
View File
@@ -1,22 +1,29 @@
# OrionOS picom compositor config
# Goal: subtle window shadows, no transparency, fast rendering.
# BeOS/OrionOS didn't have transparency — we keep it clean.
# OrionOS picom compositor config — macOS-inspired
# Rounded corners, soft macOS-style shadows, fast fading.
backend = "xrender"; # xrender works on all hardware; switch to glx for performance
backend = "xrender";
vsync = false;
# ── rounded corners ───────────────────────────────────────────────────────
corner-radius = 10;
rounded-corners-exclude = [
"_NET_WM_WINDOW_TYPE@:a *= 'DOCK'",
"_NET_WM_WINDOW_TYPE@:a *= 'DESKTOP'",
"_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'",
"_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'",
"override_redirect = 1 && !WM_CLASS@:s"
];
# ── shadows ───────────────────────────────────────────────────────────────
# macOS-style: larger, softer, offset down-right
shadow = true;
shadow-radius = 8;
shadow-offset-x = 4;
shadow-offset-y = 4;
shadow-opacity = 0.30;
shadow-radius = 18;
shadow-offset-x = 0;
shadow-offset-y = 8;
shadow-opacity = 0.35;
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'",
@@ -27,20 +34,18 @@ shadow-exclude = [
# ── fading ────────────────────────────────────────────────────────────────
fading = true;
fade-in-step = 0.06;
fade-out-step = 0.06;
fade-delta = 8; # ms between steps — snappy, not sluggish
fade-in-step = 0.08;
fade-out-step = 0.08;
fade-delta = 6;
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 ───────────────────────────────────────────────────────
@@ -55,9 +60,9 @@ mark-ovredir-focused = false;
wintypes:
{
tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; };
dock = { shadow = false; clip-shadow-above = true; };
dock = { shadow = false; clip-shadow-above = true; corner-radius = 0; };
dnd = { shadow = false; };
popup_menu = { opacity = 1.0; shadow = false; };
dropdown_menu = { opacity = 1.0; shadow = false; };
notification = { shadow = false; };
popup_menu = { opacity = 1.0; shadow = false; corner-radius = 8; };
dropdown_menu = { opacity = 1.0; shadow = false; corner-radius = 8; };
notification = { shadow = false; corner-radius = 8; };
};
+2 -2
View File
@@ -12,9 +12,9 @@ ifeq ($(PKG_XFT_C),)
endif
CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C)
LDFLAGS = $(PKG_XFT_L)
LDFLAGS = $(PKG_XFT_L) -lm
SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c src/switcher.c
SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c src/switcher.c src/expose.c
OBJ = $(SRC:.c=.o)
BIN = orionwm
+57 -27
View File
@@ -5,35 +5,40 @@
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#include <X11/Xft/Xft.h>
#include <X11/cursorfont.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.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) */
/* ── palette (macOS dark mode) ───────────────────────────────────────── */
#define COLOR_TITLE_ACTIVE 0x3C3C3C /* macOS dark titlebar */
#define COLOR_TITLE_INACTIVE 0x2C2C2C /* inactive, slightly darker */
#define COLOR_TITLE_TEXT_ACT 0xFFFFFF /* white active text */
#define COLOR_TITLE_TEXT_INACT 0x686868 /* dimmed inactive text */
#define COLOR_BORDER_LIGHT 0x505050 /* subtle border highlight */
#define COLOR_BORDER_DARK 0x1C1C1C /* near-black shadow */
#define COLOR_CLOSE_BTN 0xFF5F57 /* macOS traffic-light red */
#define COLOR_MAXIMIZE_BTN 0x28C840 /* macOS traffic-light green */
#define COLOR_MINIMIZE_BTN 0xFEBC2E /* macOS traffic-light yellow */
#define COLOR_CLOSE_BTN_HOV 0xFF3B30
#define COLOR_MAXIMIZE_BTN_HOV 0x34C759
#define COLOR_MINIMIZE_BTN_HOV 0xFFCC00
#define COLOR_BG 0x1C1C1C /* near-black desktop */
#define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */
/* ── geometry ────────────────────────────────────────────────────────── */
#define TITLE_HEIGHT 28 /* taller for modern look */
#define DESKBAR_W 200 /* must match DB_W in deskbar/include/deskbar.h */
#define TITLE_HEIGHT 28 /* macOS-style height */
#define TITLE_TAB_W 200
#define BORDER_W 3
#define BTN_SIZE 14
#define BTN_MARGIN 5
#define BORDER_W 1 /* thin 1px macOS-style border */
#define BTN_SIZE 12 /* macOS traffic-light diameter */
#define BTN_MARGIN 8 /* spacing between traffic lights */
#define BTN_LEFT_PAD 10 /* left edge → first button */
#define RESIZE_GRIP 12
#define MIN_WIN_W 120
#define MIN_WIN_H 60
@@ -41,15 +46,15 @@
#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
* macOS-style: three traffic-light buttons on LEFT side
* close = BTN_LEFT_PAD
* minimize = BTN_LEFT_PAD + BTN_SIZE + BTN_MARGIN
* maximize = BTN_LEFT_PAD + (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)
#define BTN_CLOSE_X (BTN_LEFT_PAD)
#define BTN_MIN_X (BTN_LEFT_PAD + BTN_SIZE + BTN_MARGIN)
#define BTN_MAX_X (BTN_LEFT_PAD + (BTN_SIZE + BTN_MARGIN) * 2)
#define BTN_Y (((TITLE_HEIGHT) - BTN_SIZE) / 2)
/* ── fonts ───────────────────────────────────────────────────────────── */
#define FONT_TITLE "Sans:size=9:weight=bold"
@@ -71,10 +76,18 @@ typedef struct Client {
int focused;
int minimized;
int maximized;
int desktop; /* virtual desktop this window lives on */
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];
/* ICCCM WM_NORMAL_HINTS */
int min_w, min_h; /* PMinSize */
int inc_w, inc_h; /* PResizeInc — 0 means no constraint */
int base_w, base_h; /* PBaseSize — baseline for increment calc */
/* Exposé saved geometry */
int exp_x, exp_y;
unsigned exp_w, exp_h;
struct Client *next;
} Client;
@@ -102,6 +115,9 @@ typedef struct WM {
Client *clients;
Client *focused;
int cur_desktop; /* active virtual desktop (0-based) */
int hot_corner_last; /* last hot-corner zone (0=none,1-4) */
Atom wm_delete_window;
Atom wm_protocols;
Atom wm_state;
@@ -110,6 +126,8 @@ typedef struct WM {
Atom net_wm_state_hidden;
Atom net_wm_state_maximized_vert;
Atom net_wm_state_maximized_horz;
Atom net_wm_desktop;
Atom net_current_desktop;
DragMode drag_mode;
int drag_start_x, drag_start_y;
@@ -129,6 +147,12 @@ typedef struct WM {
int swl_count;
Window swl_wins[64];
char swl_titles[64][128];
int swl_mini[64]; /* 1 if corresponding entry is minimized */
/* Exposé window overview */
int expose_active;
int expose_count;
Window expose_wins[64]; /* frames shown in overview */
} WM;
/* core */
@@ -151,6 +175,7 @@ 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);
void client_move_to_desktop(WM *wm, Client *c, int new_desk);
/* drawing */
void frame_draw(WM *wm, Client *c);
@@ -174,9 +199,14 @@ int menu_item_at(int py);
/* switcher */
void switcher_show(WM *wm);
void switcher_next(WM *wm);
void switcher_prev(WM *wm);
void switcher_hide(WM *wm, int commit);
void switcher_draw(WM *wm);
/* exposé */
void expose_show(WM *wm);
void expose_hide(WM *wm, Window selected_frame);
/* events */
void on_map_request(WM *wm, XMapRequestEvent *e);
void on_unmap_notify(WM *wm, XUnmapEvent *e);
+69 -8
View File
@@ -54,9 +54,12 @@ Client *client_add(WM *wm, Window win) {
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;
c->win = win;
c->w = wa.width;
c->h = wa.height;
c->desktop = wm->cur_desktop;
c->min_w = MIN_WIN_W;
c->min_h = MIN_WIN_H;
/* use application-requested position if non-zero, otherwise smart place */
if (wa.x == 0 && wa.y == 0) {
@@ -65,9 +68,9 @@ Client *client_add(WM *wm, Window win) {
c->x = wa.x;
c->y = wa.y;
}
/* never land behind deskbar (top-right panel ~180px wide) */
/* never land behind deskbar */
int sw = DisplayWidth(wm->dpy, wm->screen);
if (c->x + (int)c->w > sw - 185 && c->y < 200)
if (c->x + (int)c->w > sw - DESKBAR_W && c->y < 200)
c->y = 40;
/* fetch title */
@@ -93,6 +96,36 @@ Client *client_add(WM *wm, Window win) {
}
}
/* ICCCM WM_NORMAL_HINTS — size constraints and resize increments */
{
XSizeHints hints;
long supplied = 0;
if (XGetWMNormalHints(wm->dpy, win, &hints, &supplied)) {
if (hints.flags & PMinSize) {
if (hints.min_width > c->min_w) c->min_w = hints.min_width;
if (hints.min_height > c->min_h) c->min_h = hints.min_height;
}
if (hints.flags & PResizeInc) {
c->inc_w = hints.width_inc;
c->inc_h = hints.height_inc;
}
if (hints.flags & PBaseSize) {
c->base_w = hints.base_width;
c->base_h = hints.base_height;
} else if (hints.flags & PMinSize) {
c->base_w = hints.min_width;
c->base_h = hints.min_height;
}
}
}
/* advertise which desktop this window lives on */
{
long desk = wm->cur_desktop;
XChangeProperty(wm->dpy, win, wm->net_wm_desktop, XA_CARDINAL, 32,
PropModeReplace, (unsigned char *)&desk, 1);
}
int fw = (int)c->w + BORDER_W * 2;
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
@@ -228,7 +261,7 @@ void client_maximize(WM *wm, Client *c) {
int sh = DisplayHeight(wm->dpy, wm->screen);
/* leave room for deskbar on the right */
int deskbar_w = 185;
int deskbar_w = DESKBAR_W;
int new_w = sw - deskbar_w - BORDER_W * 2;
int new_h = sh - TITLE_HEIGHT - BORDER_W;
@@ -280,7 +313,7 @@ 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 deskbar_w = DESKBAR_W;
int half_w = (sw - deskbar_w) / 2;
c->x = 0;
@@ -295,11 +328,39 @@ void client_snap_left(WM *wm, Client *c) {
frame_draw(wm, c);
}
void client_move_to_desktop(WM *wm, Client *c, int new_desk) {
if (new_desk < 0 || new_desk >= 4 || c->desktop == new_desk) return;
/* hide frame if it's on the current (now-active) desktop */
if (c->desktop == wm->cur_desktop && !c->minimized)
XUnmapWindow(wm->dpy, c->frame);
c->desktop = new_desk;
/* update _NET_WM_DESKTOP */
long desk = new_desk;
XChangeProperty(wm->dpy, c->win, wm->net_wm_desktop, XA_CARDINAL, 32,
PropModeReplace, (unsigned char *)&desk, 1);
/* if we moved the focused window away, re-focus another */
if (wm->focused == c) {
wm->focused->focused = 0;
frame_draw(wm, wm->focused);
wm->focused = NULL;
for (Client *n = wm->clients; n; n = n->next) {
if (!n->minimized && n->desktop == wm->cur_desktop) {
client_focus(wm, n);
break;
}
}
}
}
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 deskbar_w = DESKBAR_W;
int half_w = (sw - deskbar_w) / 2;
c->x = half_w;
+38 -44
View File
@@ -1,33 +1,30 @@
#include "wm.h"
/*
* Frame layout (3 buttons on right):
* Frame layout — macOS style, traffic-light buttons on LEFT:
*
* ┌─[Title______________]──────[_][□][X]─┐ <- TITLE_HEIGHT
* │ content │
* │ [grip]────┘
* ┌─[●][●][●]──── Title (centered) ────────┐ <- 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 &&
(void)c;
return (x >= BTN_CLOSE_X && x <= BTN_CLOSE_X + 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 &&
(void)c;
return (x >= BTN_MAX_X && x <= BTN_MAX_X + 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 &&
(void)c;
return (x >= BTN_MIN_X && x <= BTN_MIN_X + BTN_SIZE &&
y >= BTN_Y && y <= BTN_Y + BTN_SIZE);
}
@@ -70,13 +67,14 @@ static void draw_circle_btn(WM *wm, Drawable d, int bx, int by,
}
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,
/* macOS order left-to-right: close, minimize, maximize */
draw_circle_btn(wm, c->frame, BTN_CLOSE_X, 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,
draw_circle_btn(wm, c->frame, BTN_MIN_X, BTN_Y,
color_alloc(wm, COLOR_MINIMIZE_BTN), '_');
draw_circle_btn(wm, c->frame, BTN_MAX_X, BTN_Y,
color_alloc(wm, COLOR_MAXIMIZE_BTN), 'O');
(void)c;
}
/* ── resize grip ─────────────────────────────────────────────────────── */
@@ -101,7 +99,6 @@ 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 */
@@ -110,45 +107,42 @@ void frame_draw(WM *wm, Client *c) {
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) */
/* thin borders (1px, macOS style) */
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);
/* subtle 1px separator under titlebar */
unsigned long col_sep = color_alloc(wm, 0x1A1A1A);
XSetForeground(wm->dpy, wm->gc, col_sep);
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;
/* centered title text (macOS style) */
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 };
XGlyphInfo ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title,
(const FcChar8 *)c->title, strlen(c->title), &ext);
/* center in the full title bar */
int tx = (fw - ext.width) / 2;
/* clamp so text doesn't overlap the left buttons */
int left_btn_end = BTN_MAX_X + BTN_SIZE + 6;
if (tx < left_btn_end) tx = left_btn_end;
int text_max_w = fw - tx - 8;
if (text_max_w < 10) text_max_w = 10;
XRectangle clip = { (short)tx, 1, (unsigned short)text_max_w, TITLE_HEIGHT - 2 };
XftDrawSetClipRectangles(xd, 0, 0, &clip, 1);
int ty = (TITLE_HEIGHT + wm->font_title->ascent - wm->font_title->descent) / 2;
XftDrawStringUtf8(xd, &col_text, wm->font_title,
BORDER_W + 6, ty,
tx, ty,
(const FcChar8 *)c->title, strlen(c->title));
XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_text);
XftDrawDestroy(xd);
+186 -15
View File
@@ -33,7 +33,7 @@ static void check_snap(WM *wm, Client *c) {
} else {
client_snap_left(wm, c);
}
} else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - 185 - SNAP_DIST) {
} else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - DESKBAR_W - SNAP_DIST) {
client_snap_right(wm, c);
} else if (c->y <= SNAP_DIST) {
client_maximize(wm, c);
@@ -74,6 +74,7 @@ void on_expose(WM *wm, XExposeEvent *e) {
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;
@@ -94,11 +95,87 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) {
if (c && !c->minimized) set_cursor(wm, c, e->x, e->y);
}
static void wm_switch_desktop(WM *wm, int new_desk) {
if (new_desk < 0 || new_desk >= 4 || new_desk == wm->cur_desktop) return;
/* hide windows on old desktop */
for (Client *c = wm->clients; c; c = c->next) {
if (!c->minimized && c->desktop == wm->cur_desktop)
XUnmapWindow(wm->dpy, c->frame);
}
wm->cur_desktop = new_desk;
/* show windows on new desktop */
for (Client *c = wm->clients; c; c = c->next) {
if (!c->minimized && c->desktop == new_desk)
XMapWindow(wm->dpy, c->frame);
}
/* update focus */
if (wm->focused && wm->focused->desktop != new_desk) {
wm->focused->focused = 0;
frame_draw(wm, wm->focused);
wm->focused = NULL;
}
if (!wm->focused) {
for (Client *c = wm->clients; c; c = c->next) {
if (!c->minimized && c->desktop == new_desk) {
client_focus(wm, c);
break;
}
}
}
/* advertise new desktop */
long nd = new_desk;
XChangeProperty(wm->dpy, wm->root, wm->net_current_desktop, XA_CARDINAL, 32,
PropModeReplace, (unsigned char *)&nd, 1);
}
void on_client_message(WM *wm, XClientMessageEvent *e) {
Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False);
Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False);
Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False);
Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False);
Atom net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False);
/* _NET_CURRENT_DESKTOP → switch virtual desktop */
if (e->message_type == wm->net_current_desktop && e->window == wm->root) {
wm_switch_desktop(wm, (int)e->data.l[0]);
return;
}
/* _NET_WM_DESKTOP → move window to desktop */
if (e->message_type == net_wm_desktop) {
Client *c = client_find_by_win(wm, e->window);
if (c) client_move_to_desktop(wm, c, (int)e->data.l[0]);
return;
}
if (e->message_type == net_active) {
Client *c = client_find_by_win(wm, e->window);
if (c) client_restore(wm, c);
return;
}
if (e->message_type == net_close) {
Client *c = client_find_by_win(wm, e->window);
if (c) {
XEvent ev = {0};
ev.xclient.type = ClientMessage;
ev.xclient.window = c->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, c->win, False, NoEventMask, &ev);
}
return;
}
/* WM_CHANGE_STATE IconicState → minimize */
if (e->message_type == wm_change_state && e->data.l[0] == 3) {
Client *c = client_find_by_win(wm, e->window);
if (c && !c->minimized) client_minimize(wm, c);
return;
}
}
@@ -140,6 +217,14 @@ void on_property_notify(WM *wm, XPropertyEvent *e) {
}
void on_button_press(WM *wm, XButtonEvent *e) {
/* Exposé — click on any frame to select and exit */
if (wm->expose_active) {
Client *c = client_find_by_frame(wm, e->window);
if (c) expose_hide(wm, c->frame);
else expose_hide(wm, None);
return;
}
/* menu window */
if (wm->menu_visible && e->window == wm->menu_win) {
if (e->button == Button1) menu_click(wm, e->x, e->y);
@@ -247,8 +332,18 @@ void on_motion_notify(WM *wm, XMotionEvent *e) {
} 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;
/* clamp to minimum */
if (nw < c->min_w) nw = c->min_w;
if (nh < c->min_h) nh = c->min_h;
/* snap to resize-increment grid (e.g. terminal character cells) */
if (c->inc_w > 1) {
nw = c->base_w + ((nw - c->base_w) / c->inc_w) * c->inc_w;
if (nw < c->min_w) nw += c->inc_w;
}
if (c->inc_h > 1) {
nh = c->base_h + ((nh - c->base_h) / c->inc_h) * c->inc_h;
if (nh < c->min_h) nh += c->inc_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);
@@ -263,10 +358,19 @@ void on_key_press(WM *wm, XKeyEvent *e) {
if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused)
send_delete(wm, wm->focused->win);
/* Alt+Tab — visual switcher */
/* Alt+Tab / Shift+Alt+Tab — visual switcher */
if ((e->state & Mod1Mask) && sym == XK_Tab) {
if (wm->swl_visible) switcher_next(wm);
else if (wm->clients) switcher_show(wm);
int backward = (e->state & ShiftMask) != 0;
if (wm->swl_visible) {
if (backward) switcher_prev(wm);
else switcher_next(wm);
} else if (wm->clients) {
switcher_show(wm);
if (backward && wm->swl_visible) {
wm->swl_idx = wm->swl_count - 1;
switcher_draw(wm);
}
}
return;
}
@@ -288,16 +392,47 @@ void on_key_press(WM *wm, XKeyEvent *e) {
if ((e->state & Mod1Mask) && sym == XK_F1)
system("orionlauncher &");
/* PrintScreen — full screenshot to ~/Pictures */
/* PrintScreen — screenshot */
if (sym == XK_Print) {
if (e->state & ShiftMask)
system("mkdir -p ~/Pictures && scrot -s ~/Pictures/screenshot_%Y%m%d_%H%M%S.png"
" && notify-send 'Screenshot' 'Region saved to ~/Pictures' &");
system("orionshot region &");
else
system("mkdir -p ~/Pictures && scrot ~/Pictures/screenshot_%Y%m%d_%H%M%S.png"
" && notify-send 'Screenshot' 'Saved to ~/Pictures' &");
system("orionshot full &");
}
/* XF86 media keys */
if (sym == XF86XK_AudioRaiseVolume)
system("pactl set-sink-volume @DEFAULT_SINK@ +5% 2>/dev/null; "
"pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | "
"grep -oP '[0-9]+%' | head -1 | "
"xargs -I{} notify-send -t 1500 'Volume' '{}' 2>/dev/null &");
if (sym == XF86XK_AudioLowerVolume)
system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null; "
"pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | "
"grep -oP '[0-9]+%' | head -1 | "
"xargs -I{} notify-send -t 1500 'Volume' '{}' 2>/dev/null &");
if (sym == XF86XK_AudioMute)
system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null; "
"notify-send -t 1500 'Volume' 'Muted toggled' 2>/dev/null &");
if (sym == XF86XK_AudioPlay)
system("playerctl play-pause 2>/dev/null &");
if (sym == XF86XK_AudioNext)
system("playerctl next 2>/dev/null &");
if (sym == XF86XK_AudioPrev)
system("playerctl previous 2>/dev/null &");
if (sym == XF86XK_MonBrightnessUp)
system("brightnessctl set +10% 2>/dev/null; "
"notify-send -t 1200 'Brightness' "
"\"$(brightnessctl g 2>/dev/null)\" 2>/dev/null &");
if (sym == XF86XK_MonBrightnessDown)
system("brightnessctl set 10%- 2>/dev/null; "
"notify-send -t 1200 'Brightness' "
"\"$(brightnessctl g 2>/dev/null)\" 2>/dev/null &");
/* Super+V — clipboard manager */
if ((e->state & Mod4Mask) && sym == XK_v)
system("orionclip --show &");
/* Super+Left — snap left */
if ((e->state & Mod4Mask) && sym == XK_Left && wm->focused)
client_snap_left(wm, wm->focused);
@@ -314,9 +449,45 @@ void on_key_press(WM *wm, XKeyEvent *e) {
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);
/* Super+Space — Exposé window overview */
if ((e->state & Mod4Mask) && sym == XK_space) {
if (wm->expose_active) expose_hide(wm, None);
else expose_show(wm);
return;
}
/* Super+1-4 — switch to desktop N */
if ((e->state & Mod4Mask) && !(e->state & ShiftMask)) {
int desk = -1;
if (sym == XK_1) desk = 0;
else if (sym == XK_2) desk = 1;
else if (sym == XK_3) desk = 2;
else if (sym == XK_4) desk = 3;
if (desk >= 0) {
if (wm->expose_active) expose_hide(wm, None);
wm_switch_desktop(wm, desk);
return;
}
}
/* Super+Shift+1-4 — move focused window to desktop N */
if ((e->state & Mod4Mask) && (e->state & ShiftMask) && wm->focused) {
int desk = -1;
if (sym == XK_1) desk = 0;
else if (sym == XK_2) desk = 1;
else if (sym == XK_3) desk = 2;
else if (sym == XK_4) desk = 3;
if (desk >= 0) {
client_move_to_desktop(wm, wm->focused, desk);
return;
}
}
/* Escape — hide menu or exit Exposé */
if (sym == XK_Escape) {
if (wm->expose_active) expose_hide(wm, None);
else if (wm->menu_visible) menu_hide(wm);
}
}
void on_key_release(WM *wm, XKeyEvent *e) {
+93
View File
@@ -0,0 +1,93 @@
#include "wm.h"
#include <math.h>
void expose_show(WM *wm) {
if (wm->expose_active) return;
/* collect non-minimized windows on the current desktop */
int n = 0;
for (Client *c = wm->clients; c; c = c->next) {
if (!c->minimized && c->desktop == wm->cur_desktop && n < 64) {
/* save original geometry */
c->exp_x = c->x;
c->exp_y = c->y;
c->exp_w = c->w;
c->exp_h = c->h;
wm->expose_wins[n++] = c->frame;
}
}
if (n == 0) return;
wm->expose_active = 1;
wm->expose_count = n;
int sw = DisplayWidth(wm->dpy, wm->screen) - DESKBAR_W;
int sh = DisplayHeight(wm->dpy, wm->screen);
int cols = (int)ceil(sqrt((double)n));
int rows = (n + cols - 1) / cols;
int pad = 16;
int cell_w = (sw - pad * (cols + 1)) / cols;
int cell_h = (sh - pad * (rows + 1)) / rows;
int idx = 0;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols && idx < n; col++, idx++) {
Client *c = client_find_by_frame(wm, wm->expose_wins[idx]);
if (!c) continue;
/* un-maximize so we can freely resize */
if (c->maximized) {
c->maximized = 0;
XDeleteProperty(wm->dpy, c->win, wm->net_wm_state);
}
int tx = pad + col * (cell_w + pad);
int ty = pad + row * (cell_h + pad);
int tw = cell_w;
int th = cell_h;
c->x = tx; c->y = ty;
c->w = (unsigned)(tw - BORDER_W * 2);
c->h = (unsigned)(th - 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);
}
}
XSync(wm->dpy, False);
}
void expose_hide(WM *wm, Window selected_frame) {
if (!wm->expose_active) return;
wm->expose_active = 0;
Client *to_focus = NULL;
for (int i = 0; i < wm->expose_count; i++) {
Client *c = client_find_by_frame(wm, wm->expose_wins[i]);
if (!c) continue;
c->x = c->exp_x;
c->y = c->exp_y;
c->w = c->exp_w;
c->h = c->exp_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);
frame_draw(wm, c);
if (c->frame == selected_frame) to_focus = c;
}
wm->expose_count = 0;
XSync(wm->dpy, False);
if (to_focus) client_focus(wm, to_focus);
else if (wm->focused) client_focus(wm, wm->focused);
}
+30 -31
View File
@@ -1,27 +1,26 @@
#include "wm.h"
#define MENU_ITEM_H 24
#define MENU_W 220
#define MENU_ITEM_H 26
#define MENU_W 230
#define MENU_PADDING 5
static const MenuItem MENU_ITEMS[] = {
{ "App Launcher", "orionlauncher &" },
{ "Terminal", "alacritty &" },
{ "File Manager", "rigel &" },
{ "Text Editor", "quill &" },
{ "──────────────────", NULL },
{ "Web Browser", "firefox &" },
{ "System Monitor", "alacritty -T 'System Monitor' -e htop &" },
{ "Volume Control", "pavucontrol &" },
{ "System Settings", "nebula &" },
{ "Software Updates", "orionpkg &" },
{ "Display Settings", "arandr &" },
{ "──────────────────", NULL },
{ "Change Wallpaper", "orionwallpaper --reset &" },
{ "Install OrionOS", "alacritty -e bash -c 'sudo orioninstall-gui 2>/dev/null || sudo orioninstall; exec bash' &" },
{ "──────────────────", NULL },
{ "Restart OrionWM", "pkill orionwm; sleep 0.2; orionwm &" },
{ "Quit OrionOS", "pkill orionwm" },
{ "Terminal", "xterm &" },
{ "File Manager", "pcmanfm &" },
{ "Text Editor", "mousepad &" },
{ "──────────────────", NULL },
{ "Web Browser", "firefox &" },
{ "System Monitor", "xterm -T 'System Monitor' -e htop &" },
{ "Volume Control", "xterm -e alsamixer &" },
{ "Software Updates", "orionpkg &" },
{ "Display Settings", "arandr &" },
{ "──────────────────", NULL },
{ "Change Wallpaper", "orionwallpaper --reset &" },
{ "Install OrionOS", "pkexec orioninstall-gui &" },
{ "──────────────────", NULL },
{ "Restart OrionWM", "pkill orionwm; sleep 0.2; orionwm &" },
{ "Quit OrionOS", "pkill orionwm" },
};
#define MENU_COUNT ((int)(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0])))
@@ -51,7 +50,7 @@ void menu_show(WM *wm, int x, int y) {
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = color_alloc(wm, 0x1C2030); /* dark menu bg */
swa.background_pixel = color_alloc(wm, 0x2C2C2C); /* macOS dark menu bg */
swa.event_mask = ExposureMask | ButtonPressMask
| ButtonReleaseMask | PointerMotionMask
| LeaveWindowMask;
@@ -65,8 +64,8 @@ void menu_show(WM *wm, int x, int y) {
&swa
);
/* 1px accent border */
XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, COLOR_BORDER_LIGHT));
/* subtle 1px border (macOS-like) */
XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, 0x505050));
XMapRaised(wm->dpy, wm->menu_win);
wm->menu_visible = 1;
@@ -110,29 +109,29 @@ void menu_draw(WM *wm) {
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);
xft_color(wm, 0xE8E8E8, &col_normal); /* macOS light menu text */
xft_color(wm, 0x4A90D9, &col_hover_bg); /* macOS blue selection */
xft_color(wm, 0xFFFFFF, &col_hover_text);
xft_color(wm, 0x444444, &col_sep);
/* clear background */
XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x1C2030));
XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x2C2C2C));
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));
XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x3C3C3C));
XDrawLine(wm->dpy, wm->menu_win, wm->gc, 8, iy + 4, MENU_W - 8, iy + 4);
continue;
}
/* hover highlight */
/* hover highlight — macOS blue selection pill */
if (i == wm->menu_hovered && wm->menu_hovered >= 0) {
XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x4A6888));
XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x4A90D9));
XFillRectangle(wm->dpy, wm->menu_win, wm->gc,
2, iy, MENU_W - 4, MENU_ITEM_H);
4, iy + 1, MENU_W - 8, MENU_ITEM_H - 2);
}
XftColor *tc = (i == wm->menu_hovered) ? &col_hover_text : &col_normal;
@@ -142,7 +141,7 @@ void menu_draw(WM *wm) {
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,
MENU_PADDING + 10, ty,
(const FcChar8 *)MENU_ITEMS[i].label,
strlen(MENU_ITEMS[i].label));
}
+34 -12
View File
@@ -8,17 +8,19 @@
static void swl_populate(WM *wm) {
wm->swl_count = 0;
/* current focused window goes first */
/* current focused (non-minimized) window goes first */
if (wm->focused && !wm->focused->minimized) {
wm->swl_wins[0] = wm->focused->win;
wm->swl_wins[0] = wm->focused->win;
wm->swl_mini[0] = 0;
strncpy(wm->swl_titles[0], wm->focused->title, 127);
wm->swl_titles[0][127] = '\0';
wm->swl_count = 1;
}
/* all other windows, including minimized ones */
for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) {
if (c->minimized) continue;
if (wm->focused && c == wm->focused) continue;
wm->swl_wins[wm->swl_count] = c->win;
wm->swl_wins[wm->swl_count] = c->win;
wm->swl_mini[wm->swl_count] = c->minimized;
strncpy(wm->swl_titles[wm->swl_count], c->title, 127);
wm->swl_titles[wm->swl_count][127] = '\0';
wm->swl_count++;
@@ -61,29 +63,34 @@ void switcher_draw(WM *wm) {
for (int vi = 0; vi < vis; vi++) {
int i = vstart + vi;
int selected = (i == wm->swl_idx);
int mini = wm->swl_mini[i];
int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD);
int iy = SWL_PAD;
unsigned long card_bg = color_alloc(wm, selected ? COLOR_TITLE_ACTIVE : 0x1C2338);
unsigned long card_bg = color_alloc(wm,
selected ? COLOR_TITLE_ACTIVE : (mini ? 0x141820 : 0x1C2338));
XSetForeground(wm->dpy, wm->gc, card_bg);
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H);
XSetForeground(wm->dpy, wm->gc,
color_alloc(wm, selected ? COLOR_BORDER_LIGHT : 0x242C3E));
color_alloc(wm, selected ? COLOR_BORDER_LIGHT : (mini ? 0x202028 : 0x242C3E)));
XDrawRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W - 1, SWL_ITEM_H - 1);
/* letter icon */
/* letter icon — dimmed for minimized */
int icon_x = ix + (SWL_ITEM_W - SWL_ICON_SZ) / 2;
int icon_y = iy + 8;
XSetForeground(wm->dpy, wm->gc,
color_alloc(wm, selected ? 0x285898 : 0x182538));
color_alloc(wm, selected ? 0x285898 : (mini ? 0x1A1E28 : 0x182538)));
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, icon_x, icon_y, SWL_ICON_SZ, SWL_ICON_SZ);
char letter[2] = { wm->swl_titles[i][0] ? wm->swl_titles[i][0] : '?', '\0' };
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
unsigned long icon_rgb = selected ? COLOR_TITLE_TEXT_ACT
: (mini ? 0x3A4860
: COLOR_TITLE_TEXT_INACT);
XftColor icon_col;
xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &icon_col);
xft_color(wm, icon_rgb, &icon_col);
XGlyphInfo ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext);
int lx = icon_x + (SWL_ICON_SZ - (int)ext.width) / 2;
@@ -91,10 +98,19 @@ void switcher_draw(WM *wm) {
XftDrawStringUtf8(xd, &icon_col, wm->font_title, lx, ly, (const FcChar8 *)letter, 1);
XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col);
/* title below icon */
const char *title = wm->swl_titles[i];
/* title below icon — prefix [..] for minimized */
char title_buf[132];
const char *base_title = wm->swl_titles[i];
if (mini && !selected)
snprintf(title_buf, sizeof(title_buf), "[%s]", base_title);
else
snprintf(title_buf, sizeof(title_buf), "%s", base_title);
const char *title = title_buf;
XftColor title_col;
xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &title_col);
unsigned long title_rgb = selected ? COLOR_TITLE_TEXT_ACT
: (mini ? 0x3A4860
: COLOR_TITLE_TEXT_INACT);
xft_color(wm, title_rgb, &title_col);
XGlyphInfo text_ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title,
(const FcChar8 *)title, strlen(title), &text_ext);
@@ -163,6 +179,12 @@ void switcher_next(WM *wm) {
switcher_draw(wm);
}
void switcher_prev(WM *wm) {
if (!wm->swl_visible) return;
wm->swl_idx = (wm->swl_idx - 1 + wm->swl_count) % wm->swl_count;
switcher_draw(wm);
}
void switcher_hide(WM *wm, int commit) {
if (!wm->swl_visible) return;
XUngrabKeyboard(wm->dpy, CurrentTime);
+122 -17
View File
@@ -1,5 +1,67 @@
#include "wm.h"
/* ── autostart ───────────────────────────────────────────────────── */
static void wm_run_autostart(void) {
const char *home = getenv("HOME");
if (!home) return;
char path[512];
snprintf(path, sizeof(path), "%s/.config/orionos/autostart", home);
FILE *f = fopen(path, "r");
if (!f) return;
char line[512];
while (fgets(line, sizeof(line), f)) {
size_t len = strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' ||
line[len-1] == ' ' || line[len-1] == '\t'))
line[--len] = '\0';
if (len == 0 || line[0] == '#') continue;
char cmd[560];
snprintf(cmd, sizeof(cmd), "%s &", line);
system(cmd);
}
fclose(f);
}
/* ── hot corners ─────────────────────────────────────────────────── */
static void hot_corner_check(WM *wm) {
Window root_ret, child;
int root_x, root_y, win_x, win_y;
unsigned int mask;
XQueryPointer(wm->dpy, wm->root, &root_ret, &child,
&root_x, &root_y, &win_x, &win_y, &mask);
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
int corner = 0;
if (root_x <= 1 && root_y <= 1) corner = 1; /* TL: Exposé */
else if (root_x >= sw - DESKBAR_W - 2 && root_y <= 1) corner = 2; /* TR: show desktop */
else if (root_x <= 1 && root_y >= sh - 2) corner = 3; /* BL: launcher */
else if (root_x >= sw - DESKBAR_W - 2 && root_y >= sh - 2) corner = 4; /* BR: lock */
if (corner == wm->hot_corner_last) return;
wm->hot_corner_last = corner;
if (corner == 0) return; /* just left a corner, no action needed */
switch (corner) {
case 1: /* top-left → Exposé */
if (!wm->expose_active) expose_show(wm);
break;
case 2: /* top-right → show desktop (minimize all on current desktop) */
if (wm->expose_active) { expose_hide(wm, None); break; }
for (Client *c = wm->clients; c; c = c->next)
if (!c->minimized && c->desktop == wm->cur_desktop)
client_minimize(wm, c);
break;
case 3: /* bottom-left → launcher */
system("orionlauncher &");
break;
case 4: /* bottom-right → lock screen */
system("orionlock &");
break;
}
}
static int x_error_handler(Display *dpy, XErrorEvent *e) {
if (e->error_code == BadWindow || e->error_code == BadAccess)
return 0;
@@ -48,6 +110,8 @@ void wm_init(WM *wm) {
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);
wm->net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False);
wm->net_current_desktop = XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False);
/* advertise basic EWMH support */
Atom net_supported = XInternAtom(wm->dpy, "_NET_SUPPORTED", False);
@@ -55,10 +119,12 @@ void wm_init(WM *wm) {
wm->net_wm_state, wm->net_wm_state_hidden,
wm->net_wm_state_maximized_vert,
wm->net_wm_state_maximized_horz,
wm->net_wm_desktop,
wm->net_current_desktop,
XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False),
XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False),
XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", 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,
@@ -90,6 +156,7 @@ void wm_init(WM *wm) {
/* global key grabs */
struct { KeySym sym; unsigned mod; } grabs[] = {
{ XK_Tab, Mod1Mask },
{ XK_Tab, Mod1Mask | ShiftMask }, /* Shift+Alt+Tab — backwards */
{ XK_F4, Mod1Mask },
{ XK_F9, Mod1Mask }, /* minimize */
{ XK_F10, Mod1Mask }, /* maximize */
@@ -98,7 +165,29 @@ void wm_init(WM *wm) {
{ XK_Right, Mod4Mask }, /* snap right */
{ XK_Up, Mod4Mask }, /* maximize */
{ XK_Down, Mod4Mask }, /* restore */
{ XK_space, Mod4Mask }, /* Exposé overview */
/* Super+1-4: switch desktop */
{ XK_1, Mod4Mask },
{ XK_2, Mod4Mask },
{ XK_3, Mod4Mask },
{ XK_4, Mod4Mask },
/* Super+Shift+1-4: move window to desktop */
{ XK_1, Mod4Mask | ShiftMask },
{ XK_2, Mod4Mask | ShiftMask },
{ XK_3, Mod4Mask | ShiftMask },
{ XK_4, Mod4Mask | ShiftMask },
{ XK_Escape, 0 },
/* XF86 media keys */
{ XF86XK_AudioRaiseVolume, 0 },
{ XF86XK_AudioLowerVolume, 0 },
{ XF86XK_AudioMute, 0 },
{ XF86XK_AudioPlay, 0 },
{ XF86XK_AudioNext, 0 },
{ XF86XK_AudioPrev, 0 },
{ XF86XK_MonBrightnessUp, 0 },
{ XF86XK_MonBrightnessDown, 0 },
/* Super+V: clipboard manager */
{ XK_v, Mod4Mask },
};
for (int i = 0; i < (int)(sizeof(grabs)/sizeof(grabs[0])); i++) {
KeyCode kc = XKeysymToKeycode(wm->dpy, grabs[i].sym);
@@ -122,28 +211,44 @@ void wm_init(WM *wm) {
XSync(wm->dpy, False);
fprintf(stderr, "orionwm: started\n");
wm_run_autostart();
}
void wm_run(WM *wm) {
int xfd = ConnectionNumber(wm->dpy);
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 KeyRelease: on_key_release(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;
/* drain all pending X events */
while (XPending(wm->dpy)) {
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 KeyRelease: on_key_release(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;
}
}
/* poll hot corners every 200ms */
hot_corner_check(wm);
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
struct timeval tv = { 0, 200000 };
select(xfd + 1, &fds, NULL, NULL, &tv);
}
}