Add hot corners, media keys, orionshot, orionclip, notification history, autostart, GTK accent update, wallpaper slideshow, ISO build

- wm: hot corner polling via select()-based wm_run loop; TL=Exposé, TR=show desktop, BL=launcher, BR=lock
- wm: XF86 media key grabs (volume, brightness, playback) + Super+V for clipboard
- wm: autostart reads ~/.config/orionos/autostart on startup
- events: media key dispatch (amixer/brightnessctl/playerctl); Super+V → orionclip --show; Print → orionshot
- orionshot: new X11 screenshot tool, full-screen and rubber-band region modes, saves PNG to ~/Pictures/Screenshots/
- orionclip: new clipboard manager daemon; polls clipboard, stores 20 entries, popup via Super+V IPC
- orionnotify: append-only notification history log at ~/.local/share/orionos/notif-history.log
- themes: GTK accent updated from #3A6898 to #1E58A8 to match WM/deskbar
- themes: orionwallpaper gains --slideshow DIR [SECS] mode using feh + sleep loop
- iso: Docker-based archiso builder, live-session xinitrc, profiledef.sh
- Makefile: add orionshot and orionclip to all/clean/install; Docker-based iso target
This commit is contained in:
Claude committed 2026-08-30 20:47:47 +00:00
1 parent 2a42b826fb
commit a2092fd989
15 files changed
+932 -59

No files matched your search

