1 /*
2 * This file is part of Siril, an astronomy image processor.
3 * Copyright (C) 2005-2011 Francois Meyer (dulle at free.fr)
4 * Copyright (C) 2012-2021 team free-astro (see more in AUTHORS file)
5 * Reference site is https://free-astro.org/index.php/Siril
6 *
7 * Siril is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Siril is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Siril. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <ctype.h>
24
25 #include "core/siril.h"
26 #include "core/proto.h"
27 #include "core/initfile.h"
28 #include "core/OS_utils.h"
29 #include "gui/utils.h"
30 #include "gui/progress_and_log.h"
31 #include "gui/utils.h"
32 #include "gui/callbacks.h"
33 #include "core/processing.h"
34 #include "core/command_list.h"
35 #include "io/sequence.h"
36
37 #include "command_line_processor.h"
38
parse_line(char * myline,int len,int * nb)39 static void parse_line(char *myline, int len, int *nb) {
40 int i = 0, wordnb = 0;
41 char string_starter = '\0'; // quotes don't split words on spaces
42 word[0] = NULL;
43
44 do {
45 while (i < len && isblank(myline[i]))
46 i++;
47 if (myline[i] == '"' || myline[i] == '\'')
48 string_starter = myline[i++];
49 if (myline[i] == '\0' || myline[i] == '\n' || myline[i] == '\r')
50 break;
51 word[wordnb++] = myline + i; // the beginning of the word
52 word[wordnb] = NULL; // put next word to NULL
53 do {
54 i++;
55 if (string_starter != '\0' && myline[i] == string_starter) {
56 string_starter = '\0';
57 break;
58 }
59 } while (i < len && (!isblank(myline[i]) || string_starter != '\0')
60 && myline[i] != '\r' && myline[i] != '\n');
61 if (myline[i] == '\0') // the end of the word and line (i == len)
62 break;
63 myline[i++] = '\0'; // the end of the word
64 } while (wordnb < MAX_COMMAND_WORDS - 1);
65 *nb = wordnb;
66 }
67
remove_trailing_cr(char * str)68 static void remove_trailing_cr(char *str) {
69 if (str == NULL || str[0] == '\0')
70 return;
71 int length = strlen(str);
72 if (str[length - 1] == '\r')
73 str[length - 1] = '\0';
74 }
75
execute_command(int wordnb)76 static int execute_command(int wordnb) {
77 // search for the command in the list
78 if (word[0] == NULL) return 1;
79 int i = G_N_ELEMENTS(commands);
80 while (g_ascii_strcasecmp(commands[--i].name, word[0])) {
81 if (i == 0) {
82 siril_log_message(_("Unknown command: '%s' or not implemented yet\n"), word[0]);
83 return 1 ;
84 }
85 }
86
87 // verify argument count
88 if (wordnb - 1 < commands[i].nbarg) {
89 siril_log_message(_("Usage: %s\n"), commands[i].usage);
90 return 1;
91 }
92
93 // verify if command is scriptable
94 if (com.script) {
95 if (!commands[i].scriptable) {
96 siril_log_message(_("This command cannot be used in a script: %s\n"), commands[i].name);
97 return 1;
98 }
99 }
100
101 // process the command
102 siril_log_color_message(_("Running command: %s\n"), "salmon", word[0]);
103 fprintf(stdout, "%lu: running command %s\n", time(NULL), word[0]);
104 return commands[i].process(wordnb);
105 }
106
update_log_icon(gboolean is_running)107 static void update_log_icon(gboolean is_running) {
108 GtkImage *image = GTK_IMAGE(lookup_widget("image_log"));
109 if (is_running)
110 gtk_image_set_from_icon_name(image, "gtk-yes", GTK_ICON_SIZE_LARGE_TOOLBAR);
111 else
112 gtk_image_set_from_icon_name(image, "gtk-no", GTK_ICON_SIZE_LARGE_TOOLBAR);
113 }
114
115 struct log_status_bar_idle_data {
116 gchar *myline;
117 int line;
118 };
119
log_status_bar_idle_callback(gpointer p)120 static gboolean log_status_bar_idle_callback(gpointer p) {
121 struct log_status_bar_idle_data *data = (struct log_status_bar_idle_data *) p;
122
123 GtkStatusbar *statusbar_script = GTK_STATUSBAR(lookup_widget("statusbar_script"));
124 gchar *status;
125 gchar *newline;
126
127 update_log_icon(TRUE);
128
129 newline = g_strdup(data->myline);
130 status = g_strdup_printf(_("Processing line %d: %s"), data->line, newline);
131
132 gtk_statusbar_push(statusbar_script, 0, status);
133 g_free(newline);
134 g_free(status);
135 g_free(data->myline);
136 free(data);
137
138 return FALSE; // only run once
139 }
140
display_command_on_status_bar(int line,char * myline)141 static void display_command_on_status_bar(int line, char *myline) {
142 if (!com.headless) {
143 struct log_status_bar_idle_data *data;
144
145 data = malloc(sizeof(struct log_status_bar_idle_data));
146 data->line = line;
147 data->myline = myline ? g_strdup(myline) : NULL;
148 gdk_threads_add_idle(log_status_bar_idle_callback, data);
149 }
150 }
151
clear_status_bar()152 static void clear_status_bar() {
153 if (!com.headless) {
154 GtkStatusbar *bar = GTK_STATUSBAR(lookup_widget("statusbar_script"));
155 gtk_statusbar_remove_all(bar, 0);
156 update_log_icon(FALSE);
157 }
158 }
159
end_script(gpointer p)160 static gboolean end_script(gpointer p) {
161 clear_status_bar();
162 set_GUI_CWD();
163
164 set_cursor_waiting(FALSE);
165 return FALSE;
166 }
167
execute_script(gpointer p)168 gpointer execute_script(gpointer p) {
169 GInputStream *input_stream = (GInputStream*) p;
170 gboolean check_required = FALSE;
171 gchar *buffer;
172 int line = 0, retval = 0;
173 int wordnb;
174 int startmem, endmem;
175 struct timeval t_start, t_end;
176
177 com.script = TRUE;
178 com.stop_script = FALSE;
179
180 gettimeofday(&t_start, NULL);
181
182 /* Now we want to save the cwd in order to come back after
183 * script execution
184 */
185 gchar *saved_cwd = g_strdup(com.wd);
186 startmem = get_available_memory() / BYTES_IN_A_MB;
187 gsize length = 0;
188 GDataInputStream *data_input = g_data_input_stream_new(input_stream);
189 while ((buffer = g_data_input_stream_read_line_utf8(data_input, &length,
190 NULL, NULL))) {
191 ++line;
192 if (com.stop_script) {
193 retval = 1;
194 g_free (buffer);
195 break;
196 }
197 /* Displays comments */
198 if (buffer[0] == '#') {
199 siril_log_color_message("%s\n", "blue", buffer);
200 g_free (buffer);
201 continue;
202 }
203
204 /* in Windows case, remove trailing CR */
205 remove_trailing_cr(buffer);
206
207 if (buffer[0] == '\0') {
208 g_free (buffer);
209 continue;
210 }
211
212
213 display_command_on_status_bar(line, buffer);
214 parse_line(buffer, length, &wordnb);
215 /* check for requires command */
216 if (!g_ascii_strcasecmp(word[0], "requires")) {
217 check_required = TRUE;
218 } else {
219 if (com.pref.script_check_requires && !check_required) {
220 siril_log_color_message(_("The \"requires\" command is missing at the top of the script file."
221 " This command is needed to check script compatibility.\n"), "red");
222 retval = 1;
223 g_free (buffer);
224 break;
225 }
226 }
227 if ((retval = execute_command(wordnb))) {
228 siril_log_message(_("Error in line %d: '%s'.\n"), line, buffer);
229 siril_log_message(_("Exiting batch processing.\n"));
230 g_free (buffer);
231 break;
232 }
233 if (waiting_for_thread()) {
234 retval = 1;
235 g_free (buffer);
236 break; // abort script on command failure
237 }
238 endmem = get_available_memory() / BYTES_IN_A_MB;
239 siril_debug_print("End of command %s, memory difference: %d MB\n", word[0], startmem - endmem);
240 startmem = endmem;
241 memset(word, 0, sizeof word);
242 g_free (buffer);
243 }
244 g_object_unref(data_input);
245 g_object_unref(input_stream);
246 com.script = FALSE;
247 /* Now we want to restore the saved cwd */
248 siril_change_dir(saved_cwd, NULL);
249 writeinitfile();
250 siril_add_idle(end_script, NULL);
251 if (!retval) {
252 siril_log_message(_("Script execution finished successfully.\n"));
253 gettimeofday(&t_end, NULL);
254 show_time_msg(t_start, t_end, _("Total execution time"));
255 } else {
256 char *msg = siril_log_message(_("Script execution failed.\n"));
257 msg[strlen(msg) - 1] = '\0';
258 set_progress_bar_data(msg, PROGRESS_DONE);
259 }
260 g_free(saved_cwd);
261 fprintf(stdout, "Script thread exiting\n");
262 return GINT_TO_POINTER(retval);
263 }
264
show_command_help_popup(GtkEntry * entry)265 static void show_command_help_popup(GtkEntry *entry) {
266 gchar *helper = NULL;
267
268 const gchar *text = gtk_entry_get_text(entry);
269 if (*text == '\0') {
270 helper = g_strdup(_("Please enter an existing command before hitting this button"));
271 } else {
272 command *current = commands;
273 gchar **command_line = g_strsplit_set(text, " ", -1);
274 while (current->process) {
275 if (!g_ascii_strcasecmp(current->name, command_line[0])) {
276 gchar **token;
277
278 token = g_strsplit_set(current->usage, " ", -1);
279 GString *str = g_string_new(token[0]);
280 str = g_string_prepend(str, "<span foreground=\"red\"><b>");
281 str = g_string_append(str, "</b>");
282 if (token[1] != NULL) {
283 str = g_string_append(str,
284 current->usage + strlen(token[0]));
285 }
286 str = g_string_append(str, "</span>\n\n\t");
287 str = g_string_append(str, _(current->definition));
288 str = g_string_append(str, "\n\n<b>");
289 str = g_string_append(str, _("Can be used in a script: "));
290 str = g_string_append(str, "<span foreground=\"red\">");
291 if (current->scriptable) {
292 str = g_string_append(str, _("YES"));
293 } else {
294 str = g_string_append(str, _("NO"));
295 }
296 str = g_string_append(str, "</span></b>");
297 helper = g_string_free(str, FALSE);
298 g_strfreev(token);
299 break;
300 }
301 current++;
302 }
303 g_strfreev(command_line);
304 }
305 if (!helper) {
306 helper = g_strdup(_("No help for this command"));
307 }
308
309 GtkWidget *popover = popover_new(lookup_widget("command"), helper);
310 #if GTK_CHECK_VERSION(3, 22, 0)
311 gtk_popover_popup(GTK_POPOVER(popover));
312 #else
313 gtk_widget_show(popover);
314 #endif
315 g_free(helper);
316 }
317
processcommand(const char * line)318 int processcommand(const char *line) {
319 int wordnb = 0;
320 gchar *myline;
321 GError *error = NULL;
322
323 if (line[0] == '\0' || line[0] == '\n')
324 return 0;
325 if (line[0] == '@') { // case of files
326 if (get_thread_run()) {
327 PRINT_ANOTHER_THREAD_RUNNING;
328 return 1;
329 }
330 if (com.script_thread)
331 g_thread_join(com.script_thread);
332
333 /* Switch to console tab */
334 control_window_switch_to_tab(OUTPUT_LOGS);
335
336 char filename[256];
337 g_strlcpy(filename, line + 1, 250);
338 expand_home_in_filename(filename, 256);
339
340 GFile *file = g_file_new_for_path(filename);
341 GInputStream *input_stream = (GInputStream *)g_file_read(file, NULL, &error);
342
343 if (input_stream == NULL) {
344 if (error != NULL) {
345 g_clear_error(&error);
346 siril_log_message(_("File [%s] does not exist\n"), filename);
347 }
348
349 g_object_unref(file);
350 return 1;
351 }
352 /* ensure that everything is closed */
353 process_close(0);
354 /* Then, run script */
355 siril_log_message(_("Starting script %s\n"), filename);
356 com.script_thread = g_thread_new("script", execute_script, input_stream);
357 g_object_unref(file);
358 } else {
359 /* Switch to console tab */
360 control_window_switch_to_tab(OUTPUT_LOGS);
361
362 myline = strdup(line);
363 int len = strlen(line);
364 parse_line(myline, len, &wordnb);
365 if (execute_command(wordnb)) {
366 siril_log_color_message(_("Command execution failed.\n"), "red");
367 if (!com.script && !com.headless) {
368 show_command_help_popup(GTK_ENTRY(lookup_widget("command")));
369 }
370 free(myline);
371 return 1;
372 }
373 free(myline);
374 }
375 set_cursor_waiting(FALSE);
376 return 0;
377 }
378
379 // loads the sequence from com.wd
load_sequence(const char * name,char ** get_filename)380 sequence *load_sequence(const char *name, char **get_filename) {
381 gchar *file = g_strdup(name);
382 gchar *altfile = NULL;
383 if (!g_str_has_suffix(name, ".seq")) {
384 str_append(&file, ".seq");
385 if (!g_str_has_suffix(name, "_"))
386 altfile = g_strdup_printf("%s_.seq", name);
387 }
388
389 if (!is_readable_file(file) && (!altfile || !is_readable_file(altfile))) {
390 if (check_seq(FALSE)) {
391 siril_log_message(_("No sequence `%s' found.\n"), name);
392 g_free(file);
393 g_free(altfile);
394 return NULL;
395 }
396 }
397
398 sequence *seq;
399 if ((seq = readseqfile(file))) {
400 if (get_filename) {
401 *get_filename = file;
402 file = NULL; // do not free
403 }
404 }
405 else if (altfile && (seq = readseqfile(altfile))) {
406 if (get_filename) {
407 *get_filename = altfile;
408 altfile = NULL; // do not free
409 }
410 }
411 if (!seq)
412 siril_log_message(_("Loading sequence `%s' failed.\n"), name);
413 else {
414 if (seq_check_basic_data(seq, FALSE) == -1) {
415 free(seq);
416 seq = NULL;
417 }
418 }
419 g_free(file);
420 g_free(altfile);
421 return seq;
422 }
423
424 /* callback functions */
425
426 #define COMPLETION_COLUMN 0
427
on_match_selected(GtkEntryCompletion * widget,GtkTreeModel * model,GtkTreeIter * iter,gpointer user_data)428 static gboolean on_match_selected(GtkEntryCompletion *widget, GtkTreeModel *model,
429 GtkTreeIter *iter, gpointer user_data) {
430 const gchar *cmd;
431 GtkEditable *e = (GtkEditable *) gtk_entry_completion_get_entry(widget);
432 gchar *s = gtk_editable_get_chars(e, 0, -1);
433 gint cur_pos = gtk_editable_get_position(e);
434 gint p = cur_pos;
435 gchar *end;
436 gint del_end_pos = -1;
437
438 gtk_tree_model_get(model, iter, COMPLETION_COLUMN, &cmd, -1);
439
440 end = s + cur_pos;
441
442 if (end) {
443 del_end_pos = end - s + 1;
444 } else {
445 del_end_pos = cur_pos;
446 }
447
448 gtk_editable_delete_text(e, 0, del_end_pos);
449 gtk_editable_insert_text(e, cmd, -1, &p);
450 gtk_editable_set_position(e, p);
451
452 return TRUE;
453 }
454
completion_match_func(GtkEntryCompletion * completion,const gchar * key,GtkTreeIter * iter,gpointer user_data)455 static gboolean completion_match_func(GtkEntryCompletion *completion,
456 const gchar *key, GtkTreeIter *iter, gpointer user_data) {
457 gboolean res = FALSE;
458 char *tag = NULL;
459
460 if (*key == '\0') return FALSE;
461
462 GtkTreeModel *model = gtk_entry_completion_get_model(completion);
463 int column = gtk_entry_completion_get_text_column(completion);
464
465 if (gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING)
466 return FALSE;
467
468 gtk_tree_model_get(model, iter, column, &tag, -1);
469
470 if (tag) {
471 char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL);
472 if (normalized) {
473 char *casefold = g_utf8_casefold(normalized, -1);
474 if (casefold) {
475 res = g_strstr_len(casefold, -1, key) != NULL;
476 }
477 g_free(casefold);
478 }
479 g_free(normalized);
480 g_free(tag);
481 }
482
483 return res;
484 }
485
init_completion_command()486 void init_completion_command() {
487 GtkEntryCompletion *completion = gtk_entry_completion_new();
488 GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING);
489 GtkTreeIter iter;
490 GtkEntry *entry = GTK_ENTRY(lookup_widget("command"));
491
492 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
493 gtk_entry_completion_set_text_column(completion, COMPLETION_COLUMN);
494 gtk_entry_completion_set_minimum_key_length(completion, 2);
495 gtk_entry_completion_set_popup_completion(completion, TRUE);
496 gtk_entry_completion_set_inline_completion(completion, TRUE);
497 gtk_entry_completion_set_popup_single_match(completion, FALSE);
498 gtk_entry_completion_set_match_func(completion, completion_match_func, NULL, NULL);
499 gtk_entry_set_completion(entry, completion);
500 g_signal_connect(G_OBJECT(completion), "match-selected", G_CALLBACK(on_match_selected), NULL);
501
502 /* Populate the completion database. */
503 command *current = commands;
504
505 while (current->process){
506 gtk_list_store_append(model, &iter);
507 gtk_list_store_set(model, &iter, COMPLETION_COLUMN, current->name, -1);
508 current++;
509 }
510 g_object_unref(model);
511 }
512
on_GtkCommandHelper_clicked(GtkButton * button,gpointer user_data)513 void on_GtkCommandHelper_clicked(GtkButton *button, gpointer user_data) {
514 show_command_help_popup((GtkEntry *)user_data);
515 }
516
517 /** Callbacks **/
518
519 /*
520 * Command line history static function
521 */
522
history_add_line(char * line)523 static void history_add_line(char *line) {
524 if (!com.cmd_history) {
525 com.cmd_hist_size = CMD_HISTORY_SIZE;
526 com.cmd_history = calloc(com.cmd_hist_size, sizeof(const char*));
527 com.cmd_hist_current = 0;
528 com.cmd_hist_display = 0;
529 }
530 com.cmd_history[com.cmd_hist_current] = line;
531 com.cmd_hist_current++;
532 // circle at the end
533 if (com.cmd_hist_current == com.cmd_hist_size)
534 com.cmd_hist_current = 0;
535 if (com.cmd_history[com.cmd_hist_current]) {
536 free(com.cmd_history[com.cmd_hist_current]);
537 com.cmd_history[com.cmd_hist_current] = NULL;
538 }
539 com.cmd_hist_display = com.cmd_hist_current;
540 }
541
on_command_activate(GtkEntry * entry,gpointer user_data)542 void on_command_activate(GtkEntry *entry, gpointer user_data) {
543 const gchar *text = gtk_entry_get_text(entry);
544 history_add_line(strdup(text));
545 if (!(processcommand(text))) {
546 gtk_entry_set_text(entry, "");
547 set_precision_switch();
548 }
549 }
550
551 /* handler for the single-line console */
on_command_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer user_data)552 gboolean on_command_key_press_event(GtkWidget *widget, GdkEventKey *event,
553 gpointer user_data) {
554 int handled = 0;
555 static GtkEntry *entry = NULL;
556 if (!entry)
557 entry = GTK_ENTRY(widget);
558 GtkEditable *editable = GTK_EDITABLE(entry);
559 int entrylength = 0;
560
561 switch (event->keyval) {
562 case GDK_KEY_Up:
563 handled = 1;
564 if (!com.cmd_history)
565 break;
566 if (com.cmd_hist_display > 0) {
567 if (com.cmd_history[com.cmd_hist_display - 1])
568 --com.cmd_hist_display;
569 // display previous entry
570 gtk_entry_set_text(entry, com.cmd_history[com.cmd_hist_display]);
571 } else if (com.cmd_history[com.cmd_hist_size - 1]) {
572 // ring back, display previous
573 com.cmd_hist_display = com.cmd_hist_size - 1;
574 gtk_entry_set_text(entry, com.cmd_history[com.cmd_hist_display]);
575 }
576 entrylength = gtk_entry_get_text_length(entry);
577 gtk_editable_set_position(editable, entrylength);
578 break;
579 case GDK_KEY_Down:
580 handled = 1;
581 if (!com.cmd_history)
582 break;
583 if (com.cmd_hist_display == com.cmd_hist_current)
584 break;
585 if (com.cmd_hist_display == com.cmd_hist_size - 1) {
586 if (com.cmd_hist_current == 0) {
587 // ring forward, end
588 gtk_entry_set_text(entry, "");
589 com.cmd_hist_display++;
590 } else if (com.cmd_history[0]) {
591 // ring forward, display next
592 com.cmd_hist_display = 0;
593 gtk_entry_set_text(entry, com.cmd_history[0]);
594 }
595 } else {
596 if (com.cmd_hist_display == com.cmd_hist_current - 1) {
597 // end
598 gtk_entry_set_text(entry, "");
599 com.cmd_hist_display++;
600 } else if (com.cmd_history[com.cmd_hist_display + 1]) {
601 // display next
602 gtk_entry_set_text(entry,
603 com.cmd_history[++com.cmd_hist_display]);
604 }
605 }
606 entrylength = gtk_entry_get_text_length(entry);
607 gtk_editable_set_position(editable, entrylength);
608 break;
609 case GDK_KEY_Page_Up:
610 case GDK_KEY_Page_Down:
611 handled = 1;
612 // go to first and last in history
613 break;
614 }
615 return (handled == 1);
616 }
617
on_command_focus_in_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)618 gboolean on_command_focus_in_event(GtkWidget *widget, GdkEvent *event,
619 gpointer user_data) {
620
621 static const gchar * const accelmap[] = {
622 "win.astrometry", NULL, NULL,
623
624 NULL /* Terminating NULL */
625 };
626 set_accel_map(accelmap);
627
628 return GDK_EVENT_PROPAGATE;
629 }
630
on_command_focus_out_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)631 gboolean on_command_focus_out_event(GtkWidget *widget, GdkEvent *event,
632 gpointer user_data) {
633
634 static const gchar * const accelmap[] = {
635 "win.astrometry", "<Primary>a", NULL,
636
637 NULL /* Terminating NULL */
638 };
639 set_accel_map(accelmap);
640
641 return GDK_EVENT_PROPAGATE;
642 }
643