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