girara
|
00001 /* See LICENSE file for license and copyright information */ 00002 00003 #include <math.h> 00004 #include <string.h> 00005 #include <stdlib.h> 00006 00007 #include "completion.h" 00008 #include "internal.h" 00009 #include "session.h" 00010 #include "settings.h" 00011 #include "datastructures.h" 00012 00013 #if GTK_MAJOR_VERSION == 2 00014 #include "gtk2-compat.h" 00015 #endif 00016 00017 static GtkEventBox* girara_completion_row_create(girara_session_t*, const char*, const char*, bool); 00018 static void girara_completion_row_set_color(girara_session_t*, GtkEventBox*, int); 00019 00020 /* completion */ 00021 struct girara_internal_completion_entry_s 00022 { 00023 bool group; 00024 char* value; 00025 GtkEventBox* widget; 00026 }; 00027 00031 struct girara_completion_element_s 00032 { 00033 char *value; 00034 char *description; 00035 }; 00036 00040 struct girara_completion_group_s 00041 { 00042 char *value; 00043 girara_list_t *elements; 00044 }; 00045 00049 struct girara_completion_s 00050 { 00051 girara_list_t *groups; 00052 }; 00053 00054 typedef struct girara_internal_completion_entry_s girara_internal_completion_entry_t; 00055 00056 static void 00057 completion_element_free(girara_completion_element_t* element) 00058 { 00059 if (element == NULL) { 00060 return; 00061 } 00062 00063 /* free element */ 00064 g_free(element->value); 00065 g_free(element->description); 00066 g_slice_free(girara_completion_element_t, element); 00067 } 00068 00069 static char* 00070 escape(const char* value) 00071 { 00072 if (value == NULL) { 00073 return NULL; 00074 } 00075 00076 GString* str = g_string_new(""); 00077 while (*value != '\0') { 00078 const char c = *value++; 00079 if (strchr("\\ \t\"\'", c) != NULL) { 00080 g_string_append_c(str, '\\'); 00081 } 00082 g_string_append_c(str, c); 00083 } 00084 00085 return g_string_free(str, FALSE); 00086 } 00087 00088 girara_completion_t* 00089 girara_completion_init() 00090 { 00091 girara_completion_t *completion = g_slice_new(girara_completion_t); 00092 completion->groups = girara_list_new2( 00093 (girara_free_function_t) girara_completion_group_free); 00094 00095 return completion; 00096 } 00097 00098 girara_completion_group_t* 00099 girara_completion_group_create(girara_session_t* UNUSED(session), const char* name) 00100 { 00101 girara_completion_group_t* group = g_slice_new(girara_completion_group_t); 00102 00103 group->value = name ? g_strdup(name) : NULL; 00104 group->elements = girara_list_new2( 00105 (girara_free_function_t) completion_element_free); 00106 00107 if (group->elements == NULL) { 00108 g_slice_free(girara_completion_group_t, group); 00109 return NULL; 00110 } 00111 00112 return group; 00113 } 00114 00115 void 00116 girara_completion_add_group(girara_completion_t* completion, girara_completion_group_t* group) 00117 { 00118 g_return_if_fail(completion != NULL); 00119 g_return_if_fail(group != NULL); 00120 00121 girara_list_append(completion->groups, group); 00122 } 00123 00124 void 00125 girara_completion_group_free(girara_completion_group_t* group) 00126 { 00127 if (group == NULL) { 00128 return; 00129 } 00130 00131 g_free(group->value); 00132 girara_list_free(group->elements); 00133 g_slice_free(girara_completion_group_t, group); 00134 } 00135 00136 void 00137 girara_completion_free(girara_completion_t* completion) 00138 { 00139 g_return_if_fail(completion != NULL); 00140 00141 girara_list_free(completion->groups); 00142 /* free completion */ 00143 g_slice_free(girara_completion_t, completion); 00144 } 00145 00146 void 00147 girara_completion_group_add_element(girara_completion_group_t* group, const char* name, const char* description) 00148 { 00149 g_return_if_fail(group != NULL); 00150 g_return_if_fail(name != NULL); 00151 00152 girara_completion_element_t* new_element = g_slice_new(girara_completion_element_t); 00153 00154 new_element->value = g_strdup(name); 00155 new_element->description = description ? g_strdup(description) : NULL; 00156 00157 girara_list_append(group->elements, new_element); 00158 } 00159 00160 bool 00161 girara_isc_completion(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event), unsigned int UNUSED(t)) 00162 { 00163 g_return_val_if_fail(session != NULL, false); 00164 00165 /* get current text */ 00166 gchar *input = gtk_editable_get_chars(GTK_EDITABLE(session->gtk.inputbar_entry), 0, -1); 00167 if (input == NULL) { 00168 return false; 00169 } 00170 00171 const size_t input_length = strlen(input); 00172 00173 if (input_length == 0 || input[0] != ':') { 00174 g_free(input); 00175 return false; 00176 } 00177 00178 gchar** elements = NULL; 00179 gint n_parameter = 0; 00180 if (input_length > 1) { 00181 if (g_shell_parse_argv(input + 1, &n_parameter, &elements, NULL) == FALSE) { 00182 g_free(input); 00183 return FALSE; 00184 } 00185 } else { 00186 elements = g_malloc0(2 * sizeof(char*)); 00187 elements[0] = g_strdup(""); 00188 } 00189 00190 if (n_parameter == 1 && input[input_length-1] == ' ') { 00191 n_parameter += 1; 00192 } 00193 00194 g_free(input); 00195 00196 /* get current values */ 00197 gchar *current_command = (elements[0] != NULL && elements[0][0] != '\0') ? g_strdup(elements[0]) : NULL; 00198 gchar *current_parameter = (elements[0] != NULL && elements[1] != NULL) ? g_strdup(elements[1]) : NULL; 00199 00200 size_t current_command_length = current_command ? strlen(current_command) : 0; 00201 00202 static GList* entries = NULL; 00203 static GList* entries_current = NULL; 00204 static char *previous_command = NULL; 00205 static char *previous_parameter = NULL; 00206 static bool command_mode = true; 00207 static size_t previous_length = 0; 00208 00209 /* delete old list iff 00210 * the completion should be hidden 00211 * the current command differs from the previous one 00212 * the current parameter differs from the previous one 00213 * no current command is given 00214 */ 00215 if ( (argument->n == GIRARA_HIDE) || 00216 (current_parameter && previous_parameter && strcmp(current_parameter, previous_parameter)) || 00217 (current_command && previous_command && strcmp(current_command, previous_command)) || 00218 input_length != previous_length 00219 ) 00220 { 00221 if (session->gtk.results != NULL) { 00222 /* destroy elements */ 00223 for (GList* element = entries; element; element = g_list_next(element)) { 00224 girara_internal_completion_entry_t* entry = (girara_internal_completion_entry_t*) element->data; 00225 00226 if (entry != NULL) { 00227 gtk_widget_destroy(GTK_WIDGET(entry->widget)); 00228 g_free(entry->value); 00229 g_slice_free(girara_internal_completion_entry_t, entry); 00230 } 00231 } 00232 00233 g_list_free(entries); 00234 entries = NULL; 00235 entries_current = NULL; 00236 00237 /* delete row box */ 00238 gtk_widget_destroy(GTK_WIDGET(session->gtk.results)); 00239 session->gtk.results = NULL; 00240 } 00241 00242 command_mode = true; 00243 00244 if (argument->n == GIRARA_HIDE) { 00245 g_free(previous_command); 00246 previous_command = NULL; 00247 00248 g_free(previous_parameter); 00249 previous_parameter = NULL; 00250 00251 g_strfreev(elements); 00252 00253 g_free(current_command); 00254 g_free(current_parameter); 00255 00256 return false; 00257 } 00258 } 00259 00260 /* create new list iff 00261 * there is no current list 00262 */ 00263 if (session->gtk.results == NULL) { 00264 #if GTK_MAJOR_VERSION == 2 00265 session->gtk.results = GTK_BOX(gtk_vbox_new(FALSE, 0)); 00266 #else 00267 session->gtk.results = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); 00268 #endif 00269 00270 if (session->gtk.results == NULL) { 00271 g_free(current_command); 00272 g_free(current_parameter); 00273 00274 g_strfreev(elements); 00275 return false; 00276 } 00277 00278 if (n_parameter <= 1) { 00279 /* based on commands */ 00280 command_mode = true; 00281 00282 /* create command rows */ 00283 GIRARA_LIST_FOREACH(session->bindings.commands, girara_command_t*, iter, command) 00284 if (current_command == NULL || 00285 (command->command != NULL && !strncmp(current_command, command->command, current_command_length)) || 00286 (command->abbr != NULL && !strncmp(current_command, command->abbr, current_command_length)) 00287 ) 00288 { 00289 /* create entry */ 00290 girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t); 00291 entry->group = FALSE; 00292 entry->value = g_strdup(command->command); 00293 entry->widget = girara_completion_row_create(session, command->command, command->description, FALSE); 00294 00295 entries = g_list_append(entries, entry); 00296 00297 /* show entry row */ 00298 gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0); 00299 } 00300 GIRARA_LIST_FOREACH_END(session->bindings.commands, girara_command_t*, iter, command); 00301 } 00302 00303 /* based on parameters */ 00304 if (n_parameter > 1 || g_list_length(entries) == 1) { 00305 /* if only one command exists try to run parameter completion */ 00306 if (g_list_length(entries) == 1) { 00307 girara_internal_completion_entry_t* entry = g_list_first(entries)->data; 00308 00309 /* unset command mode */ 00310 command_mode = false; 00311 current_command = entry->value; 00312 current_command_length = strlen(current_command); 00313 00314 /* clear list */ 00315 gtk_widget_destroy(GTK_WIDGET(entry->widget)); 00316 00317 entries = g_list_remove(entries, g_list_first(entries)->data); 00318 g_slice_free(girara_internal_completion_entry_t, entry); 00319 } 00320 00321 /* search matching command */ 00322 girara_command_t* command = NULL; 00323 GIRARA_LIST_FOREACH(session->bindings.commands, girara_command_t*, iter, command_it) 00324 if ( (current_command != NULL && command_it->command != NULL && !strncmp(current_command, command_it->command, current_command_length)) || 00325 (current_command != NULL && command_it->abbr != NULL && !strncmp(current_command, command_it->abbr, current_command_length)) 00326 ) 00327 { 00328 g_free(previous_command); 00329 previous_command = g_strdup(command_it->command); 00330 command = command_it; 00331 break; 00332 } 00333 GIRARA_LIST_FOREACH_END(session->bindings.commands, girara_command_t*, iter, command_it); 00334 00335 if (command == NULL) { 00336 g_free(current_command); 00337 g_free(current_parameter); 00338 00339 g_strfreev(elements); 00340 return false; 00341 } 00342 00343 if (command->completion == NULL) { 00344 girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t); 00345 entry->group = FALSE; 00346 entry->value = g_strdup(command->command); 00347 entry->widget = girara_completion_row_create(session, command->command, command->description, FALSE); 00348 00349 entries = g_list_append(entries, entry); 00350 00351 gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0); 00352 command_mode = true; 00353 } else { 00354 /* generate completion result 00355 * XXX: the last argument should only be current_paramater ... but 00356 * therefore the completion functions would need to handle NULL correctly 00357 * (see cc_open in zathura). */ 00358 girara_completion_t *result = command->completion(session, current_parameter ? current_parameter : ""); 00359 00360 if (result == NULL || result->groups == NULL) { 00361 g_free(current_command); 00362 g_free(current_parameter); 00363 00364 g_strfreev(elements); 00365 return false; 00366 } 00367 00368 GIRARA_LIST_FOREACH(result->groups, girara_completion_group_t*, iter, group) 00369 /* create group entry */ 00370 if (group->value != NULL) { 00371 girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t); 00372 entry->group = TRUE; 00373 entry->value = g_strdup(group->value); 00374 entry->widget = girara_completion_row_create(session, group->value, NULL, TRUE); 00375 00376 entries = g_list_append(entries, entry); 00377 00378 gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0); 00379 } 00380 00381 GIRARA_LIST_FOREACH(group->elements, girara_completion_element_t*, iter2, element) 00382 girara_internal_completion_entry_t* entry = g_slice_new(girara_internal_completion_entry_t); 00383 entry->group = FALSE; 00384 entry->value = g_strdup(element->value); 00385 entry->widget = girara_completion_row_create(session, element->value, element->description, FALSE); 00386 00387 entries = g_list_append(entries, entry); 00388 00389 gtk_box_pack_start(session->gtk.results, GTK_WIDGET(entry->widget), FALSE, FALSE, 0); 00390 00391 GIRARA_LIST_FOREACH_END(group->elements, girara_completion_element_t*, iter2, element); 00392 GIRARA_LIST_FOREACH_END(result->groups, girara_completion_group_t*, iter, group); 00393 girara_completion_free(result); 00394 00395 command_mode = false; 00396 } 00397 } 00398 00399 if (entries != NULL) { 00400 entries_current = (argument->n == GIRARA_NEXT) ? g_list_last(entries) : entries; 00401 gtk_box_pack_start(session->gtk.box, GTK_WIDGET(session->gtk.results), FALSE, FALSE, 0); 00402 gtk_widget_show(GTK_WIDGET(session->gtk.results)); 00403 } 00404 } 00405 00406 /* update entries */ 00407 unsigned int n_elements = g_list_length(entries); 00408 if (entries != NULL && n_elements > 0) { 00409 if (n_elements > 1) { 00410 girara_completion_row_set_color(session, ((girara_internal_completion_entry_t *) entries_current->data)->widget, GIRARA_NORMAL); 00411 00412 bool next_group = FALSE; 00413 00414 for (unsigned int i = 0; i < n_elements; i++) { 00415 if (argument->n == GIRARA_NEXT || argument->n == GIRARA_NEXT_GROUP) { 00416 GList* entry = g_list_next(entries_current); 00417 if (entry == NULL) { 00418 entry = g_list_first(entries); 00419 } 00420 00421 entries_current = entry; 00422 } else if (argument->n == GIRARA_PREVIOUS || argument->n == GIRARA_PREVIOUS_GROUP) { 00423 GList* entry = g_list_previous(entries_current); 00424 if (entry == NULL) { 00425 entry = g_list_last(entries); 00426 } 00427 00428 entries_current = entry; 00429 } 00430 00431 if (((girara_internal_completion_entry_t*) entries_current->data)->group) { 00432 if (command_mode == false && (argument->n == GIRARA_NEXT_GROUP || argument->n == GIRARA_PREVIOUS_GROUP)) { 00433 next_group = TRUE; 00434 } 00435 continue; 00436 } else { 00437 if (command_mode == false && (next_group == 0) && (argument->n == GIRARA_NEXT_GROUP || argument->n == GIRARA_PREVIOUS_GROUP)) { 00438 continue; 00439 } 00440 break; 00441 } 00442 } 00443 00444 girara_completion_row_set_color(session, ((girara_internal_completion_entry_t *) entries_current->data)->widget, GIRARA_HIGHLIGHT); 00445 00446 /* hide other items */ 00447 unsigned int n_completion_items = 15; 00448 girara_setting_get(session, "n-completion-items", &n_completion_items); 00449 unsigned int uh = ceil( n_completion_items / 2); 00450 unsigned int lh = floor(n_completion_items / 2); 00451 00452 unsigned int current_item = g_list_position(entries, entries_current); 00453 00454 GList* tmpentry = entries; 00455 for (unsigned int i = 0; i < n_elements; i++) { 00456 if ( 00457 (i >= (current_item - lh) && (i <= current_item + uh)) || 00458 (i < n_completion_items && current_item < lh) || 00459 (i >= (n_elements - n_completion_items) && (current_item >= (n_elements - uh))) 00460 ) 00461 { 00462 gtk_widget_show(GTK_WIDGET(((girara_internal_completion_entry_t*) tmpentry->data)->widget)); 00463 } else { 00464 gtk_widget_hide(GTK_WIDGET(((girara_internal_completion_entry_t*) tmpentry->data)->widget)); 00465 } 00466 00467 tmpentry = g_list_next(tmpentry); 00468 } 00469 } else { 00470 gtk_widget_hide(GTK_WIDGET(((girara_internal_completion_entry_t*) (g_list_nth(entries, 0))->data)->widget)); 00471 } 00472 00473 /* update text */ 00474 char* temp; 00475 char* escaped_value = escape(((girara_internal_completion_entry_t *) entries_current->data)->value); 00476 if (command_mode == true) { 00477 char* space = (n_elements == 1) ? " " : ""; 00478 temp = g_strconcat(":", escaped_value, space, NULL); 00479 } else { 00480 temp = g_strconcat(":", previous_command, " ", escaped_value, NULL); 00481 } 00482 00483 gtk_entry_set_text(session->gtk.inputbar_entry, temp); 00484 gtk_editable_set_position(GTK_EDITABLE(session->gtk.inputbar_entry), -1); 00485 g_free(escaped_value); 00486 00487 /* update previous */ 00488 g_free(previous_command); 00489 g_free(previous_parameter); 00490 previous_command = g_strdup((command_mode) ? ((girara_internal_completion_entry_t*) entries_current->data)->value : current_command); 00491 previous_parameter = g_strdup((command_mode) ? current_parameter : ((girara_internal_completion_entry_t*) entries_current->data)->value); 00492 previous_length = strlen(temp); 00493 g_free(temp); 00494 } 00495 00496 g_free(current_command); 00497 g_free(current_parameter); 00498 00499 g_strfreev(elements); 00500 00501 return false; 00502 } 00503 00504 static GtkEventBox* 00505 girara_completion_row_create(girara_session_t* session, const char* command, const char* description, bool group) 00506 { 00507 #if GTK_MAJOR_VERSION == 2 00508 GtkBox *col = GTK_BOX(gtk_hbox_new(FALSE, 0)); 00509 #else 00510 GtkBox *col = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); 00511 #endif 00512 00513 GtkEventBox *row = GTK_EVENT_BOX(gtk_event_box_new()); 00514 00515 GtkLabel *show_command = GTK_LABEL(gtk_label_new(NULL)); 00516 GtkLabel *show_description = GTK_LABEL(gtk_label_new(NULL)); 00517 00518 gtk_misc_set_alignment(GTK_MISC(show_command), 0.0, 0.0); 00519 gtk_misc_set_alignment(GTK_MISC(show_description), 0.0, 0.0); 00520 00521 if (group == true) { 00522 gtk_misc_set_padding(GTK_MISC(show_command), 2, 4); 00523 gtk_misc_set_padding(GTK_MISC(show_description), 2, 4); 00524 } else { 00525 gtk_misc_set_padding(GTK_MISC(show_command), 1, 1); 00526 gtk_misc_set_padding(GTK_MISC(show_description), 1, 1); 00527 } 00528 00529 gtk_label_set_use_markup(show_command, TRUE); 00530 gtk_label_set_use_markup(show_description, TRUE); 00531 00532 gchar* c = g_markup_printf_escaped(FORMAT_COMMAND, command ? command : ""); 00533 gchar* d = g_markup_printf_escaped(FORMAT_DESCRIPTION, description ? description : ""); 00534 gtk_label_set_markup(show_command, c); 00535 gtk_label_set_markup(show_description, d); 00536 g_free(c); 00537 g_free(d); 00538 00539 if (group == true) { 00540 gtk_widget_override_color(GTK_WIDGET(show_command), GTK_STATE_NORMAL, &(session->style.completion_group_foreground)); 00541 gtk_widget_override_color(GTK_WIDGET(show_description), GTK_STATE_NORMAL, &(session->style.completion_group_foreground)); 00542 gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_group_background)); 00543 } else { 00544 gtk_widget_override_color(GTK_WIDGET(show_command), GTK_STATE_NORMAL, &(session->style.completion_foreground)); 00545 gtk_widget_override_color(GTK_WIDGET(show_description), GTK_STATE_NORMAL, &(session->style.completion_foreground)); 00546 gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_background)); 00547 } 00548 00549 gtk_widget_override_font(GTK_WIDGET(show_command), session->style.font); 00550 gtk_widget_override_font(GTK_WIDGET(show_description), session->style.font); 00551 00552 gtk_box_pack_start(GTK_BOX(col), GTK_WIDGET(show_command), TRUE, TRUE, 2); 00553 gtk_box_pack_start(GTK_BOX(col), GTK_WIDGET(show_description), FALSE, FALSE, 2); 00554 00555 gtk_container_add(GTK_CONTAINER(row), GTK_WIDGET(col)); 00556 gtk_widget_show_all(GTK_WIDGET(row)); 00557 00558 return row; 00559 } 00560 00561 static void 00562 girara_completion_row_set_color(girara_session_t* session, GtkEventBox* row, int mode) 00563 { 00564 g_return_if_fail(session != NULL); 00565 g_return_if_fail(row != NULL); 00566 00567 GtkBox *col = GTK_BOX(gtk_bin_get_child(GTK_BIN(row))); 00568 GList* items = gtk_container_get_children(GTK_CONTAINER(col)); 00569 GtkLabel *cmd = GTK_LABEL(g_list_nth_data(items, 0)); 00570 GtkLabel *desc = GTK_LABEL(g_list_nth_data(items, 1)); 00571 00572 if (mode == GIRARA_HIGHLIGHT) { 00573 gtk_widget_override_color(GTK_WIDGET(cmd), GTK_STATE_NORMAL, &(session->style.completion_highlight_foreground)); 00574 gtk_widget_override_color(GTK_WIDGET(desc), GTK_STATE_NORMAL, &(session->style.completion_highlight_foreground)); 00575 gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_highlight_background)); 00576 } else { 00577 gtk_widget_override_color(GTK_WIDGET(cmd), GTK_STATE_NORMAL, &(session->style.completion_foreground)); 00578 gtk_widget_override_color(GTK_WIDGET(desc), GTK_STATE_NORMAL, &(session->style.completion_foreground)); 00579 gtk_widget_override_background_color(GTK_WIDGET(row), GTK_STATE_NORMAL, &(session->style.completion_background)); 00580 } 00581 00582 g_list_free(items); 00583 }