NeoMutt  2025-12-11-1039-g550ac6
Teaching an old dog new tricks
DOXYGEN
Loading...
Searching...
No Matches
history.c
Go to the documentation of this file.
1
24
72
73#include "config.h"
74#include <stdbool.h>
75#include <stdint.h>
76#include <stdio.h>
77#include <string.h>
78#include "mutt/lib.h"
79#include "config/lib.h"
80#include "core/lib.h"
81#include "lib.h"
82#include "module_data.h"
83
84#define HC_FIRST HC_EXT_COMMAND
85
91static struct History *get_history(enum HistoryClass hclass)
92{
94 const short c_history = cs_subset_number(NeoMutt->sub, "history");
95 if ((hclass >= HC_MAX) || (c_history == 0))
96 return NULL;
97
98 struct History *hist = &mod_data->histories[hclass];
99 return hist->hist ? hist : NULL;
100}
101
109static void init_history(struct History *h, int old_size)
110{
111 if (old_size != 0)
112 {
113 if (h->hist)
114 {
115 for (int i = 0; i <= old_size; i++)
116 FREE(&h->hist[i]);
117 FREE(&h->hist);
118 }
119 }
120
121 const short c_history = cs_subset_number(NeoMutt->sub, "history");
122 if (c_history != 0)
123 h->hist = MUTT_MEM_CALLOC(c_history + 1, char *);
124
125 h->cur = 0;
126 h->last = 0;
127}
128
139static int dup_hash_dec(struct HashTable *dup_hash, char *str)
140{
141 struct HashElem *he = mutt_hash_find_elem(dup_hash, str);
142 if (!he)
143 return -1;
144
145 uintptr_t count = (uintptr_t) he->data;
146 if (count <= 1)
147 {
148 mutt_hash_delete(dup_hash, str, NULL);
149 return 0;
150 }
151
152 count--;
153 he->data = (void *) count;
154 return count;
155}
156
165static int dup_hash_inc(struct HashTable *dup_hash, char *str)
166{
167 uintptr_t count;
168
169 struct HashElem *he = mutt_hash_find_elem(dup_hash, str);
170 if (!he)
171 {
172 count = 1;
173 mutt_hash_insert(dup_hash, str, (void *) count);
174 return count;
175 }
176
177 count = (uintptr_t) he->data;
178 count++;
179 he->data = (void *) count;
180 return count;
181}
182
186static void shrink_histfile(void)
187{
188 FILE *fp_tmp = NULL;
189 int n[HC_MAX] = { 0 };
190 int line = 0;
191 int hclass = 0;
192 int read = 0;
193 char *linebuf = NULL;
194 char *p = NULL;
195 size_t buflen = 0;
196 bool regen_file = false;
197 struct HashTable *dup_hashes[HC_MAX] = { 0 };
198
199 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
200 FILE *fp = mutt_file_fopen(c_history_file, "r");
201 if (!fp)
202 return;
203
204 /* If duplicate removal is enabled, build per-class hash tables to track
205 * how many times each history entry appears in the file */
206 const bool c_history_remove_dups = cs_subset_bool(NeoMutt->sub, "history_remove_dups");
207 const short c_save_history = cs_subset_number(NeoMutt->sub, "save_history");
208 if (c_history_remove_dups)
209 {
210 for (hclass = 0; hclass < HC_MAX; hclass++)
211 dup_hashes[hclass] = mutt_hash_new(MAX(10, c_save_history * 2), MUTT_HASH_STRDUP_KEYS);
212 }
213
214 /* First pass: count entries per class, detect duplicates, and validate
215 * the file format (each line is "class:entry|") */
216 while ((linebuf = mutt_file_read_line(linebuf, &buflen, fp, &line, MUTT_RL_NONE)))
217 {
218 if ((sscanf(linebuf, "%d:%n", &hclass, &read) < 1) || (read == 0) ||
219 (*(p = linebuf + strlen(linebuf) - 1) != '|') || (hclass < 0))
220 {
221 mutt_error(_("%s:%d: Bad history file format"), c_history_file, line);
222 regen_file = true;
223 continue;
224 }
225 /* silently ignore too high class (probably newer neomutt) */
226 if (hclass >= HC_MAX)
227 continue;
228 *p = '\0';
229 if (c_history_remove_dups && (dup_hash_inc(dup_hashes[hclass], linebuf + read) > 1))
230 {
231 regen_file = true;
232 continue;
233 }
234 n[hclass]++;
235 }
236
237 if (!regen_file)
238 {
239 for (hclass = HC_FIRST; hclass < HC_MAX; hclass++)
240 {
241 if (n[hclass] > c_save_history)
242 {
243 regen_file = true;
244 break;
245 }
246 }
247 }
248
249 if (regen_file)
250 {
251 /* Second pass: rewrite the file keeping only the most recent
252 * c_save_history entries per class, omitting duplicates */
253 fp_tmp = mutt_file_mkstemp();
254 if (!fp_tmp)
255 {
256 mutt_perror(_("Can't create temporary file"));
257 goto cleanup;
258 }
259 fseek(fp, 0, SEEK_SET);
260 clearerr(fp);
261 line = 0;
262 while ((linebuf = mutt_file_read_line(linebuf, &buflen, fp, &line, MUTT_RL_NONE)))
263 {
264 if ((sscanf(linebuf, "%d:%n", &hclass, &read) < 1) || (read == 0) ||
265 (*(p = linebuf + strlen(linebuf) - 1) != '|') || (hclass < 0))
266 {
267 continue;
268 }
269 if (hclass >= HC_MAX)
270 continue;
271 *p = '\0';
272 if (c_history_remove_dups && (dup_hash_dec(dup_hashes[hclass], linebuf + read) > 0))
273 {
274 continue;
275 }
276 *p = '|';
277 if (n[hclass]-- <= c_save_history)
278 fprintf(fp_tmp, "%s\n", linebuf);
279 }
280 }
281
282cleanup:
283 mutt_file_fclose(&fp);
284 FREE(&linebuf);
285 if (fp_tmp)
286 {
287 if (fflush(fp_tmp) == 0)
288 {
289 fp = mutt_file_fopen(c_history_file, "w");
290 if (fp)
291 {
292 fseek(fp_tmp, 0, SEEK_SET);
293 clearerr(fp_tmp);
294 mutt_file_copy_stream(fp_tmp, fp);
295 mutt_file_fclose(&fp);
296 }
297 }
298 mutt_file_fclose(&fp_tmp);
299 }
300 if (c_history_remove_dups)
301 for (hclass = 0; hclass < HC_MAX; hclass++)
302 mutt_hash_free(&dup_hashes[hclass]);
303}
304
310static void save_history(enum HistoryClass hclass, const char *str)
311{
312 if (!str || (*str == '\0')) // This shouldn't happen, but it's safer
313 return;
314
315 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
316 FILE *fp = mutt_file_fopen(c_history_file, "a");
317 if (!fp)
318 return;
319
320 char *tmp = mutt_str_dup(str);
322
323 // If tmp contains '\n' terminate it there.
324 char *nl = strchr(tmp, '\n');
325 if (nl)
326 *nl = '\0';
327
328 /* Format of a history item (1 line): "<histclass>:<string>|".
329 * We add a '|' in order to avoid lines ending with '\'. */
330 fprintf(fp, "%d:%s|\n", (int) hclass, tmp);
331
332 mutt_file_fclose(&fp);
333 FREE(&tmp);
334
336}
337
348static void remove_history_dups(enum HistoryClass hclass, const char *str)
349{
350 struct History *h = get_history(hclass);
351 if (!h)
352 return; /* disabled */
353
354 /* Remove dups from 0..last-1 compacting up. */
355 int source = 0;
356 int dest = 0;
357 while (source < h->last)
358 {
359 if (mutt_str_equal(h->hist[source], str))
360 FREE(&h->hist[source++]);
361 else
362 h->hist[dest++] = h->hist[source++];
363 }
364
365 /* Move 'last' entry up. */
366 h->hist[dest] = h->hist[source];
367 int old_last = h->last;
368 h->last = dest;
369
370 /* Fill in moved entries with NULL */
371 while (source > h->last)
372 h->hist[source--] = NULL;
373
374 /* Remove dups from last+1 .. `$history` compacting down. */
375 const short c_history = cs_subset_number(NeoMutt->sub, "history");
376 source = c_history;
377 dest = c_history;
378 while (source > old_last)
379 {
380 if (mutt_str_equal(h->hist[source], str))
381 FREE(&h->hist[source--]);
382 else
383 h->hist[dest--] = h->hist[source--];
384 }
385
386 /* Fill in moved entries with NULL */
387 while (dest > old_last)
388 h->hist[dest--] = NULL;
389}
390
398int mutt_hist_search(const char *find, enum HistoryClass hclass, struct StringArray *matches)
399{
400 if (!find || !matches)
401 return 0;
402
403 struct History *h = get_history(hclass);
404 if (!h)
405 return 0;
406
407 int cur = h->last;
408 const short c_history = cs_subset_number(NeoMutt->sub, "history");
409
410 do
411 {
412 cur--;
413 if (cur < 0)
414 cur = c_history;
415
416 if (cur == h->last)
417 break;
418
419 if (mutt_istr_find(h->hist[cur], find))
420 ARRAY_ADD(matches, h->hist[cur]);
421
422 } while (ARRAY_SIZE(matches) < c_history);
423
424 return ARRAY_SIZE(matches);
425}
426
432{
433 for (enum HistoryClass hclass = HC_FIRST; hclass < HC_MAX; hclass++)
434 {
435 struct History *h = &mod_data->histories[hclass];
436 if (!h->hist)
437 continue;
438
439 /* The array has (mod_data->old_size+1) elements */
440 for (int i = 0; i <= mod_data->old_size; i++)
441 {
442 FREE(&h->hist[i]);
443 }
444 FREE(&h->hist);
445 }
446}
447
455void mutt_hist_init(struct HistoryModuleData *mod_data)
456{
457 const short c_history = cs_subset_number(NeoMutt->sub, "history");
458 if (c_history == mod_data->old_size)
459 return;
460
461 for (enum HistoryClass hclass = HC_FIRST; hclass < HC_MAX; hclass++)
462 init_history(&mod_data->histories[hclass], mod_data->old_size);
463
464 mod_data->old_size = c_history;
465}
466
473void mutt_hist_add(enum HistoryClass hclass, const char *str, bool save)
474{
475 struct History *h = get_history(hclass);
476 if (!h)
477 return; /* disabled */
478
479 if (str && *str)
480 {
481 int prev = h->last - 1;
482 const short c_history = cs_subset_number(NeoMutt->sub, "history");
483 if (prev < 0)
484 prev = c_history;
485
486 /* don't add to prompt history:
487 * - lines beginning by a space
488 * - repeated lines */
489 if ((*str != ' ') && (!h->hist[prev] || !mutt_str_equal(h->hist[prev], str)))
490 {
491 const bool c_history_remove_dups = cs_subset_bool(NeoMutt->sub, "history_remove_dups");
492 if (c_history_remove_dups)
493 remove_history_dups(hclass, str);
494 const short c_save_history = cs_subset_number(NeoMutt->sub, "save_history");
495 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
496 if (save && (c_save_history != 0) && c_history_file)
497 save_history(hclass, str);
498 mutt_str_replace(&h->hist[h->last++], str);
499 if (h->last > c_history)
500 h->last = 0;
501 }
502 }
503 h->cur = h->last; /* reset to the last entry */
504}
505
513char *mutt_hist_next(enum HistoryClass hclass)
514{
515 struct History *h = get_history(hclass);
516 if (!h)
517 return ""; /* disabled */
518
519 int next = h->cur;
520 const short c_history = cs_subset_number(NeoMutt->sub, "history");
521 do
522 {
523 next++;
524 if (next > c_history)
525 next = 0;
526 if (next == h->last)
527 break;
528 } while (!h->hist[next]);
529
530 h->cur = next;
531 return NONULL(h->hist[h->cur]);
532}
533
541char *mutt_hist_prev(enum HistoryClass hclass)
542{
543 struct History *h = get_history(hclass);
544 if (!h)
545 return ""; /* disabled */
546
547 int prev = h->cur;
548 const short c_history = cs_subset_number(NeoMutt->sub, "history");
549 do
550 {
551 prev--;
552 if (prev < 0)
553 prev = c_history;
554 if (prev == h->last)
555 break;
556 } while (!h->hist[prev]);
557
558 h->cur = prev;
559 return NONULL(h->hist[h->cur]);
560}
561
570{
571 struct History *h = get_history(hclass);
572 if (!h)
573 return; /* disabled */
574
575 h->cur = h->last;
576}
577
584{
585 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
586 if (!c_history_file)
587 return;
588
589 FILE *fp = mutt_file_fopen(c_history_file, "r");
590 if (!fp)
591 return;
592
593 int line = 0;
594 int hclass = 0;
595 int read = 0;
596 char *linebuf = NULL;
597 char *p = NULL;
598 size_t buflen = 0;
599
600 const char *const c_charset = cc_charset();
601 while ((linebuf = mutt_file_read_line(linebuf, &buflen, fp, &line, MUTT_RL_NONE)))
602 {
603 read = 0;
604 if ((sscanf(linebuf, "%d:%n", &hclass, &read) < 1) || (read == 0) ||
605 (*(p = linebuf + strlen(linebuf) - 1) != '|') || (hclass < 0))
606 {
607 mutt_error(_("%s:%d: Bad history file format"), c_history_file, line);
608 continue;
609 }
610 /* silently ignore too high class (probably newer neomutt) */
611 if (hclass >= HC_MAX)
612 continue;
613 *p = '\0';
614 p = mutt_str_dup(linebuf + read);
615 if (p)
616 {
617 mutt_ch_convert_string(&p, "utf-8", c_charset, MUTT_ICONV_NONE);
618 mutt_hist_add(hclass, p, false);
619 FREE(&p);
620 }
621 }
622
623 mutt_file_fclose(&fp);
624 FREE(&linebuf);
625}
626
639{
640 struct History *h = get_history(hclass);
641 if (!h)
642 return false; /* disabled */
643
644 return h->cur == h->last;
645}
646
655void mutt_hist_save_scratch(enum HistoryClass hclass, const char *str)
656{
657 struct History *h = get_history(hclass);
658 if (!h)
659 return; /* disabled */
660
661 /* Don't check if str has a value because the scratch buffer may contain
662 * an old garbage value that should be overwritten */
663 mutt_str_replace(&h->hist[h->last], str);
664}
665
671void mutt_hist_complete(struct Buffer *buf, enum HistoryClass hclass)
672{
673 struct StringArray matches = ARRAY_HEAD_INITIALIZER;
674
675 int match_count = mutt_hist_search(buf_string(buf), hclass, &matches);
676 if (match_count != 0)
677 {
678 if (match_count == 1)
679 {
680 const char **pstr = ARRAY_GET(&matches, 0);
681 buf_strcpy(buf, *pstr);
682 }
683 else
684 {
685 dlg_history(buf, &matches);
686 }
687 }
688
689 ARRAY_FREE(&matches);
690}
691
696{
697 if (nc->event_type != NT_CONFIG)
698 return 0;
699 if (!nc->event_data)
700 return -1;
701
702 struct EventConfig *ev_c = nc->event_data;
703
704 if (!mutt_str_equal(ev_c->name, "history"))
705 return 0;
706
708 mutt_hist_init(mod_data);
709 mutt_debug(LL_DEBUG5, "history done\n");
710 return 0;
711}
#define ARRAY_ADD(head, elem)
Add an element at the end of the array.
Definition array.h:157
#define ARRAY_SIZE(head)
The number of elements stored.
Definition array.h:87
#define ARRAY_FREE(head)
Release all memory.
Definition array.h:209
#define ARRAY_GET(head, idx)
Return the element at index.
Definition array.h:109
#define ARRAY_HEAD_INITIALIZER
Static initializer for arrays.
Definition array.h:58
size_t buf_strcpy(struct Buffer *buf, const char *s)
Copy a string into a Buffer.
Definition buffer.c:401
static const char * buf_string(const struct Buffer *buf)
Convert a buffer to a const char * "string".
Definition buffer.h:96
short cs_subset_number(const struct ConfigSubset *sub, const char *name)
Get a number config item by name.
Definition helpers.c:143
const char * cs_subset_path(const struct ConfigSubset *sub, const char *name)
Get a path config item by name.
Definition helpers.c:168
bool cs_subset_bool(const struct ConfigSubset *sub, const char *name)
Get a boolean config item by name.
Definition helpers.c:47
Convenience wrapper for the config headers.
const char * cc_charset(void)
Get the cached value of $charset.
Convenience wrapper for the core headers.
int mutt_file_copy_stream(FILE *fp_in, FILE *fp_out)
Copy the contents of one file into another.
Definition file.c:224
char * mutt_file_read_line(char *line, size_t *size, FILE *fp, int *line_num, ReadLineFlags flags)
Read a line from a file.
Definition file.c:678
#define mutt_file_fclose(FP)
Definition file.h:144
#define mutt_file_fopen(PATH, MODE)
Definition file.h:143
@ MUTT_RL_NONE
No flags are set.
Definition file.h:43
void dlg_history(struct Buffer *buf, struct StringArray *matches)
Select an item from a history list -.
#define mutt_error(...)
Definition logging2.h:94
#define mutt_debug(LEVEL,...)
Definition logging2.h:91
#define mutt_perror(...)
Definition logging2.h:95
int main_hist_observer(struct NotifyCallback *nc)
Notification that a Config Variable has change - Implements observer_t -.
Definition history.c:695
struct HashElem * mutt_hash_insert(struct HashTable *table, const char *strkey, void *data)
Add a new element to the Hash Table (with string keys).
Definition hash.c:338
void mutt_hash_delete(struct HashTable *table, const char *strkey, const void *data)
Remove an element from a Hash Table.
Definition hash.c:430
struct HashTable * mutt_hash_new(size_t num_elems, HashFlags flags)
Create a new Hash Table (with string keys).
Definition hash.c:262
struct HashElem * mutt_hash_find_elem(const struct HashTable *table, const char *strkey)
Find the HashElem in a Hash Table element using a key.
Definition hash.c:380
void mutt_hash_free(struct HashTable **ptr)
Free a hash table.
Definition hash.c:460
@ MUTT_HASH_STRDUP_KEYS
make a copy of the keys
Definition hash.h:117
Read/write command history from/to a file.
HistoryClass
Type to differentiate different histories.
Definition lib.h:55
@ HC_MAX
Definition lib.h:63
History private Module data.
static void init_history(struct History *h, int old_size)
Set up a new History ring buffer.
Definition history.c:109
static void remove_history_dups(enum HistoryClass hclass, const char *str)
De-dupe the history.
Definition history.c:348
void mutt_hist_init(struct HistoryModuleData *mod_data)
Create a set of empty History ring buffers.
Definition history.c:455
char * mutt_hist_next(enum HistoryClass hclass)
Get the next string in a History.
Definition history.c:513
void mutt_hist_read_file(void)
Read the History from a file.
Definition history.c:583
static int dup_hash_inc(struct HashTable *dup_hash, char *str)
Increase the refcount of a history string.
Definition history.c:165
void mutt_hist_save_scratch(enum HistoryClass hclass, const char *str)
Save a temporary string to the History.
Definition history.c:655
void mutt_hist_complete(struct Buffer *buf, enum HistoryClass hclass)
Complete a string from a history list.
Definition history.c:671
#define HC_FIRST
Definition history.c:84
bool mutt_hist_at_scratch(enum HistoryClass hclass)
Is the current History position at the 'scratch' place?
Definition history.c:638
static struct History * get_history(enum HistoryClass hclass)
Get a particular history.
Definition history.c:91
static void save_history(enum HistoryClass hclass, const char *str)
Save one history string to a file.
Definition history.c:310
void mutt_hist_add(enum HistoryClass hclass, const char *str, bool save)
Add a string to a history.
Definition history.c:473
void mutt_hist_reset_state(enum HistoryClass hclass)
Move the 'current' position to the end of the History.
Definition history.c:569
static int dup_hash_dec(struct HashTable *dup_hash, char *str)
Decrease the refcount of a history string.
Definition history.c:139
char * mutt_hist_prev(enum HistoryClass hclass)
Get the previous string in a History.
Definition history.c:541
int mutt_hist_search(const char *find, enum HistoryClass hclass, struct StringArray *matches)
Find matches in a history list.
Definition history.c:398
static void shrink_histfile(void)
Read, de-dupe and write the history file.
Definition history.c:186
void mutt_hist_cleanup(struct HistoryModuleData *mod_data)
Free all the history lists.
Definition history.c:431
@ LL_DEBUG5
Log at debug level 5.
Definition logging2.h:49
#define FREE(x)
Free memory and set the pointer to NULL.
Definition memory.h:68
#define MUTT_MEM_CALLOC(n, type)
Definition memory.h:52
#define MAX(a, b)
Return the maximum of two values.
Definition memory.h:38
@ MODULE_ID_HISTORY
ModuleHistory, History
Definition module_api.h:70
int mutt_ch_convert_string(char **ps, const char *from, const char *to, uint8_t flags)
Convert a string between encodings.
Definition charset.c:819
#define MUTT_ICONV_NONE
No flags are set.
Definition charset.h:66
Convenience wrapper for the library headers.
#define _(a)
Definition message.h:28
char * mutt_str_dup(const char *str)
Copy a string, safely.
Definition string.c:257
bool mutt_str_equal(const char *a, const char *b)
Compare two strings.
Definition string.c:666
const char * mutt_istr_find(const char *haystack, const char *needle)
Find first occurrence of string (ignoring case).
Definition string.c:528
char * mutt_str_replace(char **p, const char *s)
Replace one string with another.
Definition string.c:284
void * neomutt_get_module_data(struct NeoMutt *n, enum ModuleId id)
Get the private data for a Module.
Definition neomutt.c:666
@ NT_CONFIG
Config has changed, NotifyConfig, EventConfig.
Definition notify_type.h:43
#define NONULL(x)
Definition string2.h:44
String manipulation buffer.
Definition buffer.h:36
A config-change event.
Definition subset.h:70
const char * name
Name of config item that changed.
Definition subset.h:72
The item stored in a Hash Table.
Definition hash.h:44
void * data
User-supplied data.
Definition hash.h:47
A Hash Table.
Definition hash.h:99
History private Module data.
Definition module_data.h:44
struct History histories[HC_MAX]
Command histories, one for each HistoryClass.
Definition module_data.h:46
int old_size
The previous number of history entries to save.
Definition module_data.h:47
Saved list of user-entered commands/searches.
Definition module_data.h:34
short cur
Current history item.
Definition module_data.h:36
short last
Last history item.
Definition module_data.h:37
char ** hist
Array of history items.
Definition module_data.h:35
Container for Accounts, Notifications.
Definition neomutt.h:41
struct ConfigSubset * sub
Inherited config items.
Definition neomutt.h:49
Data passed to a notification function.
Definition observer.h:34
void * event_data
Data from notify_send().
Definition observer.h:38
enum NotifyType event_type
Send: Event type, e.g. NT_ACCOUNT.
Definition observer.h:36
#define mutt_file_mkstemp()
Definition tmp.h:36