Merge pull request #1 from nmemmert/claude/elemetryos-macos-fork-mmkshh
OrionOS macOS-style fork — full feature build
This commit is contained in:
43 files changed
+3433
-559
No files matched your search
@@ -5,9 +5,9 @@
|
|||||||
# make clean — remove build artifacts
|
# make clean — remove build artifacts
|
||||||
# make iso — build the bootable ISO (requires Arch Linux + archiso)
|
# 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:
|
wm:
|
||||||
$(MAKE) -C wm
|
$(MAKE) -C wm
|
||||||
@@ -39,6 +39,18 @@ quill:
|
|||||||
lens:
|
lens:
|
||||||
$(MAKE) -C lens
|
$(MAKE) -C lens
|
||||||
|
|
||||||
|
orionnotify:
|
||||||
|
$(MAKE) -C orionnotify
|
||||||
|
|
||||||
|
orionlock:
|
||||||
|
$(MAKE) -C orionlock
|
||||||
|
|
||||||
|
orionshot:
|
||||||
|
$(MAKE) -C orionshot
|
||||||
|
|
||||||
|
orionclip:
|
||||||
|
$(MAKE) -C orionclip
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(MAKE) -C wm clean
|
$(MAKE) -C wm clean
|
||||||
$(MAKE) -C deskbar clean
|
$(MAKE) -C deskbar clean
|
||||||
@@ -50,6 +62,10 @@ clean:
|
|||||||
$(MAKE) -C rigel clean
|
$(MAKE) -C rigel clean
|
||||||
$(MAKE) -C quill 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
|
install: all
|
||||||
# binaries
|
# binaries
|
||||||
@@ -62,6 +78,10 @@ install: all
|
|||||||
install -Dm755 rigel/rigel /usr/local/bin/rigel
|
install -Dm755 rigel/rigel /usr/local/bin/rigel
|
||||||
install -Dm755 quill/quill /usr/local/bin/quill
|
install -Dm755 quill/quill /usr/local/bin/quill
|
||||||
install -Dm755 lens/lens /usr/local/bin/lens
|
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
|
# GTK theme
|
||||||
mkdir -p /usr/share/themes
|
mkdir -p /usr/share/themes
|
||||||
cp -r themes/OrionOS /usr/share/themes/
|
cp -r themes/OrionOS /usr/share/themes/
|
||||||
@@ -77,8 +97,23 @@ install: all
|
|||||||
@echo " orionwallpaper"
|
@echo " orionwallpaper"
|
||||||
@echo " picom --config /etc/orionos/picom.conf &"
|
@echo " picom --config /etc/orionos/picom.conf &"
|
||||||
@echo " oriondeskbar &"
|
@echo " oriondeskbar &"
|
||||||
|
@echo " orionnotify &"
|
||||||
|
@echo " orionclip &"
|
||||||
@echo " exec orionwm"
|
@echo " exec orionwm"
|
||||||
|
|
||||||
iso:
|
iso:
|
||||||
chmod +x build-iso.sh
|
@if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then \
|
||||||
./build-iso.sh
|
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
|
||||||
@@ -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
|
## 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
|
### Components
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),)
|
|||||||
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
|
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
|
||||||
endif
|
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)
|
CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C)
|
||||||
LDFLAGS = $(PKG_XFT_L)
|
LDFLAGS = $(PKG_XFT_L)
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,14 @@
|
|||||||
#define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */
|
#define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */
|
||||||
#define DB_MAX_APPS 32
|
#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 ────────────────────────────────────────────────────── */
|
/* ── fly-out menu ────────────────────────────────────────────────────── */
|
||||||
#define DB_FLYOUT_W 196 /* fly-out menu panel width */
|
#define DB_FLYOUT_W 196 /* fly-out menu panel width */
|
||||||
#define DB_FLYOUT_IH 28 /* item row height */
|
#define DB_FLYOUT_IH 28 /* item row height */
|
||||||
@@ -96,8 +104,10 @@ static const unsigned long DB_ICON_COLORS[] = {
|
|||||||
typedef struct AppEntry {
|
typedef struct AppEntry {
|
||||||
Window win;
|
Window win;
|
||||||
char title[64];
|
char title[64];
|
||||||
|
char cls[64]; /* WM_CLASS instance for icon lookup */
|
||||||
int focused;
|
int focused;
|
||||||
int minimized;
|
int minimized;
|
||||||
|
int desktop; /* which desktop this window is on */
|
||||||
} AppEntry;
|
} AppEntry;
|
||||||
|
|
||||||
typedef struct Deskbar {
|
typedef struct Deskbar {
|
||||||
@@ -139,6 +149,11 @@ typedef struct Deskbar {
|
|||||||
int sub_hov; /* hovered item in sub-menu */
|
int sub_hov; /* hovered item in sub-menu */
|
||||||
int sw_cache; /* cached screen width for menu positioning */
|
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) */
|
/* system tray (freedesktop.org XEmbed tray protocol) */
|
||||||
Window tray_icons[DB_TRAY_MAX];
|
Window tray_icons[DB_TRAY_MAX];
|
||||||
int n_tray_icons;
|
int n_tray_icons;
|
||||||
|
|||||||
+321
-3
@@ -1,5 +1,9 @@
|
|||||||
#include "deskbar.h"
|
#include "deskbar.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_IMLIB2
|
||||||
|
#include <Imlib2.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define TRAY_REQUEST_DOCK 0
|
#define TRAY_REQUEST_DOCK 0
|
||||||
#define XEMBED_EMBEDDED_NOTIFY 0
|
#define XEMBED_EMBEDDED_NOTIFY 0
|
||||||
#define XEMBED_VERSION 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 = XInternAtom(db->dpy, "_NET_WM_STATE", False);
|
||||||
Atom net_wm_state_hidden = XInternAtom(db->dpy, "_NET_WM_STATE_HIDDEN", 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++) {
|
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].win = wins[i];
|
||||||
db->apps[db->napps].focused = (wins[i] == (Window)active_win);
|
db->apps[db->napps].focused = (wins[i] == (Window)active_win);
|
||||||
db->apps[db->napps].minimized = 0;
|
db->apps[db->napps].minimized = 0;
|
||||||
@@ -135,6 +145,24 @@ void db_update_apps(Deskbar *db) {
|
|||||||
ewmh_get_title(db, wins[i],
|
ewmh_get_title(db, wins[i],
|
||||||
db->apps[db->napps].title,
|
db->apps[db->napps].title,
|
||||||
sizeof(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')
|
if (db->apps[db->napps].title[0] != '\0')
|
||||||
db->napps++;
|
db->napps++;
|
||||||
}
|
}
|
||||||
@@ -420,12 +448,52 @@ void db_draw(Deskbar *db) {
|
|||||||
XSetForeground(db->dpy, db->gc, row_bg);
|
XSetForeground(db->dpy, db->gc, row_bg);
|
||||||
XFillRectangle(db->dpy, db->win, db->gc, 0, iy, DB_W, DB_ITEM_H);
|
XFillRectangle(db->dpy, db->win, db->gc, 0, iy, DB_W, DB_ITEM_H);
|
||||||
|
|
||||||
/* letter icon */
|
/* icon: imlib2 if available, else letter-square */
|
||||||
unsigned long icon_bg = db_app_icon_color(db->apps[i].title);
|
|
||||||
int icon_x = 5;
|
int icon_x = 5;
|
||||||
int icon_y = iy + (DB_ITEM_H - DB_ICON_SZ) / 2;
|
int icon_y = iy + (DB_ITEM_H - DB_ICON_SZ) / 2;
|
||||||
|
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,
|
draw_letter_icon(db, xd, icon_x, icon_y,
|
||||||
db->apps[i].title, icon_bg, mini);
|
db->apps[i].title, icon_bg, mini);
|
||||||
|
}
|
||||||
|
|
||||||
/* app name */
|
/* app name */
|
||||||
int name_x = icon_x + DB_ICON_SZ + 6;
|
int name_x = icon_x + DB_ICON_SZ + 6;
|
||||||
@@ -578,7 +646,7 @@ static const MI MENU[] = {
|
|||||||
{ "Settings", NULL, 0, 1, 1 },
|
{ "Settings", NULL, 0, 1, 1 },
|
||||||
{ NULL, NULL, 1, 0,-1 },
|
{ NULL, NULL, 1, 0,-1 },
|
||||||
{ "About OrionOS", "alacritty -e bash -c 'fastfetch; read' &", 0, 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 },
|
{ NULL, NULL, 1, 0,-1 },
|
||||||
{ "Restart", "systemctl reboot", 0, 0,-1 },
|
{ "Restart", "systemctl reboot", 0, 0,-1 },
|
||||||
{ "Shut Down", "systemctl poweroff", 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);
|
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 ────────────────────────────────────────────────────────────── */
|
/* ── input ────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
void db_hover(Deskbar *db, int x, int y) {
|
void db_hover(Deskbar *db, int x, int y) {
|
||||||
@@ -981,6 +1265,26 @@ void db_run(Deskbar *db) {
|
|||||||
XEvent e;
|
XEvent e;
|
||||||
XNextEvent(db->dpy, &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.
|
/* While the fly-out menu is open, the pointer is grabbed.
|
||||||
Route all motion and button events through the menu handlers
|
Route all motion and button events through the menu handlers
|
||||||
(they use root-relative coords to determine which panel was hit). */
|
(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 &");
|
system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &");
|
||||||
break;
|
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)
|
if (e.xbutton.button == Button1 && e.xbutton.y >= sy)
|
||||||
system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &");
|
system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &");
|
||||||
else
|
else
|
||||||
@@ -1091,6 +1407,8 @@ void db_run(Deskbar *db) {
|
|||||||
db_draw(db);
|
db_draw(db);
|
||||||
} else if (e.xproperty.atom == db->net_current_desktop) {
|
} else if (e.xproperty.atom == db->net_current_desktop) {
|
||||||
db_update_workspace(db);
|
db_update_workspace(db);
|
||||||
|
db_update_apps(db); /* re-filter by new desktop */
|
||||||
|
db_resize(db);
|
||||||
db_draw(db);
|
db_draw(db);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source /etc/orionos/xinitrc
|
||||||
Executable
+37
@@ -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
@@ -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)"
|
||||||
+46
-42
@@ -1,58 +1,62 @@
|
|||||||
# 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
|
FROM archlinux:latest
|
||||||
|
|
||||||
# ── disable pacman seccomp sandbox (breaks under QEMU on Apple Silicon) ──
|
# Update and install archiso + OrionOS build dependencies
|
||||||
RUN echo "DisableSandbox" >> /etc/pacman.conf
|
|
||||||
|
|
||||||
# ── install build tools ────────────────────────────────────────────────
|
|
||||||
RUN pacman -Syu --noconfirm && \
|
RUN pacman -Syu --noconfirm && \
|
||||||
pacman -S --noconfirm \
|
pacman -S --noconfirm \
|
||||||
archiso \
|
archiso \
|
||||||
base-devel \
|
base-devel \
|
||||||
|
git \
|
||||||
libx11 \
|
libx11 \
|
||||||
libxft \
|
libxft \
|
||||||
|
libxfixes \
|
||||||
|
libxext \
|
||||||
|
libpng \
|
||||||
|
dbus \
|
||||||
|
pam \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
freetype2 \
|
freetype2 \
|
||||||
imlib2 \
|
xorg-server \
|
||||||
imagemagick \
|
xorg-xinit \
|
||||||
fakeroot \
|
feh \
|
||||||
git \
|
picom \
|
||||||
sudo \
|
alsa-utils \
|
||||||
&& pacman -Scc --noconfirm
|
ttf-dejavu \
|
||||||
|
noto-fonts \
|
||||||
|
pkgconf && \
|
||||||
|
pacman -Scc --noconfirm
|
||||||
|
|
||||||
# ── build OnlyOffice from AUR (as a non-root build user) ─────────────
|
WORKDIR /build
|
||||||
RUN useradd -m builder && \
|
|
||||||
echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder
|
|
||||||
RUN su builder -c " \
|
|
||||||
git clone --depth=1 https://aur.archlinux.org/onlyoffice-bin.git /tmp/onlyoffice && \
|
|
||||||
cd /tmp/onlyoffice && \
|
|
||||||
makepkg -s --noconfirm 2>&1 | tail -5 \
|
|
||||||
" && \
|
|
||||||
find /tmp/onlyoffice -name '*.pkg.tar.zst' -exec cp {} /onlyoffice.pkg.tar.zst \; || \
|
|
||||||
echo "OnlyOffice AUR build failed — will be skipped in ISO"
|
|
||||||
|
|
||||||
# ── show which mkinitcpio hooks archiso installed (helps debugging) ───
|
# Copy OrionOS source
|
||||||
RUN echo "=== archiso initcpio hooks ===" && \
|
COPY . /build/orionos
|
||||||
ls /usr/lib/initcpio/hooks/ 2>/dev/null || echo "(none found)"
|
|
||||||
|
|
||||||
# ── copy the entire OrionOS source tree ──────────────────────────────
|
# Build OrionOS components
|
||||||
WORKDIR /orionos
|
RUN cd /build/orionos && make 2>&1 | tail -20
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# ── build the OrionOS binaries inside the container ───────────────────
|
# Install binaries into the staging root that archiso will include
|
||||||
# pkg-config finds X11/Xft headers automatically on Arch — no overrides needed
|
RUN cd /build/orionos && \
|
||||||
RUN make
|
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 && \
|
||||||
|
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 the default wallpaper ───────────────────────────────────
|
# Generate default wallpaper
|
||||||
RUN bash themes/wallpapers/generate-default.sh || true
|
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
|
||||||
|
|
||||||
# ── run the ISO build script ──────────────────────────────────────────
|
# Build the ISO
|
||||||
# --privileged is required for loop device mounts inside the container
|
RUN mkarchiso -v \
|
||||||
ENTRYPOINT ["/orionos/iso/docker/entrypoint.sh"]
|
-w /tmp/iso-work \
|
||||||
|
-o /output \
|
||||||
|
/build/orionos/iso/profile
|
||||||
|
|
||||||
|
CMD ["/bin/bash"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
orionos
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# OrionOS live session autostart
|
||||||
|
|
||||||
|
picom --config /etc/orionos/picom.conf &
|
||||||
|
orionwallpaper &
|
||||||
|
orionnotify &
|
||||||
|
orionclip &
|
||||||
|
oriondeskbar &
|
||||||
|
exec orionwm
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source /etc/orionos/xinitrc
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source /etc/orionos/xinitrc
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# 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
|
||||||
|
dbus
|
||||||
|
libpam
|
||||||
|
fontconfig
|
||||||
|
freetype2
|
||||||
|
|
||||||
|
# Desktop utilities
|
||||||
|
feh
|
||||||
|
picom
|
||||||
|
xterm
|
||||||
|
libnotify
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
alsa-utils
|
||||||
|
pulseaudio
|
||||||
|
|
||||||
|
# Brightness
|
||||||
|
light
|
||||||
|
|
||||||
|
# Media playback control
|
||||||
|
playerctl
|
||||||
|
|
||||||
|
# Arch tools
|
||||||
|
pacman-contrib
|
||||||
|
pkexec
|
||||||
|
polkit
|
||||||
|
|
||||||
|
# System tools
|
||||||
|
bash
|
||||||
|
coreutils
|
||||||
|
util-linux
|
||||||
|
procps-ng
|
||||||
|
grep
|
||||||
|
gawk
|
||||||
|
sed
|
||||||
|
tar
|
||||||
|
gzip
|
||||||
|
|
||||||
|
# Network
|
||||||
|
networkmanager
|
||||||
|
dhcpcd
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
git
|
||||||
|
vim
|
||||||
|
less
|
||||||
|
which
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[options]
|
||||||
|
HoldPkg = pacman glibc
|
||||||
|
Architecture = auto
|
||||||
|
CheckSpace
|
||||||
|
ParallelDownloads = 5
|
||||||
|
|
||||||
|
[core]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[multilib]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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"
|
||||||
|
["/etc/skel/.xinitrc"]="0:0:755"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),)
|
|||||||
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
|
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
|
||||||
endif
|
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)
|
CFLAGS = -Wall -Wextra -O2 $(PKG_XFT_C)
|
||||||
LDFLAGS = $(PKG_XFT_L)
|
LDFLAGS = $(PKG_XFT_L)
|
||||||
|
|
||||||
|
|||||||
+46
-5
@@ -58,13 +58,13 @@ static const unsigned long ICON_BG[] = {
|
|||||||
#define FONT_ICON "Sans:size=13:weight=bold"
|
#define FONT_ICON "Sans:size=13:weight=bold"
|
||||||
#define MAX_FLT 64
|
#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 App APPS[MAX_APPS];
|
||||||
static int NAPP = 0;
|
static int NAPP = 0;
|
||||||
|
|
||||||
static const char *CATS[] = {
|
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])))
|
#define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0])))
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ static const char *map_category(const char *cats) {
|
|||||||
strstr(cats, "FileManager") || strstr(cats, "Utility") ||
|
strstr(cats, "FileManager") || strstr(cats, "Utility") ||
|
||||||
strstr(cats, "Monitor") || strstr(cats, "Security"))
|
strstr(cats, "Monitor") || strstr(cats, "Security"))
|
||||||
return "System";
|
return "System";
|
||||||
return "";
|
return "Other";
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clean_exec(char *dst, const char *src, size_t dstsz) {
|
static void clean_exec(char *dst, const char *src, size_t dstsz) {
|
||||||
@@ -108,7 +108,7 @@ static void load_desktop_file(const char *path) {
|
|||||||
FILE *f = fopen(path, "r");
|
FILE *f = fopen(path, "r");
|
||||||
if (!f) return;
|
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;
|
int in_entry = 0, nodisplay = 0, is_app = 0;
|
||||||
char line[512];
|
char line[512];
|
||||||
|
|
||||||
@@ -126,6 +126,8 @@ static void load_desktop_file(const char *path) {
|
|||||||
snprintf(exec, sizeof(exec), "%s", line + 5);
|
snprintf(exec, sizeof(exec), "%s", line + 5);
|
||||||
else if (!cats[0] && strncmp(line, "Categories=", 11) == 0)
|
else if (!cats[0] && strncmp(line, "Categories=", 11) == 0)
|
||||||
snprintf(cats, sizeof(cats), "%s", line + 11);
|
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)
|
else if (strncmp(line, "NoDisplay=true", 14) == 0)
|
||||||
nodisplay = 1;
|
nodisplay = 1;
|
||||||
else if (strncmp(line, "Type=Application", 16) == 0)
|
else if (strncmp(line, "Type=Application", 16) == 0)
|
||||||
@@ -137,6 +139,7 @@ static void load_desktop_file(const char *path) {
|
|||||||
snprintf(APPS[NAPP].name, 64, "%s", name);
|
snprintf(APPS[NAPP].name, 64, "%s", name);
|
||||||
clean_exec(APPS[NAPP].cmd, exec, 256);
|
clean_exec(APPS[NAPP].cmd, exec, 256);
|
||||||
snprintf(APPS[NAPP].cat, 32, "%s", map_category(cats));
|
snprintf(APPS[NAPP].cat, 32, "%s", map_category(cats));
|
||||||
|
snprintf(APPS[NAPP].icon, 64, "%s", icon);
|
||||||
NAPP++;
|
NAPP++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +295,39 @@ static int card_at_vis(int x, int y) {
|
|||||||
return row * COLS + col;
|
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 ────────────────────────────────────────────────────────────── */
|
/* ── draw ────────────────────────────────────────────────────────────── */
|
||||||
static void draw(L *l) {
|
static void draw(L *l) {
|
||||||
XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap);
|
XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap);
|
||||||
@@ -382,12 +418,17 @@ static void draw(L *l) {
|
|||||||
for (const char *p = APPS[app_i].name; *p; p++) h = h * 31 + (unsigned char)*p;
|
for (const char *p = APPS[app_i].name; *p; p++) h = h * 31 + (unsigned char)*p;
|
||||||
int ix = cx + (CARD_W - ICON_SZ) / 2;
|
int ix = cx + (CARD_W - ICON_SZ) / 2;
|
||||||
int iy = cy + 10;
|
int iy = cy + 10;
|
||||||
|
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]));
|
XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON]));
|
||||||
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
|
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
|
||||||
|
|
||||||
char letter[2] = { APPS[app_i].name[0], '\0' };
|
char letter[2] = { APPS[app_i].name[0], '\0' };
|
||||||
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
|
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);
|
ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter);
|
||||||
|
}
|
||||||
|
|
||||||
char lbl[14];
|
char lbl[14];
|
||||||
snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name);
|
snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name);
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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 = ¬ifs[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 = ¬ifs[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
@@ -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
|
BIN = orionpkg
|
||||||
|
|
||||||
CC = gcc
|
.PHONY: all clean install
|
||||||
CFLAGS = -Wall -Wextra -O2 \
|
all: $(BIN)
|
||||||
$(shell pkg-config --cflags x11 xft fontconfig 2>/dev/null)
|
|
||||||
LIBS = $(shell pkg-config --libs x11 xft fontconfig 2>/dev/null) \
|
|
||||||
-lX11 -lXft -lfontconfig
|
|
||||||
|
|
||||||
$(BIN): orionpkg.c
|
$(BIN): orionpkg.c
|
||||||
$(CC) $(CFLAGS) -o $@ $< $(LIBS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
install: $(BIN)
|
||||||
|
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BIN)
|
rm -f $(BIN)
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
+551
-353
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -4,13 +4,13 @@
|
|||||||
@define-color bg_dark #161C2A;
|
@define-color bg_dark #161C2A;
|
||||||
@define-color bg_darker #0E1220;
|
@define-color bg_darker #0E1220;
|
||||||
@define-color bg_light #262E42;
|
@define-color bg_light #262E42;
|
||||||
@define-color border_light #3A6898;
|
@define-color border_light #2A60A8;
|
||||||
@define-color border_dark #080C18;
|
@define-color border_dark #080C18;
|
||||||
@define-color text_color #D8E4F4;
|
@define-color text_color #D8E4F4;
|
||||||
@define-color text_disabled #48587A;
|
@define-color text_disabled #48587A;
|
||||||
@define-color selected_bg #1E4D78;
|
@define-color selected_bg #1E4878;
|
||||||
@define-color selected_fg #F0F6FF;
|
@define-color selected_fg #F0F6FF;
|
||||||
@define-color accent #3A6898;
|
@define-color accent #1E58A8;
|
||||||
@define-color button_bg #202A3C;
|
@define-color button_bg #202A3C;
|
||||||
@define-color button_hover #283650;
|
@define-color button_hover #283650;
|
||||||
@define-color button_active #181E2E;
|
@define-color button_active #181E2E;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# orionwallpaper /path/to/image.jpg # set new wallpaper and save it
|
# orionwallpaper /path/to/image.jpg # set new wallpaper and save it
|
||||||
# orionwallpaper --color "#6B8FB5" # set solid color
|
# orionwallpaper --color "#6B8FB5" # set solid color
|
||||||
# orionwallpaper --reset # reset to default blue-gray 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
|
# Wallpaper preference is stored in ~/.config/orionos/wallpaper
|
||||||
|
|
||||||
@@ -17,6 +18,21 @@ WALLPAPER_DIR="/usr/share/orionos/wallpapers"
|
|||||||
mkdir -p "$CONF_DIR"
|
mkdir -p "$CONF_DIR"
|
||||||
|
|
||||||
case "${1:-}" in
|
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)
|
--reset)
|
||||||
echo "color:$DEFAULT_COLOR" > "$CONF_FILE"
|
echo "color:$DEFAULT_COLOR" > "$CONF_FILE"
|
||||||
xsetroot -solid "$DEFAULT_COLOR"
|
xsetroot -solid "$DEFAULT_COLOR"
|
||||||
|
|||||||
+25
-20
@@ -1,22 +1,29 @@
|
|||||||
# OrionOS picom compositor config
|
# OrionOS picom compositor config — macOS-inspired
|
||||||
# Goal: subtle window shadows, no transparency, fast rendering.
|
# Rounded corners, soft macOS-style shadows, fast fading.
|
||||||
# BeOS/OrionOS didn't have transparency — we keep it clean.
|
|
||||||
|
|
||||||
backend = "xrender"; # xrender works on all hardware; switch to glx for performance
|
backend = "xrender";
|
||||||
vsync = false;
|
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 ───────────────────────────────────────────────────────────────
|
# ── shadows ───────────────────────────────────────────────────────────────
|
||||||
|
# macOS-style: larger, softer, offset down-right
|
||||||
shadow = true;
|
shadow = true;
|
||||||
shadow-radius = 8;
|
shadow-radius = 18;
|
||||||
shadow-offset-x = 4;
|
shadow-offset-x = 0;
|
||||||
shadow-offset-y = 4;
|
shadow-offset-y = 8;
|
||||||
shadow-opacity = 0.30;
|
shadow-opacity = 0.35;
|
||||||
shadow-color = "#000000";
|
shadow-color = "#000000";
|
||||||
|
|
||||||
# Don't shadow the deskbar, menus, or tooltips
|
|
||||||
shadow-exclude = [
|
shadow-exclude = [
|
||||||
"class_g = 'skydeskbar'",
|
|
||||||
"class_g = 'skylauncher'",
|
|
||||||
"_NET_WM_WINDOW_TYPE@:a *= 'DOCK'",
|
"_NET_WM_WINDOW_TYPE@:a *= 'DOCK'",
|
||||||
"_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'",
|
"_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'",
|
||||||
"_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'",
|
"_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'",
|
||||||
@@ -27,20 +34,18 @@ shadow-exclude = [
|
|||||||
|
|
||||||
# ── fading ────────────────────────────────────────────────────────────────
|
# ── fading ────────────────────────────────────────────────────────────────
|
||||||
fading = true;
|
fading = true;
|
||||||
fade-in-step = 0.06;
|
fade-in-step = 0.08;
|
||||||
fade-out-step = 0.06;
|
fade-out-step = 0.08;
|
||||||
fade-delta = 8; # ms between steps — snappy, not sluggish
|
fade-delta = 6;
|
||||||
no-fading-openclose = false;
|
no-fading-openclose = false;
|
||||||
|
|
||||||
# ── opacity ───────────────────────────────────────────────────────────────
|
# ── opacity ───────────────────────────────────────────────────────────────
|
||||||
# No transparency — BeOS aesthetic is solid, crisp windows.
|
|
||||||
inactive-opacity = 1.0;
|
inactive-opacity = 1.0;
|
||||||
active-opacity = 1.0;
|
active-opacity = 1.0;
|
||||||
frame-opacity = 1.0;
|
frame-opacity = 1.0;
|
||||||
inactive-opacity-override = false;
|
inactive-opacity-override = false;
|
||||||
|
|
||||||
# ── blur ─────────────────────────────────────────────────────────────────
|
# ── blur ─────────────────────────────────────────────────────────────────
|
||||||
# Disabled — keep it clean.
|
|
||||||
blur-method = "none";
|
blur-method = "none";
|
||||||
|
|
||||||
# ── focus & refresh ───────────────────────────────────────────────────────
|
# ── focus & refresh ───────────────────────────────────────────────────────
|
||||||
@@ -55,9 +60,9 @@ mark-ovredir-focused = false;
|
|||||||
wintypes:
|
wintypes:
|
||||||
{
|
{
|
||||||
tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; };
|
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; };
|
dnd = { shadow = false; };
|
||||||
popup_menu = { opacity = 1.0; shadow = false; };
|
popup_menu = { opacity = 1.0; shadow = false; corner-radius = 8; };
|
||||||
dropdown_menu = { opacity = 1.0; shadow = false; };
|
dropdown_menu = { opacity = 1.0; shadow = false; corner-radius = 8; };
|
||||||
notification = { shadow = false; };
|
notification = { shadow = false; corner-radius = 8; };
|
||||||
};
|
};
|
||||||
+2
-2
@@ -12,9 +12,9 @@ ifeq ($(PKG_XFT_C),)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C)
|
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)
|
OBJ = $(SRC:.c=.o)
|
||||||
BIN = orionwm
|
BIN = orionwm
|
||||||
|
|
||||||
|
|||||||
+56
-26
@@ -5,35 +5,40 @@
|
|||||||
#include <X11/Xutil.h>
|
#include <X11/Xutil.h>
|
||||||
#include <X11/Xatom.h>
|
#include <X11/Xatom.h>
|
||||||
#include <X11/keysym.h>
|
#include <X11/keysym.h>
|
||||||
|
#include <X11/XF86keysym.h>
|
||||||
#include <X11/Xft/Xft.h>
|
#include <X11/Xft/Xft.h>
|
||||||
#include <X11/cursorfont.h>
|
#include <X11/cursorfont.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
/* ── palette (modern dark) ───────────────────────────────────────────── */
|
/* ── palette (macOS dark mode) ───────────────────────────────────────── */
|
||||||
#define COLOR_TITLE_ACTIVE 0x1E4D78 /* deep blue active title bar */
|
#define COLOR_TITLE_ACTIVE 0x3C3C3C /* macOS dark titlebar */
|
||||||
#define COLOR_TITLE_INACTIVE 0x252830 /* dark grey inactive */
|
#define COLOR_TITLE_INACTIVE 0x2C2C2C /* inactive, slightly darker */
|
||||||
#define COLOR_TITLE_TEXT_ACT 0xE8EEF8 /* near-white active text */
|
#define COLOR_TITLE_TEXT_ACT 0xFFFFFF /* white active text */
|
||||||
#define COLOR_TITLE_TEXT_INACT 0x5C6070 /* dimmed inactive text */
|
#define COLOR_TITLE_TEXT_INACT 0x686868 /* dimmed inactive text */
|
||||||
#define COLOR_BORDER_LIGHT 0x3A6898 /* blue-grey highlight */
|
#define COLOR_BORDER_LIGHT 0x505050 /* subtle border highlight */
|
||||||
#define COLOR_BORDER_DARK 0x0C1018 /* near-black shadow */
|
#define COLOR_BORDER_DARK 0x1C1C1C /* near-black shadow */
|
||||||
#define COLOR_CLOSE_BTN 0xCC3333 /* red close */
|
#define COLOR_CLOSE_BTN 0xFF5F57 /* macOS traffic-light red */
|
||||||
#define COLOR_MAXIMIZE_BTN 0x2E8B44 /* green maximize */
|
#define COLOR_MAXIMIZE_BTN 0x28C840 /* macOS traffic-light green */
|
||||||
#define COLOR_MINIMIZE_BTN 0xBB8820 /* amber minimize */
|
#define COLOR_MINIMIZE_BTN 0xFEBC2E /* macOS traffic-light yellow */
|
||||||
#define COLOR_CLOSE_BTN_HOV 0xFF5050
|
#define COLOR_CLOSE_BTN_HOV 0xFF3B30
|
||||||
#define COLOR_MAXIMIZE_BTN_HOV 0x44CC66
|
#define COLOR_MAXIMIZE_BTN_HOV 0x34C759
|
||||||
#define COLOR_MINIMIZE_BTN_HOV 0xFFBB30
|
#define COLOR_MINIMIZE_BTN_HOV 0xFFCC00
|
||||||
#define COLOR_BG 0x1A2840 /* desktop background */
|
#define COLOR_BG 0x1C1C1C /* near-black desktop */
|
||||||
#define COLOR_SNAP_OVERLAY 0x3A6898 /* snap highlight (unused, placeholder) */
|
#define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */
|
||||||
|
|
||||||
/* ── geometry ────────────────────────────────────────────────────────── */
|
/* ── 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 TITLE_TAB_W 200
|
||||||
#define BORDER_W 3
|
#define BORDER_W 1 /* thin 1px macOS-style border */
|
||||||
#define BTN_SIZE 14
|
#define BTN_SIZE 12 /* macOS traffic-light diameter */
|
||||||
#define BTN_MARGIN 5
|
#define BTN_MARGIN 8 /* spacing between traffic lights */
|
||||||
|
#define BTN_LEFT_PAD 10 /* left edge → first button */
|
||||||
#define RESIZE_GRIP 12
|
#define RESIZE_GRIP 12
|
||||||
#define MIN_WIN_W 120
|
#define MIN_WIN_W 120
|
||||||
#define MIN_WIN_H 60
|
#define MIN_WIN_H 60
|
||||||
@@ -41,14 +46,14 @@
|
|||||||
#define DBL_CLICK_MS 400 /* double-click threshold */
|
#define DBL_CLICK_MS 400 /* double-click threshold */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Three buttons on right side (left to right): minimize, maximize, close
|
* macOS-style: three traffic-light buttons on LEFT side
|
||||||
* close = fw - BTN_SIZE - BTN_MARGIN - BORDER_W
|
* close = BTN_LEFT_PAD
|
||||||
* max = close - BTN_SIZE - BTN_MARGIN - 2
|
* minimize = BTN_LEFT_PAD + BTN_SIZE + BTN_MARGIN
|
||||||
* min = max - BTN_SIZE - BTN_MARGIN - 2
|
* maximize = BTN_LEFT_PAD + (BTN_SIZE + BTN_MARGIN) * 2
|
||||||
*/
|
*/
|
||||||
#define BTN_CLOSE_X(fw) ((fw) - BTN_SIZE - BTN_MARGIN - BORDER_W)
|
#define BTN_CLOSE_X (BTN_LEFT_PAD)
|
||||||
#define BTN_MAX_X(fw) ((fw) - BTN_SIZE*2 - BTN_MARGIN*2 - BORDER_W - 2)
|
#define BTN_MIN_X (BTN_LEFT_PAD + BTN_SIZE + BTN_MARGIN)
|
||||||
#define BTN_MIN_X(fw) ((fw) - BTN_SIZE*3 - BTN_MARGIN*3 - BORDER_W - 4)
|
#define BTN_MAX_X (BTN_LEFT_PAD + (BTN_SIZE + BTN_MARGIN) * 2)
|
||||||
#define BTN_Y (((TITLE_HEIGHT) - BTN_SIZE) / 2)
|
#define BTN_Y (((TITLE_HEIGHT) - BTN_SIZE) / 2)
|
||||||
|
|
||||||
/* ── fonts ───────────────────────────────────────────────────────────── */
|
/* ── fonts ───────────────────────────────────────────────────────────── */
|
||||||
@@ -71,10 +76,18 @@ typedef struct Client {
|
|||||||
int focused;
|
int focused;
|
||||||
int minimized;
|
int minimized;
|
||||||
int maximized;
|
int maximized;
|
||||||
|
int desktop; /* virtual desktop this window lives on */
|
||||||
int saved_x, saved_y; /* pre-maximize position */
|
int saved_x, saved_y; /* pre-maximize position */
|
||||||
unsigned saved_w, saved_h; /* pre-maximize size */
|
unsigned saved_w, saved_h; /* pre-maximize size */
|
||||||
Time last_click_time; /* for double-click detection */
|
Time last_click_time; /* for double-click detection */
|
||||||
char title[256];
|
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;
|
struct Client *next;
|
||||||
} Client;
|
} Client;
|
||||||
|
|
||||||
@@ -102,6 +115,9 @@ typedef struct WM {
|
|||||||
Client *clients;
|
Client *clients;
|
||||||
Client *focused;
|
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_delete_window;
|
||||||
Atom wm_protocols;
|
Atom wm_protocols;
|
||||||
Atom wm_state;
|
Atom wm_state;
|
||||||
@@ -110,6 +126,8 @@ typedef struct WM {
|
|||||||
Atom net_wm_state_hidden;
|
Atom net_wm_state_hidden;
|
||||||
Atom net_wm_state_maximized_vert;
|
Atom net_wm_state_maximized_vert;
|
||||||
Atom net_wm_state_maximized_horz;
|
Atom net_wm_state_maximized_horz;
|
||||||
|
Atom net_wm_desktop;
|
||||||
|
Atom net_current_desktop;
|
||||||
|
|
||||||
DragMode drag_mode;
|
DragMode drag_mode;
|
||||||
int drag_start_x, drag_start_y;
|
int drag_start_x, drag_start_y;
|
||||||
@@ -129,6 +147,12 @@ typedef struct WM {
|
|||||||
int swl_count;
|
int swl_count;
|
||||||
Window swl_wins[64];
|
Window swl_wins[64];
|
||||||
char swl_titles[64][128];
|
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;
|
} WM;
|
||||||
|
|
||||||
/* core */
|
/* core */
|
||||||
@@ -151,6 +175,7 @@ void client_toggle_maximize(WM *wm, Client *c);
|
|||||||
void client_snap_left(WM *wm, Client *c);
|
void client_snap_left(WM *wm, Client *c);
|
||||||
void client_snap_right(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_set_wm_state(WM *wm, Client *c, long state);
|
||||||
|
void client_move_to_desktop(WM *wm, Client *c, int new_desk);
|
||||||
|
|
||||||
/* drawing */
|
/* drawing */
|
||||||
void frame_draw(WM *wm, Client *c);
|
void frame_draw(WM *wm, Client *c);
|
||||||
@@ -174,9 +199,14 @@ int menu_item_at(int py);
|
|||||||
/* switcher */
|
/* switcher */
|
||||||
void switcher_show(WM *wm);
|
void switcher_show(WM *wm);
|
||||||
void switcher_next(WM *wm);
|
void switcher_next(WM *wm);
|
||||||
|
void switcher_prev(WM *wm);
|
||||||
void switcher_hide(WM *wm, int commit);
|
void switcher_hide(WM *wm, int commit);
|
||||||
void switcher_draw(WM *wm);
|
void switcher_draw(WM *wm);
|
||||||
|
|
||||||
|
/* exposé */
|
||||||
|
void expose_show(WM *wm);
|
||||||
|
void expose_hide(WM *wm, Window selected_frame);
|
||||||
|
|
||||||
/* events */
|
/* events */
|
||||||
void on_map_request(WM *wm, XMapRequestEvent *e);
|
void on_map_request(WM *wm, XMapRequestEvent *e);
|
||||||
void on_unmap_notify(WM *wm, XUnmapEvent *e);
|
void on_unmap_notify(WM *wm, XUnmapEvent *e);
|
||||||
|
|||||||
+66
-5
@@ -57,6 +57,9 @@ Client *client_add(WM *wm, Window win) {
|
|||||||
c->win = win;
|
c->win = win;
|
||||||
c->w = wa.width;
|
c->w = wa.width;
|
||||||
c->h = wa.height;
|
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 */
|
/* use application-requested position if non-zero, otherwise smart place */
|
||||||
if (wa.x == 0 && wa.y == 0) {
|
if (wa.x == 0 && wa.y == 0) {
|
||||||
@@ -65,9 +68,9 @@ Client *client_add(WM *wm, Window win) {
|
|||||||
c->x = wa.x;
|
c->x = wa.x;
|
||||||
c->y = wa.y;
|
c->y = wa.y;
|
||||||
}
|
}
|
||||||
/* never land behind deskbar (top-right panel ~180px wide) */
|
/* never land behind deskbar */
|
||||||
int sw = DisplayWidth(wm->dpy, wm->screen);
|
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;
|
c->y = 40;
|
||||||
|
|
||||||
/* fetch title */
|
/* 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 fw = (int)c->w + BORDER_W * 2;
|
||||||
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
|
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);
|
int sh = DisplayHeight(wm->dpy, wm->screen);
|
||||||
|
|
||||||
/* leave room for deskbar on the right */
|
/* 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_w = sw - deskbar_w - BORDER_W * 2;
|
||||||
int new_h = sh - TITLE_HEIGHT - BORDER_W;
|
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);
|
if (c->maximized) client_unmaximize(wm, c);
|
||||||
int sw = DisplayWidth(wm->dpy, wm->screen);
|
int sw = DisplayWidth(wm->dpy, wm->screen);
|
||||||
int sh = DisplayHeight(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;
|
int half_w = (sw - deskbar_w) / 2;
|
||||||
|
|
||||||
c->x = 0;
|
c->x = 0;
|
||||||
@@ -295,11 +328,39 @@ void client_snap_left(WM *wm, Client *c) {
|
|||||||
frame_draw(wm, 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) {
|
void client_snap_right(WM *wm, Client *c) {
|
||||||
if (c->maximized) client_unmaximize(wm, c);
|
if (c->maximized) client_unmaximize(wm, c);
|
||||||
int sw = DisplayWidth(wm->dpy, wm->screen);
|
int sw = DisplayWidth(wm->dpy, wm->screen);
|
||||||
int sh = DisplayHeight(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;
|
int half_w = (sw - deskbar_w) / 2;
|
||||||
|
|
||||||
c->x = half_w;
|
c->x = half_w;
|
||||||
|
|||||||
+37
-43
@@ -1,33 +1,30 @@
|
|||||||
#include "wm.h"
|
#include "wm.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Frame layout (3 buttons on right):
|
* Frame layout — macOS style, traffic-light buttons on LEFT:
|
||||||
*
|
*
|
||||||
* ┌─[Title______________]──────[_][□][X]─┐ <- TITLE_HEIGHT
|
* ┌─[●][●][●]──── Title (centered) ────────┐ <- TITLE_HEIGHT
|
||||||
* │ content │
|
* │ content │
|
||||||
* │ [grip]────┘
|
* │ [grip]──────┘
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── hit tests ───────────────────────────────────────────────────────── */
|
/* ── hit tests ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
int frame_hit_close(Client *c, int x, int y) {
|
int frame_hit_close(Client *c, int x, int y) {
|
||||||
int fw = (int)c->w + BORDER_W * 2;
|
(void)c;
|
||||||
int bx = BTN_CLOSE_X(fw);
|
return (x >= BTN_CLOSE_X && x <= BTN_CLOSE_X + BTN_SIZE &&
|
||||||
return (x >= bx && x <= bx + BTN_SIZE &&
|
|
||||||
y >= BTN_Y && y <= BTN_Y + BTN_SIZE);
|
y >= BTN_Y && y <= BTN_Y + BTN_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
int frame_hit_maximize(Client *c, int x, int y) {
|
int frame_hit_maximize(Client *c, int x, int y) {
|
||||||
int fw = (int)c->w + BORDER_W * 2;
|
(void)c;
|
||||||
int bx = BTN_MAX_X(fw);
|
return (x >= BTN_MAX_X && x <= BTN_MAX_X + BTN_SIZE &&
|
||||||
return (x >= bx && x <= bx + BTN_SIZE &&
|
|
||||||
y >= BTN_Y && y <= BTN_Y + BTN_SIZE);
|
y >= BTN_Y && y <= BTN_Y + BTN_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
int frame_hit_minimize(Client *c, int x, int y) {
|
int frame_hit_minimize(Client *c, int x, int y) {
|
||||||
int fw = (int)c->w + BORDER_W * 2;
|
(void)c;
|
||||||
int bx = BTN_MIN_X(fw);
|
return (x >= BTN_MIN_X && x <= BTN_MIN_X + BTN_SIZE &&
|
||||||
return (x >= bx && x <= bx + BTN_SIZE &&
|
|
||||||
y >= BTN_Y && y <= BTN_Y + 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) {
|
void frame_draw_buttons(WM *wm, Client *c) {
|
||||||
int fw = (int)c->w + BORDER_W * 2;
|
/* macOS order left-to-right: close, minimize, maximize */
|
||||||
draw_circle_btn(wm, c->frame, BTN_CLOSE_X(fw), BTN_Y,
|
draw_circle_btn(wm, c->frame, BTN_CLOSE_X, BTN_Y,
|
||||||
color_alloc(wm, COLOR_CLOSE_BTN), 'X');
|
color_alloc(wm, COLOR_CLOSE_BTN), 'X');
|
||||||
draw_circle_btn(wm, c->frame, BTN_MAX_X(fw), BTN_Y,
|
draw_circle_btn(wm, c->frame, BTN_MIN_X, BTN_Y,
|
||||||
color_alloc(wm, COLOR_MAXIMIZE_BTN), 'O');
|
|
||||||
draw_circle_btn(wm, c->frame, BTN_MIN_X(fw), BTN_Y,
|
|
||||||
color_alloc(wm, COLOR_MINIMIZE_BTN), '_');
|
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 ─────────────────────────────────────────────────────── */
|
/* ── resize grip ─────────────────────────────────────────────────────── */
|
||||||
@@ -101,7 +99,6 @@ void frame_draw(WM *wm, Client *c) {
|
|||||||
int fw = (int)c->w + BORDER_W * 2;
|
int fw = (int)c->w + BORDER_W * 2;
|
||||||
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
|
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);
|
unsigned long col_dark = color_alloc(wm, COLOR_BORDER_DARK);
|
||||||
|
|
||||||
/* title bar background */
|
/* title bar background */
|
||||||
@@ -110,45 +107,42 @@ void frame_draw(WM *wm, Client *c) {
|
|||||||
XSetForeground(wm->dpy, wm->gc, col_title);
|
XSetForeground(wm->dpy, wm->gc, col_title);
|
||||||
XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, TITLE_HEIGHT);
|
XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, TITLE_HEIGHT);
|
||||||
|
|
||||||
/* subtle gradient effect: lighter top strip (2px) */
|
/* thin borders (1px, macOS style) */
|
||||||
if (c->focused) {
|
|
||||||
unsigned long lighter = color_alloc(wm, 0x2A6098);
|
|
||||||
XSetForeground(wm->dpy, wm->gc, lighter);
|
|
||||||
XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* client area background strip (below title) */
|
|
||||||
XSetForeground(wm->dpy, wm->gc, col_dark);
|
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, 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, fw - BORDER_W, TITLE_HEIGHT, BORDER_W, fh - TITLE_HEIGHT);
|
||||||
XFillRectangle(wm->dpy, c->frame, wm->gc, 0, fh - BORDER_W, fw, BORDER_W);
|
XFillRectangle(wm->dpy, c->frame, wm->gc, 0, fh - BORDER_W, fw, BORDER_W);
|
||||||
|
|
||||||
/* outer bevel */
|
/* subtle 1px separator under titlebar */
|
||||||
XSetForeground(wm->dpy, wm->gc, col_light);
|
unsigned long col_sep = color_alloc(wm, 0x1A1A1A);
|
||||||
XDrawLine(wm->dpy, c->frame, wm->gc, 0, 0, fw - 1, 0);
|
XSetForeground(wm->dpy, wm->gc, col_sep);
|
||||||
XDrawLine(wm->dpy, c->frame, wm->gc, 0, 0, 0, fh - 1);
|
|
||||||
XSetForeground(wm->dpy, wm->gc, col_dark);
|
|
||||||
XDrawLine(wm->dpy, c->frame, wm->gc, fw-1, 0, fw-1, fh-1);
|
|
||||||
XDrawLine(wm->dpy, c->frame, wm->gc, 0, fh-1, fw-1, fh-1);
|
|
||||||
|
|
||||||
/* thin separator line under title */
|
|
||||||
XSetForeground(wm->dpy, wm->gc, col_dark);
|
|
||||||
XDrawLine(wm->dpy, c->frame, wm->gc, 0, TITLE_HEIGHT - 1, fw - 1, TITLE_HEIGHT - 1);
|
XDrawLine(wm->dpy, c->frame, wm->gc, 0, TITLE_HEIGHT - 1, fw - 1, TITLE_HEIGHT - 1);
|
||||||
|
|
||||||
/* title text */
|
/* centered title text (macOS style) */
|
||||||
int btn_area = BTN_SIZE * 3 + BTN_MARGIN * 3 + BORDER_W + 8;
|
|
||||||
int text_max_w = fw - BORDER_W * 2 - btn_area - 8;
|
|
||||||
if (text_max_w < 10) text_max_w = 10;
|
|
||||||
|
|
||||||
XftDraw *xd = XftDrawCreate(wm->dpy, c->frame, wm->visual, wm->cmap);
|
XftDraw *xd = XftDrawCreate(wm->dpy, c->frame, wm->visual, wm->cmap);
|
||||||
if (xd) {
|
if (xd) {
|
||||||
XftColor col_text;
|
XftColor col_text;
|
||||||
xft_color(wm, c->focused ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &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);
|
XftDrawSetClipRectangles(xd, 0, 0, &clip, 1);
|
||||||
|
|
||||||
int ty = (TITLE_HEIGHT + wm->font_title->ascent - wm->font_title->descent) / 2;
|
int ty = (TITLE_HEIGHT + wm->font_title->ascent - wm->font_title->descent) / 2;
|
||||||
XftDrawStringUtf8(xd, &col_text, wm->font_title,
|
XftDrawStringUtf8(xd, &col_text, wm->font_title,
|
||||||
BORDER_W + 6, ty,
|
tx, ty,
|
||||||
(const FcChar8 *)c->title, strlen(c->title));
|
(const FcChar8 *)c->title, strlen(c->title));
|
||||||
XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_text);
|
XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_text);
|
||||||
XftDrawDestroy(xd);
|
XftDrawDestroy(xd);
|
||||||
|
|||||||
+185
-14
@@ -33,7 +33,7 @@ static void check_snap(WM *wm, Client *c) {
|
|||||||
} else {
|
} else {
|
||||||
client_snap_left(wm, c);
|
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);
|
client_snap_right(wm, c);
|
||||||
} else if (c->y <= SNAP_DIST) {
|
} else if (c->y <= SNAP_DIST) {
|
||||||
client_maximize(wm, c);
|
client_maximize(wm, c);
|
||||||
@@ -74,6 +74,7 @@ void on_expose(WM *wm, XExposeEvent *e) {
|
|||||||
if (c && !c->minimized) frame_draw(wm, c);
|
if (c && !c->minimized) frame_draw(wm, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void on_configure_request(WM *wm, XConfigureRequestEvent *e) {
|
void on_configure_request(WM *wm, XConfigureRequestEvent *e) {
|
||||||
Client *c = client_find_by_win(wm, e->window);
|
Client *c = client_find_by_win(wm, e->window);
|
||||||
XWindowChanges wc;
|
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);
|
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) {
|
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) {
|
if (e->message_type == net_active) {
|
||||||
Client *c = client_find_by_win(wm, e->window);
|
Client *c = client_find_by_win(wm, e->window);
|
||||||
if (c) client_restore(wm, c);
|
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) {
|
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 */
|
/* menu window */
|
||||||
if (wm->menu_visible && e->window == wm->menu_win) {
|
if (wm->menu_visible && e->window == wm->menu_win) {
|
||||||
if (e->button == Button1) menu_click(wm, e->x, e->y);
|
if (e->button == Button1) menu_click(wm, e->x, e->y);
|
||||||
@@ -247,8 +332,18 @@ void on_motion_notify(WM *wm, XMotionEvent *e) {
|
|||||||
} else {
|
} else {
|
||||||
int nw = (int)wm->drag_win_w + dx;
|
int nw = (int)wm->drag_win_w + dx;
|
||||||
int nh = (int)wm->drag_win_h + dy;
|
int nh = (int)wm->drag_win_h + dy;
|
||||||
if (nw < MIN_WIN_W) nw = MIN_WIN_W;
|
/* clamp to minimum */
|
||||||
if (nh < MIN_WIN_H) nh = MIN_WIN_H;
|
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;
|
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->frame, c->w+BORDER_W*2, c->h+TITLE_HEIGHT+BORDER_W);
|
||||||
XResizeWindow(wm->dpy, c->win, c->w, c->h);
|
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)
|
if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused)
|
||||||
send_delete(wm, wm->focused->win);
|
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 ((e->state & Mod1Mask) && sym == XK_Tab) {
|
||||||
if (wm->swl_visible) switcher_next(wm);
|
int backward = (e->state & ShiftMask) != 0;
|
||||||
else if (wm->clients) switcher_show(wm);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,16 +392,47 @@ void on_key_press(WM *wm, XKeyEvent *e) {
|
|||||||
if ((e->state & Mod1Mask) && sym == XK_F1)
|
if ((e->state & Mod1Mask) && sym == XK_F1)
|
||||||
system("orionlauncher &");
|
system("orionlauncher &");
|
||||||
|
|
||||||
/* PrintScreen — full screenshot to ~/Pictures */
|
/* PrintScreen — screenshot */
|
||||||
if (sym == XK_Print) {
|
if (sym == XK_Print) {
|
||||||
if (e->state & ShiftMask)
|
if (e->state & ShiftMask)
|
||||||
system("mkdir -p ~/Pictures && scrot -s ~/Pictures/screenshot_%Y%m%d_%H%M%S.png"
|
system("orionshot region &");
|
||||||
" && notify-send 'Screenshot' 'Region saved to ~/Pictures' &");
|
|
||||||
else
|
else
|
||||||
system("mkdir -p ~/Pictures && scrot ~/Pictures/screenshot_%Y%m%d_%H%M%S.png"
|
system("orionshot full &");
|
||||||
" && notify-send 'Screenshot' 'Saved to ~/Pictures' &");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
/* Super+Left — snap left */
|
||||||
if ((e->state & Mod4Mask) && sym == XK_Left && wm->focused)
|
if ((e->state & Mod4Mask) && sym == XK_Left && wm->focused)
|
||||||
client_snap_left(wm, 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)
|
if ((e->state & Mod4Mask) && sym == XK_Down && wm->focused)
|
||||||
client_unmaximize(wm, wm->focused);
|
client_unmaximize(wm, wm->focused);
|
||||||
|
|
||||||
/* Escape — hide menu */
|
/* Super+Space — Exposé window overview */
|
||||||
if (sym == XK_Escape && wm->menu_visible)
|
if ((e->state & Mod4Mask) && sym == XK_space) {
|
||||||
menu_hide(wm);
|
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) {
|
void on_key_release(WM *wm, XKeyEvent *e) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+15
-15
@@ -1,7 +1,7 @@
|
|||||||
#include "wm.h"
|
#include "wm.h"
|
||||||
|
|
||||||
#define MENU_ITEM_H 24
|
#define MENU_ITEM_H 26
|
||||||
#define MENU_W 220
|
#define MENU_W 230
|
||||||
#define MENU_PADDING 5
|
#define MENU_PADDING 5
|
||||||
|
|
||||||
static const MenuItem MENU_ITEMS[] = {
|
static const MenuItem MENU_ITEMS[] = {
|
||||||
@@ -51,7 +51,7 @@ void menu_show(WM *wm, int x, int y) {
|
|||||||
|
|
||||||
XSetWindowAttributes swa;
|
XSetWindowAttributes swa;
|
||||||
swa.override_redirect = True;
|
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
|
swa.event_mask = ExposureMask | ButtonPressMask
|
||||||
| ButtonReleaseMask | PointerMotionMask
|
| ButtonReleaseMask | PointerMotionMask
|
||||||
| LeaveWindowMask;
|
| LeaveWindowMask;
|
||||||
@@ -65,8 +65,8 @@ void menu_show(WM *wm, int x, int y) {
|
|||||||
&swa
|
&swa
|
||||||
);
|
);
|
||||||
|
|
||||||
/* 1px accent border */
|
/* subtle 1px border (macOS-like) */
|
||||||
XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, COLOR_BORDER_LIGHT));
|
XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, 0x505050));
|
||||||
|
|
||||||
XMapRaised(wm->dpy, wm->menu_win);
|
XMapRaised(wm->dpy, wm->menu_win);
|
||||||
wm->menu_visible = 1;
|
wm->menu_visible = 1;
|
||||||
@@ -110,29 +110,29 @@ void menu_draw(WM *wm) {
|
|||||||
if (!xd) return;
|
if (!xd) return;
|
||||||
|
|
||||||
XftColor col_normal, col_hover_bg, col_hover_text, col_sep;
|
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, 0xE8E8E8, &col_normal); /* macOS light menu text */
|
||||||
xft_color(wm, 0x2060A0, &col_hover_bg); /* blue highlight */
|
xft_color(wm, 0x4A90D9, &col_hover_bg); /* macOS blue selection */
|
||||||
xft_color(wm, 0xF0F4FF, &col_hover_text);
|
xft_color(wm, 0xFFFFFF, &col_hover_text);
|
||||||
xft_color(wm, 0x303848, &col_sep);
|
xft_color(wm, 0x444444, &col_sep);
|
||||||
|
|
||||||
/* clear background */
|
/* 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());
|
XFillRectangle(wm->dpy, wm->menu_win, wm->gc, 0, 0, MENU_W, menu_height());
|
||||||
|
|
||||||
for (int i = 0; i < MENU_COUNT; i++) {
|
for (int i = 0; i < MENU_COUNT; i++) {
|
||||||
int iy = item_y(i);
|
int iy = item_y(i);
|
||||||
|
|
||||||
if (is_separator(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);
|
XDrawLine(wm->dpy, wm->menu_win, wm->gc, 8, iy + 4, MENU_W - 8, iy + 4);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* hover highlight */
|
/* hover highlight — macOS blue selection pill */
|
||||||
if (i == wm->menu_hovered && wm->menu_hovered >= 0) {
|
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,
|
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;
|
XftColor *tc = (i == wm->menu_hovered) ? &col_hover_text : &col_normal;
|
||||||
@@ -142,7 +142,7 @@ void menu_draw(WM *wm) {
|
|||||||
strlen(MENU_ITEMS[i].label), &ext);
|
strlen(MENU_ITEMS[i].label), &ext);
|
||||||
int ty = iy + (MENU_ITEM_H - ext.height) / 2 + ext.height - ext.y;
|
int ty = iy + (MENU_ITEM_H - ext.height) / 2 + ext.height - ext.y;
|
||||||
XftDrawStringUtf8(xd, tc, wm->font_menu,
|
XftDrawStringUtf8(xd, tc, wm->font_menu,
|
||||||
MENU_PADDING + 4, ty,
|
MENU_PADDING + 10, ty,
|
||||||
(const FcChar8 *)MENU_ITEMS[i].label,
|
(const FcChar8 *)MENU_ITEMS[i].label,
|
||||||
strlen(MENU_ITEMS[i].label));
|
strlen(MENU_ITEMS[i].label));
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-10
@@ -8,17 +8,19 @@
|
|||||||
|
|
||||||
static void swl_populate(WM *wm) {
|
static void swl_populate(WM *wm) {
|
||||||
wm->swl_count = 0;
|
wm->swl_count = 0;
|
||||||
/* current focused window goes first */
|
/* current focused (non-minimized) window goes first */
|
||||||
if (wm->focused && !wm->focused->minimized) {
|
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);
|
strncpy(wm->swl_titles[0], wm->focused->title, 127);
|
||||||
wm->swl_titles[0][127] = '\0';
|
wm->swl_titles[0][127] = '\0';
|
||||||
wm->swl_count = 1;
|
wm->swl_count = 1;
|
||||||
}
|
}
|
||||||
|
/* all other windows, including minimized ones */
|
||||||
for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) {
|
for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) {
|
||||||
if (c->minimized) continue;
|
|
||||||
if (wm->focused && c == wm->focused) 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);
|
strncpy(wm->swl_titles[wm->swl_count], c->title, 127);
|
||||||
wm->swl_titles[wm->swl_count][127] = '\0';
|
wm->swl_titles[wm->swl_count][127] = '\0';
|
||||||
wm->swl_count++;
|
wm->swl_count++;
|
||||||
@@ -61,29 +63,34 @@ void switcher_draw(WM *wm) {
|
|||||||
for (int vi = 0; vi < vis; vi++) {
|
for (int vi = 0; vi < vis; vi++) {
|
||||||
int i = vstart + vi;
|
int i = vstart + vi;
|
||||||
int selected = (i == wm->swl_idx);
|
int selected = (i == wm->swl_idx);
|
||||||
|
int mini = wm->swl_mini[i];
|
||||||
int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD);
|
int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD);
|
||||||
int iy = 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);
|
XSetForeground(wm->dpy, wm->gc, card_bg);
|
||||||
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H);
|
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H);
|
||||||
|
|
||||||
XSetForeground(wm->dpy, wm->gc,
|
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);
|
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_x = ix + (SWL_ITEM_W - SWL_ICON_SZ) / 2;
|
||||||
int icon_y = iy + 8;
|
int icon_y = iy + 8;
|
||||||
XSetForeground(wm->dpy, wm->gc,
|
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);
|
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' };
|
char letter[2] = { wm->swl_titles[i][0] ? wm->swl_titles[i][0] : '?', '\0' };
|
||||||
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
|
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;
|
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;
|
XGlyphInfo ext;
|
||||||
XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext);
|
XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext);
|
||||||
int lx = icon_x + (SWL_ICON_SZ - (int)ext.width) / 2;
|
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);
|
XftDrawStringUtf8(xd, &icon_col, wm->font_title, lx, ly, (const FcChar8 *)letter, 1);
|
||||||
XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col);
|
XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col);
|
||||||
|
|
||||||
/* title below icon */
|
/* title below icon — prefix [..] for minimized */
|
||||||
const char *title = wm->swl_titles[i];
|
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;
|
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;
|
XGlyphInfo text_ext;
|
||||||
XftTextExtentsUtf8(wm->dpy, wm->font_title,
|
XftTextExtentsUtf8(wm->dpy, wm->font_title,
|
||||||
(const FcChar8 *)title, strlen(title), &text_ext);
|
(const FcChar8 *)title, strlen(title), &text_ext);
|
||||||
@@ -163,6 +179,12 @@ void switcher_next(WM *wm) {
|
|||||||
switcher_draw(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) {
|
void switcher_hide(WM *wm, int commit) {
|
||||||
if (!wm->swl_visible) return;
|
if (!wm->swl_visible) return;
|
||||||
XUngrabKeyboard(wm->dpy, CurrentTime);
|
XUngrabKeyboard(wm->dpy, CurrentTime);
|
||||||
|
|||||||
+106
-1
@@ -1,5 +1,67 @@
|
|||||||
#include "wm.h"
|
#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) {
|
static int x_error_handler(Display *dpy, XErrorEvent *e) {
|
||||||
if (e->error_code == BadWindow || e->error_code == BadAccess)
|
if (e->error_code == BadWindow || e->error_code == BadAccess)
|
||||||
return 0;
|
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_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_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_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 */
|
/* advertise basic EWMH support */
|
||||||
Atom net_supported = XInternAtom(wm->dpy, "_NET_SUPPORTED", False);
|
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, wm->net_wm_state_hidden,
|
||||||
wm->net_wm_state_maximized_vert,
|
wm->net_wm_state_maximized_vert,
|
||||||
wm->net_wm_state_maximized_horz,
|
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_ACTIVE_WINDOW", False),
|
||||||
XInternAtom(wm->dpy, "_NET_CLIENT_LIST", 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_NUMBER_OF_DESKTOPS", False),
|
||||||
XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False),
|
|
||||||
XInternAtom(wm->dpy, "_NET_WM_NAME", False),
|
XInternAtom(wm->dpy, "_NET_WM_NAME", False),
|
||||||
};
|
};
|
||||||
XChangeProperty(wm->dpy, wm->root, net_supported, XA_ATOM, 32,
|
XChangeProperty(wm->dpy, wm->root, net_supported, XA_ATOM, 32,
|
||||||
@@ -90,6 +156,7 @@ void wm_init(WM *wm) {
|
|||||||
/* global key grabs */
|
/* global key grabs */
|
||||||
struct { KeySym sym; unsigned mod; } grabs[] = {
|
struct { KeySym sym; unsigned mod; } grabs[] = {
|
||||||
{ XK_Tab, Mod1Mask },
|
{ XK_Tab, Mod1Mask },
|
||||||
|
{ XK_Tab, Mod1Mask | ShiftMask }, /* Shift+Alt+Tab — backwards */
|
||||||
{ XK_F4, Mod1Mask },
|
{ XK_F4, Mod1Mask },
|
||||||
{ XK_F9, Mod1Mask }, /* minimize */
|
{ XK_F9, Mod1Mask }, /* minimize */
|
||||||
{ XK_F10, Mod1Mask }, /* maximize */
|
{ XK_F10, Mod1Mask }, /* maximize */
|
||||||
@@ -98,7 +165,29 @@ void wm_init(WM *wm) {
|
|||||||
{ XK_Right, Mod4Mask }, /* snap right */
|
{ XK_Right, Mod4Mask }, /* snap right */
|
||||||
{ XK_Up, Mod4Mask }, /* maximize */
|
{ XK_Up, Mod4Mask }, /* maximize */
|
||||||
{ XK_Down, Mod4Mask }, /* restore */
|
{ 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 },
|
{ 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++) {
|
for (int i = 0; i < (int)(sizeof(grabs)/sizeof(grabs[0])); i++) {
|
||||||
KeyCode kc = XKeysymToKeycode(wm->dpy, grabs[i].sym);
|
KeyCode kc = XKeysymToKeycode(wm->dpy, grabs[i].sym);
|
||||||
@@ -122,11 +211,17 @@ void wm_init(WM *wm) {
|
|||||||
|
|
||||||
XSync(wm->dpy, False);
|
XSync(wm->dpy, False);
|
||||||
fprintf(stderr, "orionwm: started\n");
|
fprintf(stderr, "orionwm: started\n");
|
||||||
|
|
||||||
|
wm_run_autostart();
|
||||||
}
|
}
|
||||||
|
|
||||||
void wm_run(WM *wm) {
|
void wm_run(WM *wm) {
|
||||||
|
int xfd = ConnectionNumber(wm->dpy);
|
||||||
XEvent e;
|
XEvent e;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
/* drain all pending X events */
|
||||||
|
while (XPending(wm->dpy)) {
|
||||||
XNextEvent(wm->dpy, &e);
|
XNextEvent(wm->dpy, &e);
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case MapRequest: on_map_request(wm, &e.xmaprequest); break;
|
case MapRequest: on_map_request(wm, &e.xmaprequest); break;
|
||||||
@@ -145,6 +240,16 @@ void wm_run(WM *wm) {
|
|||||||
default: 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void wm_cleanup(WM *wm) {
|
void wm_cleanup(WM *wm) {
|
||||||
|
|||||||
Reference in new issue
Block a user