+17 -4
View File
@@ -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 orionnotify orionlock 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 orionnotify orionlock all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock orionshot orionclip
wm: wm:
$(MAKE) -C wm $(MAKE) -C wm
@@ -45,6 +45,12 @@ orionnotify:
orionlock: orionlock:
$(MAKE) -C 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
@@ -58,6 +64,8 @@ clean:
$(MAKE) -C lens clean $(MAKE) -C lens clean
$(MAKE) -C orionnotify clean $(MAKE) -C orionnotify clean
$(MAKE) -C orionlock clean $(MAKE) -C orionlock clean
$(MAKE) -C orionshot clean
$(MAKE) -C orionclip clean
install: all install: all
# binaries # binaries
@@ -72,6 +80,8 @@ install: all
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 orionnotify/orionnotify /usr/local/bin/orionnotify
install -Dm755 orionlock/orionlock /usr/local/bin/orionlock 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/
@@ -88,8 +98,11 @@ install: all
@echo " picom --config /etc/orionos/picom.conf &" @echo " picom --config /etc/orionos/picom.conf &"
@echo " oriondeskbar &" @echo " oriondeskbar &"
@echo " orionnotify &" @echo " orionnotify &"
@echo " orionclip &"
@echo " exec orionwm" @echo " exec orionwm"
iso: iso:
chmod +x build-iso.sh @echo "Building OrionOS ISO via Docker..."
./build-iso.sh 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
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# OrionOS live session autostart — sourced by ~/.xinitrc on the ISO
# Compositor
picom --config /etc/orionos/picom.conf &
# Set wallpaper
orionwallpaper &
# Notification daemon
orionnotify &
# Clipboard manager
orionclip &
# Deskbar
oriondeskbar &
# Window manager (blocking)
exec orionwm
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
source /etc/orionos/xinitrc
+22 -47
View File
@@ -1,58 +1,33 @@
# 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) ── # Install archiso and build dependencies
RUN echo "DisableSandbox" >> /etc/pacman.conf RUN pacman -Syu --noconfirm \
# ── install build tools ────────────────────────────────────────────────
RUN pacman -Syu --noconfirm && \
pacman -S --noconfirm \
archiso \ archiso \
base-devel \ base-devel \
git \
libx11 \ libx11 \
libxft \ libxft \
fontconfig \ libxfixes \
freetype2 \ dbus \
imlib2 \ libpng \
imagemagick \ feh \
fakeroot \ picom \
git \ libpam \
sudo \ ttf-dejavu \
xorg-server \
xorg-xinit \
xterm \
notify-send-py \
&& pacman -Scc --noconfirm && pacman -Scc --noconfirm
# ── build OnlyOffice from AUR (as a non-root build user) ───────────── WORKDIR /build
RUN useradd -m builder && \ COPY . /build/orionos
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) ─── # Build OrionOS components
RUN echo "=== archiso initcpio hooks ===" && \ RUN cd /build/orionos && make && make install
ls /usr/lib/initcpio/hooks/ 2>/dev/null || echo "(none found)"
# ── copy the entire OrionOS source tree ────────────────────────────── # Copy archiso profile and build the ISO
WORKDIR /orionos RUN cp -r /build/orionos/iso/profile /tmp/orionos-profile
COPY . . RUN mkarchiso -v -w /tmp/iso-work -o /output /tmp/orionos-profile
# ── build the OrionOS binaries inside the container ─────────────────── CMD ["/bin/bash"]
# pkg-config finds X11/Xft headers automatically on Arch — no overrides needed
RUN make
# ── generate the default wallpaper ───────────────────────────────────
RUN bash themes/wallpapers/generate-default.sh || true
# ── run the ISO build script ──────────────────────────────────────────
# --privileged is required for loop device mounts inside the container
ENTRYPOINT ["/orionos/iso/docker/entrypoint.sh"]
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# archiso profile definition for OrionOS
iso_name="OrionOS"
iso_label="ORIONOS_$(date +%Y%m)"
iso_publisher="OrionOS Project"
iso_application="OrionOS Live"
iso_version="$(date +%Y.%m)"
install_dir="arch"
buildmodes=('iso')
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito'
'uefi-ia32.grub.esp' 'uefi-x64.grub.esp'
'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito')
arch="x86_64"
pacman_conf="pacman.conf"
airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '15')
file_permissions=(
["/etc/orionos/xinitrc"]="0:0:755"
["/root/.xinitrc"]="0:0:755"
)
+22
View File
@@ -0,0 +1,22 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags x11 xfixes xft 2>/dev/null)
PKG_L := $(shell pkg-config --libs x11 xfixes xft 2>/dev/null)
ifeq ($(PKG_C),)
$(error "Required: libx11-dev libxfixes-dev libxft-dev")
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L)
BIN = orionclip
.PHONY: all clean install
all: $(BIN)
$(BIN): orionclip.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
clean:
rm -f $(BIN)
+407
View File
@@ -0,0 +1,407 @@
/*
* orionclip — clipboard manager for OrionOS
* Usage:
* orionclip run as daemon, monitors clipboard
* orionclip --show toggle the history popup (sent by Super+V hotkey)
*
* Stores up to CLIP_MAX entries. Popup appears in bottom-right corner
* (left of the deskbar). Click an entry to restore it to the clipboard.
* Press Escape or click outside to dismiss.
*
* IPC: the daemon registers the _ORIONCLIP_WIN atom on the root window.
* orionclip --show sends a ClientMessage to that window.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/Xft/Xft.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define CLIP_MAX 20
#define POPUP_W 340
#define POPUP_ROW_H 28
#define POPUP_PAD 8
#define DESKBAR_W 200
#define POLL_MS 300 /* clipboard poll interval in ms */
#define COL_BG 0x2A2D36
#define COL_BORDER 0x4A6080
#define COL_TEXT 0xD8E4F4
#define COL_SEL_BG 0x1E58A8
#define COL_SEL_FG 0xF0F8FF
#define COL_ACCENT 0x1E58A8
#define COL_DIM 0x607080
static char clip_history[CLIP_MAX][512];
static int clip_count = 0;
static Display *dpy;
static int screen;
static Visual *visual;
static Colormap cmap;
static GC gc;
static XftFont *font;
static Window popup_win = None;
static int popup_hovered = -1;
static Atom atom_clip_win; /* _ORIONCLIP_WIN — IPC handle */
static Atom atom_show; /* _ORIONCLIP_SHOW — toggle message */
static Atom atom_utf8;
static Atom atom_clipboard;
static Atom atom_targets;
/* ── helpers ─────────────────────────────────────────────────────────── */
static unsigned long xcolor(unsigned long rgb) {
XColor c;
c.red = ((rgb >> 16) & 0xFF) * 257;
c.green = ((rgb >> 8) & 0xFF) * 257;
c.blue = ((rgb) & 0xFF) * 257;
c.flags = DoRed | DoGreen | DoBlue;
XAllocColor(dpy, cmap, &c);
return c.pixel;
}
static void xft_color(unsigned long rgb, XftColor *out) {
XRenderColor rc;
rc.red = ((rgb >> 16) & 0xFF) * 257;
rc.green = ((rgb >> 8) & 0xFF) * 257;
rc.blue = ((rgb) & 0xFF) * 257;
rc.alpha = 0xFFFF;
XftColorAllocValue(dpy, visual, cmap, &rc, out);
}
/* ── clipboard read ──────────────────────────────────────────────────── */
static char *clipboard_get(void) {
/* request clipboard content via XConvertSelection */
Window owner = XGetSelectionOwner(dpy, atom_clipboard);
if (owner == None) return NULL;
/* use a small utility window as the requestor */
static Window req_win = None;
if (req_win == None)
req_win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen),
-10, -10, 1, 1, 0, 0, 0);
Atom prop = XInternAtom(dpy, "_ORIONCLIP_PROP", False);
XConvertSelection(dpy, atom_clipboard, atom_utf8, prop, req_win, CurrentTime);
/* wait up to ~300ms for SelectionNotify */
struct timespec deadline, now;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_nsec += 300000000L;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_nsec -= 1000000000L;
deadline.tv_sec++;
}
for (;;) {
if (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == SelectionNotify && e.xselection.requestor == req_win) {
if (e.xselection.property == None) return NULL;
Atom type; int fmt; unsigned long nitems, left;
unsigned char *data = NULL;
XGetWindowProperty(dpy, req_win, prop, 0, 65536/4, True,
atom_utf8, &type, &fmt, &nitems, &left, &data);
if (!data) return NULL;
char *copy = strndup((char *)data, nitems < 511 ? nitems : 511);
XFree(data);
return copy;
}
}
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec))
break;
usleep(10000);
}
return NULL;
}
static void clipboard_set(const char *text) {
/* put text on the clipboard by becoming the selection owner */
static Window sel_win = None;
static char sel_buf[512];
if (sel_win == None)
sel_win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen),
-10, -10, 1, 1, 0, 0, 0);
strncpy(sel_buf, text, 511); sel_buf[511] = '\0';
XSetSelectionOwner(dpy, atom_clipboard, sel_win, CurrentTime);
/* SelectionRequest events are handled in the main event loop */
(void)sel_buf;
}
/* ── history management ──────────────────────────────────────────────── */
static void history_push(const char *text) {
if (!text || !*text) return;
/* deduplicate: if same text is already entry 0, skip */
if (clip_count > 0 && strcmp(clip_history[0], text) == 0) return;
/* shift down */
int top = clip_count < CLIP_MAX ? clip_count : CLIP_MAX - 1;
for (int i = top; i > 0; i--)
memcpy(clip_history[i], clip_history[i-1], sizeof(clip_history[0]));
strncpy(clip_history[0], text, 511);
clip_history[0][511] = '\0';
if (clip_count < CLIP_MAX) clip_count++;
}
/* ── popup ───────────────────────────────────────────────────────────── */
static int popup_h(void) {
return POPUP_PAD * 2 + clip_count * POPUP_ROW_H;
}
static void popup_draw(void) {
if (!popup_win) return;
int ph = popup_h();
XSetForeground(dpy, gc, xcolor(COL_BG));
XFillRectangle(dpy, popup_win, gc, 0, 0, POPUP_W, ph);
XSetForeground(dpy, gc, xcolor(COL_ACCENT));
XFillRectangle(dpy, popup_win, gc, 0, 0, 3, ph);
XSetForeground(dpy, gc, xcolor(COL_BORDER));
XDrawRectangle(dpy, popup_win, gc, 0, 0, POPUP_W - 1, ph - 1);
XftDraw *xd = XftDrawCreate(dpy, popup_win, visual, cmap);
for (int i = 0; i < clip_count; i++) {
int ry = POPUP_PAD + i * POPUP_ROW_H;
if (i == popup_hovered) {
XSetForeground(dpy, gc, xcolor(COL_SEL_BG));
XFillRectangle(dpy, popup_win, gc, 4, ry, POPUP_W - 8, POPUP_ROW_H - 2);
}
unsigned long text_col = (i == popup_hovered) ? COL_SEL_FG : COL_TEXT;
if (i > 0) text_col = COL_DIM;
XftColor col;
xft_color(text_col, &col);
/* truncate display text */
char disp[64];
strncpy(disp, clip_history[i], 63); disp[63] = '\0';
/* replace newlines with spaces */
for (char *p = disp; *p; p++) if (*p == '\n' || *p == '\r' || *p == '\t') *p = ' ';
XftDrawStringUtf8(xd, &col, font, 12, ry + POPUP_ROW_H - 7,
(const FcChar8 *)disp, strlen(disp));
XftColorFree(dpy, visual, cmap, &col);
}
XftDrawDestroy(xd);
XFlush(dpy);
}
static void popup_show(void) {
if (popup_win != None) { popup_draw(); return; }
if (clip_count == 0) return;
int ph = popup_h();
int sw = DisplayWidth(dpy, screen);
int sh = DisplayHeight(dpy, screen);
int px = sw - DESKBAR_W - POPUP_W - 12;
int py = sh - ph - 40;
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = xcolor(COL_BG);
swa.event_mask = ExposureMask | ButtonPressMask |
PointerMotionMask | LeaveWindowMask;
popup_win = XCreateWindow(dpy, RootWindow(dpy, screen),
px, py, POPUP_W, ph, 0,
DefaultDepth(dpy, screen), InputOutput,
visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
XMapRaised(dpy, popup_win);
popup_draw();
}
static void popup_hide(void) {
if (popup_win == None) return;
XDestroyWindow(dpy, popup_win);
popup_win = None;
popup_hovered = -1;
XFlush(dpy);
}
static void popup_toggle(void) {
if (popup_win != None) popup_hide();
else popup_show();
}
static int popup_row_at(int y) {
int row = (y - POPUP_PAD) / POPUP_ROW_H;
if (row < 0 || row >= clip_count) return -1;
return row;
}
/* ── --show mode: send IPC to daemon ─────────────────────────────────── */
static void send_show(void) {
Display *d = XOpenDisplay(NULL);
if (!d) { fprintf(stderr, "orionclip: cannot open display\n"); return; }
Atom a = XInternAtom(d, "_ORIONCLIP_WIN", True);
if (a == None) { XCloseDisplay(d); return; }
Atom type; int fmt; unsigned long n, left;
unsigned char *data = NULL;
XGetWindowProperty(d, RootWindow(d, DefaultScreen(d)), a, 0, 1, False,
XA_WINDOW, &type, &fmt, &n, &left, &data);
if (!data) { XCloseDisplay(d); return; }
Window target = *(Window *)data;
XFree(data);
Atom show_atom = XInternAtom(d, "_ORIONCLIP_SHOW", False);
XClientMessageEvent ev = {0};
ev.type = ClientMessage;
ev.window = target;
ev.message_type = show_atom;
ev.format = 32;
XSendEvent(d, target, False, 0, (XEvent *)&ev);
XFlush(d);
XCloseDisplay(d);
}
/* ── main daemon ─────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
if (argc > 1 && strcmp(argv[1], "--show") == 0) {
send_show();
return 0;
}
dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionclip: cannot open display\n"); return 1; }
screen = DefaultScreen(dpy);
visual = DefaultVisual(dpy, screen);
cmap = DefaultColormap(dpy, screen);
gc = XCreateGC(dpy, RootWindow(dpy, screen), 0, NULL);
font = XftFontOpenName(dpy, screen, "Sans:size=9");
if (!font) font = XftFontOpenName(dpy, screen, "fixed:size=9");
atom_utf8 = XInternAtom(dpy, "UTF8_STRING", False);
atom_clipboard = XInternAtom(dpy, "CLIPBOARD", False);
atom_targets = XInternAtom(dpy, "TARGETS", False);
atom_clip_win = XInternAtom(dpy, "_ORIONCLIP_WIN", False);
atom_show = XInternAtom(dpy, "_ORIONCLIP_SHOW", False);
/* register ourselves on the root window */
Window self = XCreateSimpleWindow(dpy, RootWindow(dpy, screen),
-10, -10, 1, 1, 0, 0, 0);
XSelectInput(dpy, self, StructureNotifyMask | PropertyChangeMask);
XChangeProperty(dpy, RootWindow(dpy, screen), atom_clip_win, XA_WINDOW, 32,
PropModeReplace, (unsigned char *)&self, 1);
XFlush(dpy);
int xfd = ConnectionNumber(dpy);
struct timespec last_poll = {0, 0};
for (;;) {
/* pump X events */
while (XPending(dpy)) {
XEvent e;
XNextEvent(dpy, &e);
/* IPC: show/hide toggle */
if (e.type == ClientMessage &&
(Atom)e.xclient.message_type == atom_show) {
popup_toggle();
continue;
}
/* selection ownership request — serve clipboard content */
if (e.type == SelectionRequest) {
XSelectionRequestEvent *req = &e.xselectionrequest;
XSelectionEvent sev = {0};
sev.type = SelectionNotify;
sev.requestor = req->requestor;
sev.selection = req->selection;
sev.target = req->target;
sev.time = req->time;
sev.property = None;
if (clip_count > 0) {
if (req->target == atom_targets) {
Atom targets[2] = { atom_targets, atom_utf8 };
XChangeProperty(dpy, req->requestor, req->property, XA_ATOM, 32,
PropModeReplace, (unsigned char *)targets, 2);
sev.property = req->property;
} else if (req->target == atom_utf8) {
XChangeProperty(dpy, req->requestor, req->property, atom_utf8, 8,
PropModeReplace,
(unsigned char *)clip_history[0],
strlen(clip_history[0]));
sev.property = req->property;
}
}
XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&sev);
continue;
}
if (popup_win == None) continue;
if (e.type == Expose && e.xexpose.count == 0 &&
e.xany.window == popup_win)
popup_draw();
if (e.type == LeaveNotify && e.xany.window == popup_win) {
popup_hovered = -1;
popup_draw();
}
if (e.type == MotionNotify && e.xany.window == popup_win) {
int row = popup_row_at(e.xmotion.y);
if (row != popup_hovered) {
popup_hovered = row;
popup_draw();
}
}
if (e.type == ButtonPress && e.xany.window == popup_win) {
int row = popup_row_at(e.xbutton.y);
if (row >= 0 && row < clip_count) {
/* move chosen entry to top and set as clipboard */
char chosen[512];
strncpy(chosen, clip_history[row], 511); chosen[511] = '\0';
history_push(chosen);
clipboard_set(clip_history[0]);
}
popup_hide();
}
}
/* poll clipboard every POLL_MS ms */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long elapsed_ms = (now.tv_sec - last_poll.tv_sec) * 1000
+ (now.tv_nsec - last_poll.tv_nsec) / 1000000;
if (elapsed_ms >= POLL_MS) {
last_poll = now;
char *text = clipboard_get();
if (text) {
history_push(text);
free(text);
}
}
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
struct timeval tv = { 0, POLL_MS * 1000 };
select(xfd + 1, &fds, NULL, NULL, &tv);
}
return 0;
}
+22
View File
@@ -12,6 +12,7 @@
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h>
#define DESKBAR_W 200 #define DESKBAR_W 200
#define NOTIF_W 320 #define NOTIF_W 320
@@ -124,6 +125,26 @@ static void notif_close(int idx) {
notif_restack(); 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) { static unsigned int notif_add(const char *summary, const char *body, int expire_ms) {
if (n_notifs >= NOTIF_MAX) { if (n_notifs >= NOTIF_MAX) {
/* evict oldest */ /* evict oldest */
@@ -145,6 +166,7 @@ static unsigned int notif_add(const char *summary, const char *body, int expire_
clock_gettime(CLOCK_MONOTONIC, &n->show_time); clock_gettime(CLOCK_MONOTONIC, &n->show_time);
n->active = 1; n->active = 1;
n_notifs++; n_notifs++;
notif_log(summary, body);
int nx = notif_x(); int nx = notif_x();
int ny = notif_y(n_notifs - 1); int ny = notif_y(n_notifs - 1);
+22
View File
@@ -0,0 +1,22 @@
CC = gcc
PKG_C := $(shell pkg-config --cflags x11 libpng 2>/dev/null)
PKG_L := $(shell pkg-config --libs x11 libpng 2>/dev/null)
ifeq ($(PKG_C),)
$(error "Required: libx11-dev libpng-dev")
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_C)
LDFLAGS = $(PKG_L)
BIN = orionshot
.PHONY: all clean install
all: $(BIN)
$(BIN): orionshot.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
install: $(BIN)
install -Dm755 $(BIN) /usr/local/bin/$(BIN)
clean:
rm -f $(BIN)
+229
View File
@@ -0,0 +1,229 @@
/*
* orionshot — X11 screenshot tool for OrionOS
* Usage: orionshot full full-screen PNG capture
* orionshot region interactive rubber-band region select
*
* Saves to ~/Pictures/Screenshots/orionos_YYYYMMDD_HHMMSS.png
* Sends a desktop notification on success.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#include <png.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif
/* ── PNG writer ──────────────────────────────────────────────────────── */
static int get_shift(unsigned long mask) {
if (!mask) return 0;
int s = 0;
while (!(mask & 1)) { mask >>= 1; s++; }
return s;
}
static int write_png(const char *path, XImage *img, int ox, int oy, int w, int h) {
FILE *fp = fopen(path, "wb");
if (!fp) { perror("orionshot: fopen"); return -1; }
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) { fclose(fp); return -1; }
png_infop info = png_create_info_struct(png);
if (!info) { png_destroy_write_struct(&png, NULL); fclose(fp); return -1; }
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
fclose(fp);
return -1;
}
png_init_io(png, fp);
png_set_IHDR(png, info, (unsigned)w, (unsigned)h, 8,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
int r_shift = get_shift(img->red_mask);
int g_shift = get_shift(img->green_mask);
int b_shift = get_shift(img->blue_mask);
int r_scale = (img->red_mask >> r_shift) < 0xFF ? 2 : 1;
int g_scale = (img->green_mask >> g_shift) < 0xFF ? 2 : 1;
int b_scale = (img->blue_mask >> b_shift) < 0xFF ? 2 : 1;
(void)r_scale; (void)g_scale; (void)b_scale;
unsigned char *row = malloc((size_t)w * 3);
if (!row) { png_destroy_write_struct(&png, &info); fclose(fp); return -1; }
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
unsigned long px = XGetPixel(img, ox + i, oy + j);
row[i*3+0] = (unsigned char)((px & img->red_mask) >> r_shift);
row[i*3+1] = (unsigned char)((px & img->green_mask) >> g_shift);
row[i*3+2] = (unsigned char)((px & img->blue_mask) >> b_shift);
}
png_write_row(png, row);
}
free(row);
png_write_end(png, NULL);
png_destroy_write_struct(&png, &info);
fclose(fp);
return 0;
}
/* ── output path ─────────────────────────────────────────────────────── */
static void make_save_path(char *buf, size_t bufsz) {
const char *home = getenv("HOME");
if (!home) home = "/tmp";
char dir[512];
snprintf(dir, sizeof(dir), "%s/Pictures/Screenshots", home);
mkdir(dir, 0755);
time_t t = time(NULL);
struct tm *tm = localtime(&t);
char ts[32];
strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", tm);
snprintf(buf, bufsz, "%s/orionos_%s.png", dir, ts);
}
/* ── region selection ────────────────────────────────────────────────── */
static int select_region(Display *dpy, Window root,
int *rx, int *ry, int *rw, int *rh) {
Cursor cross = XCreateFontCursor(dpy, XC_crosshair);
if (XGrabPointer(dpy, root, False,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync,
root, cross, CurrentTime) != GrabSuccess) {
XFreeCursor(dpy, cross);
return -1;
}
XGrabKeyboard(dpy, root, False, GrabModeAsync, GrabModeAsync, CurrentTime);
GC gc;
XGCValues gcv;
gcv.function = GXxor;
gcv.foreground = 0xFFFFFF;
gcv.subwindow_mode = IncludeInferiors;
gcv.line_width = 1;
gc = XCreateGC(dpy, root,
GCFunction | GCForeground | GCSubwindowMode | GCLineWidth, &gcv);
int sx = 0, sy = 0, ex = 0, ey = 0;
int started = 0, drawn = 0, done = 0, cancelled = 0;
while (!done) {
XEvent e;
XNextEvent(dpy, &e);
if (e.type == ButtonPress && e.xbutton.button == Button1) {
sx = e.xbutton.x_root; sy = e.xbutton.y_root;
ex = sx; ey = sy; started = 1;
} else if (e.type == MotionNotify && started) {
/* erase previous rubber band */
if (drawn) {
int dx = MIN(sx,ex), dy = MIN(sy,ey);
int dw = abs(ex-sx), dh = abs(ey-sy);
if (dw > 0 && dh > 0)
XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh);
XFlush(dpy); drawn = 0;
}
ex = e.xmotion.x_root; ey = e.xmotion.y_root;
{
int dx = MIN(sx,ex), dy = MIN(sy,ey);
int dw = abs(ex-sx), dh = abs(ey-sy);
if (dw > 0 && dh > 0) {
XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh);
XFlush(dpy); drawn = 1;
}
}
} else if (e.type == ButtonRelease && e.xbutton.button == Button1) {
if (drawn) {
int dx = MIN(sx,ex), dy = MIN(sy,ey);
int dw = abs(ex-sx), dh = abs(ey-sy);
if (dw > 0 && dh > 0)
XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh);
XFlush(dpy);
}
ex = e.xbutton.x_root; ey = e.xbutton.y_root;
done = 1;
} else if (e.type == KeyPress) {
cancelled = 1; done = 1;
}
}
XUngrabPointer(dpy, CurrentTime);
XUngrabKeyboard(dpy, CurrentTime);
XFreeGC(dpy, gc);
XFreeCursor(dpy, cross);
XSync(dpy, False);
if (cancelled) return -1;
*rx = MIN(sx,ex); *ry = MIN(sy,ey);
*rw = abs(ex-sx); *rh = abs(ey-sy);
if (*rw < 2 || *rh < 2) return -1;
return 0;
}
/* ── main ────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
int do_region = (argc > 1 && strcmp(argv[1], "region") == 0);
Display *dpy = XOpenDisplay(NULL);
if (!dpy) { fprintf(stderr, "orionshot: cannot open display\n"); return 1; }
int screen = DefaultScreen(dpy);
Window root = RootWindow(dpy, screen);
int sw = DisplayWidth(dpy, screen);
int sh = DisplayHeight(dpy, screen);
int cx = 0, cy = 0, cw = sw, ch = sh;
if (do_region) {
if (select_region(dpy, root, &cx, &cy, &cw, &ch) < 0) {
XCloseDisplay(dpy);
return 0; /* cancelled — silently exit */
}
/* tiny pause so rubber-band XOR erasure is fully flushed */
usleep(60000);
}
XImage *img = XGetImage(dpy, root, cx, cy, (unsigned)cw, (unsigned)ch,
AllPlanes, ZPixmap);
XCloseDisplay(dpy);
if (!img) { fprintf(stderr, "orionshot: XGetImage failed\n"); return 1; }
char path[1024];
make_save_path(path, sizeof(path));
if (write_png(path, img, 0, 0, cw, ch) < 0) {
XDestroyImage(img);
fprintf(stderr, "orionshot: failed to write %s\n", path);
return 1;
}
XDestroyImage(img);
/* extract just the filename for the notification body */
const char *fname = strrchr(path, '/');
fname = fname ? fname + 1 : path;
char notif[512];
snprintf(notif, sizeof(notif),
"notify-send -t 3000 'Screenshot' '%s' 2>/dev/null &", fname);
system(notif);
fprintf(stderr, "orionshot: saved %s\n", path);
return 0;
}
+3 -3
View File
@@ -4,13 +4,13 @@
@define-color bg_dark #161C2A; @define-color bg_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;
+16
View File
@@ -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"
+4
View File
@@ -5,12 +5,15 @@
#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 (macOS dark mode) ───────────────────────────────────────── */ /* ── palette (macOS dark mode) ───────────────────────────────────────── */
#define COLOR_TITLE_ACTIVE 0x3C3C3C /* macOS dark titlebar */ #define COLOR_TITLE_ACTIVE 0x3C3C3C /* macOS dark titlebar */
@@ -113,6 +116,7 @@ typedef struct WM {
Client *focused; Client *focused;
int cur_desktop; /* active virtual desktop (0-based) */ 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;
+36 -5
View File
@@ -392,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);
+89
View File
@@ -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;
@@ -115,6 +177,17 @@ void wm_init(WM *wm) {
{ XK_3, Mod4Mask | ShiftMask }, { XK_3, Mod4Mask | ShiftMask },
{ XK_4, 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);
@@ -138,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;
@@ -161,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) {