Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add INCR support #236

Merged
merged 5 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 202 additions & 33 deletions src/clipmenud.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ static Window win;
static int enabled = 1;
static int sig_fd;

static Atom incr_atom;
static struct incr_transfer *it_list;

static struct cm_selections sels[CM_SEL_MAX];

/**
Expand Down Expand Up @@ -69,11 +72,20 @@ static char *get_clipboard_text(Atom clip_atom) {
int actual_format;
unsigned long nitems, bytes_after;

int res =
XGetWindowProperty(dpy, DefaultRootWindow(dpy), clip_atom, 0L, (~0L),
False, AnyPropertyType, &actual_type, &actual_format,
&nitems, &bytes_after, &cur_text);
return res == Success ? (char *)cur_text : NULL;
int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False,
AnyPropertyType, &actual_type, &actual_format,
&nitems, &bytes_after, &cur_text);
if (res != Success) {
return NULL;
}

if (actual_type == incr_atom) {
dbg("Unexpected INCR transfer detected\n");
XFree(cur_text);
return NULL;
}

return (char *)cur_text;
}

/**
Expand Down Expand Up @@ -150,14 +162,19 @@ static void handle_signalfd_event(void) {
* desired property type.
*/
static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) {
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;
}

enum selection_type sel =
selection_atom_to_selection_type(se->selection, sels);
dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n",
cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner);
XConvertSelection(dpy, se->selection,
Expand All @@ -177,6 +194,10 @@ 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;
Expand Down Expand Up @@ -231,45 +252,179 @@ static uint64_t store_clip(char *text) {
}

/**
* Something changed in our clip storage atoms. Work out whether we want to
* store the new content as a clipboard entry.
* Process the final data collected during an INCR transfer.
*/
static int handle_property_notify(const XPropertyEvent *pe) {
bool found = false;
for (size_t i = 0; i < CM_SEL_MAX; ++i) {
if (sels[i].storage == pe->atom) {
found = true;
break;
}
}
if (!found || pe->state != PropertyNewValue) {
return -EINVAL;
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;
}

dbg("Received notification that selection conversion is ready\n");
char *text = get_clipboard_text(pe->atom);
it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size);
_drop_(free) char *text = malloc(it->data_size + 1);
expect(text);
memcpy(text, it->data, it->data_size);
text[it->data_size] = '\0';

char line[CS_SNIP_LINE_SIZE];
first_line(text, line);
dbg("First line: %s\n", line);
it_dbg(it, "First line: %s\n", line);

if (is_salient_text(text)) {
uint64_t hash = store_clip(text);
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
*/
enum selection_type sel =
storage_atom_to_selection_type(pe->atom, sels);
if (cfg.owned_selections[sel].active && cfg.own_clipboard) {
run_clipserve(hash);
}
} else {
dbg("Clipboard text is whitespace only, ignoring\n");
XFree(text);
it_dbg(it, "Clipboard text is whitespace only, ignoring\n");
}

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.
char *text = get_clipboard_text(pe->atom);
if (!text) {
dbg("Failed to get clipboard text\n");
return -EINVAL;
}
char line[CS_SNIP_LINE_SIZE];
first_line(text, line);
dbg("First line: %s\n", line);

if (is_salient_text(text)) {
uint64_t hash = store_clip(text);
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.owned_selections[sel].active && cfg.own_clipboard) {
run_clipserve(hash);
}
} else {
dbg("Clipboard text is whitespace only, ignoring\n");
XFree(text);
}
}

return 0;
Expand Down Expand Up @@ -323,6 +478,18 @@ static int handle_x11_event(int evt_base) {
/**
* 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) {
Expand Down Expand Up @@ -407,6 +574,8 @@ int main(int argc, char *argv[]) {
win = DefaultRootWindow(dpy);
setup_selections(dpy, sels);

incr_atom = XInternAtom(dpy, "INCR", False);

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);
Expand Down
Loading
Loading