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