git-subtree-dir: clipmenu git-subtree-split: e0ec3bc6608ecc70ed17e8da76e27bf90b1e8e90
765 lines
23 KiB
C
765 lines
23 KiB
C
#include <X11/Xatom.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/extensions/Xfixes.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <inttypes.h>
|
|
#include <regex.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/file.h>
|
|
#include <sys/select.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "config.h"
|
|
#include "store.h"
|
|
#include "util.h"
|
|
#include "x.h"
|
|
|
|
static Display *dpy;
|
|
static struct clip_store cs;
|
|
static struct config cfg;
|
|
static Window win;
|
|
|
|
static int enabled = 1;
|
|
static int sig_pipe[2];
|
|
|
|
static Atom timestamp_atom;
|
|
static Atom incr_atom;
|
|
static struct incr_transfer *it_list;
|
|
|
|
static struct cm_selections sels[CM_SEL_MAX];
|
|
|
|
static Time last_disable_time = 0;
|
|
static Time last_enable_time = 0;
|
|
|
|
static bool has_owned_selections(void) {
|
|
for (size_t i = 0; i < CM_SEL_MAX; i++) {
|
|
if (cfg.owned_selections[i].active) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
enum clip_text_source {
|
|
CLIP_TEXT_SOURCE_X,
|
|
CLIP_TEXT_SOURCE_MALLOC,
|
|
CLIP_TEXT_SOURCE_INVALID
|
|
};
|
|
|
|
struct clip_text {
|
|
char *data;
|
|
enum clip_text_source source;
|
|
};
|
|
|
|
static struct clip_text last_text[CM_SEL_MAX] = {
|
|
{NULL, CLIP_TEXT_SOURCE_MALLOC},
|
|
{NULL, CLIP_TEXT_SOURCE_MALLOC},
|
|
{NULL, CLIP_TEXT_SOURCE_MALLOC},
|
|
};
|
|
static struct timespec last_text_time[CM_SEL_MAX];
|
|
|
|
static void free_clip_text(struct clip_text *ct) {
|
|
expect(ct->source != CLIP_TEXT_SOURCE_INVALID);
|
|
|
|
if (ct->data) {
|
|
if (ct->source == CLIP_TEXT_SOURCE_X) {
|
|
XFree(ct->data);
|
|
} else {
|
|
free(ct->data);
|
|
}
|
|
ct->data = NULL;
|
|
}
|
|
|
|
ct->source = CLIP_TEXT_SOURCE_INVALID;
|
|
}
|
|
|
|
static struct timespec get_monotonic_time(void) {
|
|
struct timespec ts;
|
|
expect(clock_gettime(CLOCK_MONOTONIC, &ts) == 0);
|
|
return ts;
|
|
}
|
|
|
|
static bool within_partial_merge_window(struct timespec current_time,
|
|
struct timespec last_time) {
|
|
time_t elapsed_secs = current_time.tv_sec - last_time.tv_sec;
|
|
|
|
if (elapsed_secs < cfg.partial_merge_secs) {
|
|
return true;
|
|
}
|
|
if (elapsed_secs > cfg.partial_merge_secs) {
|
|
return false;
|
|
}
|
|
return current_time.tv_nsec <= last_time.tv_nsec;
|
|
}
|
|
|
|
// cppcheck-suppress [constParameterCallback,unmatchedSuppression]
|
|
static Bool is_timestamp_event(Display *display _unused_, XEvent *event,
|
|
XPointer arg) {
|
|
const void *argp = arg;
|
|
return event->type == PropertyNotify &&
|
|
event->xproperty.atom == *(const Atom *)argp;
|
|
}
|
|
|
|
/**
|
|
* Get the current X server time by triggering a PropertyNotify.
|
|
*/
|
|
static Time get_current_server_time(void) {
|
|
XEvent ev;
|
|
XChangeProperty(dpy, win, timestamp_atom, XA_INTEGER, 32, PropModeReplace,
|
|
NULL, 0);
|
|
XIfEvent(dpy, &ev, is_timestamp_event, (XPointer)×tamp_atom);
|
|
return ev.xproperty.time;
|
|
}
|
|
|
|
/**
|
|
* Check if a text s1 is a possible partial of s2.
|
|
*
|
|
* Chromium and some other badly behaved applications spam PRIMARY during
|
|
* selection, so if you're selecting the text "abc", you get three clips: "a",
|
|
* "ab", and "abc" (or "c", "bc", "abc" if selecting right to left). Attempt to
|
|
* detect these. It's possible we were not fast enough to get all of them, so
|
|
* unfortunately we can't check for strlen(s1)+1 either. It's also possible the
|
|
* user first expands, and then retracts the selection, so we need to handle
|
|
* that too.
|
|
*/
|
|
static bool is_possible_partial(const char *s1, const char *s2) {
|
|
size_t len1 = strlen(s1), len2 = strlen(s2);
|
|
|
|
// Is one a prefix of the other?
|
|
if (strncmp(s1, s2, len1 < len2 ? len1 : len2) == 0) {
|
|
return true;
|
|
}
|
|
|
|
// Is one a suffix of the other?
|
|
if (len1 < len2) {
|
|
return strcmp(s1, s2 + len2 - len1) == 0;
|
|
} else {
|
|
return strcmp(s2, s1 + len1 - len2) == 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the converted text put into our clip atom. In order for this to
|
|
* happen a conversion must have been performed in an earlier iteration with
|
|
* XConvertSelection.
|
|
*/
|
|
static struct clip_text get_clipboard_text(Atom clip_atom) {
|
|
struct clip_text ct = {NULL, CLIP_TEXT_SOURCE_X};
|
|
unsigned char *cur_text;
|
|
Atom actual_type;
|
|
int actual_format;
|
|
unsigned long nitems, bytes_after;
|
|
|
|
int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False,
|
|
AnyPropertyType, &actual_type, &actual_format,
|
|
&nitems, &bytes_after, &cur_text);
|
|
if (res != Success) {
|
|
return ct;
|
|
}
|
|
|
|
if (actual_type == incr_atom) {
|
|
dbg("Unexpected INCR transfer detected\n");
|
|
XFree(cur_text);
|
|
return ct;
|
|
}
|
|
|
|
ct.data = (char *)cur_text;
|
|
|
|
return ct;
|
|
}
|
|
|
|
/**
|
|
* Return true if the given string contains any non-whitespace characters.
|
|
*/
|
|
static bool is_salient_text(const char *str) {
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
|
|
for (; *str; str++) {
|
|
if (!isspace((unsigned char)*str)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Write the current enabled status to a designated status file.
|
|
*/
|
|
static void write_status(void) {
|
|
_drop_(close) int fd =
|
|
open(get_enabled_path(&cfg), O_WRONLY | O_CREAT, 0600);
|
|
die_on(fd < 0, "Failed to update status: %s\n", strerror(errno));
|
|
dprintf(fd, "%d", (int)enabled);
|
|
}
|
|
|
|
/**
|
|
* Return true if the given window title matches the title of the clipserve
|
|
* window.
|
|
*/
|
|
static bool is_clipserve(const char *win_title) {
|
|
return win_title && streq(win_title, "clipserve");
|
|
}
|
|
|
|
/**
|
|
* Determine if a window with the given title should be ignored based on user
|
|
* configuration.
|
|
*/
|
|
static bool is_ignored_window(char *win_title) {
|
|
if (!win_title || !cfg.ignore_window.set) {
|
|
return 0;
|
|
}
|
|
int ret = regexec(&cfg.ignore_window.rgx, win_title, 0, NULL, 0);
|
|
expect(ret == 0 || ret == REG_NOMATCH);
|
|
return !ret;
|
|
}
|
|
|
|
/**
|
|
* Disable or enable clip collection based on received signals.
|
|
*/
|
|
static void handle_signal_event(int signo) {
|
|
dbg("Got signal %d\n", signo);
|
|
switch (signo) {
|
|
case SIGUSR1:
|
|
// If we're already disabled, we need to keep the original
|
|
// timestamp to properly filter all events that were queued during
|
|
// any part of the disabled period.
|
|
if (enabled) {
|
|
last_disable_time = get_current_server_time();
|
|
}
|
|
enabled = 0;
|
|
dbg("Clipboard collection disabled by signal at time %lu\n",
|
|
(unsigned long)last_disable_time);
|
|
break;
|
|
case SIGUSR2:
|
|
// If we're already enabled, we need to keep the original timestamp
|
|
// so we don't mistakenly filter out valid messages.
|
|
if (!enabled) {
|
|
last_enable_time = get_current_server_time();
|
|
}
|
|
enabled = 1;
|
|
dbg("Clipboard collection enabled by signal at time %lu\n",
|
|
(unsigned long)last_enable_time);
|
|
break;
|
|
}
|
|
write_status();
|
|
}
|
|
|
|
static void handle_signal_pipe(void) {
|
|
unsigned char sigs[32];
|
|
ssize_t read_sz;
|
|
while ((read_sz = read(sig_pipe[0], sigs, sizeof(sigs))) > 0) {
|
|
for (ssize_t i = 0; i < read_sz; i++) {
|
|
handle_signal_event((int)sigs[i]);
|
|
}
|
|
}
|
|
if (read_sz < 0) {
|
|
expect(errno == EAGAIN || errno == EINTR);
|
|
}
|
|
}
|
|
|
|
static void signal_handler(int signo) {
|
|
int saved_errno = errno;
|
|
unsigned char sig = (unsigned char)signo;
|
|
ssize_t written = write(sig_pipe[1], &sig, sizeof(sig));
|
|
(void)written;
|
|
errno = saved_errno;
|
|
}
|
|
|
|
static void set_fd_flags(int fd) {
|
|
int flags = fcntl(fd, F_GETFL);
|
|
expect(flags >= 0);
|
|
expect(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
|
|
|
|
int fd_flags = fcntl(fd, F_GETFD);
|
|
expect(fd_flags >= 0);
|
|
expect(fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == 0);
|
|
}
|
|
|
|
/**
|
|
* Something changed about the watched selection, consider converting it to our
|
|
* desired property type.
|
|
*/
|
|
static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) {
|
|
if (last_disable_time > 0 && se->timestamp >= last_disable_time &&
|
|
se->timestamp < last_enable_time) {
|
|
dbg("Ignoring selection event from disabled period (event time: %lu, disabled: %lu, enabled: %lu)\n",
|
|
(unsigned long)se->timestamp, (unsigned long)last_disable_time,
|
|
(unsigned long)last_enable_time);
|
|
return;
|
|
}
|
|
|
|
enum selection_type sel =
|
|
selection_atom_to_selection_type(se->selection, sels);
|
|
if (sel == CM_SEL_INVALID) {
|
|
dbg("Received XFixesSelectionNotify for unknown sel\n");
|
|
return;
|
|
}
|
|
|
|
_drop_(XFree) char *win_title = get_window_title(dpy, se->owner);
|
|
if (is_clipserve(win_title) || is_ignored_window(win_title)) {
|
|
dbg("Ignoring clip from window titled '%s'\n", win_title);
|
|
return;
|
|
}
|
|
|
|
dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n",
|
|
cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner);
|
|
|
|
struct incr_transfer *it = it_list;
|
|
while (it) {
|
|
struct incr_transfer *next = it->next;
|
|
if (it->property == sels[sel].storage) {
|
|
it_dbg(it, "Cleaning up stale INCR transfer\n");
|
|
free(it->data);
|
|
it_remove(&it_list, it);
|
|
free(it);
|
|
}
|
|
it = next;
|
|
}
|
|
|
|
XConvertSelection(dpy, se->selection,
|
|
XInternAtom(dpy, "UTF8_STRING", False), sels[sel].storage,
|
|
win, CurrentTime);
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Something changed about the watched selection, but we don't explicitly
|
|
* listen for SelectionNotify, so in reality this only happens in response to
|
|
* an explicit request to tell us that there is no owner. In that case, return
|
|
* -ENOENT.
|
|
*/
|
|
static int handle_selection_notify(const XSelectionEvent *se) {
|
|
if (se->property == None) {
|
|
enum selection_type sel =
|
|
selection_atom_to_selection_type(se->selection, sels);
|
|
if (sel == CM_SEL_INVALID) {
|
|
dbg("Received no owner notification for unknown sel\n");
|
|
return 0;
|
|
}
|
|
dbg("X reports that %s has no current owner\n",
|
|
cfg.selections[sel].name);
|
|
return -ENOENT;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Trims the clip store if the number of clips exceeds the configured batch
|
|
* size.
|
|
*/
|
|
static void maybe_trim(void) {
|
|
size_t cur_clips;
|
|
expect(cs_len(&cs, &cur_clips) == 0);
|
|
if (cur_clips > (size_t)cfg.max_clips + (size_t)cfg.max_clips_batch) {
|
|
expect(cs_trim(&cs, CS_ITER_NEWEST_FIRST, (size_t)cfg.max_clips) == 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store the clipboard text for a selection. If the text is a possible partial
|
|
* of the last clip on the same selection and it was received shortly
|
|
* afterwards, replace instead of adding.
|
|
*/
|
|
static uint64_t store_clip(enum selection_type sel, struct clip_text *ct) {
|
|
dbg("Clipboard text is considered salient, storing\n");
|
|
struct timespec current_time = get_monotonic_time();
|
|
uint64_t hash;
|
|
|
|
if (cfg.partial_merge_secs > 0 && last_text[sel].data &&
|
|
within_partial_merge_window(current_time, last_text_time[sel]) &&
|
|
is_possible_partial(last_text[sel].data, ct->data)) {
|
|
dbg("Possible partial of last clip on %s, replacing\n",
|
|
cfg.selections[sel].name);
|
|
expect(cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, ct->data, &hash) == 0);
|
|
} else {
|
|
expect(cs_add(&cs, ct->data, &hash,
|
|
cfg.deduplicate ? CS_DUPE_KEEP_LAST : CS_DUPE_KEEP_ALL) ==
|
|
0);
|
|
}
|
|
|
|
free_clip_text(&last_text[sel]);
|
|
last_text[sel] = *ct;
|
|
last_text_time[sel] = current_time;
|
|
|
|
// The caller no longer owns this data.
|
|
ct->data = NULL;
|
|
ct->source = CLIP_TEXT_SOURCE_INVALID;
|
|
|
|
return hash;
|
|
}
|
|
|
|
/**
|
|
* Process the final data collected during an INCR transfer.
|
|
*/
|
|
static void incr_receive_finish(struct incr_transfer *it) {
|
|
enum selection_type sel =
|
|
storage_atom_to_selection_type(it->property, sels);
|
|
if (sel == CM_SEL_INVALID) {
|
|
it_dbg(it, "Received INCR finish for unknown sel\n");
|
|
return;
|
|
}
|
|
|
|
it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size);
|
|
char *text = malloc(it->data_size + 1);
|
|
expect(text);
|
|
memcpy(text, it->data, it->data_size);
|
|
text[it->data_size] = '\0';
|
|
|
|
struct clip_text ct = {text, CLIP_TEXT_SOURCE_MALLOC};
|
|
|
|
char line[CS_SNIP_LINE_SIZE];
|
|
first_line(ct.data, line);
|
|
it_dbg(it, "First line: %s\n", line);
|
|
|
|
if (is_salient_text(ct.data)) {
|
|
uint64_t hash = store_clip(sel, &ct);
|
|
maybe_trim();
|
|
if (cfg.own_clipboard && has_owned_selections()) {
|
|
run_clipserve(hash, cfg.owned_selections);
|
|
}
|
|
} else {
|
|
it_dbg(it, "Clipboard text is whitespace only, ignoring\n");
|
|
free_clip_text(&ct);
|
|
}
|
|
|
|
free(it->data);
|
|
it_remove(&it_list, it);
|
|
free(it);
|
|
}
|
|
|
|
#define INCR_DATA_START_BYTES 1024 * 1024
|
|
|
|
/**
|
|
* Acknowledge and start an INCR transfer.
|
|
*/
|
|
static void incr_receive_start(const XPropertyEvent *pe) {
|
|
struct incr_transfer *it = malloc(sizeof(struct incr_transfer));
|
|
expect(it);
|
|
*it = (struct incr_transfer){
|
|
.property = pe->atom,
|
|
.requestor = pe->window,
|
|
.data_size = 0,
|
|
.data_capacity = INCR_DATA_START_BYTES,
|
|
.data = malloc(INCR_DATA_START_BYTES),
|
|
};
|
|
expect(it->data);
|
|
|
|
it_dbg(it, "Starting transfer\n");
|
|
it_add(&it_list, it);
|
|
|
|
// Signal readiness for chunks
|
|
XDeleteProperty(dpy, win, pe->atom);
|
|
}
|
|
|
|
/**
|
|
* Continue receiving data during an INCR transfer.
|
|
*/
|
|
static void incr_receive_data(const XPropertyEvent *pe,
|
|
struct incr_transfer *it) {
|
|
if (pe->state != PropertyNewValue) {
|
|
return;
|
|
}
|
|
|
|
it_dbg(it, "Receiving chunk (bytes buffered: %zu)\n", it->data_size);
|
|
|
|
_drop_(XFree) unsigned char *chunk = NULL;
|
|
Atom actual_type;
|
|
int actual_format;
|
|
unsigned long nitems, bytes_after;
|
|
XGetWindowProperty(dpy, win, pe->atom, 0, LONG_MAX, False, AnyPropertyType,
|
|
&actual_type, &actual_format, &nitems, &bytes_after,
|
|
&chunk);
|
|
|
|
size_t chunk_size = nitems * (actual_format / 8);
|
|
|
|
if (chunk_size == 0) {
|
|
it_dbg(it, "Transfer complete\n");
|
|
incr_receive_finish(it);
|
|
return;
|
|
}
|
|
|
|
if (it->data_size + chunk_size > it->data_capacity) {
|
|
it->data_capacity = (it->data_size + chunk_size) * 2;
|
|
it->data = realloc(it->data, it->data_capacity);
|
|
expect(it->data);
|
|
it_dbg(it, "Expanded data buffer to %zu bytes\n", it->data_capacity);
|
|
}
|
|
|
|
memcpy(it->data + it->data_size, chunk, chunk_size);
|
|
it->data_size += chunk_size;
|
|
|
|
// Signal readiness for next chunk
|
|
XDeleteProperty(dpy, win, pe->atom);
|
|
}
|
|
|
|
/**
|
|
* Something changed in our clip storage atoms. Work out whether we want to
|
|
* store the new content as a clipboard entry.
|
|
*/
|
|
static int handle_property_notify(const XPropertyEvent *pe) {
|
|
if (pe->state != PropertyNewValue && pe->state != PropertyDelete) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
enum selection_type sel = storage_atom_to_selection_type(pe->atom, sels);
|
|
if (sel == CM_SEL_INVALID) {
|
|
dbg("Received PropertyNotify for unknown sel\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
// Check if this property corresponds to an INCR transfer in progress
|
|
struct incr_transfer *it = it_list;
|
|
while (it) {
|
|
if (it->property == pe->atom && it->requestor == pe->window) {
|
|
break;
|
|
}
|
|
it = it->next;
|
|
}
|
|
|
|
if (it) {
|
|
incr_receive_data(pe, it);
|
|
return 0;
|
|
}
|
|
|
|
// Not an INCR transfer in progress. Check if this is an INCR transfer
|
|
// starting
|
|
Atom actual_type;
|
|
int actual_format;
|
|
unsigned long nitems, bytes_after;
|
|
_drop_(XFree) unsigned char *prop = NULL;
|
|
|
|
XGetWindowProperty(dpy, win, pe->atom, 0, 0, False, AnyPropertyType,
|
|
&actual_type, &actual_format, &nitems, &bytes_after,
|
|
&prop);
|
|
|
|
if (actual_type == incr_atom) {
|
|
incr_receive_start(pe);
|
|
} else {
|
|
dbg("Received non-INCR PropertyNotify\n");
|
|
|
|
// store_clip will take care of freeing this later when it's gone from
|
|
// last_text.
|
|
struct clip_text ct = get_clipboard_text(pe->atom);
|
|
if (!ct.data) {
|
|
dbg("Failed to get clipboard text\n");
|
|
return -EINVAL;
|
|
}
|
|
char line[CS_SNIP_LINE_SIZE];
|
|
first_line(ct.data, line);
|
|
dbg("First line: %s\n", line);
|
|
|
|
if (is_salient_text(ct.data)) {
|
|
uint64_t hash = store_clip(sel, &ct);
|
|
maybe_trim();
|
|
/* We only own CLIPBOARD because otherwise the behaviour is wonky:
|
|
*
|
|
* 1. When you select in a browser and press ^V, it repastes what
|
|
* you have selected instead of the previous content
|
|
* 2. urxvt and some other terminal emulators will unhilight on
|
|
* PRIMARY ownership being taken away from them
|
|
*/
|
|
if (cfg.own_clipboard && has_owned_selections()) {
|
|
run_clipserve(hash, cfg.owned_selections);
|
|
}
|
|
} else {
|
|
dbg("Clipboard text is whitespace only, ignoring\n");
|
|
free_clip_text(&ct);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Process X11 events, returning when we have either processed one clip, or
|
|
* have received an indication that the selection is not owned.
|
|
*
|
|
* The usual sequence is:
|
|
*
|
|
* 1. Get an XFixesSelectionNotify that we have a new selection.
|
|
* 2. Call XConvertSelection() on it to get a string in our prop.
|
|
* 3. Wait for a PropertyNotify that says that's ready.
|
|
* 4. When it's ready, store it, and return from the function.
|
|
*
|
|
* Another possible outcome, especially when trying to get the initial state at
|
|
* startup, is that we get a SelectionNotify even with owner == None, which
|
|
* means the selection is unowned. At that point we also return, since it's
|
|
* clear that an explicit request has been nacked.
|
|
*/
|
|
static int handle_x11_event(int evt_base) {
|
|
while (XPending(dpy)) {
|
|
XEvent evt;
|
|
XNextEvent(dpy, &evt);
|
|
|
|
if (!enabled) {
|
|
dbg("Got X event, but ignoring as collection is disabled\n");
|
|
continue;
|
|
}
|
|
|
|
int ret;
|
|
if (evt.type == evt_base + XFixesSelectionNotify) {
|
|
handle_xfixes_selection_notify((XFixesSelectionNotifyEvent *)&evt);
|
|
} else if (evt.type == PropertyNotify) {
|
|
ret = handle_property_notify((XPropertyEvent *)&evt);
|
|
if (ret == 0) {
|
|
return ret;
|
|
}
|
|
} else if (evt.type == SelectionNotify) {
|
|
ret = handle_selection_notify((XSelectionEvent *)&evt);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -EINPROGRESS;
|
|
}
|
|
|
|
/**
|
|
* Continuously wait for and process X11 or signal events until we fully
|
|
* process success or failure for a clip.
|
|
*
|
|
* The usual sequence is:
|
|
*
|
|
* 1. Get an XFixesSelectionNotify that we have a new selection.
|
|
* 2. Call XConvertSelection() on it to get a string in our prop.
|
|
* 3. Wait for a PropertyNotify that says that's ready.
|
|
* 4. When it's ready, store it, and return from the function.
|
|
*
|
|
* Another possible outcome, especially when trying to get the initial state at
|
|
* startup, is that we get a SelectionNotify even with owner == None, which
|
|
* means the selection is unowned. At that point we also return, since it's
|
|
* clear that an explicit request has been nacked.
|
|
*/
|
|
static int get_one_clip(int evt_base) {
|
|
while (1) {
|
|
// It's possible that we have more X events to process, but because of
|
|
// the way the protocol works, we won't get told about them until we
|
|
// next get an event if we wait for select(). Check for them first.
|
|
if (XPending(dpy)) {
|
|
return handle_x11_event(evt_base);
|
|
}
|
|
|
|
fd_set fds;
|
|
int x_fd = ConnectionNumber(dpy);
|
|
|
|
FD_ZERO(&fds);
|
|
FD_SET(sig_pipe[0], &fds);
|
|
FD_SET(x_fd, &fds);
|
|
|
|
int max_fd = sig_pipe[0] > x_fd ? sig_pipe[0] : x_fd;
|
|
int ret = select(max_fd + 1, &fds, NULL, NULL, NULL);
|
|
if (ret < 0) {
|
|
if (errno == EINTR) {
|
|
return 0;
|
|
}
|
|
expect(ret > 0);
|
|
}
|
|
|
|
if (FD_ISSET(sig_pipe[0], &fds)) {
|
|
handle_signal_pipe();
|
|
}
|
|
|
|
if (FD_ISSET(x_fd, &fds)) {
|
|
return handle_x11_event(evt_base);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int setup_watches(int evt_base) {
|
|
XSelectInput(dpy, win, PropertyChangeMask);
|
|
|
|
for (size_t i = 0; i < CM_SEL_MAX; i++) {
|
|
struct selection sel = cfg.selections[i];
|
|
if (!sel.active) {
|
|
continue;
|
|
}
|
|
Atom sel_atom = sels[i].selection;
|
|
XFixesSelectSelectionInput(dpy, win, sel_atom,
|
|
XFixesSetSelectionOwnerNotifyMask);
|
|
dbg("Getting initial value for selection %s\n", sel.name);
|
|
XConvertSelection(dpy, sel_atom, XInternAtom(dpy, "UTF8_STRING", False),
|
|
sels[i].storage, win, CurrentTime);
|
|
get_one_clip(evt_base);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int _noreturn_ run(int evt_base) {
|
|
while (1) {
|
|
get_one_clip(evt_base);
|
|
}
|
|
}
|
|
|
|
#ifndef UNIT_TEST
|
|
int main(int argc, char *argv[]) {
|
|
int evt_base;
|
|
|
|
cfg = setup("clipmenud");
|
|
exec_man_on_help(argc, argv);
|
|
die_on(argc != 1, "clipmenud doesn't accept any arguments\n");
|
|
|
|
_drop_(close) int session_fd =
|
|
open(get_session_lock_path(&cfg), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
|
die_on(session_fd < 0, "Failed to open session file: %s\n",
|
|
strerror(errno));
|
|
die_on(flock(session_fd, LOCK_EX | LOCK_NB) < 0,
|
|
"Failed to lock session file -- is another clipmenud running?\n");
|
|
|
|
expect(ftruncate(session_fd, 0) == 0);
|
|
expect(lseek(session_fd, 0, SEEK_SET) == 0);
|
|
dprintf(session_fd, "%ld\n", (long)getpid());
|
|
|
|
write_status();
|
|
|
|
_drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY);
|
|
_drop_(close) int snip_fd =
|
|
open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600);
|
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
|
|
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
|
|
|
die_on(!(dpy = XOpenDisplay(NULL)), "Cannot open display\n");
|
|
win = DefaultRootWindow(dpy);
|
|
setup_selections(dpy, sels);
|
|
|
|
incr_atom = XInternAtom(dpy, "INCR", False);
|
|
timestamp_atom = XInternAtom(dpy, "CLIPMENUD_TIMESTAMP", False);
|
|
|
|
expect(pipe(sig_pipe) == 0);
|
|
set_fd_flags(sig_pipe[0]);
|
|
set_fd_flags(sig_pipe[1]);
|
|
|
|
struct sigaction sa = {0};
|
|
sa.sa_handler = signal_handler;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART;
|
|
expect(sigaction(SIGUSR1, &sa, NULL) == 0);
|
|
expect(sigaction(SIGUSR2, &sa, NULL) == 0);
|
|
expect(signal(SIGCHLD, SIG_IGN) != SIG_ERR);
|
|
|
|
int unused;
|
|
die_on(!XFixesQueryExtension(dpy, &evt_base, &unused), "XFixes missing\n");
|
|
|
|
setup_watches(evt_base);
|
|
|
|
if (!cfg.oneshot) {
|
|
run(evt_base);
|
|
}
|
|
|
|
expect(cs_destroy(&cs) == 0);
|
|
config_free(&cfg);
|
|
XCloseDisplay(dpy);
|
|
return 0;
|
|
}
|
|
#endif
|