Dunst
Lightweight notification daemon
Loading...
Searching...
No Matches
menu.c
Go to the documentation of this file.
1/* SPDX-License-Identifier: BSD-3-Clause */
8
9#include "menu.h"
10
11#include <errno.h>
12#include <glib.h>
13#include <regex.h>
14#include <stdbool.h>
15#include <stdio.h>
16#include <stdlib.h>
17#include <string.h>
18#include <sys/wait.h>
19#include <unistd.h>
20
21#include "dbus.h"
22#include "dunst.h"
23#include "log.h"
24#include "notification.h"
25#include "queues.h"
26#include "settings.h"
27#include "utils.h"
28
29static bool is_initialized = false;
30static regex_t url_regex;
31
32static gpointer context_menu_thread(gpointer data);
33
34struct {
35 GList *locked_notifications;
36} menu_ctx;
37
43static bool regex_init(void)
44{
45 if (is_initialized)
46 return true;
47
48 char *regex =
49 "\\<(https?://|ftps?://|news://|mailto:|file://|www\\.)"
50 "[-[:alnum:]_\\@;/?:&=%$.+!*\x27,~#]*"
51 "(\\([-[:alnum:]_\\@;/?:&=%$.+!*\x27,~#]*\\)|[-[:alnum:]_\\@;/?:&=%$+*~])+";
52 int code = regcomp(&url_regex, regex, REG_EXTENDED | REG_ICASE);
53 if (code != 0) {
54 char error_buf[120];
55 regerror(code, &url_regex, error_buf, sizeof(error_buf));
56 LOG_W("Failed to compile URL-matching regex: %s", error_buf);
57 return false;
58 } else {
59 is_initialized = true;
60 return true;
61 }
62}
63
64void regex_teardown(void)
65{
66 if (is_initialized) {
67 regfree(&url_regex);
68 is_initialized = false;
69 }
70}
71
72char *extract_urls(const char *to_match)
73{
74 if (!to_match)
75 return NULL;
76
77 if (!regex_init())
78 return NULL;
79
80 char *urls = NULL;
81 const char *p = to_match;
82 regmatch_t m;
83
84 while (1) {
85 int nomatch = regexec(&url_regex, p, 1, &m, 0);
86
87 if (nomatch || m.rm_so == -1)
88 break;
89
90 int start = m.rm_so + (p - to_match);
91 int finish = m.rm_eo + (p - to_match);
92
93 char *match = g_strndup(to_match + start, finish - start);
94 urls = string_append(urls, match, "\n");
95
96 g_free(match);
97 p += m.rm_eo;
98 }
99 return urls;
100}
101
102/*
103 * Open url in browser.
104 *
105 */
106void open_browser(const char *in)
107{
108 if (!settings.browser_cmd) {
109 LOG_C("Unable to open browser: No browser command set.");
110 return;
111 }
112
113 char *url, *end;
114 // If any, remove leading [ linktext ] from URL
115 if (*in == '[' && (end = strstr(in, "] ")))
116 url = g_strdup(end + 2);
117 else
118 url = g_strdup(in);
119
120 int argc = 2+g_strv_length(settings.browser_cmd);
121 char **argv = g_malloc_n(argc, sizeof(char*));
122
123 memcpy(argv, settings.browser_cmd, argc * sizeof(char*));
124 argv[argc-2] = url;
125 argv[argc-1] = NULL;
126
127 GError *err = NULL;
128 g_spawn_async(NULL,
129 argv,
130 NULL,
131 G_SPAWN_DEFAULT
132 | G_SPAWN_SEARCH_PATH
133 | G_SPAWN_STDOUT_TO_DEV_NULL
134 | G_SPAWN_STDERR_TO_DEV_NULL,
135 NULL,
136 NULL,
137 NULL,
138 &err);
139
140 if (err) {
141 LOG_C("Cannot spawn browser: %s", err->message);
142 g_error_free(err);
143 }
144
145 g_free(argv);
146 g_free(url);
147}
148
149char *notification_dmenu_string(struct notification *n)
150{
151 char *dmenu_str = NULL;
152
153 gpointer p_key;
154 gpointer p_value;
155 GHashTableIter iter;
156 g_hash_table_iter_init(&iter, n->actions);
157 while (g_hash_table_iter_next(&iter, &p_key, &p_value)) {
158
159 char *key = (char*) p_key;
160 char *value = (char*) p_value;
161
162 char *act_str = g_strdup_printf("#%s (%s) [%d,%s]", value, n->summary, n->id, key);
163 dmenu_str = string_append(dmenu_str, act_str, "\n");
164
165 g_free(act_str);
166 }
167 return dmenu_str;
168}
169
170/*
171 * Notify the corresponding client
172 * that an action has been invoked
173 */
174void invoke_action(const char *action)
175{
176 struct notification *invoked = NULL;
177 gint id;
178
179 char *data_start, *data_comma, *data_end;
180
181 /* format: #<human readable> (<summary>)[<id>,<action>] */
182 data_start = strrchr(action, '[');
183 if (!data_start) {
184 LOG_W("Invalid action: '%s'", action);
185 return;
186 }
187
188 id = strtol(++data_start, &data_comma, 10);
189 if (*data_comma != ',') {
190 LOG_W("Invalid action: '%s'", action);
191 return;
192 }
193
194 data_end = strchr(data_comma+1, ']');
195 if (!data_end) {
196 LOG_W("Invalid action: '%s'", action);
197 return;
198 }
199
200 char *action_key = g_strndup(data_comma+1, data_end-data_comma-1);
201
202 for (const GList *iter = queues_get_displayed();
203 iter;
204 iter = iter->next) {
205 struct notification *n = iter->data;
206 if (n->id != id)
207 continue;
208
209 if (g_hash_table_contains(n->actions, action_key)) {
210 invoked = n;
211 break;
212 }
213 }
214
215 if (invoked && action_key) {
216 signal_action_invoked(invoked, action_key);
217 }
218
219 g_free(action_key);
220}
221
228void dispatch_menu_result(const char *input)
229{
230 ASSERT_OR_RET(input,);
231
232 char *in = g_strdup(input);
233 g_strstrip(in);
234
235 if (in[0] == '#')
236 invoke_action(in + 1);
237 else if (in[0] != '\0')
238 open_browser(in);
239
240 g_free(in);
241}
242
249char *invoke_dmenu(const char *dmenu_input)
250{
251 if (!settings.dmenu_cmd) {
252 LOG_C("Unable to open dmenu: No dmenu command set.");
253 return NULL;
254 }
255
256 ASSERT_OR_RET(STR_FULL(dmenu_input), NULL);
257
258 gint dunst_to_dmenu;
259 gint dmenu_to_dunst;
260 GError *err = NULL;
261 char buf[1024];
262 char *ret = NULL;
263
264 g_spawn_async_with_pipes(NULL,
265 settings.dmenu_cmd,
266 NULL,
267 G_SPAWN_DEFAULT
268 | G_SPAWN_SEARCH_PATH,
269 NULL,
270 NULL,
271 NULL,
272 &dunst_to_dmenu,
273 &dmenu_to_dunst,
274 NULL,
275 &err);
276
277 if (err) {
278 LOG_C("Cannot spawn dmenu: %s", err->message);
279 g_error_free(err);
280 } else {
281 size_t wlen = strlen(dmenu_input);
282 ssize_t n = write(dunst_to_dmenu, dmenu_input, wlen);
283 if (n < 0 || (size_t)n != wlen) {
284 LOG_W("Cannot feed dmenu with input: %s", strerror(errno));
285 }
286 close(dunst_to_dmenu);
287
288 ssize_t rlen = read(dmenu_to_dunst, buf, sizeof(buf));
289 close(dmenu_to_dunst);
290
291 if (rlen > 0)
292 ret = g_strndup(buf, rlen);
293 else
294 LOG_W("Didn't receive input from dmenu.");
295 }
296
297 return ret;
298}
299
304{
305 GList *locked_notifications = NULL;
306
307 for (const GList *iter = queues_get_displayed(); iter;
308 iter = iter->next) {
309 struct notification *n = iter->data;
310
311 if (n->urls || g_hash_table_size(n->actions)) {
312 notification_lock(n);
313 locked_notifications = g_list_prepend(locked_notifications, n);
314 }
315 }
316
317 return g_list_reverse(locked_notifications);
318}
319
320void context_menu(void)
321{
322 GList *notifications = get_actionable_notifications();
323 context_menu_for(notifications);
324}
325
326void context_menu_for(GList *notifications)
327{
328 if (menu_ctx.locked_notifications) {
329 LOG_W("Context menu already running, refusing to rerun");
330 return;
331 }
332
333 menu_ctx.locked_notifications = notifications;
334
335 GError *err = NULL;
336 g_thread_unref(g_thread_try_new("dmenu",
337 context_menu_thread,
338 NULL,
339 &err));
340
341 if (err) {
342 LOG_C("Cannot start thread to call dmenu: %s", err->message);
343 g_error_free(err);
344 }
345}
346
347static gboolean context_menu_result_dispatch(gpointer user_data)
348{
349 char *dmenu_output = (char*)user_data;
350
351 dispatch_menu_result(dmenu_output);
352
353 for (GList *iter = menu_ctx.locked_notifications; iter; iter = iter->next) {
354 struct notification *n = iter->data;
355 notification_unlock(n);
356 if (n->marked_for_closure) {
357 // Don't close notification if context was aborted
358 if (dmenu_output != NULL)
359 queues_notification_close(n, n->marked_for_closure);
360 n->marked_for_closure = 0;
361 }
362
363 // If the notification was marked for removal, remove it from history
364 if (n->marked_for_removal) {
365 // Don't close notification if context was aborted
366 // if (dmenu_output != NULL)
368 n->marked_for_removal = 0;
369 }
370 }
371
372 menu_ctx.locked_notifications = NULL;
373
374 g_list_free(menu_ctx.locked_notifications);
375 g_free(dmenu_output);
376
377 wake_up();
378
379 return G_SOURCE_REMOVE;
380}
381
382static gpointer context_menu_thread(gpointer data)
383{
384 (void)data;
385
386 char *dmenu_input = NULL;
387 char *dmenu_output;
388
389 for (GList *iter = menu_ctx.locked_notifications; iter; iter = iter->next) {
390 struct notification *n = iter->data;
391
392 char *dmenu_str = notification_dmenu_string(n);
393 dmenu_input = string_append(dmenu_input, dmenu_str, "\n");
394 g_free(dmenu_str);
395
396 if (n->urls)
397 dmenu_input = string_append(dmenu_input, n->urls, "\n");
398 }
399
400 dmenu_output = invoke_dmenu(dmenu_input);
401 g_timeout_add(50, context_menu_result_dispatch, dmenu_output);
402
403 g_free(dmenu_input);
404
405 return NULL;
406}
DBus support and implementation of the Desktop Notifications Specification.
Main event loop logic.
Logging subsystem and helpers.
void dispatch_menu_result(const char *input)
Dispatch whatever has been returned by dmenu.
Definition menu.c:228
char * invoke_dmenu(const char *dmenu_input)
Call dmenu with the specified input.
Definition menu.c:249
static bool regex_init(void)
Initializes regexes needed for matching.
Definition menu.c:43
void context_menu(void)
Open the context menu that lets the user select urls/actions/etc for all displayed notifications.
Definition menu.c:320
void context_menu_for(GList *notifications)
Open the context menu that lets the user select urls/actions/etc for the specified notifications.
Definition menu.c:326
static GList * get_actionable_notifications(void)
Lock and get all notifications with an action or URL.
Definition menu.c:303
char * extract_urls(const char *to_match)
Extract all urls from the given string.
Definition menu.c:72
Context menu for actions and helpers.
Notification type definitions.
void queues_notification_remove(struct notification *n, enum reason reason)
Remove the given notification from all queues.
Definition queues.c:386
GList * queues_get_displayed(void)
Receive the current list of displayed notifications.
Definition queues.c:41
void queues_notification_close(struct notification *n, enum reason reason)
Close the given notification.
Definition queues.c:380
Queues for history, waiting and displayed notifications.
Type definitions for settings.
guint8 marked_for_removal
If set, the notification is marked for removal in history.
char * urls
urllist delimited by '\n'
char * string_append(char *a, const char *b, const char *sep)
Append b to string a, then concatenate both with sep (if they are non-empty).
Definition utils.c:92
String, time and other various helpers.
#define STR_FULL(s)
Test if a string is non-NULL and not empty.
Definition utils.h:23
#define ASSERT_OR_RET(expr, val)
Assert that expr evaluates to true, if not return val.
Definition utils.h:42