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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user