NeoMutt  2025-12-11-769-g906513
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, hclass = 0, read = 0;
191 char *linebuf = NULL, *p = NULL;
192 size_t buflen;
193 bool regen_file = false;
194 struct HashTable *dup_hashes[HC_MAX] = { 0 };
195
196 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
197 FILE *fp = mutt_file_fopen(c_history_file, "r");
198 if (!fp)
199 return;
200
201 /* If duplicate removal is enabled, build per-class hash tables to track
202 * how many times each history entry appears in the file */
203 const bool c_history_remove_dups = cs_subset_bool(NeoMutt->sub, "history_remove_dups");
204 const short c_save_history = cs_subset_number(NeoMutt->sub, "save_history");
205 if (c_history_remove_dups)
206 {
207 for (hclass = 0; hclass < HC_MAX; hclass++)
208 dup_hashes[hclass] = mutt_hash_new(MAX(10, c_save_history * 2), MUTT_HASH_STRDUP_KEYS);
209 }
210
211 /* First pass: count entries per class, detect duplicates, and validate
212 * the file format (each line is "class:entry|") */
213 line = 0;
214 while ((linebuf = mutt_file_read_line(linebuf, &buflen, fp, &line, MUTT_RL_NO_FLAGS)))
215 {
216 if ((sscanf(linebuf, "%d:%n", &hclass, &read) < 1) || (read == 0) ||
217 (*(p = linebuf + strlen(linebuf) - 1) != '|') || (hclass < 0))
218 {
219 mutt_error(_("%s:%d: Bad history file format"), c_history_file, line);
220 regen_file = true;
221 continue;
222 }
223 /* silently ignore too high class (probably newer neomutt) */
224 if (hclass >= HC_MAX)
225 continue;
226 *p = '\0';
227 if (c_history_remove_dups && (dup_hash_inc(dup_hashes[hclass], linebuf + read) > 1))
228 {
229 regen_file = true;
230 continue;
231 }
232 n[hclass]++;
233 }
234
235 if (!regen_file)
236 {
237 for (hclass = HC_FIRST; hclass < HC_MAX; hclass++)
238 {
239 if (n[hclass] > c_save_history)
240 {
241 regen_file = true;
242 break;
243 }
244 }
245 }
246
247 if (regen_file)
248 {
249 /* Second pass: rewrite the file keeping only the most recent
250 * c_save_history entries per class, omitting duplicates */
251 fp_tmp = mutt_file_mkstemp();
252 if (!fp_tmp)
253 {
254 mutt_perror(_("Can't create temporary file"));
255 goto cleanup;
256 }
257 rewind(fp);
258 line = 0;
259 while ((linebuf = mutt_file_read_line(linebuf, &buflen, fp, &line, MUTT_RL_NO_FLAGS)))
260 {
261 if ((sscanf(linebuf, "%d:%n", &hclass, &read) < 1) || (read == 0) ||
262 (*(p = linebuf + strlen(linebuf) - 1) != '|') || (hclass < 0))
263 {
264 continue;
265 }
266 if (hclass >= HC_MAX)
267 continue;
268 *p = '\0';
269 if (c_history_remove_dups && (dup_hash_dec(dup_hashes[hclass], linebuf + read) > 0))
270 {
271 continue;
272 }
273 *p = '|';
274 if (n[hclass]-- <= c_save_history)
275 fprintf(fp_tmp, "%s\n", linebuf);
276 }
277 }
278
279cleanup:
280 mutt_file_fclose(&fp);
281 FREE(&linebuf);
282 if (fp_tmp)
283 {
284 if (fflush(fp_tmp) == 0)
285 {
286 fp = mutt_file_fopen(c_history_file, "w");
287 if (fp)
288 {
289 rewind(fp_tmp);
290 mutt_file_copy_stream(fp_tmp, fp);
291 mutt_file_fclose(&fp);
292 }
293 }
294 mutt_file_fclose(&fp_tmp);
295 }
296 if (c_history_remove_dups)
297 for (hclass = 0; hclass < HC_MAX; hclass++)
298 mutt_hash_free(&dup_hashes[hclass]);
299}
300
306static void save_history(enum HistoryClass hclass, const char *str)
307{
308 if (!str || (*str == '\0')) // This shouldn't happen, but it's safer
309 return;
310
311 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
312 FILE *fp = mutt_file_fopen(c_history_file, "a");
313 if (!fp)
314 return;
315
316 char *tmp = mutt_str_dup(str);
318
319 // If tmp contains '\n' terminate it there.
320 char *nl = strchr(tmp, '\n');
321 if (nl)
322 *nl = '\0';
323
324 /* Format of a history item (1 line): "<histclass>:<string>|".
325 * We add a '|' in order to avoid lines ending with '\'. */
326 fprintf(fp, "%d:%s|\n", (int) hclass, tmp);
327
328 mutt_file_fclose(&fp);
329 FREE(&tmp);
330
332}
333
344static void remove_history_dups(enum HistoryClass hclass, const char *str)
345{
346 struct History *h = get_history(hclass);
347 if (!h)
348 return; /* disabled */
349
350 /* Remove dups from 0..last-1 compacting up. */
351 int source = 0;
352 int dest = 0;
353 while (source < h->last)
354 {
355 if (mutt_str_equal(h->hist[source], str))
356 FREE(&h->hist[source++]);
357 else
358 h->hist[dest++] = h->hist[source++];
359 }
360
361 /* Move 'last' entry up. */
362 h->hist[dest] = h->hist[source];
363 int old_last = h->last;
364 h->last = dest;
365
366 /* Fill in moved entries with NULL */
367 while (source > h->last)
368 h->hist[source--] = NULL;
369
370 /* Remove dups from last+1 .. `$history` compacting down. */
371 const short c_history = cs_subset_number(NeoMutt->sub, "history");
372 source = c_history;
373 dest = c_history;
374 while (source > old_last)
375 {
376 if (mutt_str_equal(h->hist[source], str))
377 FREE(&h->hist[source--]);
378 else
379 h->hist[dest--] = h->hist[source--];
380 }
381
382 /* Fill in moved entries with NULL */
383 while (dest > old_last)
384 h->hist[dest--] = NULL;
385}
386
394int mutt_hist_search(const char *find, enum HistoryClass hclass, struct StringArray *matches)
395{
396 if (!find || !matches)
397 return 0;
398
399 struct History *h = get_history(hclass);
400 if (!h)
401 return 0;
402
403 int cur = h->last;
404 const short c_history = cs_subset_number(NeoMutt->sub, "history");
405
406 do
407 {
408 cur--;
409 if (cur < 0)
410 cur = c_history;
411
412 if (cur == h->last)
413 break;
414
415 if (mutt_istr_find(h->hist[cur], find))
416 ARRAY_ADD(matches, h->hist[cur]);
417
418 } while (ARRAY_SIZE(matches) < c_history);
419
420 return ARRAY_SIZE(matches);
421}
422
427{
429 for (enum HistoryClass hclass = HC_FIRST; hclass < HC_MAX; hclass++)
430 {
431 struct History *h = &mod_data->histories[hclass];
432 if (!h->hist)
433 continue;
434
435 /* The array has (mod_data->old_size+1) elements */
436 for (int i = 0; i <= mod_data->old_size; i++)
437 {
438 FREE(&h->hist[i]);
439 }
440 FREE(&h->hist);
441 }
442}
443
451{
453 const short c_history = cs_subset_number(NeoMutt->sub, "history");
454 if (c_history == mod_data->old_size)
455 return;
456
457 for (enum HistoryClass hclass = HC_FIRST; hclass < HC_MAX; hclass++)
458 init_history(&mod_data->histories[hclass], mod_data->old_size);
459
460 mod_data->old_size = c_history;
461}
462
469void mutt_hist_add(enum HistoryClass hclass, const char *str, bool save)
470{
471 struct History *h = get_history(hclass);
472 if (!h)
473 return; /* disabled */
474
475 if (str && *str)
476 {
477 int prev = h->last - 1;
478 const short c_history = cs_subset_number(NeoMutt->sub, "history");
479 if (prev < 0)
480 prev = c_history;
481
482 /* don't add to prompt history:
483 * - lines beginning by a space
484 * - repeated lines */
485 if ((*str != ' ') && (!h->hist[prev] || !mutt_str_equal(h->hist[prev], str)))
486 {
487 const bool c_history_remove_dups = cs_subset_bool(NeoMutt->sub, "history_remove_dups");
488 if (c_history_remove_dups)
489 remove_history_dups(hclass, str);
490 const short c_save_history = cs_subset_number(NeoMutt->sub, "save_history");
491 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
492 if (save && (c_save_history != 0) && c_history_file)
493 save_history(hclass, str);
494 mutt_str_replace(&h->hist[h->last++], str);
495 if (h->last > c_history)
496 h->last = 0;
497 }
498 }
499 h->cur = h->last; /* reset to the last entry */
500}
501
509char *mutt_hist_next(enum HistoryClass hclass)
510{
511 struct History *h = get_history(hclass);
512 if (!h)
513 return ""; /* disabled */
514
515 int next = h->cur;
516 const short c_history = cs_subset_number(NeoMutt->sub, "history");
517 do
518 {
519 next++;
520 if (next > c_history)
521 next = 0;
522 if (next == h->last)
523 break;
524 } while (!h->hist[next]);
525
526 h->cur = next;
527 return NONULL(h->hist[h->cur]);
528}
529
537char *mutt_hist_prev(enum HistoryClass hclass)
538{
539 struct History *h = get_history(hclass);
540 if (!h)
541 return ""; /* disabled */
542
543 int prev = h->cur;
544 const short c_history = cs_subset_number(NeoMutt->sub, "history");
545 do
546 {
547 prev--;
548 if (prev < 0)
549 prev = c_history;
550 if (prev == h->last)
551 break;
552 } while (!h->hist[prev]);
553
554 h->cur = prev;
555 return NONULL(h->hist[h->cur]);
556}
557
566{
567 struct History *h = get_history(hclass);
568 if (!h)
569 return; /* disabled */
570
571 h->cur = h->last;
572}
573
580{
581 const char *const c_history_file = cs_subset_path(NeoMutt->sub, "history_file");
582 if (!c_history_file)
583 return;
584
585 FILE *fp = mutt_file_fopen(c_history_file, "r");
586 if (!fp)
587 return;
588
589 int line = 0, hclass = 0, read = 0;
590 char *linebuf = NULL, *p = NULL;
591 size_t buflen;
592
593 const char *const c_charset = cc_charset();
594 while ((linebuf = mutt_file_read_line(linebuf, &buflen, fp, &line, MUTT_RL_NO_FLAGS)))
595 {
596 read = 0;
597 if ((sscanf(linebuf, "%d:%n", &hclass, &read) < 1) || (read == 0) ||
598 (*(p = linebuf + strlen(linebuf) - 1) != '|') || (hclass < 0))
599 {
600 mutt_error(_("%s:%d: Bad history file format"), c_history_file, line);
601 continue;
602 }
603 /* silently ignore too high class (probably newer neomutt) */
604 if (hclass >= HC_MAX)
605 continue;
606 *p = '\0';
607 p = mutt_str_dup(linebuf + read);
608 if (p)
609 {
610 mutt_ch_convert_string(&p, "utf-8", c_charset, MUTT_ICONV_NO_FLAGS);
611 mutt_hist_add(hclass, p, false);
612 FREE(&p);
613 }
614 }
615
616 mutt_file_fclose(&fp);
617 FREE(&linebuf);
618}
619
632{
633 struct History *h = get_history(hclass);
634 if (!h)
635 return false; /* disabled */
636
637 return h->cur == h->last;
638}
639
648void mutt_hist_save_scratch(enum HistoryClass hclass, const char *str)
649{
650 struct History *h = get_history(hclass);
651 if (!h)
652 return; /* disabled */
653
654 /* Don't check if str has a value because the scratch buffer may contain
655 * an old garbage value that should be overwritten */
656 mutt_str_replace(&h->hist[h->last], str);
657}
658
664void mutt_hist_complete(struct Buffer *buf, enum HistoryClass hclass)
665{
666 struct StringArray matches = ARRAY_HEAD_INITIALIZER;
667
668 int match_count = mutt_hist_search(buf_string(buf), hclass, &matches);
669 if (match_count != 0)
670 {
671 if (match_count == 1)
672 {
673 const char **pstr = ARRAY_GET(&matches, 0);
674 buf_strcpy(buf, *pstr);
675 }
676 else
677 {
678 dlg_history(buf, &matches);
679 }
680 }
681
682 ARRAY_FREE(&matches);
683}
684
689{
690 if (nc->event_type != NT_CONFIG)
691 return 0;
692 if (!nc->event_data)
693 return -1;
694
695 struct EventConfig *ev_c = nc->event_data;
696
697 if (!mutt_str_equal(ev_c->name, "history"))
698 return 0;
699
701 mutt_debug(LL_DEBUG5, "history done\n");
702 return 0;
703}
#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:395
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:139
#define mutt_file_fopen(PATH, MODE)
Definition file.h:138
#define MUTT_RL_NO_FLAGS
No flags are set.
Definition file.h:40
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:688
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:337
void mutt_hash_delete(struct HashTable *table, const char *strkey, const void *data)
Remove an element from a Hash Table.
Definition hash.c:429
struct HashTable * mutt_hash_new(size_t num_elems, HashFlags flags)
Create a new Hash Table (with string keys)
Definition hash.c:261
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:379
void mutt_hash_free(struct HashTable **ptr)
Free a hash table.
Definition hash.c:459
#define MUTT_HASH_STRDUP_KEYS
make a copy of the keys
Definition hash.h:113
Read/write command history from/to a file.
HistoryClass
Type to differentiate different histories.
Definition lib.h:54
@ HC_MAX
Definition lib.h:62
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:344
char * mutt_hist_next(enum HistoryClass hclass)
Get the next string in a History.
Definition history.c:509
void mutt_hist_read_file(void)
Read the History from a file.
Definition history.c:579
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:648
void mutt_hist_complete(struct Buffer *buf, enum HistoryClass hclass)
Complete a string from a history list.
Definition history.c:664
#define HC_FIRST
Definition history.c:84
void mutt_hist_init(void)
Create a set of empty History ring buffers.
Definition history.c:450
bool mutt_hist_at_scratch(enum HistoryClass hclass)
Is the current History position at the 'scratch' place?
Definition history.c:631
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:306
void mutt_hist_add(enum HistoryClass hclass, const char *str, bool save)
Add a string to a history.
Definition history.c:469
void mutt_hist_reset_state(enum HistoryClass hclass)
Move the 'current' position to the end of the History.
Definition history.c:565
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:537
int mutt_hist_search(const char *find, enum HistoryClass hclass, struct StringArray *matches)
Find matches in a history list.
Definition history.c:394
static void shrink_histfile(void)
Read, de-dupe and write the history file.
Definition history.c:186
void mutt_hist_cleanup(void)
Free all the history lists.
Definition history.c:426
@ 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:69
int mutt_ch_convert_string(char **ps, const char *from, const char *to, uint8_t flags)
Convert a string between encodings.
Definition charset.c:817
#define MUTT_ICONV_NO_FLAGS
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:665
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:665
@ 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