1 /* history.c -- functions for the history system */
2
3 /*
4 * This file is part of CliFM
5 *
6 * Copyright (C) 2016-2021, L. Abramovich <johndoe.arch@outlook.com>
7 * All rights reserved.
8
9 * CliFM is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * CliFM is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 * MA 02110-1301, USA.
23 */
24
25 #include "helpers.h"
26
27 #include <errno.h>
28 #include <stdio.h>
29 #include <string.h>
30 #include <time.h>
31 #include <readline/history.h>
32
33 #include "aux.h"
34 #include "checks.h"
35 #include "exec.h"
36 #include "history.h"
37 #include "init.h"
38 #include "misc.h"
39 #include "messages.h"
40 #include "file_operations.h"
41
42 /* Log COMM into LOG_FILE (global) */
43 int
log_function(char ** comm)44 log_function(char **comm)
45 {
46 /* If cmd logs are disabled, allow only "log" commands */
47 if (!logs_enabled) {
48 if (comm && comm[0] && strcmp(comm[0], "log") != 0)
49 return EXIT_SUCCESS;
50 }
51
52 if (!config_ok)
53 return EXIT_FAILURE;
54
55 int clear_log = 0;
56
57 /* If the command was just 'log' */
58 if (comm && comm[0] && *comm[0] == 'l' && strcmp(comm[0], "log") == 0 && !comm[1]) {
59 FILE *log_fp;
60 log_fp = fopen(log_file, "r");
61 if (!log_fp) {
62 _err(0, NOPRINT_PROMPT, "%s: log: '%s': %s\n",
63 PROGRAM_NAME, log_file, strerror(errno));
64 return EXIT_FAILURE;
65 } else {
66 size_t line_size = 0;
67 char *line_buff = (char *)NULL;
68
69 while (getline(&line_buff, &line_size, log_fp) > 0)
70 fputs(line_buff, stdout);
71
72 free(line_buff);
73 fclose(log_fp);
74 return EXIT_SUCCESS;
75 }
76 }
77
78 else if (comm && comm[0] && *comm[0] == 'l' && strcmp(comm[0], "log") == 0
79 && comm[1]) {
80 if (*comm[1] == 'c' && strcmp(comm[1], "clear") == 0)
81 clear_log = 1;
82 else if (*comm[1] == 's' && strcmp(comm[1], "status") == 0) {
83 printf(_("Logs %s\n"), (logs_enabled) ? _("enabled")
84 : _("disabled"));
85 return EXIT_SUCCESS;
86 } else if (*comm[1] == 'o' && strcmp(comm[1], "on") == 0) {
87 if (logs_enabled) {
88 puts(_("Logs already enabled"));
89 } else {
90 logs_enabled = 1;
91 puts(_("Logs successfully enabled"));
92 }
93 return EXIT_SUCCESS;
94 } else if (*comm[1] == 'o' && strcmp(comm[1], "off") == 0) {
95 /* If logs were already disabled, just exit. Otherwise, log
96 * the "log off" command */
97 if (!logs_enabled) {
98 puts(_("Logs already disabled"));
99 return EXIT_SUCCESS;
100 } else {
101 puts(_("Logs succesfully disabled"));
102 logs_enabled = 0;
103 }
104 }
105 }
106
107 /* Construct the log line */
108 if (!last_cmd) {
109 if (!logs_enabled) {
110 /* When cmd logs are disabled, "log clear" and "log off" are
111 * the only commands that can reach this code */
112 if (clear_log) {
113 last_cmd = (char *)xnmalloc(10, sizeof(char));
114 strcpy(last_cmd, "log clear");
115 } else {
116 last_cmd = (char *)xnmalloc(8, sizeof(char));
117 strcpy(last_cmd, "log off");
118 }
119 } else {
120 /* last_cmd should never be NULL if logs are enabled (this
121 * variable is set immediately after taking valid user input
122 * in the prompt function). However ... */
123 last_cmd = (char *)xnmalloc(23, sizeof(char));
124 strcpy(last_cmd, _("Error getting command!"));
125 }
126 }
127
128 char *date = get_date();
129 size_t log_len = strlen(date) + strlen(ws[cur_ws].path)
130 + strlen(last_cmd) + 6;
131 char *full_log = (char *)xnmalloc(log_len, sizeof(char));
132
133 snprintf(full_log, log_len, "[%s] %s:%s\n", date, ws[cur_ws].path, last_cmd);
134
135 free(date);
136 free(last_cmd);
137 last_cmd = (char *)NULL;
138
139 /* Write the log into LOG_FILE */
140 FILE *log_fp;
141 /* If not 'log clear', append the log to the existing logs */
142
143 if (!clear_log)
144 log_fp = fopen(log_file, "a");
145 else
146 /* Else, overwrite the log file leaving only the 'log clear'
147 * command */
148 log_fp = fopen(log_file, "w+");
149
150 if (!log_fp) {
151 _err('e', PRINT_PROMPT, "%s: log: '%s': %s\n", PROGRAM_NAME,
152 log_file, strerror(errno));
153 free(full_log);
154 return EXIT_FAILURE;
155 } else { /* If LOG_FILE was correctly opened, write the log */
156 fputs(full_log, log_fp);
157 free(full_log);
158 fclose(log_fp);
159
160 return EXIT_SUCCESS;
161 }
162 }
163
164 /* Handle the error message MSG. Store MSG in an array of error
165 * messages, write it into an error log file, and print it immediately
166 * (if PRINT is zero (NOPRINT_PROMPT) or tell the next prompt, if PRINT
167 * is one to do it (PRINT_PROMPT)). Messages wrote to the error log file
168 * have the following format:
169 * "[date] msg", where 'date' is YYYY-MM-DDTHH:MM:SS */
170 void
log_msg(char * _msg,int print)171 log_msg(char *_msg, int print)
172 {
173 if (!_msg)
174 return;
175
176 size_t msg_len = strlen(_msg);
177 if (msg_len == 0)
178 return;
179
180 /* Store messages (for current session only) in an array, so that
181 * the user can check them via the 'msg' command */
182 msgs_n++;
183 messages = (char **)xrealloc(messages, (size_t)(msgs_n + 1) * sizeof(char *));
184 messages[msgs_n - 1] = savestring(_msg, msg_len);
185 messages[msgs_n] = (char *)NULL;
186
187 if (print) /* PRINT_PROMPT */
188 /* The next prompt will take care of printing the message */
189 print_msg = 1;
190 else /* NOPRINT_PROMPT */
191 /* Print the message directly here */
192 fputs(_msg, stderr);
193
194 /* If the config dir cannot be found or if msg log file isn't set
195 * yet... This will happen if an error occurs before running
196 * init_config(), for example, if the user's home cannot be found */
197 if (!config_ok || !msg_log_file || !*msg_log_file)
198 return;
199
200 FILE *msg_fp = fopen(msg_log_file, "a");
201 if (!msg_fp) {
202 /* Do not log this error: We might incur in an infinite loop
203 * trying to access a file that cannot be accessed */
204 fprintf(stderr, "%s: %s: %s\n", PROGRAM_NAME, msg_log_file,
205 strerror(errno));
206 fputs("Press any key to continue... ", stdout);
207 xgetchar();
208 putchar('\n');
209 } else {
210 /* Write message to messages file: [date] msg */
211 time_t rawtime = time(NULL);
212 struct tm tm;
213 localtime_r(&rawtime, &tm);
214 char date[64] = "";
215
216 strftime(date, sizeof(date), "%b %d %H:%M:%S %Y", &tm);
217 fprintf(msg_fp, "[%d-%d-%dT%d:%d:%d] ", tm.tm_year + 1900,
218 tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
219 fputs(_msg, msg_fp);
220 fclose(msg_fp);
221 }
222 }
223
224 int
save_dirhist(void)225 save_dirhist(void)
226 {
227 if (!dirhist_file)
228 return EXIT_FAILURE;
229
230 if (!old_pwd || !old_pwd[0])
231 return EXIT_SUCCESS;
232
233 FILE *fp = fopen(dirhist_file, "w");
234 if (!fp) {
235 fprintf(stderr, _("%s: Could not save directory history: %s\n"),
236 PROGRAM_NAME, strerror(errno));
237 return EXIT_FAILURE;
238 }
239
240 int i;
241 for (i = 0; i < dirhist_total_index; i++) {
242 /* Exclude invalid entries */
243 if (!old_pwd[i] || *old_pwd[i] == _ESC)
244 continue;
245 fprintf(fp, "%s\n", old_pwd[i]);
246 }
247
248 fclose(fp);
249 return EXIT_SUCCESS;
250 }
251
252 /* Add DIR_PATH to visited directory history (old_pwd) */
253 void
add_to_dirhist(const char * dir_path)254 add_to_dirhist(const char *dir_path)
255 {
256 /* static size_t end_counter = 11, mid_counter = 11; */
257
258 /* If already at the end of dirhist, add new entry */
259 if (dirhist_cur_index + 1 >= dirhist_total_index) {
260 /* Do not add anything if new path equals last entry in
261 * directory history */
262 if ((dirhist_total_index - 1) >= 0 && old_pwd[dirhist_total_index - 1] && *(dir_path + 1) == *(old_pwd[dirhist_total_index - 1] + 1) && strcmp(dir_path, old_pwd[dirhist_total_index - 1]) == 0)
263 return;
264
265 /* Realloc only once per 10 operations */
266 /* if (end_counter > 10) {
267 end_counter = 1; */
268 /* 20: Realloc dirhist_total + (2 * 10) */
269 old_pwd = (char **)xrealloc(old_pwd,
270 (size_t)(dirhist_total_index + 2) * sizeof(char *));
271 /* }
272
273 end_counter++; */
274
275 dirhist_cur_index = dirhist_total_index;
276 old_pwd[dirhist_total_index++] = savestring(dir_path, strlen(dir_path));
277 old_pwd[dirhist_total_index] = (char *)NULL;
278 }
279
280 /* I not at the end of dirhist, add previous AND new entry */
281 else {
282 /* if (mid_counter > 10) {
283 mid_counter = 1; */
284 /* 30: Realloc dirhist_total + (3 * 10) */
285 old_pwd = (char **)xrealloc(old_pwd,
286 (size_t)(dirhist_total_index + 3) * sizeof(char *));
287 /* }
288
289 mid_counter++; */
290
291 old_pwd[dirhist_total_index++] = savestring(
292 old_pwd[dirhist_cur_index],
293 strlen(old_pwd[dirhist_cur_index]));
294
295 dirhist_cur_index = dirhist_total_index;
296 old_pwd[dirhist_total_index++] = savestring(dir_path, strlen(dir_path));
297
298 old_pwd[dirhist_total_index] = (char *)NULL;
299 }
300 }
301
302 static int
reload_history(char ** comm)303 reload_history(char **comm)
304 {
305 clear_history();
306 read_history(hist_file);
307 history_truncate_file(hist_file, max_hist);
308
309 /* Update the history array */
310 if (get_history() != 0)
311 return EXIT_FAILURE;
312
313 if (log_function(comm) != 0)
314 return EXIT_FAILURE;
315
316 return EXIT_SUCCESS;
317 }
318
319 int
history_function(char ** comm)320 history_function(char **comm)
321 {
322 if (!config_ok) {
323 fprintf(stderr, _("%s: History function disabled\n"), PROGRAM_NAME);
324 return EXIT_FAILURE;
325 }
326
327 /* If no arguments, print the history list */
328 if (args_n == 0) {
329 size_t i;
330 for (i = 0; i < current_hist_n; i++)
331 printf(" %zu %s\n", i + 1, history[i]);
332 return EXIT_SUCCESS;
333 }
334
335 if (args_n >= 1 && *comm[1] == 'e' && strcmp(comm[1], "edit") == 0) {
336 struct stat attr;
337 if (stat(hist_file, &attr) == -1) {
338 fprintf(stderr, "%s: history: %s: %s\n", PROGRAM_NAME, hist_file,
339 strerror(errno));
340 return EXIT_FAILURE;
341 }
342 time_t mtime_bfr = (time_t)attr.st_mtime;
343
344 int ret = EXIT_SUCCESS;
345
346 /* If there is an argument... */
347 if (comm[2]) {
348 char *cmd[] = {comm[2], hist_file, NULL};
349 ret = launch_execve(cmd, FOREGROUND, E_NOSTDERR);
350 } else {
351 /* If no application was passed as 2nd argument */
352 open_in_foreground = 1;
353 ret = open_file(hist_file);
354 open_in_foreground = 0;
355 }
356
357 if (ret != EXIT_SUCCESS)
358 return EXIT_FAILURE;
359
360 /* Get modification time after opening the config file */
361 stat(config_file, &attr);
362 /* If modification times differ, the file was modified after being
363 * opened */
364 if (mtime_bfr != (time_t)attr.st_mtime)
365 return reload_history(comm);
366
367 return EXIT_SUCCESS;
368 }
369
370 /* If 'history clear', guess what, clear the history list! */
371 if (args_n == 1 && *comm[1] == 'c' && strcmp(comm[1], "clear") == 0) {
372 FILE *hist_fp = fopen(hist_file, "w+");
373 if (!hist_fp) {
374 _err(0, NOPRINT_PROMPT, "%s: history: %s: %s\n",
375 PROGRAM_NAME, hist_file, strerror(errno));
376 return EXIT_FAILURE;
377 }
378
379 /* Do not create an empty file */
380 fprintf(hist_fp, "%s %s\n", comm[0], comm[1]);
381 fclose(hist_fp);
382
383 /* Reset readline history */
384 return reload_history(comm);
385 /* clear_history();
386 read_history(hist_file);
387 history_truncate_file(hist_file, max_hist);
388
389 // Update the history array
390 int exit_status = EXIT_SUCCESS;
391
392 if (get_history() != 0)
393 exit_status = EXIT_FAILURE;
394
395 if (log_function(comm) != 0)
396 exit_code = EXIT_FAILURE; */
397
398 // return exit_status;
399 }
400
401 /* If 'history -n', print the last -n elements */
402 if (args_n == 1 && comm[1][0] == '-' && is_number(comm[1] + 1)) {
403 int num = atoi(comm[1] + 1);
404
405 if (num < 0 || num > (int)current_hist_n)
406 num = (int)current_hist_n;
407
408 size_t i;
409 for (i = current_hist_n - (size_t)num; i < current_hist_n; i++)
410 printf("%zu %s\n", i + 1, history[i]);
411
412 return EXIT_SUCCESS;
413 }
414
415 /* None of the above */
416 puts(_(HISTORY_USAGE));
417 return EXIT_SUCCESS;
418 }
419
420 static int
exec_hist_cmd(char ** cmd)421 exec_hist_cmd(char **cmd)
422 {
423 int i;
424 int exit_status = EXIT_SUCCESS;
425
426 char **alias_cmd = check_for_alias(cmd);
427 if (alias_cmd) {
428 /* If an alias is found, check_for_alias frees CMD and
429 * returns alias_cmd in its place to be executed by
430 * exec_cmd() */
431
432 if (exec_cmd(alias_cmd) != 0)
433 exit_status = EXIT_FAILURE;
434
435 for (i = 0; alias_cmd[i]; i++)
436 free(alias_cmd[i]);
437 free(alias_cmd);
438 alias_cmd = (char **)NULL;
439 } else {
440 if (exec_cmd(cmd) != 0)
441 exit_status = EXIT_FAILURE;
442
443 for (i = 0; cmd[i]; i++)
444 free(cmd[i]);
445 free(cmd);
446 }
447
448 return exit_status;
449 }
450
451 /* Takes as argument the history cmd less the first exclamation mark.
452 * Example: if exec_cmd() gets "!-10" it pass to this function "-10",
453 * that is, comm + 1 */
454 int
run_history_cmd(const char * cmd)455 run_history_cmd(const char *cmd)
456 {
457 /* If "!n" */
458 int exit_status = EXIT_SUCCESS;
459 size_t old_args = args_n;
460
461 if (is_number(cmd)) {
462 int num = atoi(cmd);
463
464 if (num <= 0 || num > (int)current_hist_n) {
465 fprintf(stderr, _("%s: !%d: event not found\n"), PROGRAM_NAME, num);
466 return EXIT_FAILURE;
467 }
468
469 if (record_cmd(history[num - 1]))
470 add_to_cmdhist(history[num - 1]);
471
472 char **cmd_hist = parse_input_str(history[num - 1]);
473 if (!cmd_hist) {
474 fprintf(stderr, _("%s: Error parsing history command\n"),
475 PROGRAM_NAME);
476 return EXIT_FAILURE;
477 }
478
479 exit_status = exec_hist_cmd(cmd_hist);
480 args_n = old_args;
481 return exit_status;
482 }
483
484 /* If "!!", execute the last command */
485 if (*cmd == '!' && !cmd[1]) {
486
487 if (record_cmd(history[current_hist_n - 1]))
488 add_to_cmdhist(history[current_hist_n - 1]);
489
490 char **cmd_hist = parse_input_str(history[current_hist_n - 1]);
491 if (!cmd_hist) {
492 fprintf(stderr, _("%s: Error parsing history command\n"),
493 PROGRAM_NAME);
494 return EXIT_FAILURE;
495 }
496
497 exit_status = exec_hist_cmd(cmd_hist);
498 args_n = old_args;
499 return exit_status;
500 }
501
502 /* If "!-n" */
503 if (*cmd == '-') {
504 /* If not number or zero or bigger than max... */
505 int acmd = atoi(cmd + 1);
506
507 if (!is_number(cmd + 1) || acmd == 0 || acmd > (int)current_hist_n - 1) {
508 fprintf(stderr, _("%s: !%s: Event not found\n"), PROGRAM_NAME, cmd);
509 return EXIT_FAILURE;
510 }
511
512 char **cmd_hist = parse_input_str(history[current_hist_n - (size_t)acmd - 1]);
513 if (cmd_hist) {
514 exit_status = exec_hist_cmd(cmd_hist);
515
516 if (record_cmd(history[current_hist_n - (size_t)acmd - 1]))
517 add_to_cmdhist(history[current_hist_n - (size_t)acmd - 1]);
518
519 args_n = old_args;
520 return exit_status;
521 }
522
523 if (record_cmd(history[current_hist_n - (size_t)acmd - 1]))
524 add_to_cmdhist(history[current_hist_n - (size_t)acmd - 1]);
525
526 fprintf(stderr, _("%s: Error parsing history command\n"), PROGRAM_NAME);
527 return EXIT_FAILURE;
528 }
529
530 /* If !STRING */
531 if ((*cmd >= 'a' && *cmd <= 'z') || (*cmd >= 'A' && *cmd <= 'Z')) {
532 size_t len = strlen(cmd);
533 size_t i;
534 for (i = 0; history[i]; i++) {
535 if (*cmd == *history[i] && strncmp(cmd, history[i], len) == 0) {
536 char **cmd_hist = parse_input_str(history[i]);
537 if (!cmd_hist)
538 continue;
539
540 exit_status = exec_hist_cmd(cmd_hist);
541 args_n = old_args;
542 return exit_status;
543 }
544 }
545
546 fprintf(stderr, _("%s: !%s: Event not found\n"), PROGRAM_NAME, cmd);
547 return EXIT_FAILURE;
548 }
549
550 puts(_(HISTEXEC_USAGE));
551 return EXIT_SUCCESS;
552 }
553
554 int
get_history(void)555 get_history(void)
556 {
557 if (!config_ok)
558 return EXIT_FAILURE;
559
560 if (current_hist_n == 0) { /* Coming from main() */
561 history = (char **)xcalloc(1, sizeof(char *));
562 } else { /* Only true when comming from 'history clear' */
563 size_t i;
564 for (i = 0; history[i]; i++)
565 free(history[i]);
566 history = (char **)xrealloc(history, 1 * sizeof(char *));
567 current_hist_n = 0;
568 }
569
570 FILE *hist_fp = fopen(hist_file, "r");
571 if (!hist_fp) {
572 _err('e', PRINT_PROMPT, "%s: history: '%s': %s\n",
573 PROGRAM_NAME, hist_file, strerror(errno));
574 return EXIT_FAILURE;
575 }
576
577 size_t line_size = 0;
578 char *line_buff = (char *)NULL;
579 ssize_t line_len = 0;
580
581 while ((line_len = getline(&line_buff, &line_size, hist_fp)) > 0) {
582 line_buff[line_len - 1] = '\0';
583 history = (char **)xrealloc(history, (current_hist_n + 2) * sizeof(char *));
584 history[current_hist_n++] = savestring(line_buff, (size_t)line_len);
585 }
586
587 curhistindex = current_hist_n ? current_hist_n - 1 : 0;
588 history[current_hist_n] = (char *)NULL;
589 free(line_buff);
590 fclose(hist_fp);
591 return EXIT_SUCCESS;
592 }
593
594 void
add_to_cmdhist(const char * cmd)595 add_to_cmdhist(const char *cmd)
596 {
597 if (!cmd)
598 return;
599
600 /* For readline */
601 add_history(cmd);
602
603 if (config_ok)
604 append_history(1, hist_file);
605
606 /* For us */
607 /* Add the new input to the history array */
608 size_t cmd_len = strlen(cmd);
609 history = (char **)xrealloc(history, (size_t)(current_hist_n + 2) * sizeof(char *));
610 history[current_hist_n++] = savestring(cmd, cmd_len);
611 history[current_hist_n] = (char *)NULL;
612 }
613
614 /* Returns 1 if INPUT should be stored in history and 0 if not */
615 int
record_cmd(char * input)616 record_cmd(char *input)
617 {
618 /* NULL input */
619 if (!input || !*input)
620 return 0;
621
622 if (SELFORPARENT(input))
623 return 0;
624
625 /* Blank lines */
626 unsigned int blank = 1;
627 char *p = input;
628
629 while (*p) {
630 if (*p > ' ') {
631 blank = 0;
632 break;
633 }
634 p++;
635 }
636
637 if (blank)
638 return 0;
639
640 /* Rewind the pointer to the beginning of the input line */
641 p = input;
642
643 /* Commands starting with space */
644 if (*p == ' ')
645 return 0;
646
647 switch (*p) {
648 /* Do not record single ELN's */
649 case '0': /* fallthrough */
650 case '1': /* fallthrough */
651 case '2': /* fallthrough */
652 case '3': /* fallthrough */
653 case '4': /* fallthrough */
654 case '5': /* fallthrough */
655 case '6': /* fallthrough */
656 case '7': /* fallthrough */
657 case '8': /* fallthrough */
658 case '9':
659 if (is_number(p))
660 return 0;
661 break;
662
663 case '.': /* . */
664 if (!*(p + 1))
665 return 0;
666 break;
667
668 /* Do not record the history command itself */
669 case 'h':
670 if (*(p + 1) == 'i' && strcmp(p, "history") == 0)
671 return 0;
672 break;
673
674 case 'r': /* rf command */
675 if (*(p + 1) == 'f' && !*(p + 2))
676 return 0;
677 break;
678
679 /* Do not record exit commands */
680 case 'q':
681 if (*(p + 1) == '\0' || strcmp(p, "quit") == 0)
682 return 0;
683 break;
684
685 case 'Q':
686 if (*(p + 1) == '\0')
687 return 0;
688 break;
689
690 case 'e':
691 if (*(p + 1) == 'x' && strcmp(p, "exit") == 0)
692 return 0;
693 break;
694
695 /* case 'z':
696 if (*(p + 1) == 'z' && *(p + 2) == '\0')
697 return 0;
698 break;
699
700 case 's':
701 if (*(p + 1) == 'a' && strcmp(p, "salir") == 0)
702 return 0;
703 break;
704
705 case 'c':
706 if (*(p + 1) == 'h' && strcmp(p, "chau") == 0)
707 return 0;
708 break; */
709
710 default: break;
711 }
712
713 /* History */
714 if (*p == '!' && (_ISDIGIT(*(p + 1)) || (*(p + 1) == '-'
715 && _ISDIGIT(*(p + 2))) || ((*(p + 1) == '!') && *(p + 2) == '\0')))
716 return 0;
717
718 /* Consequtively equal commands in history */
719 if (history && history[current_hist_n - 1]
720 && *p == *history[current_hist_n - 1]
721 && strcmp(p, history[current_hist_n - 1]) == 0)
722 return 0;
723
724 return 1;
725 }
726