girara
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros
completion.c
Go to the documentation of this file.
1 /* See LICENSE file for license and copyright information */
2 
3 #include <math.h>
4 #include <string.h>
5 #include <stdlib.h>
6 
7 #include "completion.h"
8 #include "internal.h"
9 #include "session.h"
10 #include "settings.h"
11 #include "datastructures.h"
12 
13 #if GTK_MAJOR_VERSION == 2
14 #include "gtk2-compat.h"
15 #endif
16 
17 static GtkEventBox* girara_completion_row_create(girara_session_t*, const char*, const char*, bool);
18 static void girara_completion_row_set_color(girara_session_t*, GtkEventBox*, int);
19 
20 /* completion */
22 {
23  bool group;
24  char* value;
25  GtkEventBox* widget;
26 };
27 
32 {
33  char *value;
34  char *description;
35 };
36 
41 {
42  char *value;
43  girara_list_t *elements;
44 };
45 
50 {
51  girara_list_t *groups;
52 };
53 
54 typedef struct girara_internal_completion_entry_s girara_internal_completion_entry_t;
55 
56 static void
57 completion_element_free(girara_completion_element_t* element)
58 {
59  if (element == NULL) {
60  return;
61  }
62 
63  /* free element */
64  g_free(element->value);
65  g_free(element->description);
66  g_slice_free(girara_completion_element_t, element);
67 }
68 
69 static char*
70 escape(const char* value)
71 {
72  if (value == NULL) {
73  return NULL;
74  }
75 
76  GString* str = g_string_new("");
77  while (*value != '\0') {
78  const char c = *value++;
79  if (strchr("\\ \t\"\'", c) != NULL) {
80  g_string_append_c(str, '\\');
81  }
82  g_string_append_c(str, c);
83  }
84 
85  return g_string_free(str, FALSE);
86 }
87 
88 girara_completion_t*
90 {
91  girara_completion_t *completion = g_slice_new(girara_completion_t);
92  completion->groups = girara_list_new2(
94 
95  return completion;
96 }
97 
98 girara_completion_group_t*
99 girara_completion_group_create(girara_session_t* UNUSED(session), const char* name)
100 {
101  girara_completion_group_t* group = g_slice_new(girara_completion_group_t);
102 
103  group->value = name ? g_strdup(name) : NULL;
104  group->elements = girara_list_new2(
105  (girara_free_function_t) completion_element_free);
106 
107  if (group->elements == NULL) {
108  g_slice_free(girara_completion_group_t, group);
109  return NULL;
110  }
111 
112  return group;
113 }
114 
115 void
116 girara_completion_add_group(girara_completion_t* completion, girara_completion_group_t* group)
117 {
118  g_return_if_fail(completion != NULL);
119  g_return_if_fail(group != NULL);
120 
121  girara_list_append(completion->groups, group);
122 }
123 
124 void
125 girara_completion_group_free(girara_completion_group_t* group)
126 {
127  if (group == NULL) {
128  return;
129  }
130 
131  g_free(group->value);
132  girara_list_free(group->elements);
133  g_slice_free(girara_completion_group_t, group);
134 }
135 
136 void
137 girara_completion_free(girara_completion_t* completion)
138 {
139  g_return_if_fail(completion != NULL);
140 
141  girara_list_free(completion->groups);
142  /* free completion */
143  g_slice_free(girara_completion_t, completion);
144 }
145 
146 void
147 girara_completion_group_add_element(girara_completion_group_t* group, const char* name, const char* description)
148 {
149  g_return_if_fail(group != NULL);
150  g_return_if_fail(name != NULL);
151 
152  girara_completion_element_t* new_element = g_slice_new(girara_completion_element_t);
153 
154  new_element->value = g_strdup(name);
155  new_element->description = description ? g_strdup(description) : NULL;
156 
157  girara_list_append(group->elements, new_element);
158 }
159 
160 bool
161 girara_isc_completion(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event), unsigned int UNUSED(t))
162 {
163  g_return_val_if_fail(session != NULL, false);
164 
165  /* get current text */
166  gchar *input = gtk_editable_get_chars(GTK_EDITABLE(session->gtk.inputbar_entry), 0, -1);
167  if (input == NULL) {
168  return false;
169  }
170 
171  const size_t input_length = strlen(input);
172 
173  if (input_length == 0 || input[0] != ':') {
174  g_free(input);
175  return false;
176  }
177 
178  gchar** elements = NULL;
179  gint n_parameter = 0;
180  if (input_length > 1) {
181  if (g_shell_parse_argv(input + 1, &n_parameter, &elements, NULL) == FALSE) {
182  g_free(input);
183  return FALSE;
184  }
185  } else {
186  elements = g_malloc0(2 * sizeof(char*));
187  elements[0] = g_strdup("");
188  }
189 
190  if (n_parameter == 1 && input[input_length-1] == ' ') {
191  n_parameter += 1;
192  }
193 
194  g_free(input);
195 
196  /* get current values */
197  gchar *current_command = (elements[0] != NULL && elements[0][0] != '\0') ? g_strdup(elements[0]) : NULL;
198  gchar *current_parameter = (elements[0] != NULL && elements[1] != NULL) ? g_strdup(elements[1]) : NULL;
199 
200  size_t current_command_length = current_command ? strlen(current_command) : 0;
201 
202  static GList* entries = NULL;
203  static GList* entries_current = NULL;
204  static char *previous_command = NULL;
205  static char *previous_parameter = NULL;
206  static bool command_mode = true;
207  static size_t previous_length = 0;
208 
209  /* delete old list iff
210  * the completion should be hidden
211  * the current command differs from the previous one
212  * the current parameter differs from the previous one
213  * no current command is given
214  */
215  if ( (argument->n == GIRARA_HIDE) ||
216  (current_parameter && previous_parameter && strcmp(current_parameter, previous_parameter)) ||
217  (current_command && previous_command && strcmp(current_command, previous_command)) ||
218  input_length != previous_length
219  )
220  {
221  if (session->gtk.results != NULL) {
222  /* destroy elements */
223  for (GList* element = entries; element; element = g_list_next(element)) {
224  girara_internal_completion_entry_t* entry = (girara_internal_completion_entry_t*) element->data;
225 
226  if (entry != NULL) {
227  gtk_widget_destroy(GTK_WIDGET(entry->widget));
228  g_free(entry->value);
229  g_slice_free(girara_internal_completion_entry_t, entry);
230  }
231  }
232 
233  g_list_free(entries);
234  entries = NULL;
235  entries_current = NULL;
236 
237  /* delete row box */
238  gtk_widget_destroy(GTK_WIDGET(session->gtk.results));
239  session->gtk.results = NULL;
240  }
241 
242  command_mode = true;
243 
244  if (argument->n == GIRARA_HIDE) {
245  g_free(previous_command);
246  previous_command = NULL;
247 
248  g_free(previous_parameter);
249  previous_parameter = NULL;
250 
251  g_strfreev(elements);
252 
253  g_free(current_command);
254  g_free(current_parameter);
255 
256  return false;
257  }
258  }
259 
260  /* create new list iff
261  * there is no current list
262  */
263  if (session->gtk.results == NULL) {
264 #if GTK_MAJOR_VERSION == 2
265  session->gtk.results = GTK_BOX(gtk_vbox_new(FALSE, 0));
266 #else
267  session->gtk.results = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
268 #endif
269 
270  if (session->gtk.results == NULL) {
271  g_free(current_command);
272  g_free(current_parameter);
273 
274  g_strfreev(elements);
275  return false;
276  }
277 
278  if (n_parameter <= 1) {
279  /* based on commands */
280  command_mode = true;
281 
282  /* create command rows */
283  GIRARA_LIST_FOREACH(session->bindings.commands, girara_command_t*, iter, command)
284  if (current_command == NULL ||
285  (command->command != NULL && !strncmp(current_command, command->command, current_command_length)) ||
286  (command->abbr != NULL && !strncmp(current_command, command->abbr, current_command_length))
287  )
288  {
289  /* create entry */
290  girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t);
291  entry->group = FALSE;
292  entry->value = g_strdup(command->command);
293  entry->widget = girara_completion_row_create(session, command->command, command->description, FALSE);
294 
295  entries = g_list_append(entries, entry);
296 
297  /* show entry row */
298  gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0);
299  }
300  GIRARA_LIST_FOREACH_END(session->bindings.commands, girara_command_t*, iter, command);
301  }
302 
303  /* based on parameters */
304  if (n_parameter > 1 || g_list_length(entries) == 1) {
305  /* if only one command exists try to run parameter completion */
306  if (g_list_length(entries) == 1) {
307  girara_internal_completion_entry_t* entry = g_list_first(entries)->data;
308 
309  /* unset command mode */
310  command_mode = false;
311  current_command = entry->value;
312  current_command_length = strlen(current_command);
313 
314  /* clear list */
315  gtk_widget_destroy(GTK_WIDGET(entry->widget));
316 
317  entries = g_list_remove(entries, g_list_first(entries)->data);
318  g_slice_free(girara_internal_completion_entry_t, entry);
319  }
320 
321  /* search matching command */
322  girara_command_t* command = NULL;
323  GIRARA_LIST_FOREACH(session->bindings.commands, girara_command_t*, iter, command_it)
324  if ( (current_command != NULL && command_it->command != NULL && !strncmp(current_command, command_it->command, current_command_length)) ||
325  (current_command != NULL && command_it->abbr != NULL && !strncmp(current_command, command_it->abbr, current_command_length))
326  )
327  {
328  g_free(previous_command);
329  previous_command = g_strdup(command_it->command);
330  command = command_it;
331  break;
332  }
333  GIRARA_LIST_FOREACH_END(session->bindings.commands, girara_command_t*, iter, command_it);
334 
335  if (command == NULL) {
336  g_free(current_command);
337  g_free(current_parameter);
338 
339  g_strfreev(elements);
340  return false;
341  }
342 
343  if (command->completion == NULL) {
344  girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t);
345  entry->group = FALSE;
346  entry->value = g_strdup(command->command);
347  entry->widget = girara_completion_row_create(session, command->command, command->description, FALSE);
348 
349  entries = g_list_append(entries, entry);
350 
351  gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0);
352  command_mode = true;
353  } else {
354  /* generate completion result
355  * XXX: the last argument should only be current_paramater ... but
356  * therefore the completion functions would need to handle NULL correctly
357  * (see cc_open in zathura). */
358  girara_completion_t *result = command->completion(session, current_parameter ? current_parameter : "");
359 
360  if (result == NULL || result->groups == NULL) {
361  g_free(current_command);
362  g_free(current_parameter);
363 
364  g_strfreev(elements);
365  return false;
366  }
367 
368  GIRARA_LIST_FOREACH(result->groups, girara_completion_group_t*, iter, group)
369  /* create group entry */
370  if (group->value != NULL) {
371  girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t);
372  entry->group = TRUE;
373  entry->value = g_strdup(group->value);
374  entry->widget = girara_completion_row_create(session, group->value, NULL, TRUE);
375 
376  entries = g_list_append(entries, entry);
377 
378  gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0);
379  }
380 
381  GIRARA_LIST_FOREACH(group->elements, girara_completion_element_t*, iter2, element)
382  girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t);
383  entry->group = FALSE;
384  entry->value = g_strdup(element->value);
385  entry->widget = girara_completion_row_create(session, element->value, element->description, FALSE);
386 
387  entries = g_list_append(entries, entry);
388 
389  gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0);
390 
391  GIRARA_LIST_FOREACH_END(group->elements, girara_completion_element_t*, iter2, element);
392  GIRARA_LIST_FOREACH_END(result->groups, girara_completion_group_t*, iter, group);
393  girara_completion_free(result);
394 
395  command_mode = false;
396  }
397  }
398 
399  if (entries != NULL) {
400  entries_current = (argument->n == GIRARA_NEXT) ? g_list_last(entries) : entries;
401  gtk_box_pack_start(session->gtk.box, GTK_WIDGET(session->gtk.results), FALSE, FALSE, 0);
402  gtk_widget_show(GTK_WIDGET(session->gtk.results));
403  }
404  }
405 
406  /* update entries */
407  unsigned int n_elements = g_list_length(entries);
408  if (entries != NULL && n_elements > 0) {
409  if (n_elements > 1) {
410  girara_completion_row_set_color(session, ((girara_internal_completion_entry_t *) entries_current->data)->widget, GIRARA_NORMAL);
411 
412  bool next_group = FALSE;
413 
414  for (unsigned int i = 0; i < n_elements; i++) {
415  if (argument->n == GIRARA_NEXT || argument->n == GIRARA_NEXT_GROUP) {
416  GList* entry = g_list_next(entries_current);
417  if (entry == NULL) {
418  entry = g_list_first(entries);
419  }
420 
421  entries_current = entry;
422  } else if (argument->n == GIRARA_PREVIOUS || argument->n == GIRARA_PREVIOUS_GROUP) {
423  GList* entry = g_list_previous(entries_current);
424  if (entry == NULL) {
425  entry = g_list_last(entries);
426  }
427 
428  entries_current = entry;
429  }
430 
431  if (((girara_internal_completion_entry_t*) entries_current->data)->group) {
432  if (command_mode == false && (argument->n == GIRARA_NEXT_GROUP || argument->n == GIRARA_PREVIOUS_GROUP)) {
433  next_group = TRUE;
434  }
435  continue;
436  } else {
437  if (command_mode == false && (next_group == 0) && (argument->n == GIRARA_NEXT_GROUP || argument->n == GIRARA_PREVIOUS_GROUP)) {
438  continue;
439  }
440  break;
441  }
442  }
443 
444  girara_completion_row_set_color(session, ((girara_internal_completion_entry_t *) entries_current->data)->widget, GIRARA_HIGHLIGHT);
445 
446  /* hide other items */
447  unsigned int n_completion_items = 15;
448  girara_setting_get(session, "n-completion-items", &n_completion_items);
449  unsigned int uh = ceil( n_completion_items / 2);
450  unsigned int lh = floor(n_completion_items / 2);
451 
452  unsigned int current_item = g_list_position(entries, entries_current);
453 
454  GList* tmpentry = entries;
455  for (unsigned int i = 0; i < n_elements; i++) {
456  if (
457  (i >= (current_item - lh) && (i <= current_item + uh)) ||
458  (i < n_completion_items && current_item < lh) ||
459  (i >= (n_elements - n_completion_items) && (current_item >= (n_elements - uh)))
460  )
461  {
462  gtk_widget_show(GTK_WIDGET(((girara_internal_completion_entry_t*) tmpentry->data)->widget));
463  } else {
464  gtk_widget_hide(GTK_WIDGET(((girara_internal_completion_entry_t*) tmpentry->data)->widget));
465  }
466 
467  tmpentry = g_list_next(tmpentry);
468  }
469  } else {
470  gtk_widget_hide(GTK_WIDGET(((girara_internal_completion_entry_t*) (g_list_nth(entries, 0))->data)->widget));
471  }
472 
473  /* update text */
474  char* temp;
475  char* escaped_value = escape(((girara_internal_completion_entry_t *) entries_current->data)->value);
476  if (command_mode == true) {
477  char* space = (n_elements == 1) ? " " : "";
478  temp = g_strconcat(":", escaped_value, space, NULL);
479  } else {
480  temp = g_strconcat(":", previous_command, " ", escaped_value, NULL);
481  }
482 
483  gtk_entry_set_text(session->gtk.inputbar_entry, temp);
484  gtk_editable_set_position(GTK_EDITABLE(session->gtk.inputbar_entry), -1);
485  g_free(escaped_value);
486 
487  /* update previous */
488  g_free(previous_command);
489  g_free(previous_parameter);
490  previous_command = g_strdup((command_mode) ? ((girara_internal_completion_entry_t*) entries_current->data)->value : current_command);
491  previous_parameter = g_strdup((command_mode) ? current_parameter : ((girara_internal_completion_entry_t*) entries_current->data)->value);
492  previous_length = strlen(temp);
493  g_free(temp);
494  }
495 
496  g_free(current_command);
497  g_free(current_parameter);
498 
499  g_strfreev(elements);
500 
501  return false;
502 }
503 
504 static GtkEventBox*
505 girara_completion_row_create(girara_session_t* session, const char* command, const char* description, bool group)
506 {
507 #if GTK_MAJOR_VERSION == 2
508  GtkBox *col = GTK_BOX(gtk_hbox_new(FALSE, 0));
509 #else
510  GtkBox *col = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
511 #endif
512 
513  GtkEventBox *row = GTK_EVENT_BOX(gtk_event_box_new());
514 
515  GtkLabel *show_command = GTK_LABEL(gtk_label_new(NULL));
516  GtkLabel *show_description = GTK_LABEL(gtk_label_new(NULL));
517 
518  gtk_misc_set_alignment(GTK_MISC(show_command), 0.0, 0.0);
519  gtk_misc_set_alignment(GTK_MISC(show_description), 0.0, 0.0);
520 
521  if (group == true) {
522  gtk_misc_set_padding(GTK_MISC(show_command), 2, 4);
523  gtk_misc_set_padding(GTK_MISC(show_description), 2, 4);
524  } else {
525  gtk_misc_set_padding(GTK_MISC(show_command), 1, 1);
526  gtk_misc_set_padding(GTK_MISC(show_description), 1, 1);
527  }
528 
529  gtk_label_set_use_markup(show_command, TRUE);
530  gtk_label_set_use_markup(show_description, TRUE);
531 
532  gchar* c = g_markup_printf_escaped(FORMAT_COMMAND, command ? command : "");
533  gchar* d = g_markup_printf_escaped(FORMAT_DESCRIPTION, description ? description : "");
534  gtk_label_set_markup(show_command, c);
535  gtk_label_set_markup(show_description, d);
536  g_free(c);
537  g_free(d);
538 
539  if (group == true) {
540  gtk_widget_override_color(GTK_WIDGET(show_command), GTK_STATE_NORMAL, &(session->style.completion_group_foreground));
541  gtk_widget_override_color(GTK_WIDGET(show_description), GTK_STATE_NORMAL, &(session->style.completion_group_foreground));
542  gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_group_background));
543  } else {
544  gtk_widget_override_color(GTK_WIDGET(show_command), GTK_STATE_NORMAL, &(session->style.completion_foreground));
545  gtk_widget_override_color(GTK_WIDGET(show_description), GTK_STATE_NORMAL, &(session->style.completion_foreground));
546  gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_background));
547  }
548 
549  gtk_widget_override_font(GTK_WIDGET(show_command), session->style.font);
550  gtk_widget_override_font(GTK_WIDGET(show_description), session->style.font);
551 
552  gtk_box_pack_start(GTK_BOX(col), GTK_WIDGET(show_command), TRUE, TRUE, 2);
553  gtk_box_pack_start(GTK_BOX(col), GTK_WIDGET(show_description), FALSE, FALSE, 2);
554 
555  gtk_container_add(GTK_CONTAINER(row), GTK_WIDGET(col));
556  gtk_widget_show_all(GTK_WIDGET(row));
557 
558  return row;
559 }
560 
561 static void
562 girara_completion_row_set_color(girara_session_t* session, GtkEventBox* row, int mode)
563 {
564  g_return_if_fail(session != NULL);
565  g_return_if_fail(row != NULL);
566 
567  GtkBox *col = GTK_BOX(gtk_bin_get_child(GTK_BIN(row)));
568  GList* items = gtk_container_get_children(GTK_CONTAINER(col));
569  GtkLabel *cmd = GTK_LABEL(g_list_nth_data(items, 0));
570  GtkLabel *desc = GTK_LABEL(g_list_nth_data(items, 1));
571 
572  if (mode == GIRARA_HIGHLIGHT) {
573  gtk_widget_override_color(GTK_WIDGET(cmd), GTK_STATE_NORMAL, &(session->style.completion_highlight_foreground));
574  gtk_widget_override_color(GTK_WIDGET(desc), GTK_STATE_NORMAL, &(session->style.completion_highlight_foreground));
575  gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_highlight_background));
576  } else {
577  gtk_widget_override_color(GTK_WIDGET(cmd), GTK_STATE_NORMAL, &(session->style.completion_foreground));
578  gtk_widget_override_color(GTK_WIDGET(desc), GTK_STATE_NORMAL, &(session->style.completion_foreground));
579  gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_background));
580  }
581 
582  g_list_free(items);
583 }