#include #include #include #include #include #include #include #include struct selection { const char * name; uint32_t len; char * data; xcb_atom_t atom; }; enum Status { STATUS_TARGETS, STATUS_SELECTION, STATUS_DONE, }; int get_selection_index(const char *); xcb_window_t find_owner(int); void update_selection(int); xcb_atom_t get_atom(xcb_connection_t *, const char *); int clip_getattr(const char *, struct stat *, struct fuse_file_info *); int clip_readdir(const char *, void *, fuse_fill_dir_t, off_t, struct fuse_file_info *, enum fuse_readdir_flags); int clip_open(const char *, struct fuse_file_info *); int clip_read(const char *, char *, size_t, off_t, struct fuse_file_info *); const uint32_t mask = XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; const uint32_t values[] = {1, XCB_EVENT_MASK_PROPERTY_CHANGE}; const struct fuse_operations clip_ops = { .getattr = clip_getattr, .readdir = clip_readdir, .open = clip_open, .read = clip_read, }; static xcb_connection_t * c; static xcb_screen_t * s; static xcb_window_t w; static struct selection x[] = { {.name = "primary", .len = 0, .data = NULL}, {.name = "secondary", .len = 0, .data = NULL}, {.name = "clipboard", .len = 0, .data = NULL}, }; int main(int argc, char ** argv) { c = xcb_connect(NULL, NULL); s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; w = xcb_generate_id(c); xcb_create_window( c, XCB_COPY_FROM_PARENT, w, s->root, 0, 0, 1, 1, 0, XCB_COPY_FROM_PARENT, s->root_visual, mask, values); xcb_flush(c); x[0].atom = get_atom(c, "PRIMARY"); x[1].atom = get_atom(c, "SECONDARY"); x[2].atom = get_atom(c, "CLIPBOARD"); fuse_main(argc, argv, &clip_ops, NULL); xcb_disconnect(c); } int get_selection_index(const char * path) { for (int i = 0; i < 3; ++i) if (0 == strcmp(path + 1, x[i].name)) return i; return -ENOENT; } xcb_window_t find_owner(const int i) { xcb_get_selection_owner_cookie_t cookie = xcb_get_selection_owner(c, x[i].atom); xcb_get_selection_owner_reply_t * reply = xcb_get_selection_owner_reply(c, cookie, NULL); xcb_window_t owner = XCB_WINDOW_NONE; if (reply) owner = reply->owner; free(reply); return owner; } void update_selection(const int i) { const xcb_atom_t PROPERTY = get_atom(c, "_RECEIVED_SELECTION"); // TODO: Get intern atoms beforehand. const xcb_atom_t PNG = get_atom(c, "image/png"); const xcb_atom_t JPG = get_atom(c, "image/jpg"); const xcb_atom_t PIXMAP = get_atom(c, "PIXMAP"); // TODO: Format of this one depends on the application. const xcb_atom_t UTF8_STRING = get_atom(c, "UTF8_STRING"); const xcb_atom_t TARGETS = get_atom(c, "TARGETS"); // TODO-multiple: MULTIPLE? if (XCB_WINDOW_NONE == find_owner(i)) return; xcb_convert_selection(c, w, x[i].atom, TARGETS, PROPERTY, XCB_CURRENT_TIME); xcb_flush(c); xcb_atom_t target = UTF8_STRING; xcb_generic_event_t * e; int status = STATUS_TARGETS; // TODO: Look into continuous notifications. while (STATUS_DONE != status) { e = xcb_wait_for_event(c); if (NULL == e) return; if (XCB_SELECTION_NOTIFY != XCB_EVENT_RESPONSE_TYPE(e)) { free(e); continue; } xcb_selection_notify_event_t * n = (xcb_selection_notify_event_t *) e; if (x[i].atom == n->selection && XCB_NONE != n->property) { xcb_get_property_cookie_t cookie = xcb_get_property( c, 1, w, PROPERTY, XCB_ATOM_ANY, 0, UINT32_MAX); xcb_get_property_reply_t * reply = xcb_get_property_reply(c, cookie, NULL); if (reply) { const unsigned len = xcb_get_property_value_length(reply); xcb_atom_t * atoms; switch (status) { case STATUS_TARGETS: atoms = (xcb_atom_t *) xcb_get_property_value(reply); for (unsigned j = 0; j < len / sizeof(xcb_atom_t); ++j) { // TODO: Find a way to get original format? if (JPG != target && PNG == atoms[j]) target = PNG; if (JPG == atoms[j]) target = JPG; if (PNG != target && JPG != target && PIXMAP == atoms[j]) target = PIXMAP; } xcb_convert_selection(c, w, x[i].atom, target, PROPERTY, XCB_CURRENT_TIME); xcb_flush(c); status = STATUS_SELECTION; break; case STATUS_SELECTION: if (NULL == x[i].data && len != x[i].len) x[i].data = realloc(x[i].data, len); if (NULL == x[i].data) x[i].len = 0; x[i].len = len; memcpy(x[i].data, xcb_get_property_value(reply), len); status = STATUS_DONE; break; case STATUS_DONE: default: break; // Unreachable } free(reply); } } free(e); } } xcb_atom_t get_atom(xcb_connection_t * c, const char * name) { xcb_intern_atom_cookie_t cookie = xcb_intern_atom(c, 0, strlen(name), name); xcb_intern_atom_reply_t * reply = xcb_intern_atom_reply(c, cookie, NULL); xcb_atom_t atom = reply->atom; free(reply); return atom; } int clip_getattr(const char * path, struct stat * st, struct fuse_file_info * fi) { (void) fi; memset(st, 0, sizeof(struct stat)); if (0 == strcmp(path, "/")) { st->st_mode = S_IFDIR | 0755; st->st_nlink = 2; } else { const int i = get_selection_index(path); if (-ENOENT == i) return -ENOENT; update_selection(i); // TODO: The content is updated way too often. st->st_mode = S_IFREG | 0444; st->st_nlink = 1; st->st_size = x[i].len; } return 0; } int clip_readdir( const char * path, void * buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info * fi, enum fuse_readdir_flags flags) { (void) offset; (void) fi; (void) flags; if (0 != strcmp(path, "/")) return -ENOENT; filler(buf, ".", NULL, 0, 0); filler(buf, "..", NULL, 0, 0); for (int i = 0; i < 3; ++i) filler(buf, x[i].name, NULL, 0, 0); return 0; } int clip_open(const char * path, struct fuse_file_info * fi) { const int i = get_selection_index(path); if (-ENOENT == i) return -ENOENT; if (O_RDONLY != (fi->flags & O_ACCMODE)) return -EACCES; update_selection(i); // TODO: The content is updated way too often. return 0; // TODO: Maybe queue selection content retrieval here? } int clip_read(const char * path, char * buf, size_t size, off_t offset, struct fuse_file_info * fi) { (void) fi; const int i = get_selection_index(path); if (-ENOENT == i) return -ENOENT; if (offset < x[i].len) { if (offset + size > x[i].len) size = x[i].len - offset; memcpy(buf, x[i].data + offset, size); } else size = 0; return size; }