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

408 lines
15 KiB
C

/*
* 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;
}