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:
15 files changed
+957
-84
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 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
|
||||||
@@ -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
|
||||||
+27
-52
@@ -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 \
|
||||||
|
archiso \
|
||||||
|
base-devel \
|
||||||
|
git \
|
||||||
|
libx11 \
|
||||||
|
libxft \
|
||||||
|
libxfixes \
|
||||||
|
dbus \
|
||||||
|
libpng \
|
||||||
|
feh \
|
||||||
|
picom \
|
||||||
|
libpam \
|
||||||
|
ttf-dejavu \
|
||||||
|
xorg-server \
|
||||||
|
xorg-xinit \
|
||||||
|
xterm \
|
||||||
|
notify-send-py \
|
||||||
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
# ── install build tools ────────────────────────────────────────────────
|
WORKDIR /build
|
||||||
RUN pacman -Syu --noconfirm && \
|
COPY . /build/orionos
|
||||||
pacman -S --noconfirm \
|
|
||||||
archiso \
|
|
||||||
base-devel \
|
|
||||||
libx11 \
|
|
||||||
libxft \
|
|
||||||
fontconfig \
|
|
||||||
freetype2 \
|
|
||||||
imlib2 \
|
|
||||||
imagemagick \
|
|
||||||
fakeroot \
|
|
||||||
git \
|
|
||||||
sudo \
|
|
||||||
&& pacman -Scc --noconfirm
|
|
||||||
|
|
||||||
# ── build OnlyOffice from AUR (as a non-root build user) ─────────────
|
# Build OrionOS components
|
||||||
RUN useradd -m builder && \
|
RUN cd /build/orionos && make && make install
|
||||||
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 archiso profile and build the ISO
|
||||||
RUN echo "=== archiso initcpio hooks ===" && \
|
RUN cp -r /build/orionos/iso/profile /tmp/orionos-profile
|
||||||
ls /usr/lib/initcpio/hooks/ 2>/dev/null || echo "(none found)"
|
RUN mkarchiso -v -w /tmp/iso-work -o /output /tmp/orionos-profile
|
||||||
|
|
||||||
# ── copy the entire OrionOS source tree ──────────────────────────────
|
CMD ["/bin/bash"]
|
||||||
WORKDIR /orionos
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# ── build the OrionOS binaries inside the container ───────────────────
|
|
||||||
# pkg-config finds X11/Xft headers automatically on Arch — no overrides needed
|
|
||||||
RUN make
|
|
||||||
|
|
||||||
# ── generate the default wallpaper ───────────────────────────────────
|
|
||||||
RUN bash themes/wallpapers/generate-default.sh || true
|
|
||||||
|
|
||||||
# ── run the ISO build script ──────────────────────────────────────────
|
|
||||||
# --privileged is required for loop device mounts inside the container
|
|
||||||
ENTRYPOINT ["/orionos/iso/docker/entrypoint.sh"]
|
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+20
-4
@@ -2,10 +2,11 @@
|
|||||||
# orionwallpaper — set or restore the OrionOS desktop wallpaper
|
# orionwallpaper — set or restore the OrionOS desktop wallpaper
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# orionwallpaper # restore saved wallpaper (called from xinitrc)
|
# orionwallpaper # restore saved wallpaper (called from xinitrc)
|
||||||
# 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"
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
+105
-16
@@ -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,28 +211,44 @@ 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 (;;) {
|
||||||
XNextEvent(wm->dpy, &e);
|
/* drain all pending X events */
|
||||||
switch (e.type) {
|
while (XPending(wm->dpy)) {
|
||||||
case MapRequest: on_map_request(wm, &e.xmaprequest); break;
|
XNextEvent(wm->dpy, &e);
|
||||||
case UnmapNotify: on_unmap_notify(wm, &e.xunmap); break;
|
switch (e.type) {
|
||||||
case DestroyNotify: on_destroy_notify(wm, &e.xdestroywindow); break;
|
case MapRequest: on_map_request(wm, &e.xmaprequest); break;
|
||||||
case ConfigureRequest: on_configure_request(wm, &e.xconfigurerequest); break;
|
case UnmapNotify: on_unmap_notify(wm, &e.xunmap); break;
|
||||||
case Expose: on_expose(wm, &e.xexpose); break;
|
case DestroyNotify: on_destroy_notify(wm, &e.xdestroywindow); break;
|
||||||
case ButtonPress: on_button_press(wm, &e.xbutton); break;
|
case ConfigureRequest: on_configure_request(wm, &e.xconfigurerequest); break;
|
||||||
case ButtonRelease: on_button_release(wm, &e.xbutton); break;
|
case Expose: on_expose(wm, &e.xexpose); break;
|
||||||
case MotionNotify: on_motion_notify(wm, &e.xmotion); break;
|
case ButtonPress: on_button_press(wm, &e.xbutton); break;
|
||||||
case KeyPress: on_key_press(wm, &e.xkey); break;
|
case ButtonRelease: on_button_release(wm, &e.xbutton); break;
|
||||||
case KeyRelease: on_key_release(wm, &e.xkey); break;
|
case MotionNotify: on_motion_notify(wm, &e.xmotion); break;
|
||||||
case EnterNotify: on_enter_notify(wm, &e.xcrossing); break;
|
case KeyPress: on_key_press(wm, &e.xkey); break;
|
||||||
case ClientMessage: on_client_message(wm, &e.xclient); break;
|
case KeyRelease: on_key_release(wm, &e.xkey); break;
|
||||||
case PropertyNotify: on_property_notify(wm, &e.xproperty); break;
|
case EnterNotify: on_enter_notify(wm, &e.xcrossing); break;
|
||||||
default: break;
|
case ClientMessage: on_client_message(wm, &e.xclient); break;
|
||||||
|
case PropertyNotify: on_property_notify(wm, &e.xproperty); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* poll hot corners every 200ms */
|
||||||
|
hot_corner_check(wm);
|
||||||
|
|
||||||
|
fd_set fds;
|
||||||
|
FD_ZERO(&fds);
|
||||||
|
FD_SET(xfd, &fds);
|
||||||
|
struct timeval tv = { 0, 200000 };
|
||||||
|
select(xfd + 1, &fds, NULL, NULL, &tv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user