mutt stable branch with some hacks
1/*
2 * Copyright (C) 1996-2000 Michael R. Elkins <me@mutt.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19#if HAVE_CONFIG_H
20# include "config.h"
21#endif
22
23#include "mutt.h"
24#include "history.h"
25#include "mutt_menu.h"
26
27#include <stdint.h>
28
29/* This history ring grows from 0..HistSize, with last marking the
30 * where new entries go:
31 * 0 the oldest entry in the ring
32 * 1 entry
33 * ...
34 * x-1 most recently entered text
35 * last-> x NULL (this will be overwritten next)
36 * x+1 NULL
37 * ...
38 * HistSize NULL
39 *
40 * Once the array fills up, it is used as a ring. last points where a new
41 * entry will go. Older entries are "up", and wrap around:
42 * 0 entry
43 * 1 entry
44 * ...
45 * y-1 most recently entered text
46 * last-> y entry (this will be overwritten next)
47 * y+1 the oldest entry in the ring
48 * ...
49 * HistSize entry
50 *
51 * When $history_remove_dups is set, duplicate entries are scanned and removed
52 * each time a new entry is added. In order to preserve the history ring size,
53 * entries 0..last are compacted up. Entries last+1..HistSize are
54 * compacted down:
55 * 0 entry
56 * 1 entry
57 * ...
58 * z-1 most recently entered text
59 * last-> z entry (this will be overwritten next)
60 * z+1 NULL
61 * z+2 NULL
62 * ...
63 * the oldest entry in the ring
64 * next oldest entry
65 * HistSize entry
66 */
67struct history
68{
69 char **hist;
70 short cur;
71 short last;
72};
73
74static const struct mapping_t HistoryHelp[] = {
75 { N_("Exit"), OP_EXIT },
76 { N_("Select"), OP_GENERIC_SELECT_ENTRY },
77 { N_("Search"), OP_SEARCH },
78 { N_("Help"), OP_HELP },
79 { NULL, 0 }
80};
81
82/* global vars used for the string-history routines */
83
84static struct history History[HC_LAST];
85static int OldSize = 0;
86
87#define GET_HISTORY(CLASS) ((CLASS >= HC_LAST) ? NULL : &History[CLASS])
88
89static void init_history (struct history *h)
90{
91 int i;
92
93 if (OldSize)
94 {
95 if (h->hist)
96 {
97 for (i = 0 ; i <= OldSize ; i ++)
98 FREE (&h->hist[i]);
99 FREE (&h->hist);
100 }
101 }
102
103 if (HistSize)
104 h->hist = safe_calloc (HistSize + 1, sizeof (char *));
105
106 h->cur = 0;
107 h->last = 0;
108}
109
110void mutt_read_histfile (void)
111{
112 FILE *f;
113 int line = 0, hclass, read;
114 char *linebuf = NULL, *p;
115 size_t buflen;
116
117 if (!HistFile)
118 return;
119
120 if ((f = fopen (HistFile, "r")) == NULL)
121 return;
122
123 while ((linebuf = mutt_read_line (linebuf, &buflen, f, &line, 0)) != NULL)
124 {
125 read = 0;
126 if (sscanf (linebuf, "%d:%n", &hclass, &read) < 1 || read == 0 ||
127 *(p = linebuf + strlen (linebuf) - 1) != '|' || hclass < 0)
128 {
129 mutt_error (_("Bad history file format (line %d)"), line);
130 break;
131 }
132 /* silently ignore too high class (probably newer mutt) */
133 if (hclass >= HC_LAST)
134 continue;
135 *p = '\0';
136 p = safe_strdup (linebuf + read);
137 if (p)
138 {
139 mutt_convert_string (&p, "utf-8", Charset, 0);
140 mutt_history_add (hclass, p, 0);
141 FREE (&p);
142 }
143 }
144
145 safe_fclose (&f);
146 FREE (&linebuf);
147}
148
149static int dup_hash_dec (HASH *dup_hash, char *s)
150{
151 struct hash_elem *elem;
152 uintptr_t count;
153
154 elem = hash_find_elem (dup_hash, s);
155 if (!elem)
156 return -1;
157
158 count = (uintptr_t)elem->data;
159 if (count <= 1)
160 {
161 hash_delete (dup_hash, s, NULL, NULL);
162 return 0;
163 }
164
165 count--;
166 elem->data = (void *)count;
167 return count;
168}
169
170static int dup_hash_inc (HASH *dup_hash, char *s)
171{
172 struct hash_elem *elem;
173 uintptr_t count;
174
175 elem = hash_find_elem (dup_hash, s);
176 if (!elem)
177 {
178 count = 1;
179 hash_insert (dup_hash, s, (void *)count);
180 return count;
181 }
182
183 count = (uintptr_t)elem->data;
184 count++;
185 elem->data = (void *)count;
186 return count;
187}
188
189static void shrink_histfile (void)
190{
191 BUFFER *tmpfname = NULL;
192 FILE *f, *tmp = NULL;
193 int n[HC_LAST] = { 0 };
194 int line, hclass, read;
195 char *linebuf = NULL, *p;
196 size_t buflen;
197 int regen_file = 0;
198 HASH *dup_hashes[HC_LAST] = { 0 };
199
200 if ((f = fopen (HistFile, "r")) == NULL)
201 return;
202
203 if (option (OPTHISTREMOVEDUPS))
204 for (hclass = 0; hclass < HC_LAST; hclass++)
205 dup_hashes[hclass] = hash_create (MAX (10, SaveHist * 2), MUTT_HASH_STRDUP_KEYS);
206
207 line = 0;
208 while ((linebuf = mutt_read_line (linebuf, &buflen, f, &line, 0)) != NULL)
209 {
210 if (sscanf (linebuf, "%d:%n", &hclass, &read) < 1 || read == 0 ||
211 *(p = linebuf + strlen (linebuf) - 1) != '|' || hclass < 0)
212 {
213 mutt_error (_("Bad history file format (line %d)"), line);
214 goto cleanup;
215 }
216 /* silently ignore too high class (probably newer mutt) */
217 if (hclass >= HC_LAST)
218 continue;
219 *p = '\0';
220 if (option (OPTHISTREMOVEDUPS) &&
221 (dup_hash_inc (dup_hashes[hclass], linebuf + read) > 1))
222 {
223 regen_file = 1;
224 continue;
225 }
226 n[hclass]++;
227 }
228
229 if (!regen_file)
230 for (hclass = HC_FIRST; hclass < HC_LAST; hclass++)
231 if (n[hclass] > SaveHist)
232 {
233 regen_file = 1;
234 break;
235 }
236
237 if (regen_file)
238 {
239 tmpfname = mutt_buffer_pool_get ();
240 mutt_buffer_mktemp (tmpfname);
241 if ((tmp = safe_fopen (mutt_b2s (tmpfname), "w+")) == NULL)
242 {
243 mutt_perror (mutt_b2s (tmpfname));
244 goto cleanup;
245 }
246 rewind (f);
247 line = 0;
248 while ((linebuf = mutt_read_line (linebuf, &buflen, f, &line, 0)) != NULL)
249 {
250 if (sscanf (linebuf, "%d:%n", &hclass, &read) < 1 || read == 0 ||
251 *(p = linebuf + strlen (linebuf) - 1) != '|' || hclass < 0)
252 {
253 mutt_error (_("Bad history file format (line %d)"), line);
254 goto cleanup;
255 }
256 if (hclass >= HC_LAST)
257 continue;
258 *p = '\0';
259 if (option (OPTHISTREMOVEDUPS) &&
260 (dup_hash_dec (dup_hashes[hclass], linebuf + read) > 0))
261 continue;
262 *p = '|';
263 if (n[hclass]-- <= SaveHist)
264 fprintf (tmp, "%s\n", linebuf);
265 }
266 }
267
268cleanup:
269 safe_fclose (&f);
270 FREE (&linebuf);
271 if (tmp != NULL)
272 {
273 if (fflush (tmp) == 0 &&
274 (f = fopen (HistFile, "w")) != NULL) /* __FOPEN_CHECKED__ */
275 {
276 rewind (tmp);
277 mutt_copy_stream (tmp, f);
278 safe_fclose (&f);
279 }
280 safe_fclose (&tmp);
281 unlink (mutt_b2s (tmpfname));
282 }
283 mutt_buffer_pool_release (&tmpfname);
284 if (option (OPTHISTREMOVEDUPS))
285 for (hclass = 0; hclass < HC_LAST; hclass++)
286 hash_destroy (&dup_hashes[hclass], NULL);
287}
288
289static void save_history (history_class_t hclass, const char *s)
290{
291 static int n = 0;
292 FILE *f;
293 char *tmp, *p;
294
295 if (!s || !*s) /* This shouldn't happen, but it's safer. */
296 return;
297
298 if ((f = fopen (HistFile, "a")) == NULL)
299 {
300 mutt_perror ("fopen");
301 return;
302 }
303
304 tmp = safe_strdup (s);
305 mutt_convert_string (&tmp, Charset, "utf-8", 0);
306
307 /* Format of a history item (1 line): "<histclass>:<string>|".
308 We add a '|' in order to avoid lines ending with '\'. */
309 fprintf (f, "%d:", (int) hclass);
310 for (p = tmp; *p; p++)
311 {
312 /* Don't copy \n as a history item must fit on one line. The string
313 shouldn't contain such a character anyway, but as this can happen
314 in practice, we must deal with that. */
315 if (*p != '\n')
316 putc ((unsigned char) *p, f);
317 }
318 fputs ("|\n", f);
319
320 safe_fclose (&f);
321 FREE (&tmp);
322
323 if (--n < 0)
324 {
325 n = SaveHist;
326 shrink_histfile();
327 }
328}
329
330/* When removing dups, we want the created "blanks" to be right below the
331 * resulting h->last position. See the comment section above 'struct history'.
332 */
333static void remove_history_dups (history_class_t hclass, const char *s)
334{
335 int source, dest, old_last;
336 struct history *h = GET_HISTORY(hclass);
337
338 if (!HistSize || !h)
339 return; /* disabled */
340
341 /* Remove dups from 0..last-1 compacting up. */
342 source = dest = 0;
343 while (source < h->last)
344 {
345 if (!mutt_strcmp (h->hist[source], s))
346 FREE (&h->hist[source++]);
347 else
348 h->hist[dest++] = h->hist[source++];
349 }
350
351 /* Move 'last' entry up. */
352 h->hist[dest] = h->hist[source];
353 old_last = h->last;
354 h->last = dest;
355
356 /* Fill in moved entries with NULL */
357 while (source > h->last)
358 h->hist[source--] = NULL;
359
360 /* Remove dups from last+1 .. HistSize compacting down. */
361 source = dest = HistSize;
362 while (source > old_last)
363 {
364 if (!mutt_strcmp (h->hist[source], s))
365 FREE (&h->hist[source--]);
366 else
367 h->hist[dest--] = h->hist[source--];
368 }
369
370 /* Fill in moved entries with NULL */
371 while (dest > old_last)
372 h->hist[dest--] = NULL;
373}
374
375void mutt_init_history(void)
376{
377 history_class_t hclass;
378
379 if (HistSize == OldSize)
380 return;
381
382 for (hclass = HC_FIRST; hclass < HC_LAST; hclass++)
383 init_history(&History[hclass]);
384
385 OldSize = HistSize;
386}
387
388void mutt_history_add (history_class_t hclass, const char *s, int save)
389{
390 int prev;
391 struct history *h = GET_HISTORY(hclass);
392
393 if (!HistSize || !h)
394 return; /* disabled */
395
396 if (*s)
397 {
398 prev = h->last - 1;
399 if (prev < 0) prev = HistSize;
400
401 /* don't add to prompt history:
402 * - lines beginning by a space
403 * - repeated lines
404 */
405 if (*s != ' ' && (!h->hist[prev] || mutt_strcmp (h->hist[prev], s) != 0))
406 {
407 if (option (OPTHISTREMOVEDUPS))
408 remove_history_dups (hclass, s);
409 if (save && SaveHist && HistFile)
410 save_history (hclass, s);
411 mutt_str_replace (&h->hist[h->last++], s);
412 if (h->last > HistSize)
413 h->last = 0;
414 }
415 }
416 h->cur = h->last; /* reset to the last entry */
417}
418
419char *mutt_history_next (history_class_t hclass)
420{
421 int next;
422 struct history *h = GET_HISTORY(hclass);
423
424 if (!HistSize || !h)
425 return (""); /* disabled */
426
427 next = h->cur;
428 do
429 {
430 next++;
431 if (next > HistSize)
432 next = 0;
433 if (next == h->last)
434 break;
435 } while (h->hist[next] == NULL);
436
437 h->cur = next;
438 return (h->hist[h->cur] ? h->hist[h->cur] : "");
439}
440
441char *mutt_history_prev (history_class_t hclass)
442{
443 int prev;
444 struct history *h = GET_HISTORY(hclass);
445
446 if (!HistSize || !h)
447 return (""); /* disabled */
448
449 prev = h->cur;
450 do
451 {
452 prev--;
453 if (prev < 0)
454 prev = HistSize;
455 if (prev == h->last)
456 break;
457 } while (h->hist[prev] == NULL);
458
459 h->cur = prev;
460 return (h->hist[h->cur] ? h->hist[h->cur] : "");
461}
462
463void mutt_reset_history_state (history_class_t hclass)
464{
465 struct history *h = GET_HISTORY(hclass);
466
467 if (!HistSize || !h)
468 return; /* disabled */
469
470 h->cur = h->last;
471}
472
473int mutt_history_at_scratch (history_class_t hclass)
474{
475 struct history *h = GET_HISTORY(hclass);
476
477 if (!HistSize || !h)
478 return 0; /* disabled */
479
480 return h->cur == h->last;
481}
482
483void mutt_history_save_scratch (history_class_t hclass, const char *s)
484{
485 struct history *h = GET_HISTORY(hclass);
486
487 if (!HistSize || !h)
488 return; /* disabled */
489
490 /* Don't check if s has a value because the scratch buffer may contain
491 * an old garbage value that should be overwritten */
492 mutt_str_replace (&h->hist[h->last], s);
493}
494
495static const char *
496history_format_str (char *dest, size_t destlen, size_t col, int cols, char op, const char *src,
497 const char *fmt, const char *ifstring, const char *elsestring,
498 unsigned long data, format_flag flags)
499{
500 char *match = (char *)data;
501
502 switch (op)
503 {
504 case 's':
505 mutt_format_s (dest, destlen, fmt, match);
506 break;
507 }
508
509 return (src);
510}
511
512static void history_entry (char *s, size_t slen, MUTTMENU *m, int num)
513{
514 char *entry = ((char **)m->data)[num];
515
516 mutt_FormatString (s, slen, 0, MuttIndexWindow->cols, "%s", history_format_str,
517 (unsigned long) entry, MUTT_FORMAT_ARROWCURSOR);
518}
519
520static void history_menu (char *buf, size_t buflen, char **matches, int match_count)
521{
522 MUTTMENU *menu;
523 int done = 0;
524 char helpstr[LONG_STRING];
525 char title[STRING];
526
527 snprintf (title, sizeof (title), _("History '%s'"), buf);
528
529 menu = mutt_new_menu (MENU_GENERIC);
530 menu->make_entry = history_entry;
531 menu->title = title;
532 menu->help = mutt_compile_help (helpstr, sizeof (helpstr), MENU_GENERIC, HistoryHelp);
533 mutt_push_current_menu (menu);
534
535 menu->max = match_count;
536 menu->data = matches;
537
538 while (!done)
539 {
540 switch (mutt_menuLoop (menu))
541 {
542 case OP_GENERIC_SELECT_ENTRY:
543 strfcpy (buf, matches[menu->current], buflen);
544 /* fall through */
545
546 case OP_EXIT:
547 done = 1;
548 break;
549 }
550 }
551
552 mutt_pop_current_menu (menu);
553 mutt_menuDestroy (&menu);
554}
555
556static int search_history (char *search_buf, history_class_t hclass, char **matches)
557{
558 struct history *h = GET_HISTORY(hclass);
559 int match_count = 0, cur;
560
561 if (!HistSize || !h)
562 return 0;
563
564 cur = h->last;
565 do
566 {
567 cur--;
568 if (cur < 0)
569 cur = HistSize;
570 if (cur == h->last)
571 break;
572 if (mutt_stristr (h->hist[cur], search_buf))
573 matches[match_count++] = h->hist[cur];
574 } while (match_count < HistSize);
575
576 return match_count;
577}
578
579void mutt_history_complete (char *buf, size_t buflen, history_class_t hclass)
580{
581 char **matches;
582 int match_count;
583
584 matches = safe_calloc (HistSize, sizeof (char *));
585 match_count = search_history (buf, hclass, matches);
586 if (match_count)
587 {
588 if (match_count == 1)
589 strfcpy (buf, matches[0], buflen);
590 else
591 history_menu (buf, buflen, matches, match_count);
592 }
593 FREE(&matches);
594}