diff --git a/config.h b/config.h index 14da2c411..17b7cbf6b 100644 --- a/config.h +++ b/config.h @@ -103,6 +103,10 @@ struct settings defaults = { .code = 0,.sym = NoSymbol,.is_valid = false }, /* ignore this */ +.copy_ks = {.str = "none", + .code = 0,.sym = NoSymbol,.is_valid = false +}, /* ignore this */ + .mouse_left_click = MOUSE_CLOSE_CURRENT, .mouse_middle_click = MOUSE_DO_ACTION, diff --git a/src/clipboard.c b/src/clipboard.c new file mode 100644 index 000000000..f260d7c81 --- /dev/null +++ b/src/clipboard.c @@ -0,0 +1,120 @@ +/* copyright 2020 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "clipboard.h" + +#include +#include + +#include "log.h" +#include "notification.h" +#include "queues.h" +#include "settings.h" +#include "utils.h" + +struct notification_lock { + struct notification *n; + gint64 timeout; +}; +static gpointer copy_alert_thread(gpointer data); + +/** Call xclip with the specified input. Blocks until xclip is finished. + * + * @param xclip_input The data to be copied by xclip + */ +void invoke_xclip(const char *xclip_input) +{ + if (!settings.xclip_cmd) { + LOG_C("Unable to open xclip: No xclip command set."); + return; + } + + ASSERT_OR_RET(STR_FULL(xclip_input),); + + gint dunst_to_xclip; + GError *err = NULL; + + g_spawn_async_with_pipes(NULL, + settings.xclip_cmd, + NULL, + G_SPAWN_DEFAULT + | G_SPAWN_SEARCH_PATH, + NULL, + NULL, + NULL, + &dunst_to_xclip, + NULL, + NULL, + &err); + + if (err) { + LOG_C("Cannot spawn xclip: %s", err->message); + g_error_free(err); + } else { + size_t wlen = strlen(xclip_input); + if (write(dunst_to_xclip, xclip_input, wlen) != wlen) { + LOG_W("Cannot feed xclip with input: %s", strerror(errno)); + } + close(dunst_to_xclip); + } +} + +void copy_alert_contents() +{ + GError *err = NULL; + g_thread_unref(g_thread_try_new("xclip", + copy_alert_thread, + NULL, + &err)); + + if (err) { + LOG_C("Cannot start thread to call xclip: %s", err->message); + g_error_free(err); + } +} + +static gpointer copy_alert_thread(gpointer data) +{ + char *xclip_input = NULL; + GList *locked_notifications = NULL; + + for (const GList *iter = queues_get_displayed(); iter; + iter = iter->next) { + struct notification *n = iter->data; + + + // Reference and lock the notification if we need it + notification_ref(n); + + struct notification_lock *nl = + g_malloc(sizeof(struct notification_lock)); + + nl->n = n; + nl->timeout = n->timeout; + n->timeout = 0; + + locked_notifications = g_list_prepend(locked_notifications, nl); + + xclip_input = string_append(xclip_input, n->clipboard_msg, "\n"); + } + + invoke_xclip(xclip_input); + g_free(xclip_input); + + // unref all notifications + for (GList *iter = locked_notifications; + iter; + iter = iter->next) { + + struct notification_lock *nl = iter->data; + struct notification *n = nl->n; + + n->timeout = nl->timeout; + + g_free(nl); + notification_unref(n); + } + g_list_free(locked_notifications); + + return NULL; +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/clipboard.h b/src/clipboard.h new file mode 100644 index 000000000..8eecdc04e --- /dev/null +++ b/src/clipboard.h @@ -0,0 +1,8 @@ +/* copyright 2020 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_CLIPBOARD_H +#define DUNST_CLIPBOARD_H + +void copy_alert_contents(); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/notification.c b/src/notification.c index 6518b49ac..b480f94f3 100644 --- a/src/notification.c +++ b/src/notification.c @@ -26,6 +26,7 @@ static void notification_extract_urls(struct notification *n); static void notification_format_message(struct notification *n); +static void notification_format_clipboard_message(struct notification *n); /* see notification.h */ const char *enum_to_string_fullscreen(enum behavior_fullscreen in) @@ -308,6 +309,7 @@ struct notification *notification_create(void) n->first_render = true; n->markup = settings.markup; n->format = settings.format; + n->clipboard_format = settings.clipboard_format; n->timestamp = time_monotonic_now(); @@ -389,6 +391,110 @@ void notification_init(struct notification *n) /* UPDATE derived fields */ notification_extract_urls(n); notification_format_message(n); + notification_format_clipboard_message(n); +} + +static void notification_format_clipboard_message(struct notification *n) +{ + g_clear_pointer(&n->clipboard_msg, g_free); + + n->clipboard_msg = string_replace_all("\\n", "\n", g_strdup(n->clipboard_format)); + + /* replace all formatter */ + for(char *substr = strchr(n->clipboard_msg, '%'); + substr && *substr; + substr = strchr(substr, '%')) { + + char pg[16]; + char *icon_tmp; + + switch(substr[1]) { + case 'a': + notification_replace_single_field( + &n->clipboard_msg, + &substr, + n->appname, + MARKUP_NO); + break; + case 's': + notification_replace_single_field( + &n->clipboard_msg, + &substr, + n->summary, + MARKUP_NO); + break; + case 'b': + notification_replace_single_field( + &n->clipboard_msg, + &substr, + n->body, + n->markup); + break; + case 'I': + icon_tmp = g_strdup(n->iconname); + notification_replace_single_field( + &n->clipboard_msg, + &substr, + icon_tmp ? basename(icon_tmp) : "", + MARKUP_NO); + g_free(icon_tmp); + break; + case 'i': + notification_replace_single_field( + &n->clipboard_msg, + &substr, + n->iconname ? n->iconname : "", + MARKUP_NO); + break; + case 'p': + if (n->progress != -1) + sprintf(pg, "[%3d%%]", n->progress); + + notification_replace_single_field( + &n->clipboard_msg, + &substr, + n->progress != -1 ? pg : "", + MARKUP_NO); + break; + case 'n': + if (n->progress != -1) + sprintf(pg, "%d", n->progress); + + notification_replace_single_field( + &n->clipboard_msg, + &substr, + n->progress != -1 ? pg : "", + MARKUP_NO); + break; + case '%': + notification_replace_single_field( + &n->clipboard_msg, + &substr, + "%", + MARKUP_NO); + break; + case '\0': + LOG_W("format_string has trailing %% character. " + "To escape it use %%%%."); + substr++; + break; + default: + LOG_W("format_string %%%c is unknown.", substr[1]); + // shift substr pointer forward, + // as we can't interpret the format string + substr++; + break; + } + } + + n->clipboard_msg = g_strchomp(n->clipboard_msg); + + /* truncate overlong messages */ + if (strnlen(n->clipboard_msg, DUNST_NOTIF_MAX_CHARS + 1) > DUNST_NOTIF_MAX_CHARS) { + char * buffer = g_strndup(n->clipboard_msg, DUNST_NOTIF_MAX_CHARS); + g_free(n->clipboard_msg); + n->clipboard_msg = buffer; + } } static void notification_format_message(struct notification *n) diff --git a/src/notification.h b/src/notification.h index d78d5f96f..c08c8c61d 100644 --- a/src/notification.h +++ b/src/notification.h @@ -62,6 +62,7 @@ struct notification { enum markup_mode markup; const char *format; + const char *clipboard_format; const char *script; struct notification_colors colors; @@ -83,6 +84,7 @@ struct notification { /* derived fields */ char *msg; /**< formatted message */ + char *clipboard_msg; /**< formatted message (in the manner the user prefers for the clipboard) */ char *text_to_render; /**< formatted message (with age and action indicators) */ char *urls; /**< urllist delimited by '\\n' */ }; diff --git a/src/rules.c b/src/rules.c index 70a15f041..2b2fa0438 100644 --- a/src/rules.c +++ b/src/rules.c @@ -44,6 +44,8 @@ void rule_apply(struct rule *r, struct notification *n) } if (r->format) n->format = r->format; + if (r->clipboard_format) + n->clipboard_format = r->clipboard_format; if (r->script) n->script = r->script; if (r->set_stack_tag) { diff --git a/src/rules.h b/src/rules.h index 6b2d7b290..41c8e7dba 100644 --- a/src/rules.h +++ b/src/rules.h @@ -33,6 +33,7 @@ struct rule { char *bg; char *fc; const char *format; + const char *clipboard_format; const char *script; enum behavior_fullscreen fullscreen; char *set_stack_tag; diff --git a/src/settings.c b/src/settings.c index a3dbf1e9f..b14b15cfc 100644 --- a/src/settings.c +++ b/src/settings.c @@ -159,6 +159,12 @@ void load_settings(char *cmdline_config_path) "The format template for the notifications" ); + settings.clipboard_format = option_get_string( + "global", + "clipboard_format", "-clipboard_format", defaults.clipboard_format, + "The format template for how the notification will look if copied to the clipboard" + ); + settings.sort = option_get_bool( "global", "sort", "-sort", defaults.sort, @@ -391,6 +397,21 @@ void load_settings(char *cmdline_config_path) } } + settings.xclip = option_get_path( + "global", + "xclip", "-xclip", defaults.xclip, + "path to xclip" + ); + + { + GError *error = NULL; + if (!g_shell_parse_argv(settings.xclip, NULL, &settings.xclip_cmd, &error)) { + LOG_W("Unable to parse xclip command: '%s'." + "xclip functionality will be disabled.", error->message); + g_error_free(error); + settings.xclip_cmd = NULL; + } + } settings.browser = option_get_path( "global", @@ -687,6 +708,12 @@ void load_settings(char *cmdline_config_path) "Shortcut for context menu" ); + settings.copy_ks.str = option_get_string( + "shortcuts", + "copy", "-copy_key", defaults.copy_ks.str, + "Shortcut to copy the contents of the notification to the clipboard" + ); + settings.print_notifications = cmdline_get_bool( "-print", false, "Print notifications to cmdline (DEBUG)" @@ -759,6 +786,7 @@ void load_settings(char *cmdline_config_path) r->bg = ini_get_string(cur_section, "background", r->bg); r->fc = ini_get_string(cur_section, "frame_color", r->fc); r->format = ini_get_string(cur_section, "format", r->format); + r->clipboard_format = ini_get_string(cur_section, "clipboard_format", r->clipboard_format); r->new_icon = ini_get_string(cur_section, "new_icon", r->new_icon); r->history_ignore = ini_get_bool(cur_section, "history_ignore", r->history_ignore); r->match_transient = ini_get_bool(cur_section, "match_transient", r->match_transient); diff --git a/src/settings.h b/src/settings.h index 011074587..e1552fe06 100644 --- a/src/settings.h +++ b/src/settings.h @@ -43,6 +43,7 @@ struct settings { struct notification_colors colors_norm; struct notification_colors colors_crit; char *format; + char *clipboard_format; gint64 timeouts[3]; char *icons[3]; unsigned int transparency; @@ -75,6 +76,8 @@ struct settings { char **dmenu_cmd; char *browser; char **browser_cmd; + char *xclip; + char **xclip_cmd; enum icon_position icon_position; enum vertical_alignment vertical_alignment; int min_icon_size; @@ -86,6 +89,7 @@ struct settings { struct keyboard_shortcut close_all_ks; struct keyboard_shortcut history_ks; struct keyboard_shortcut context_ks; + struct keyboard_shortcut copy_ks; bool force_xinerama; int corner_radius; enum mouse_action mouse_left_click; diff --git a/src/x11/x.c b/src/x11/x.c index 060473531..be0cfe953 100644 --- a/src/x11/x.c +++ b/src/x11/x.c @@ -20,6 +20,7 @@ #include #include +#include "../clipboard.h" #include "../dbus.h" #include "../draw.h" #include "../dunst.h" @@ -329,6 +330,13 @@ gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer context_menu(); wake_up(); } + if (settings.copy_ks.str + && XLookupKeysym(&ev.xkey, + 0) == settings.copy_ks.sym + && settings.copy_ks.mask == state) { + copy_alert_contents(); + wake_up(); + } break; case CreateNotify: LOG_D("XEvent: processing 'CreateNotify'"); @@ -514,6 +522,7 @@ void x_setup(void) x_shortcut_init(&settings.close_all_ks); x_shortcut_init(&settings.history_ks); x_shortcut_init(&settings.context_ks); + x_shortcut_init(&settings.copy_ks); x_shortcut_grab(&settings.close_ks); x_shortcut_ungrab(&settings.close_ks); @@ -523,6 +532,8 @@ void x_setup(void) x_shortcut_ungrab(&settings.history_ks); x_shortcut_grab(&settings.context_ks); x_shortcut_ungrab(&settings.context_ks); + x_shortcut_grab(&settings.copy_ks); + x_shortcut_ungrab(&settings.copy_ks); xctx.screensaver_info = XScreenSaverAllocInfo(); @@ -718,6 +729,7 @@ void x_win_show(struct window_x11 *win) x_shortcut_grab(&settings.close_ks); x_shortcut_grab(&settings.close_all_ks); x_shortcut_grab(&settings.context_ks); + x_shortcut_grab(&settings.copy_ks); x_shortcut_setup_error_handler(); XGrabButton(xctx.dpy, @@ -748,6 +760,7 @@ void x_win_hide(struct window_x11 *win) x_shortcut_ungrab(&settings.close_ks); x_shortcut_ungrab(&settings.close_all_ks); x_shortcut_ungrab(&settings.context_ks); + x_shortcut_ungrab(&settings.copy_ks); XUngrabButton(xctx.dpy, AnyButton, AnyModifier, win->xwin); XUnmapWindow(xctx.dpy, win->xwin);