diff --git a/cfg/checks/x11.mk b/cfg/checks/x11.mk index fd952ac..f7c5c07 100644 --- a/cfg/checks/x11.mk +++ b/cfg/checks/x11.mk @@ -1,16 +1,18 @@ # Variables for X11 support X11_LIBS = x11 X11_CFLAGS = -DX11 +X11_OBJ = xtra.o # Check if we can build X11 support CHECK_X11_LIBS = $(shell pkg-config --exists $(X11_LIBS) || echo -n "error") ifneq ($(CHECK_X11_LIBS), error) LIBS += $(X11_LIBS) CFLAGS += $(X11_CFLAGS) + OBJ += $(X11_OBJ) else ifneq ($(MAKECMDGOALS), clean) MISSING_X11_LIBS = $(shell for lib in $(X11_LIBS) ; do if ! pkg-config --exists $$lib ; then echo $$lib ; fi ; done) -$(warning WARNING -- Toxic will be compiled without x11 support (needed for focus tracking)) +$(warning WARNING -- Toxic will be compiled without x11 support (needed for focus tracking and drag&drop support)) $(warning WARNING -- You need these libraries for x11 support) $(warning WARNING -- $(MISSING_X11_LIBS)) endif diff --git a/src/notify.c b/src/notify.c index 16a939d..2346991 100644 --- a/src/notify.c +++ b/src/notify.c @@ -35,6 +35,7 @@ #include "settings.h" #include "line_info.h" #include "misc_tools.h" +#include "xtra.h" #if defined(AUDIO) || defined(SOUND_NOTIFY) #ifdef __APPLE__ @@ -53,10 +54,6 @@ #endif #endif /* AUDIO */ -#ifdef X11 - #include -#endif /* X11 */ - #ifdef BOX_NOTIFY #include #endif @@ -70,11 +67,7 @@ extern struct user_settings *user_settings; struct Control { time_t cooldown; time_t notif_timeout; -#ifdef X11 - Display *display; - unsigned long this_window; -#endif /* X11 */ - + #if defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) pthread_mutex_t poll_mutex[1]; bool poll_active; @@ -122,23 +115,11 @@ static void tab_notify(ToxWindow *self, uint64_t flags) self->alert = WINDOW_ALERT_2; } -#ifdef X11 -long unsigned int get_focused_window_id() -{ - if (!Control.display) return 0; - - Window focus; - int revert; - XGetInputFocus(Control.display, &focus, &revert); - return focus; -} -#endif /* X11 */ - static bool notifications_are_disabled(uint64_t flags) { bool res = flags & NT_RESTOL && Control.cooldown > get_unix_time(); #ifdef X11 - return res || (flags & NT_NOFOCUS && Control.this_window == get_focused_window_id()); + return res || (flags & NT_NOFOCUS && is_focused()); #else return res; #endif @@ -389,12 +370,7 @@ int init_notify(int login_cooldown, int notification_timeout) Control.poll_active = 1; #endif - Control.cooldown = get_unix_time() + login_cooldown; -#ifdef X11 - Control.display = XOpenDisplay(NULL); - Control.this_window = get_focused_window_id(); -#endif /* X11 */ #ifdef BOX_NOTIFY diff --git a/src/toxic.c b/src/toxic.c index 57299e1..4a898da 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -58,18 +58,19 @@ #include "message_queue.h" #include "execute.h" -#ifdef AUDIO -#include "audio_call.h" -#endif /* AUDIO */ - -#ifndef PACKAGE_DATADIR -#define PACKAGE_DATADIR "." +#ifdef X11 + #include "xtra.h" #endif #ifdef AUDIO +#include "audio_call.h" ToxAv *av; #endif /* AUDIO */ +#ifndef PACKAGE_DATADIR + #define PACKAGE_DATADIR "." +#endif + /* Export for use in Callbacks */ char *DATA_FILE = NULL; char *BLOCK_FILE = NULL; @@ -134,6 +135,14 @@ void exit_toxic_success(Tox *m) tox_kill(m); endwin(); + +#ifdef X11 + /* We have to terminate xtra last coz reasons + * Please don't call this anywhere else coz trust me + */ + terminate_xtra(); +#endif /* X11 */ + exit(EXIT_SUCCESS); } @@ -969,6 +978,14 @@ static useconds_t optimal_msleepval(uint64_t *looptimer, uint64_t *loopcount, ui return new_sleep; } +#ifdef X11 +void cb(const char* asdv, DropType dt) +{ + if (dt != DT_plain) + line_info_add(prompt, NULL, NULL, NULL, SYS_MSG, 0, 0, asdv); +} +#endif /* X11 */ + int main(int argc, char *argv[]) { parse_args(argc, argv); @@ -1005,36 +1022,40 @@ int main(int argc, char *argv[]) const char *p = arg_opts.config_path[0] ? arg_opts.config_path : NULL; int settings_err = settings_load(user_settings, p); +#ifdef X11 + init_xtra(cb); +#endif + Tox *m = init_tox(); - + if (m == NULL) exit_toxic_err("failed in main", FATALERR_NETWORKINIT); - + if (!arg_opts.ignore_data_file) { if (arg_opts.encrypt_data && !datafile_exists) arg_opts.encrypt_data = 0; - + load_data(m, DATA_FILE); - + } - + init_term(); prompt = init_windows(m); prompt_init_statusbar(prompt, m); - + /* thread for ncurses stuff */ if (pthread_mutex_init(&Winthread.lock, NULL) != 0) exit_toxic_err("failed in main", FATALERR_MUTEX_INIT); - + if (pthread_create(&Winthread.tid, NULL, thread_winref, (void *) m) != 0) exit_toxic_err("failed in main", FATALERR_THREAD_CREATE); - + /* thread for message queue */ if (pthread_create(&cqueue_thread.tid, NULL, thread_cqueue, (void *) m) != 0) exit_toxic_err("failed in main", FATALERR_THREAD_CREATE); - + #ifdef AUDIO - + av = init_audio(prompt, m); set_primary_device(input, user_settings->audio_in_dev); diff --git a/src/xtra.c b/src/xtra.c new file mode 100644 index 0000000..adaec88 --- /dev/null +++ b/src/xtra.c @@ -0,0 +1,355 @@ +#include "xtra.h" + +#include +#include + +#include +#include +#include +#include +#include + + +const Atom XtraTerminate = 1; +const Atom XtraNil = 0; + +static Atom XdndAware; +static Atom XdndEnter; +static Atom XdndLeave; +static Atom XdndPosition; +static Atom XdndStatus; +static Atom XdndDrop; +static Atom XdndSelection; +static Atom XdndDATA; +static Atom XdndTypeList; +static Atom XdndActionCopy; +static Atom XdndFinished; + +struct _Xtra { + drop_callback on_drop; + Display *display; + Window terminal_window; + Window proxy_window; + Window source_window; /* When we have a drop */ + Atom handling_version; + Atom expecting_type; +} Xtra; + +typedef struct _Property +{ + unsigned char *data; + int read_format; + unsigned long read_num; + Atom read_type; +} Property; + +Property read_property(Window s, Atom p) +{ + Atom read_type; + int read_format; + unsigned long read_num; + unsigned long left_bytes; + unsigned char *data = NULL; + + int read_bytes = 1024; + + /* Keep trying to read the property until there are no bytes unread */ + do { + if (data) XFree(data); + + XGetWindowProperty(Xtra.display, s, + p, 0, + read_bytes, + False, AnyPropertyType, + &read_type, &read_format, + &read_num, &left_bytes, + &data); + + read_bytes *= 2; + } while (left_bytes != 0); + + Property property = {data, read_format, read_num, read_type}; + return property; +} + +Atom get_dnd_type(long *a, int l) +{ + int i = 0; + for (; i < l; i ++) { + if (a[i] != XtraNil) return a[i]; /* Get first valid */ + } + return XtraNil; +} + +/* TODO maybe support only certain types in the future */ +static void handle_xdnd_enter(XClientMessageEvent* e) +{ + Xtra.handling_version = (e->data.l[1] >> 24); + + if ((e->data.l[1] & 1)) { + // Fetch the list of possible conversions + Property p = read_property(e->data.l[0], XdndTypeList); + Xtra.expecting_type = get_dnd_type((long*)p.data, p.read_num); + XFree(p.data); + } else { + // Use the available list + Xtra.expecting_type = get_dnd_type(e->data.l + 2, 3); + } +} + +static void handle_xdnd_position(XClientMessageEvent* e) +{ + XEvent ev = { + .xclient = { + .type = ClientMessage, + .display = e->display, + .window = e->data.l[0], + .message_type = XdndStatus, + .format = 32, + .data = { + .l = { + Xtra.proxy_window, + (Xtra.expecting_type != XtraNil), + 0, 0, + XdndActionCopy + } + } + } + }; + + XSendEvent(Xtra.display, e->data.l[0], False, NoEventMask, &ev); + XFlush(Xtra.display); +} + +static void handle_xdnd_drop(XClientMessageEvent* e) +{ + /* Not expecting any type */ + if (Xtra.expecting_type == XtraNil) { + XEvent ev = { + .xclient = { + .type = ClientMessage, + .display = e->display, + .window = e->data.l[0], + .message_type = XdndFinished, + .format = 32, + .data = { + .l = {Xtra.proxy_window, 0, 0} + } + } + }; + + XSendEvent(Xtra.display, e->data.l[0], False, NoEventMask, &ev); + } else { + Xtra.source_window = e->data.l[0]; + XConvertSelection(Xtra.display, + XdndSelection, + Xtra.expecting_type, + XdndSelection, + Xtra.proxy_window, + Xtra.handling_version >= 1 ? e->data.l[2] : CurrentTime); + } +} + +static void handle_xdnd_selection(XSelectionEvent* e) +{ + /* DnD succesfully finished, send finished and call callback */ + XEvent ev = { + .xclient = { + .type = ClientMessage, + .display = Xtra.display, + .window = Xtra.source_window, + .message_type = XdndFinished, + .format = 32, + .data = { + .l = {Xtra.proxy_window, 1, XdndActionCopy} + } + } + }; + XSendEvent(Xtra.display, Xtra.source_window, False, NoEventMask, &ev); + + Property p = read_property(Xtra.proxy_window, XdndSelection); + DropType dt; + + if (strcmp(XGetAtomName(Xtra.display, p.read_type), "text/uri-list") == 0) + dt = DT_file_list; + else /* text/uri-list */ + dt = DT_plain; + + + /* Call callback for every entry */ + if (Xtra.on_drop && p.read_num) + { + char *sptr; + char *str = strtok_r((char*)p.data, "\n\r", &sptr); + + if (str) Xtra.on_drop(str, dt); + while ((str = strtok_r(NULL, "\n\r", &sptr))) + Xtra.on_drop(str, dt); + } + + if (p.data) XFree(p.data); +} + +void *event_loop(void* p) +{ + /* Handle events like a real nigga */ + + (void) p; /* DINDUNOTHIN */ + + XEvent event; + int pending; + + while (Xtra.display) + { + /* NEEDMOEVENTSFODEMPROGRAMS */ + + XLockDisplay(Xtra.display); + if((pending = XPending(Xtra.display))) XNextEvent(Xtra.display, &event); + + if (!pending) + { + XUnlockDisplay(Xtra.display); + usleep(10000); + continue; + } + + if (event.type == ClientMessage) + { + Atom type = event.xclient.message_type; + + if (type == XdndEnter) handle_xdnd_enter(&event.xclient); + else if (type == XdndPosition) handle_xdnd_position(&event.xclient); + else if (type == XdndDrop) handle_xdnd_drop(&event.xclient); + else if (type == XtraTerminate) break; + } + else if (event.type == SelectionNotify) handle_xdnd_selection(&event.xselection); + /* AINNOBODYCANHANDLEDEMEVENTS*/ + else XSendEvent(Xtra.display, Xtra.terminal_window, 0, 0, &event); + + XUnlockDisplay(Xtra.display); + } + + /* Actual XTRA termination + * Please call xtra_terminate() at exit + * otherwise HEWUSAGUDBOI happens + */ + if (Xtra.display) XCloseDisplay(Xtra.display); + return (Xtra.display = NULL); +} + +int init_xtra(drop_callback d) +{ + memset(&Xtra, 0, sizeof(Xtra)); + + if (!d) return -1; + else Xtra.on_drop = d; + + XInitThreads(); + if ( !(Xtra.display = XOpenDisplay(NULL))) return -1; + + Xtra.terminal_window = focused_window_id(); + + { + /* Create an invisible window which will act as proxy for the DnD operation. */ + XSetWindowAttributes attr = {0}; + attr.event_mask = EnterWindowMask | + LeaveWindowMask | + ButtonMotionMask | + ButtonPressMask | + ButtonReleaseMask | + ResizeRedirectMask; + + attr.do_not_propagate_mask = NoEventMask; + + Window root; + int x, y; + unsigned int wht, hht, b, d; + + /* Since we cannot capture resize events for parent window we will have to create + * this window to have maximum size as defined in root window + */ + XGetGeometry(Xtra.display, + XDefaultRootWindow(Xtra.display), + &root, &x, &y, &wht, &hht, &b, &d); + + if (! (Xtra.proxy_window = XCreateWindow + (Xtra.display, Xtra.terminal_window, /* Parent */ + 0, 0, /* Position */ + wht, hht, /* Width + height */ + 0, /* Border width */ + CopyFromParent, /* Depth */ + InputOnly, /* Class */ + CopyFromParent, /* Visual */ + CWEventMask | CWCursor, /* Value mask */ + &attr)) ) /* Attributes for value mask */ + return -1; + } + + XMapWindow(Xtra.display, Xtra.proxy_window); /* Show window (sandwich) */ + XLowerWindow(Xtra.display, Xtra.proxy_window); /* Don't interfere with parent lmao */ + + XdndAware = XInternAtom(Xtra.display, "XdndAware", False); + XdndEnter = XInternAtom(Xtra.display, "XdndEnter", False); + XdndLeave = XInternAtom(Xtra.display, "XdndLeave", False); + XdndPosition = XInternAtom(Xtra.display, "XdndPosition", False); + XdndStatus = XInternAtom(Xtra.display, "XdndStatus", False); + XdndDrop = XInternAtom(Xtra.display, "XdndDrop", False); + XdndSelection = XInternAtom(Xtra.display, "XdndSelection", False); + XdndDATA = XInternAtom(Xtra.display, "XdndDATA", False); + XdndTypeList = XInternAtom(Xtra.display, "XdndTypeList", False); + XdndActionCopy = XInternAtom(Xtra.display, "XdndActionCopy", False); + XdndFinished = XInternAtom(Xtra.display, "XdndFinished", False); + + /* Inform my nigga windows that we are aware of dnd */ + Atom XdndVersion = 3; + XChangeProperty(Xtra.display, + Xtra.proxy_window, + XdndAware, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char*)&XdndVersion, 1); + + pthread_t id; + pthread_create(&id, NULL, event_loop, NULL); + pthread_detach(id); + + return 0; +} + +void terminate_xtra() +{ + if (!Xtra.display) return; + + XEvent terminate = { + .xclient = { + .type = ClientMessage, + .display = Xtra.display, + .message_type = XtraTerminate, + } + }; + + XLockDisplay(Xtra.display); + XDeleteProperty(Xtra.display, Xtra.proxy_window, XdndAware); + XSendEvent(Xtra.display, Xtra.proxy_window, 0, NoEventMask, &terminate); + XUnlockDisplay(Xtra.display); + + while (Xtra.display); /* Wait for termination */ +} + +long unsigned int focused_window_id() +{ + if (!Xtra.display) return 0; + + Window focus; + int revert; + XLockDisplay(Xtra.display); + XGetInputFocus(Xtra.display, &focus, &revert); + XUnlockDisplay(Xtra.display); + return focus; +} + +int is_focused() +{ + return Xtra.proxy_window == focused_window_id(); +} diff --git a/src/xtra.h b/src/xtra.h new file mode 100644 index 0000000..a2acee6 --- /dev/null +++ b/src/xtra.h @@ -0,0 +1,19 @@ +#ifndef XTRA_H +#define XTRA_H + +/* NOTE: If no xlib present don't compile */ + +typedef enum { + DT_plain, + DT_file_list +} +DropType; + +typedef void (*drop_callback) (const char*, DropType); + +int init_xtra(drop_callback d); +void terminate_xtra(); +long unsigned int focused_window_id(); +int is_focused(); /* returns bool */ + +#endif /* XTRA_H */ \ No newline at end of file