- 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
408 lines
15 KiB
C
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;
|
|
}
|