1 /* SPDX-License-Identifier: Zlib */
2 
3 #include "session.h"
4 
5 #include "callbacks.h"
6 #include "commands.h"
7 #include "config.h"
8 #include "css-definitions.h"
9 #include "datastructures.h"
10 #include "entry.h"
11 #include "input-history.h"
12 #include "internal.h"
13 #include "settings.h"
14 #include "shortcuts.h"
15 #include "template.h"
16 #include "utils.h"
17 
18 #include <glib/gi18n-lib.h>
19 #include <pango/pango-font.h>
20 #include <stdlib.h>
21 
22 #ifdef WITH_LIBNOTIFY
23 #include <libnotify/notify.h>
24 #endif
25 
26 
27 static int
cb_sort_settings(const void * data1,const void * data2)28 cb_sort_settings(const void* data1, const void* data2)
29 {
30   const girara_setting_t* lhs = data1;
31   const girara_setting_t* rhs = data2;
32 
33   return g_strcmp0(girara_setting_get_name(lhs), girara_setting_get_name(rhs));
34 }
35 
36 static void
ensure_gettext_initialized(void)37 ensure_gettext_initialized(void)
38 {
39   static gsize initialized = 0;
40   if (g_once_init_enter(&initialized) == TRUE) {
41     bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
42     bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
43     g_once_init_leave(&initialized, 1);
44   }
45 }
46 
47 static void
init_template_engine(GiraraTemplate * csstemplate)48 init_template_engine(GiraraTemplate* csstemplate)
49 {
50   static const char variable_names[][24] = {
51     "session",
52     "default-fg",
53     "default-bg",
54     "inputbar-fg",
55     "inputbar-bg",
56     "statusbar-fg",
57     "statusbar-bg",
58     "completion-fg",
59     "completion-bg",
60     "completion-group-fg",
61     "completion-group-bg",
62     "completion-highlight-fg",
63     "completion-highlight-bg",
64     "notification-error-fg",
65     "notification-error-bg",
66     "notification-warning-fg",
67     "notification-warning-bg",
68     "notification-fg",
69     "notification-bg",
70     "scrollbar-fg",
71     "scrollbar-bg",
72     "bottombox-padding1",
73     "bottombox-padding2",
74     "bottombox-padding3",
75     "bottombox-padding4",
76     "font-family",
77     "font-size",
78     "font-weight"
79   };
80 
81   for (size_t idx = 0; idx < LENGTH(variable_names); ++idx) {
82     girara_template_add_variable(csstemplate, variable_names[idx]);
83   }
84 }
85 
86 void
css_template_fill_font(GiraraTemplate * csstemplate,const char * font)87 css_template_fill_font(GiraraTemplate* csstemplate, const char* font)
88 {
89   PangoFontDescription* descr = pango_font_description_from_string(font);
90   if (descr == NULL) {
91     return;
92   }
93 
94   girara_template_set_variable_value(csstemplate, "font-family",
95       pango_font_description_get_family(descr));
96 
97   const int font_size = pango_font_description_get_size(descr);
98   if (!font_size) {
99     girara_debug("no font size given, defaulting to 9");
100     girara_template_set_variable_value(csstemplate, "font-size", "9pt");
101   } else {
102     char* size = g_strdup_printf("%d%s", pango_font_description_get_size(descr) / PANGO_SCALE,
103         pango_font_description_get_size_is_absolute(descr) == FALSE ? "pt" : "");
104     girara_template_set_variable_value(csstemplate, "font-size", size);
105     g_free(size);
106   }
107 
108   switch (pango_font_description_get_weight(descr)) {
109     case PANGO_WEIGHT_THIN:
110       girara_template_set_variable_value(csstemplate, "font-weight", "thin");
111       break;
112 
113     case PANGO_WEIGHT_ULTRALIGHT:
114       girara_template_set_variable_value(csstemplate, "font-weight", "ultralight");
115       break;
116 
117     case PANGO_WEIGHT_SEMILIGHT:
118       girara_template_set_variable_value(csstemplate, "font-weight", "semilight");
119       break;
120 
121     case PANGO_WEIGHT_LIGHT:
122       girara_template_set_variable_value(csstemplate, "font-weight", "light");
123       break;
124 
125     case PANGO_WEIGHT_BOOK:
126       girara_template_set_variable_value(csstemplate, "font-weight", "book");
127       break;
128 
129     case PANGO_WEIGHT_MEDIUM:
130       girara_template_set_variable_value(csstemplate, "font-weight", "medium");
131       break;
132 
133     case PANGO_WEIGHT_SEMIBOLD:
134       girara_template_set_variable_value(csstemplate, "font-weight", "semibold");
135       break;
136 
137     case PANGO_WEIGHT_BOLD:
138       girara_template_set_variable_value(csstemplate, "font-weight", "bold");
139       break;
140 
141     case PANGO_WEIGHT_ULTRABOLD:
142       girara_template_set_variable_value(csstemplate, "font-weight", "ultrabold");
143       break;
144 
145     case PANGO_WEIGHT_HEAVY:
146       girara_template_set_variable_value(csstemplate, "font-weight", "heavy");
147       break;
148 
149     case PANGO_WEIGHT_ULTRAHEAVY:
150       girara_template_set_variable_value(csstemplate, "font-weight", "ultraheavy");
151       break;
152 
153     default:
154       girara_template_set_variable_value(csstemplate, "font-weight", "normal");
155       break;
156   }
157 
158   pango_font_description_free(descr);
159 }
160 
161 static void
fill_template_with_values(girara_session_t * session)162 fill_template_with_values(girara_session_t* session)
163 {
164   GiraraTemplate* csstemplate = session->private_data->csstemplate;
165 
166   girara_template_set_variable_value(csstemplate, "session",
167       session->private_data->session_name);
168 
169   char* font = NULL;
170   girara_setting_get(session, "font", &font);
171   if (font != NULL) {
172     css_template_fill_font(csstemplate, font);
173     g_free(font);
174   } else {
175     girara_template_set_variable_value(csstemplate, "font-family", "monospace");
176     girara_template_set_variable_value(csstemplate, "font-size", "9pt");
177     girara_template_set_variable_value(csstemplate, "font-weight", "normal");
178   };
179 
180   /* parse color values */
181   static const char* const color_settings[] = {
182     "default-fg",
183     "default-bg",
184     "inputbar-fg",
185     "inputbar-bg",
186     "statusbar-fg",
187     "statusbar-bg",
188     "completion-fg",
189     "completion-bg",
190     "completion-group-fg",
191     "completion-group-bg",
192     "completion-highlight-fg",
193     "completion-highlight-bg",
194     "notification-error-fg",
195     "notification-error-bg",
196     "notification-warning-fg",
197     "notification-warning-bg",
198     "notification-fg",
199     "notification-bg",
200     "scrollbar-fg",
201     "scrollbar-bg"
202   };
203 
204   for (size_t i = 0; i < LENGTH(color_settings); ++i) {
205     char* tmp_value = NULL;
206     girara_setting_get(session, color_settings[i], &tmp_value);
207 
208     GdkRGBA color = { 0, 0, 0, 0 };
209     if (tmp_value != NULL) {
210       gdk_rgba_parse(&color, tmp_value);
211       g_free(tmp_value);
212     }
213 
214     char* colorstr = gdk_rgba_to_string(&color);
215     girara_template_set_variable_value(csstemplate, color_settings[i],
216         colorstr);
217     g_free(colorstr);
218   }
219 
220   /* we want inputbar_entry the same height as notification_text and statusbar,
221     so that when inputbar_entry is hidden, the size of the bottom_box remains
222     the same. We need to get rid of the builtin padding in the GtkEntry
223     widget. */
224 
225   int ypadding = 2;         /* total amount of padding (top + bottom) */
226   int xpadding = 8;         /* total amount of padding (left + right) */
227   girara_setting_get(session, "statusbar-h-padding", &xpadding);
228   girara_setting_get(session, "statusbar-v-padding", &ypadding);
229 
230   typedef struct padding_mapping_s {
231     const char* identifier;
232     char* value;
233   } padding_mapping_t;
234 
235   const padding_mapping_t padding_mapping[] = {
236     {"bottombox-padding1", g_strdup_printf("%d", ypadding - ypadding/2)},
237     {"bottombox-padding2", g_strdup_printf("%d", xpadding/2)},
238     {"bottombox-padding3", g_strdup_printf("%d", ypadding/2)},
239     {"bottombox-padding4", g_strdup_printf("%d", xpadding - xpadding/2)},
240   };
241 
242   for (size_t i = 0; i < LENGTH(padding_mapping); ++i) {
243     girara_template_set_variable_value(csstemplate,
244         padding_mapping[i].identifier, padding_mapping[i].value);
245     g_free(padding_mapping[i].value);
246   }
247 }
248 
249 static void
css_template_changed(GiraraTemplate * csstemplate,girara_session_t * session)250 css_template_changed(GiraraTemplate* csstemplate, girara_session_t* session)
251 {
252   GtkCssProvider* provider = session->private_data->gtk.cssprovider;
253   char* css_data           = girara_template_evaluate(csstemplate);
254   if (css_data == NULL) {
255     girara_error("Error while evaluating templates.");
256     return;
257   }
258 
259   if (provider == NULL) {
260     provider = session->private_data->gtk.cssprovider = gtk_css_provider_new();
261 
262     /* add CSS style provider */
263     GdkDisplay* display = gdk_display_get_default();
264     GdkScreen* screen = gdk_display_get_default_screen(display);
265     gtk_style_context_add_provider_for_screen(screen,
266                                               GTK_STYLE_PROVIDER(provider),
267                                               GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
268   }
269 
270   GError* error = NULL;
271   if (gtk_css_provider_load_from_data(provider, css_data, -1, &error) == FALSE) {
272     girara_error("Unable to load CSS: %s", error->message);
273     g_free(css_data);
274     g_error_free(error);
275     return;
276   }
277   g_free(css_data);
278 }
279 
280 void
scrolled_window_set_scrollbar_visibility(GtkScrolledWindow * window,bool show_horizontal,bool show_vertical)281 scrolled_window_set_scrollbar_visibility(GtkScrolledWindow* window,
282                                          bool               show_horizontal,
283                                          bool               show_vertical)
284 {
285   GtkPolicyType hpolicy = GTK_POLICY_AUTOMATIC;
286   GtkPolicyType vpolicy = GTK_POLICY_AUTOMATIC;
287 
288   if (show_horizontal == false) {
289     hpolicy = GTK_POLICY_EXTERNAL;
290   }
291   if (show_vertical == false) {
292     vpolicy = GTK_POLICY_EXTERNAL;
293   }
294 
295   gtk_scrolled_window_set_policy(window, hpolicy, vpolicy);
296 }
297 
298 
299 girara_session_t*
girara_session_create(void)300 girara_session_create(void)
301 {
302   ensure_gettext_initialized();
303 
304   girara_session_t* session = g_slice_alloc0(sizeof(girara_session_t));
305   session->private_data     = g_slice_alloc0(sizeof(girara_session_private_t));
306 
307   girara_session_private_t* session_private = session->private_data;
308 
309   /* init values */
310   session->bindings.mouse_events       = girara_list_new2(
311       (girara_free_function_t) girara_mouse_event_free);
312   session->bindings.commands           = girara_list_new2(
313       (girara_free_function_t) girara_command_free);
314   session->bindings.special_commands   = girara_list_new2(
315       (girara_free_function_t) girara_special_command_free);
316   session->bindings.shortcuts          = girara_list_new2(
317       (girara_free_function_t) girara_shortcut_free);
318   session->bindings.inputbar_shortcuts = girara_list_new2(
319       (girara_free_function_t) girara_inputbar_shortcut_free);
320 
321   session_private->elements.statusbar_items = girara_list_new2(
322       (girara_free_function_t) girara_statusbar_item_free);
323   g_mutex_init(&session_private->feedkeys_mutex);
324 
325   /* settings */
326   session_private->settings = girara_sorted_list_new2(
327       cb_sort_settings,
328       (girara_free_function_t) girara_setting_free);
329 
330   /* CSS style provider */
331   GResource* css_resource = girara_css_get_resource();
332   GBytes* css_data = g_resource_lookup_data(css_resource, "/org/pwmt/girara/CSS/girara.css_t", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
333   if (css_data != NULL) {
334     session_private->csstemplate = girara_template_new(g_bytes_get_data(css_data, NULL));
335     g_bytes_unref(css_data);
336   }
337 
338   session_private->gtk.cssprovider = NULL;
339   init_template_engine(session_private->csstemplate);
340 
341   /* init modes */
342   session->modes.identifiers  = girara_list_new2(
343       (girara_free_function_t) girara_mode_string_free);
344   girara_mode_t normal_mode   = girara_mode_add(session, "normal");
345   girara_mode_t inputbar_mode = girara_mode_add(session, "inputbar");
346   session->modes.normal       = normal_mode;
347   session->modes.current_mode = normal_mode;
348   session->modes.inputbar     = inputbar_mode;
349 
350   /* config handles */
351   session_private->config.handles           = girara_list_new2(
352       (girara_free_function_t) girara_config_handle_free);
353   session_private->config.shortcut_mappings = girara_list_new2(
354       (girara_free_function_t) girara_shortcut_mapping_free);
355   session_private->config.argument_mappings = girara_list_new2(
356       (girara_free_function_t) girara_argument_mapping_free);
357 
358   /* command history */
359   session->command_history = girara_input_history_new(NULL);
360 
361   /* load default values */
362   girara_config_load_default(session);
363 
364   /* create widgets */
365   session->gtk.box                = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
366   session_private->gtk.overlay    = gtk_overlay_new();
367   session_private->gtk.bottom_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
368   session->gtk.statusbar_entries  = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
369   session->gtk.inputbar_box       = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
370   gtk_box_set_homogeneous(session->gtk.inputbar_box, TRUE);
371   session->gtk.view              = gtk_scrolled_window_new(NULL, NULL);
372   session->gtk.viewport          = gtk_viewport_new(NULL, NULL);
373   gtk_widget_add_events(session->gtk.viewport, GDK_SCROLL_MASK);
374   session->gtk.statusbar         = gtk_event_box_new();
375   session->gtk.notification_area = gtk_event_box_new();
376   session->gtk.notification_text = gtk_label_new(NULL);
377   session->gtk.inputbar_dialog   = GTK_LABEL(gtk_label_new(NULL));
378   session->gtk.inputbar_entry    = GTK_ENTRY(girara_entry_new());
379   session->gtk.inputbar          = gtk_event_box_new();
380 
381   /* make notification text selectable */
382   gtk_label_set_selectable(GTK_LABEL(session->gtk.notification_text), TRUE);
383 
384   return session;
385 }
386 
387 static void
screen_changed(GtkWidget * widget,GdkScreen * GIRARA_UNUSED (old_screen),gpointer GIRARA_UNUSED (userdata))388 screen_changed(GtkWidget* widget, GdkScreen* GIRARA_UNUSED(old_screen), gpointer GIRARA_UNUSED(userdata)) {
389   GdkScreen* screen = gtk_widget_get_screen(widget);
390   GdkVisual* visual = gdk_screen_get_rgba_visual(screen);
391   if (!visual) {
392     visual = gdk_screen_get_system_visual(screen);
393   }
394   gtk_widget_set_visual(widget, visual);
395 }
396 
397 bool
girara_session_init(girara_session_t * session,const char * sessionname)398 girara_session_init(girara_session_t* session, const char* sessionname)
399 {
400   if (session == NULL) {
401     return false;
402   }
403 
404   /* set session name */
405   session->private_data->session_name = g_strdup(
406       (sessionname == NULL) ? "girara" : sessionname);
407 
408   /* enable smooth scroll events */
409   gtk_widget_add_events(session->gtk.viewport, GDK_SMOOTH_SCROLL_MASK);
410 
411   /* load CSS style */
412   fill_template_with_values(session);
413   g_signal_connect(G_OBJECT(session->private_data->csstemplate), "changed",
414       G_CALLBACK(css_template_changed), session);
415 
416   /* window */
417 #ifdef GDK_WINDOWING_X11
418   if (session->gtk.embed != 0) {
419     session->gtk.window = gtk_plug_new(session->gtk.embed);
420   } else {
421 #endif
422     session->gtk.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
423 #ifdef GDK_WINDOWING_X11
424   }
425 #endif
426 
427   gtk_widget_set_name(session->gtk.window, session->private_data->session_name);
428 
429   g_signal_connect(G_OBJECT(session->gtk.window), "screen-changed", G_CALLBACK(screen_changed), NULL);
430   screen_changed(GTK_WIDGET(session->gtk.window), NULL, NULL);
431 
432   /* apply CSS style */
433   css_template_changed(session->private_data->csstemplate, session);
434 
435   GdkGeometry hints = {
436     .base_height = 1,
437     .base_width  = 1,
438     .height_inc  = 0,
439     .max_aspect  = 0,
440     .max_height  = 0,
441     .max_width   = 0,
442     .min_aspect  = 0,
443     .min_height  = 0,
444     .min_width   = 0,
445     .width_inc   = 0
446   };
447 
448   gtk_window_set_geometry_hints(GTK_WINDOW(session->gtk.window), NULL, &hints, GDK_HINT_MIN_SIZE);
449 
450   /* view */
451   session->signals.view_key_pressed = g_signal_connect(G_OBJECT(session->gtk.view), "key-press-event",
452       G_CALLBACK(girara_callback_view_key_press_event), session);
453 
454   session->signals.view_button_press_event = g_signal_connect(G_OBJECT(session->gtk.view), "button-press-event",
455       G_CALLBACK(girara_callback_view_button_press_event), session);
456 
457   session->signals.view_button_release_event = g_signal_connect(G_OBJECT(session->gtk.view), "button-release-event",
458       G_CALLBACK(girara_callback_view_button_release_event), session);
459 
460   session->signals.view_motion_notify_event = g_signal_connect(G_OBJECT(session->gtk.view), "motion-notify-event",
461       G_CALLBACK(girara_callback_view_button_motion_notify_event), session);
462 
463   session->signals.view_scroll_event = g_signal_connect(G_OBJECT(session->gtk.view), "scroll-event",
464       G_CALLBACK(girara_callback_view_scroll_event), session);
465 
466   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(session->gtk.view), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
467 
468   /* invisible scrollbars */
469   char* guioptions = NULL;
470   girara_setting_get(session, "guioptions", &guioptions);
471 
472   const bool show_hscrollbar = guioptions != NULL && strchr(guioptions, 'h') != NULL;
473   const bool show_vscrollbar = guioptions != NULL && strchr(guioptions, 'v') != NULL;
474   g_free(guioptions);
475 
476   scrolled_window_set_scrollbar_visibility(
477     GTK_SCROLLED_WINDOW(session->gtk.view), show_hscrollbar, show_vscrollbar);
478 
479   /* viewport */
480   gtk_container_add(GTK_CONTAINER(session->gtk.view), session->gtk.viewport);
481   gtk_viewport_set_shadow_type(GTK_VIEWPORT(session->gtk.viewport), GTK_SHADOW_NONE);
482 
483   /* statusbar */
484   gtk_container_add(GTK_CONTAINER(session->gtk.statusbar), GTK_WIDGET(session->gtk.statusbar_entries));
485 
486   /* notification area */
487   gtk_container_add(GTK_CONTAINER(session->gtk.notification_area), session->gtk.notification_text);
488   gtk_widget_set_halign(session->gtk.notification_text, GTK_ALIGN_START);
489   gtk_widget_set_valign(session->gtk.notification_text, GTK_ALIGN_CENTER);
490   gtk_label_set_use_markup(GTK_LABEL(session->gtk.notification_text), TRUE);
491 
492   /* inputbar */
493   gtk_entry_set_has_frame(session->gtk.inputbar_entry, FALSE);
494   gtk_editable_set_editable(GTK_EDITABLE(session->gtk.inputbar_entry), TRUE);
495 
496   widget_add_class(GTK_WIDGET(session->gtk.inputbar_entry), "bottom_box");
497   widget_add_class(session->gtk.notification_text, "bottom_box");
498   widget_add_class(GTK_WIDGET(session->gtk.statusbar_entries), "bottom_box");
499 
500   session->signals.inputbar_key_pressed = g_signal_connect(
501       G_OBJECT(session->gtk.inputbar_entry),
502       "key-press-event",
503       G_CALLBACK(girara_callback_inputbar_key_press_event),
504       session
505     );
506 
507   session->signals.inputbar_changed = g_signal_connect(
508       G_OBJECT(session->gtk.inputbar_entry),
509       "changed",
510       G_CALLBACK(girara_callback_inputbar_changed_event),
511       session
512     );
513 
514   session->signals.inputbar_activate = g_signal_connect(
515       G_OBJECT(session->gtk.inputbar_entry),
516       "activate",
517       G_CALLBACK(girara_callback_inputbar_activate),
518       session
519     );
520 
521   gtk_box_set_homogeneous(session->gtk.inputbar_box, FALSE);
522   gtk_box_set_spacing(session->gtk.inputbar_box, 5);
523 
524   /* inputbar box */
525   gtk_box_pack_start(GTK_BOX(session->gtk.inputbar_box),  GTK_WIDGET(session->gtk.inputbar_dialog), FALSE, FALSE, 0);
526   gtk_box_pack_start(GTK_BOX(session->gtk.inputbar_box),  GTK_WIDGET(session->gtk.inputbar_entry),  TRUE,  TRUE,  0);
527   gtk_container_add(GTK_CONTAINER(session->gtk.inputbar), GTK_WIDGET(session->gtk.inputbar_box));
528 
529   /* bottom box */
530   gtk_box_set_spacing(session->private_data->gtk.bottom_box, 0);
531 
532   gtk_box_pack_end(GTK_BOX(session->private_data->gtk.bottom_box), GTK_WIDGET(session->gtk.inputbar), TRUE, TRUE, 0);
533   gtk_box_pack_end(GTK_BOX(session->private_data->gtk.bottom_box), GTK_WIDGET(session->gtk.notification_area), TRUE, TRUE, 0);
534   gtk_box_pack_end(GTK_BOX(session->private_data->gtk.bottom_box), GTK_WIDGET(session->gtk.statusbar), TRUE, TRUE, 0);
535 
536   /* packing */
537   gtk_box_set_spacing(session->gtk.box, 0);
538   gtk_box_pack_start(session->gtk.box, GTK_WIDGET(session->gtk.view),   TRUE,  TRUE, 0);
539 
540   /* box */
541   gtk_container_add(GTK_CONTAINER(session->private_data->gtk.overlay), GTK_WIDGET(session->gtk.box));
542   /* overlay */
543   g_object_set(session->private_data->gtk.bottom_box, "halign", GTK_ALIGN_FILL, NULL);
544   g_object_set(session->private_data->gtk.bottom_box, "valign", GTK_ALIGN_END, NULL);
545 
546   gtk_overlay_add_overlay(GTK_OVERLAY(session->private_data->gtk.overlay), GTK_WIDGET(session->private_data->gtk.bottom_box));
547   gtk_container_add(GTK_CONTAINER(session->gtk.window), GTK_WIDGET(session->private_data->gtk.overlay));
548 
549   /* statusbar */
550   widget_add_class(GTK_WIDGET(session->gtk.statusbar), "statusbar");
551 
552   /* inputbar */
553   widget_add_class(GTK_WIDGET(session->gtk.inputbar_box), "inputbar");
554   widget_add_class(GTK_WIDGET(session->gtk.inputbar_entry), "inputbar");
555   widget_add_class(GTK_WIDGET(session->gtk.inputbar), "inputbar");
556   widget_add_class(GTK_WIDGET(session->gtk.inputbar_dialog), "inputbar");
557 
558   /* notification area */
559   widget_add_class(session->gtk.notification_area, "notification");
560   widget_add_class(session->gtk.notification_text, "notification");
561 
562   /* set window size */
563   int window_width = 0;
564   int window_height = 0;
565   girara_setting_get(session, "window-width", &window_width);
566   girara_setting_get(session, "window-height", &window_height);
567 
568   if (window_width > 0 && window_height > 0) {
569     gtk_window_set_default_size(GTK_WINDOW(session->gtk.window), window_width, window_height);
570   }
571 
572   gtk_widget_show_all(GTK_WIDGET(session->gtk.window));
573   gtk_widget_hide(GTK_WIDGET(session->gtk.notification_area));
574   gtk_widget_hide(GTK_WIDGET(session->gtk.inputbar_dialog));
575 
576   if (session->global.autohide_inputbar == true) {
577     gtk_widget_hide(GTK_WIDGET(session->gtk.inputbar));
578   }
579 
580   if (session->global.hide_statusbar == true) {
581     gtk_widget_hide(GTK_WIDGET(session->gtk.statusbar));
582   }
583 
584   char* window_icon = NULL;
585   girara_setting_get(session, "window-icon", &window_icon);
586   if (window_icon != NULL && strlen(window_icon) != 0) {
587     girara_set_window_icon(session, window_icon);
588   }
589   g_free(window_icon);
590 
591   gtk_widget_grab_focus(GTK_WIDGET(session->gtk.view));
592 
593   return true;
594 }
595 
596 static void
girara_session_private_free(girara_session_private_t * session)597 girara_session_private_free(girara_session_private_t* session)
598 {
599   g_return_if_fail(session != NULL);
600 
601   if (session->session_name != NULL) {
602     g_free(session->session_name);
603   }
604 
605   /* clean up config handles */
606   girara_list_free(session->config.handles);
607   session->config.handles = NULL;
608 
609   /* clean up shortcut mappings */
610   girara_list_free(session->config.shortcut_mappings);
611   session->config.shortcut_mappings = NULL;
612 
613   /* clean up argument mappings */
614   girara_list_free(session->config.argument_mappings);
615   session->config.argument_mappings = NULL;
616 
617   /* clean up buffer */
618   if (session->buffer.command) {
619     g_string_free(session->buffer.command, TRUE);
620   }
621   session->buffer.command = NULL;
622 
623   /* clean up statusbar items */
624   girara_list_free(session->elements.statusbar_items);
625   session->elements.statusbar_items = NULL;
626 
627   /* clean up CSS style provider */
628   g_clear_object(&session->gtk.cssprovider);
629   g_clear_object(&session->csstemplate);
630 
631   /* clean up settings */
632   girara_list_free(session->settings);
633   session->settings = NULL;
634 
635   /* clean up mutex */
636   g_mutex_clear(&session->feedkeys_mutex);
637 
638   g_slice_free(girara_session_private_t, session);
639 }
640 
641 bool
girara_session_destroy(girara_session_t * session)642 girara_session_destroy(girara_session_t* session)
643 {
644   g_return_val_if_fail(session != NULL, FALSE);
645 
646   /* clean up shortcuts */
647   girara_list_free(session->bindings.shortcuts);
648   session->bindings.shortcuts = NULL;
649 
650   /* clean up inputbar shortcuts */
651   girara_list_free(session->bindings.inputbar_shortcuts);
652   session->bindings.inputbar_shortcuts = NULL;
653 
654   /* clean up commands */
655   girara_list_free(session->bindings.commands);
656   session->bindings.commands = NULL;
657 
658   /* clean up special commands */
659   girara_list_free(session->bindings.special_commands);
660   session->bindings.special_commands = NULL;
661 
662   /* clean up mouse events */
663   girara_list_free(session->bindings.mouse_events);
664   session->bindings.mouse_events = NULL;
665 
666   /* clean up input histry */
667   g_clear_object(&session->command_history);
668 
669   /* clean up modes */
670   girara_list_free(session->modes.identifiers);
671   session->modes.identifiers = NULL;
672 
673   /* clean up buffer */
674   if (session->global.buffer) {
675     g_string_free(session->global.buffer, TRUE);
676   }
677   session->global.buffer  = NULL;
678 
679   /* clean up private data */
680   girara_session_private_free(session->private_data);
681   session->private_data = NULL;
682 
683   /* clean up session */
684   g_slice_free(girara_session_t, session);
685 
686   return TRUE;
687 }
688 
689 char*
girara_buffer_get(girara_session_t * session)690 girara_buffer_get(girara_session_t* session)
691 {
692   g_return_val_if_fail(session != NULL, NULL);
693 
694   return (session->global.buffer) ? g_strdup(session->global.buffer->str) : NULL;
695 }
696 
697 void
girara_libnotify(girara_session_t * session,const char * summary,const char * body)698 girara_libnotify(girara_session_t* session, const char *summary,
699     const char *body)
700 {
701   if (session == NULL
702       || summary == NULL
703       || body == NULL) {
704     return;
705   }
706 
707 #ifdef WITH_LIBNOTIFY
708   const bool was_initialized = notify_is_initted();
709 
710   if (was_initialized == false) {
711     notify_init(session->private_data->session_name);
712   }
713 
714   NotifyNotification* libnotify_notification = NULL;
715   char* icon_name = NULL;
716 
717   /* We use the NotifyNotification constructor at many branches because
718    * libnotify does not have a notify_notification_set_image_from_name()
719    * function, and accessing private fields is frowned upon and subject to API
720    * changes.
721    */
722   icon_name = g_strdup(gtk_window_get_icon_name(GTK_WINDOW(session->gtk.window)));
723   if (icon_name != NULL) {
724     /* Icon can be loaded from theme with adequate quality for notification */
725     libnotify_notification = notify_notification_new(summary, body, icon_name);
726     g_free(icon_name);
727   } else {
728     /* Or extracted from the current window */
729     GdkPixbuf* icon_pix = gtk_window_get_icon(GTK_WINDOW(session->gtk.window));
730     if (icon_pix != NULL) {
731       libnotify_notification = notify_notification_new(summary, body, NULL);
732       notify_notification_set_image_from_pixbuf(libnotify_notification, icon_pix);
733       g_object_unref(G_OBJECT(icon_pix));
734     } else {
735       /* Or from a default image as a last resort */
736       libnotify_notification = notify_notification_new(summary, body, "info");
737     }
738   }
739 
740   g_return_if_fail(libnotify_notification != NULL);
741   notify_notification_show(libnotify_notification, NULL);
742   g_object_unref(G_OBJECT(libnotify_notification));
743 
744   if (was_initialized == false) {
745     notify_uninit();
746   }
747 #else
748   girara_notify(session, GIRARA_WARNING, "Girara was compiled without libnotify support.");
749 #endif
750 }
751 
752 void
girara_notify(girara_session_t * session,int level,const char * format,...)753 girara_notify(girara_session_t* session, int level, const char* format, ...)
754 {
755   if (session == NULL
756       || session->gtk.notification_text == NULL
757       || session->gtk.notification_area == NULL
758       || session->gtk.inputbar == NULL
759       || session->gtk.view == NULL) {
760     return;
761   }
762 
763   if (level == GIRARA_ERROR) {
764     widget_add_class(session->gtk.notification_area, "notification-error");
765     widget_add_class(session->gtk.notification_text, "notification-error");
766   } else {
767     widget_remove_class(session->gtk.notification_area, "notification-error");
768     widget_remove_class(session->gtk.notification_text, "notification-error");
769   }
770   if (level == GIRARA_WARNING) {
771     widget_add_class(session->gtk.notification_area, "notification-warning");
772     widget_add_class(session->gtk.notification_text, "notification-warning");
773   } else {
774     widget_remove_class(session->gtk.notification_area, "notification-warning");
775     widget_remove_class(session->gtk.notification_text, "notification-warning");
776   }
777 
778   /* prepare message */
779   va_list ap;
780   va_start(ap, format);
781   char* message = g_strdup_vprintf(format, ap);
782   va_end(ap);
783 
784   gtk_label_set_markup(GTK_LABEL(session->gtk.notification_text), message);
785   g_free(message);
786 
787   /* update visibility */
788   gtk_widget_show(GTK_WIDGET(session->gtk.notification_area));
789   gtk_widget_hide(GTK_WIDGET(session->gtk.inputbar));
790 
791   gtk_widget_grab_focus(GTK_WIDGET(session->gtk.view));
792 }
793 
794 void
girara_dialog(girara_session_t * session,const char * dialog,bool invisible,girara_callback_inputbar_key_press_event_t key_press_event,girara_callback_inputbar_activate_t activate_event,void * data)795 girara_dialog(girara_session_t* session, const char* dialog, bool
796     invisible, girara_callback_inputbar_key_press_event_t key_press_event,
797     girara_callback_inputbar_activate_t activate_event, void* data)
798 {
799   if (session == NULL || session->gtk.inputbar == NULL
800       || session->gtk.inputbar_dialog == NULL
801       || session->gtk.inputbar_entry == NULL) {
802     return;
803   }
804 
805   gtk_widget_show(GTK_WIDGET(session->gtk.inputbar_dialog));
806 
807   /* set dialog message */
808   if (dialog != NULL) {
809     gtk_label_set_markup(session->gtk.inputbar_dialog, dialog);
810   }
811 
812   /* set input visibility */
813   if (invisible == true) {
814     gtk_entry_set_visibility(session->gtk.inputbar_entry, FALSE);
815   } else {
816     gtk_entry_set_visibility(session->gtk.inputbar_entry, TRUE);
817   }
818 
819   /* set handler */
820   session->signals.inputbar_custom_activate        = activate_event;
821   session->signals.inputbar_custom_key_press_event = key_press_event;
822   session->signals.inputbar_custom_data            = data;
823 
824   /* focus inputbar */
825   girara_sc_focus_inputbar(session, NULL, NULL, 0);
826 }
827 
828 bool
girara_set_view(girara_session_t * session,GtkWidget * widget)829 girara_set_view(girara_session_t* session, GtkWidget* widget)
830 {
831   g_return_val_if_fail(session != NULL, false);
832 
833   GtkWidget* child = gtk_bin_get_child(GTK_BIN(session->gtk.viewport));
834 
835   if (child != NULL) {
836     g_object_ref(child);
837     gtk_container_remove(GTK_CONTAINER(session->gtk.viewport), child);
838   }
839 
840   gtk_container_add(GTK_CONTAINER(session->gtk.viewport), widget);
841   gtk_widget_show_all(widget);
842   gtk_widget_grab_focus(session->gtk.view);
843 
844   return true;
845 }
846 
847 void
girara_mode_set(girara_session_t * session,girara_mode_t mode)848 girara_mode_set(girara_session_t* session, girara_mode_t mode)
849 {
850   g_return_if_fail(session != NULL);
851 
852   session->modes.current_mode = mode;
853 }
854 
855 girara_mode_t
girara_mode_add(girara_session_t * session,const char * name)856 girara_mode_add(girara_session_t* session, const char* name)
857 {
858   g_return_val_if_fail(session  != NULL, FALSE);
859   g_return_val_if_fail(name != NULL && name[0] != '\0', FALSE);
860 
861   girara_mode_t last_index = 0;
862   GIRARA_LIST_FOREACH_BODY(session->modes.identifiers, girara_mode_string_t*, mode,
863     if (mode->index > last_index) {
864       last_index = mode->index;
865     }
866   );
867 
868   /* create new mode identifier */
869   girara_mode_string_t* mode = g_slice_new(girara_mode_string_t);
870   mode->index = last_index + 1;
871   mode->name = g_strdup(name);
872   girara_list_append(session->modes.identifiers, mode);
873 
874   return mode->index;
875 }
876 
877 void
girara_mode_string_free(girara_mode_string_t * mode)878 girara_mode_string_free(girara_mode_string_t* mode)
879 {
880   if (mode == NULL) {
881     return;
882   }
883 
884   g_free(mode->name);
885   g_slice_free(girara_mode_string_t, mode);
886 }
887 
888 girara_mode_t
girara_mode_get(girara_session_t * session)889 girara_mode_get(girara_session_t* session)
890 {
891   g_return_val_if_fail(session != NULL, 0);
892 
893   return session->modes.current_mode;
894 }
895 
896 bool
girara_set_window_title(girara_session_t * session,const char * name)897 girara_set_window_title(girara_session_t* session, const char* name)
898 {
899   if (session == NULL || session->gtk.window == NULL || name == NULL) {
900     return false;
901   }
902 
903   gtk_window_set_title(GTK_WINDOW(session->gtk.window), name);
904 
905   return true;
906 }
907 
908 bool
girara_set_window_icon(girara_session_t * session,const char * name)909 girara_set_window_icon(girara_session_t* session, const char* name)
910 {
911   if (session == NULL || session->gtk.window == NULL || name == NULL) {
912     return false;
913   }
914 
915   if (strlen(name) == 0) {
916     girara_warning("Empty icon name.");
917     return false;
918   }
919 
920   GtkWindow* window = GTK_WINDOW(session->gtk.window);
921   char* path        = girara_fix_path(name);
922   bool success      = true;
923 
924   if (g_file_test(path, G_FILE_TEST_EXISTS) == TRUE) {
925     girara_debug("Loading window icon from file: %s", path);
926 
927     GError* error = NULL;
928     success = gtk_window_set_icon_from_file(window, path, &error);
929 
930     if (success == false) {
931       girara_debug("Failed to load window icon (file): %s", error->message);
932       g_error_free(error);
933     }
934   } else {
935     girara_debug("Loading window icon with name: %s", name);
936     gtk_window_set_icon_name(window, name);
937   }
938 
939   g_free(path);
940   return success;
941 }
942 
943 girara_list_t*
girara_get_command_history(girara_session_t * session)944 girara_get_command_history(girara_session_t* session)
945 {
946   g_return_val_if_fail(session != NULL, NULL);
947   return girara_input_history_list(session->command_history);
948 }
949 
950 GiraraTemplate*
girara_session_get_template(girara_session_t * session)951 girara_session_get_template(girara_session_t* session)
952 {
953   g_return_val_if_fail(session != NULL, NULL);
954 
955   return session->private_data->csstemplate;
956 }
957 
958 void
girara_session_set_template(girara_session_t * session,GiraraTemplate * template,bool init_variables)959 girara_session_set_template(girara_session_t* session, GiraraTemplate* template, bool init_variables)
960 {
961   g_return_if_fail(session != NULL);
962   g_return_if_fail(template != NULL);
963 
964   g_clear_object(&session->private_data->csstemplate);
965 
966   session->private_data->csstemplate = template;
967   if (init_variables == true) {
968     init_template_engine(template);
969     fill_template_with_values(session);
970   }
971 
972   css_template_changed(template, session);
973 }
974 
975