1 /*
2 * Copyright (C) 1984-2021 Mark Nudelman
3 *
4 * You may distribute under the terms of either the GNU General Public
5 * License or the Less License, as specified in the README file.
6 *
7 * For more information, see the README file.
8 */
9
10
11 /*
12 * Functions which manipulate the command buffer.
13 * Used only by command() and related functions.
14 */
15
16 #include "less.h"
17 #include "cmd.h"
18 #include "charset.h"
19 #if HAVE_STAT
20 #include <sys/stat.h>
21 #endif
22
23 extern int sc_width;
24 extern int utf_mode;
25 extern int no_hist_dups;
26 extern int marks_modified;
27
28 static char cmdbuf[CMDBUF_SIZE]; /* Buffer for holding a multi-char command */
29 static int cmd_col; /* Current column of the cursor */
30 static int prompt_col; /* Column of cursor just after prompt */
31 static char *cp; /* Pointer into cmdbuf */
32 static int cmd_offset; /* Index into cmdbuf of first displayed char */
33 static int literal; /* Next input char should not be interpreted */
34 static int updown_match = -1; /* Prefix length in up/down movement */
35
36 #if TAB_COMPLETE_FILENAME
37 static int cmd_complete LESSPARAMS((int action));
38 /*
39 * These variables are statics used by cmd_complete.
40 */
41 static int in_completion = 0;
42 static char *tk_text;
43 static char *tk_original;
44 static char *tk_ipoint;
45 static char *tk_trial = NULL;
46 static struct textlist tk_tlist;
47 #endif
48
49 static int cmd_left();
50 static int cmd_right();
51
52 #if SPACES_IN_FILENAMES
53 public char openquote = '"';
54 public char closequote = '"';
55 #endif
56
57 #if CMD_HISTORY
58
59 /* History file */
60 #define HISTFILE_FIRST_LINE ".less-history-file:"
61 #define HISTFILE_SEARCH_SECTION ".search"
62 #define HISTFILE_SHELL_SECTION ".shell"
63 #define HISTFILE_MARK_SECTION ".mark"
64
65 /*
66 * A mlist structure represents a command history.
67 */
68 struct mlist
69 {
70 struct mlist *next;
71 struct mlist *prev;
72 struct mlist *curr_mp;
73 char *string;
74 int modified;
75 };
76
77 /*
78 * These are the various command histories that exist.
79 */
80 struct mlist mlist_search =
81 { &mlist_search, &mlist_search, &mlist_search, NULL, 0 };
82 public void *ml_search = (void *) &mlist_search;
83
84 struct mlist mlist_examine =
85 { &mlist_examine, &mlist_examine, &mlist_examine, NULL, 0 };
86 public void *ml_examine = (void *) &mlist_examine;
87
88 #if SHELL_ESCAPE || PIPEC
89 struct mlist mlist_shell =
90 { &mlist_shell, &mlist_shell, &mlist_shell, NULL, 0 };
91 public void *ml_shell = (void *) &mlist_shell;
92 #endif
93
94 #else /* CMD_HISTORY */
95
96 /* If CMD_HISTORY is off, these are just flags. */
97 public void *ml_search = (void *)1;
98 public void *ml_examine = (void *)2;
99 #if SHELL_ESCAPE || PIPEC
100 public void *ml_shell = (void *)3;
101 #endif
102
103 #endif /* CMD_HISTORY */
104
105 /*
106 * History for the current command.
107 */
108 static struct mlist *curr_mlist = NULL;
109 static int curr_cmdflags;
110
111 static char cmd_mbc_buf[MAX_UTF_CHAR_LEN];
112 static int cmd_mbc_buf_len;
113 static int cmd_mbc_buf_index;
114
115
116 /*
117 * Reset command buffer (to empty).
118 */
119 public void
cmd_reset(VOID_PARAM)120 cmd_reset(VOID_PARAM)
121 {
122 cp = cmdbuf;
123 *cp = '\0';
124 cmd_col = 0;
125 cmd_offset = 0;
126 literal = 0;
127 cmd_mbc_buf_len = 0;
128 updown_match = -1;
129 }
130
131 /*
132 * Clear command line.
133 */
134 public void
clear_cmd(VOID_PARAM)135 clear_cmd(VOID_PARAM)
136 {
137 cmd_col = prompt_col = 0;
138 cmd_mbc_buf_len = 0;
139 updown_match = -1;
140 }
141
142 /*
143 * Display a string, usually as a prompt for input into the command buffer.
144 */
145 public void
cmd_putstr(s)146 cmd_putstr(s)
147 constant char *s;
148 {
149 LWCHAR prev_ch = 0;
150 LWCHAR ch;
151 constant char *endline = s + strlen(s);
152 while (*s != '\0')
153 {
154 char *ns = (char *) s;
155 int width;
156 ch = step_char(&ns, +1, endline);
157 while (s < ns)
158 putchr(*s++);
159 if (!utf_mode)
160 width = 1;
161 else if (is_composing_char(ch) || is_combining_char(prev_ch, ch))
162 width = 0;
163 else
164 width = is_wide_char(ch) ? 2 : 1;
165 cmd_col += width;
166 prompt_col += width;
167 prev_ch = ch;
168 }
169 }
170
171 /*
172 * How many characters are in the command buffer?
173 */
174 public int
len_cmdbuf(VOID_PARAM)175 len_cmdbuf(VOID_PARAM)
176 {
177 char *s = cmdbuf;
178 char *endline = s + strlen(s);
179 int len = 0;
180
181 while (*s != '\0')
182 {
183 step_char(&s, +1, endline);
184 len++;
185 }
186 return (len);
187 }
188
189 /*
190 * Common part of cmd_step_right() and cmd_step_left().
191 * {{ Returning pwidth and bswidth separately is a historical artifact
192 * since they're always the same. Maybe clean this up someday. }}
193 */
194 static char *
cmd_step_common(p,ch,len,pwidth,bswidth)195 cmd_step_common(p, ch, len, pwidth, bswidth)
196 char *p;
197 LWCHAR ch;
198 int len;
199 int *pwidth;
200 int *bswidth;
201 {
202 char *pr;
203 int width;
204
205 if (len == 1)
206 {
207 pr = prchar((int) ch);
208 width = (int) strlen(pr);
209 } else
210 {
211 pr = prutfchar(ch);
212 if (is_composing_char(ch))
213 width = 0;
214 else if (is_ubin_char(ch))
215 width = (int) strlen(pr);
216 else
217 {
218 LWCHAR prev_ch = step_char(&p, -1, cmdbuf);
219 if (is_combining_char(prev_ch, ch))
220 width = 0;
221 else
222 width = is_wide_char(ch) ? 2 : 1;
223 }
224 }
225 if (pwidth != NULL)
226 *pwidth = width;
227 if (bswidth != NULL)
228 *bswidth = width;
229 return (pr);
230 }
231
232 /*
233 * Step a pointer one character right in the command buffer.
234 */
235 static char *
cmd_step_right(pp,pwidth,bswidth)236 cmd_step_right(pp, pwidth, bswidth)
237 char **pp;
238 int *pwidth;
239 int *bswidth;
240 {
241 char *p = *pp;
242 LWCHAR ch = step_char(pp, +1, p + strlen(p));
243
244 return cmd_step_common(p, ch, *pp - p, pwidth, bswidth);
245 }
246
247 /*
248 * Step a pointer one character left in the command buffer.
249 */
250 static char *
cmd_step_left(pp,pwidth,bswidth)251 cmd_step_left(pp, pwidth, bswidth)
252 char **pp;
253 int *pwidth;
254 int *bswidth;
255 {
256 char *p = *pp;
257 LWCHAR ch = step_char(pp, -1, cmdbuf);
258
259 return cmd_step_common(*pp, ch, p - *pp, pwidth, bswidth);
260 }
261
262 /*
263 * Put the cursor at "home" (just after the prompt),
264 * and set cp to the corresponding char in cmdbuf.
265 */
266 static void
cmd_home(VOID_PARAM)267 cmd_home(VOID_PARAM)
268 {
269 while (cmd_col > prompt_col)
270 {
271 int width, bswidth;
272
273 cmd_step_left(&cp, &width, &bswidth);
274 while (bswidth-- > 0)
275 putbs();
276 cmd_col -= width;
277 }
278
279 cp = &cmdbuf[cmd_offset];
280 }
281
282 /*
283 * Repaint the line from cp onwards.
284 * Then position the cursor just after the char old_cp (a pointer into cmdbuf).
285 */
286 public void
cmd_repaint(old_cp)287 cmd_repaint(old_cp)
288 constant char *old_cp;
289 {
290 /*
291 * Repaint the line from the current position.
292 */
293 if (old_cp == NULL)
294 {
295 old_cp = cp;
296 cmd_home();
297 }
298 clear_eol();
299 while (*cp != '\0')
300 {
301 char *np = cp;
302 int width;
303 char *pr = cmd_step_right(&np, &width, NULL);
304 if (cmd_col + width >= sc_width)
305 break;
306 cp = np;
307 putstr(pr);
308 cmd_col += width;
309 }
310 while (*cp != '\0')
311 {
312 char *np = cp;
313 int width;
314 char *pr = cmd_step_right(&np, &width, NULL);
315 if (width > 0)
316 break;
317 cp = np;
318 putstr(pr);
319 }
320
321 /*
322 * Back up the cursor to the correct position.
323 */
324 while (cp > old_cp)
325 cmd_left();
326 }
327
328 /*
329 * Shift the cmdbuf display left a half-screen.
330 */
331 static void
cmd_lshift(VOID_PARAM)332 cmd_lshift(VOID_PARAM)
333 {
334 char *s;
335 char *save_cp;
336 int cols;
337
338 /*
339 * Start at the first displayed char, count how far to the
340 * right we'd have to move to reach the center of the screen.
341 */
342 s = cmdbuf + cmd_offset;
343 cols = 0;
344 while (cols < (sc_width - prompt_col) / 2 && *s != '\0')
345 {
346 int width;
347 cmd_step_right(&s, &width, NULL);
348 cols += width;
349 }
350 while (*s != '\0')
351 {
352 int width;
353 char *ns = s;
354 cmd_step_right(&ns, &width, NULL);
355 if (width > 0)
356 break;
357 s = ns;
358 }
359
360 cmd_offset = (int) (s - cmdbuf);
361 save_cp = cp;
362 cmd_home();
363 cmd_repaint(save_cp);
364 }
365
366 /*
367 * Shift the cmdbuf display right a half-screen.
368 */
369 static void
cmd_rshift(VOID_PARAM)370 cmd_rshift(VOID_PARAM)
371 {
372 char *s;
373 char *save_cp;
374 int cols;
375
376 /*
377 * Start at the first displayed char, count how far to the
378 * left we'd have to move to traverse a half-screen width
379 * of displayed characters.
380 */
381 s = cmdbuf + cmd_offset;
382 cols = 0;
383 while (cols < (sc_width - prompt_col) / 2 && s > cmdbuf)
384 {
385 int width;
386 cmd_step_left(&s, &width, NULL);
387 cols += width;
388 }
389
390 cmd_offset = (int) (s - cmdbuf);
391 save_cp = cp;
392 cmd_home();
393 cmd_repaint(save_cp);
394 }
395
396 /*
397 * Move cursor right one character.
398 */
399 static int
cmd_right(VOID_PARAM)400 cmd_right(VOID_PARAM)
401 {
402 char *pr;
403 char *ncp;
404 int width;
405
406 if (*cp == '\0')
407 {
408 /* Already at the end of the line. */
409 return (CC_OK);
410 }
411 ncp = cp;
412 pr = cmd_step_right(&ncp, &width, NULL);
413 if (cmd_col + width >= sc_width)
414 cmd_lshift();
415 else if (cmd_col + width == sc_width - 1 && cp[1] != '\0')
416 cmd_lshift();
417 cp = ncp;
418 cmd_col += width;
419 putstr(pr);
420 while (*cp != '\0')
421 {
422 pr = cmd_step_right(&ncp, &width, NULL);
423 if (width > 0)
424 break;
425 putstr(pr);
426 cp = ncp;
427 }
428 return (CC_OK);
429 }
430
431 /*
432 * Move cursor left one character.
433 */
434 static int
cmd_left(VOID_PARAM)435 cmd_left(VOID_PARAM)
436 {
437 char *ncp;
438 int width = 0;
439 int bswidth = 0;
440
441 if (cp <= cmdbuf)
442 {
443 /* Already at the beginning of the line */
444 return (CC_OK);
445 }
446 ncp = cp;
447 while (ncp > cmdbuf)
448 {
449 cmd_step_left(&ncp, &width, &bswidth);
450 if (width > 0)
451 break;
452 }
453 if (cmd_col < prompt_col + width)
454 cmd_rshift();
455 cp = ncp;
456 cmd_col -= width;
457 while (bswidth-- > 0)
458 putbs();
459 return (CC_OK);
460 }
461
462 /*
463 * Insert a char into the command buffer, at the current position.
464 */
465 static int
cmd_ichar(cs,clen)466 cmd_ichar(cs, clen)
467 char *cs;
468 int clen;
469 {
470 char *s;
471
472 if (strlen(cmdbuf) + clen >= sizeof(cmdbuf)-1)
473 {
474 /* No room in the command buffer for another char. */
475 bell();
476 return (CC_ERROR);
477 }
478
479 /*
480 * Make room for the new character (shift the tail of the buffer right).
481 */
482 for (s = &cmdbuf[strlen(cmdbuf)]; s >= cp; s--)
483 s[clen] = s[0];
484 /*
485 * Insert the character into the buffer.
486 */
487 for (s = cp; s < cp + clen; s++)
488 *s = *cs++;
489 /*
490 * Reprint the tail of the line from the inserted char.
491 */
492 updown_match = -1;
493 cmd_repaint(cp);
494 cmd_right();
495 return (CC_OK);
496 }
497
498 /*
499 * Backspace in the command buffer.
500 * Delete the char to the left of the cursor.
501 */
502 static int
cmd_erase(VOID_PARAM)503 cmd_erase(VOID_PARAM)
504 {
505 char *s;
506 int clen;
507
508 if (cp == cmdbuf)
509 {
510 /*
511 * Backspace past beginning of the buffer:
512 * this usually means abort the command.
513 */
514 return (CC_QUIT);
515 }
516 /*
517 * Move cursor left (to the char being erased).
518 */
519 s = cp;
520 cmd_left();
521 clen = (int) (s - cp);
522
523 /*
524 * Remove the char from the buffer (shift the buffer left).
525 */
526 for (s = cp; ; s++)
527 {
528 s[0] = s[clen];
529 if (s[0] == '\0')
530 break;
531 }
532
533 /*
534 * Repaint the buffer after the erased char.
535 */
536 updown_match = -1;
537 cmd_repaint(cp);
538
539 /*
540 * We say that erasing the entire command string causes us
541 * to abort the current command, if CF_QUIT_ON_ERASE is set.
542 */
543 if ((curr_cmdflags & CF_QUIT_ON_ERASE) && cp == cmdbuf && *cp == '\0')
544 return (CC_QUIT);
545 return (CC_OK);
546 }
547
548 /*
549 * Delete the char under the cursor.
550 */
551 static int
cmd_delete(VOID_PARAM)552 cmd_delete(VOID_PARAM)
553 {
554 if (*cp == '\0')
555 {
556 /* At end of string; there is no char under the cursor. */
557 return (CC_OK);
558 }
559 /*
560 * Move right, then use cmd_erase.
561 */
562 cmd_right();
563 cmd_erase();
564 return (CC_OK);
565 }
566
567 /*
568 * Delete the "word" to the left of the cursor.
569 */
570 static int
cmd_werase(VOID_PARAM)571 cmd_werase(VOID_PARAM)
572 {
573 if (cp > cmdbuf && cp[-1] == ' ')
574 {
575 /*
576 * If the char left of cursor is a space,
577 * erase all the spaces left of cursor (to the first non-space).
578 */
579 while (cp > cmdbuf && cp[-1] == ' ')
580 (void) cmd_erase();
581 } else
582 {
583 /*
584 * If the char left of cursor is not a space,
585 * erase all the nonspaces left of cursor (the whole "word").
586 */
587 while (cp > cmdbuf && cp[-1] != ' ')
588 (void) cmd_erase();
589 }
590 return (CC_OK);
591 }
592
593 /*
594 * Delete the "word" under the cursor.
595 */
596 static int
cmd_wdelete(VOID_PARAM)597 cmd_wdelete(VOID_PARAM)
598 {
599 if (*cp == ' ')
600 {
601 /*
602 * If the char under the cursor is a space,
603 * delete it and all the spaces right of cursor.
604 */
605 while (*cp == ' ')
606 (void) cmd_delete();
607 } else
608 {
609 /*
610 * If the char under the cursor is not a space,
611 * delete it and all nonspaces right of cursor (the whole word).
612 */
613 while (*cp != ' ' && *cp != '\0')
614 (void) cmd_delete();
615 }
616 return (CC_OK);
617 }
618
619 /*
620 * Delete all chars in the command buffer.
621 */
622 static int
cmd_kill(VOID_PARAM)623 cmd_kill(VOID_PARAM)
624 {
625 if (cmdbuf[0] == '\0')
626 {
627 /* Buffer is already empty; abort the current command. */
628 return (CC_QUIT);
629 }
630 cmd_offset = 0;
631 cmd_home();
632 *cp = '\0';
633 updown_match = -1;
634 cmd_repaint(cp);
635
636 /*
637 * We say that erasing the entire command string causes us
638 * to abort the current command, if CF_QUIT_ON_ERASE is set.
639 */
640 if (curr_cmdflags & CF_QUIT_ON_ERASE)
641 return (CC_QUIT);
642 return (CC_OK);
643 }
644
645 /*
646 * Select an mlist structure to be the current command history.
647 */
648 public void
set_mlist(mlist,cmdflags)649 set_mlist(mlist, cmdflags)
650 void *mlist;
651 int cmdflags;
652 {
653 #if CMD_HISTORY
654 curr_mlist = (struct mlist *) mlist;
655 curr_cmdflags = cmdflags;
656
657 /* Make sure the next up-arrow moves to the last string in the mlist. */
658 if (curr_mlist != NULL)
659 curr_mlist->curr_mp = curr_mlist;
660 #endif
661 }
662
663 #if CMD_HISTORY
664 /*
665 * Move up or down in the currently selected command history list.
666 * Only consider entries whose first updown_match chars are equal to
667 * cmdbuf's corresponding chars.
668 */
669 static int
cmd_updown(action)670 cmd_updown(action)
671 int action;
672 {
673 constant char *s;
674 struct mlist *ml;
675
676 if (curr_mlist == NULL)
677 {
678 /*
679 * The current command has no history list.
680 */
681 bell();
682 return (CC_OK);
683 }
684
685 if (updown_match < 0)
686 {
687 updown_match = (int) (cp - cmdbuf);
688 }
689
690 /*
691 * Find the next history entry which matches.
692 */
693 for (ml = curr_mlist->curr_mp;;)
694 {
695 ml = (action == EC_UP) ? ml->prev : ml->next;
696 if (ml == curr_mlist)
697 {
698 /*
699 * We reached the end (or beginning) of the list.
700 */
701 break;
702 }
703 if (strncmp(cmdbuf, ml->string, updown_match) == 0)
704 {
705 /*
706 * This entry matches; stop here.
707 * Copy the entry into cmdbuf and echo it on the screen.
708 */
709 curr_mlist->curr_mp = ml;
710 s = ml->string;
711 if (s == NULL)
712 s = "";
713 cmd_offset = 0;
714 cmd_home();
715 clear_eol();
716 strcpy(cmdbuf, s);
717 for (cp = cmdbuf; *cp != '\0'; )
718 cmd_right();
719 return (CC_OK);
720 }
721 }
722 /*
723 * We didn't find a history entry that matches.
724 */
725 bell();
726 return (CC_OK);
727 }
728 #endif
729
730 /*
731 *
732 */
733 static void
ml_link(mlist,ml)734 ml_link(mlist, ml)
735 struct mlist *mlist;
736 struct mlist *ml;
737 {
738 ml->next = mlist;
739 ml->prev = mlist->prev;
740 mlist->prev->next = ml;
741 mlist->prev = ml;
742 }
743
744 /*
745 *
746 */
747 static void
ml_unlink(ml)748 ml_unlink(ml)
749 struct mlist *ml;
750 {
751 ml->prev->next = ml->next;
752 ml->next->prev = ml->prev;
753 }
754
755 /*
756 * Add a string to an mlist.
757 */
758 public void
cmd_addhist(mlist,cmd,modified)759 cmd_addhist(mlist, cmd, modified)
760 struct mlist *mlist;
761 constant char *cmd;
762 int modified;
763 {
764 #if CMD_HISTORY
765 struct mlist *ml;
766
767 /*
768 * Don't save a trivial command.
769 */
770 if (strlen(cmd) == 0)
771 return;
772
773 if (no_hist_dups)
774 {
775 struct mlist *next = NULL;
776 for (ml = mlist->next; ml->string != NULL; ml = next)
777 {
778 next = ml->next;
779 if (strcmp(ml->string, cmd) == 0)
780 {
781 ml_unlink(ml);
782 free(ml->string);
783 free(ml);
784 }
785 }
786 }
787
788 /*
789 * Save the command unless it's a duplicate of the
790 * last command in the history.
791 */
792 ml = mlist->prev;
793 if (ml == mlist || strcmp(ml->string, cmd) != 0)
794 {
795 /*
796 * Did not find command in history.
797 * Save the command and put it at the end of the history list.
798 */
799 ml = (struct mlist *) ecalloc(1, sizeof(struct mlist));
800 ml->string = save(cmd);
801 ml->modified = modified;
802 ml_link(mlist, ml);
803 }
804 /*
805 * Point to the cmd just after the just-accepted command.
806 * Thus, an UPARROW will always retrieve the previous command.
807 */
808 mlist->curr_mp = ml->next;
809 #endif
810 }
811
812 /*
813 * Accept the command in the command buffer.
814 * Add it to the currently selected history list.
815 */
816 public void
cmd_accept(VOID_PARAM)817 cmd_accept(VOID_PARAM)
818 {
819 #if CMD_HISTORY
820 /*
821 * Nothing to do if there is no currently selected history list.
822 */
823 if (curr_mlist == NULL || curr_mlist == ml_examine)
824 return;
825 cmd_addhist(curr_mlist, cmdbuf, 1);
826 curr_mlist->modified = 1;
827 #endif
828 }
829
830 /*
831 * Try to perform a line-edit function on the command buffer,
832 * using a specified char as a line-editing command.
833 * Returns:
834 * CC_PASS The char does not invoke a line edit function.
835 * CC_OK Line edit function done.
836 * CC_QUIT The char requests the current command to be aborted.
837 */
838 static int
cmd_edit(c)839 cmd_edit(c)
840 int c;
841 {
842 int action;
843 int flags;
844
845 #if TAB_COMPLETE_FILENAME
846 #define not_in_completion() in_completion = 0
847 #else
848 #define not_in_completion(VOID_PARAM)
849 #endif
850
851 /*
852 * See if the char is indeed a line-editing command.
853 */
854 flags = 0;
855 #if CMD_HISTORY
856 if (curr_mlist == NULL)
857 /*
858 * No current history; don't accept history manipulation cmds.
859 */
860 flags |= ECF_NOHISTORY;
861 #endif
862 #if TAB_COMPLETE_FILENAME
863 if (curr_mlist == ml_search)
864 /*
865 * In a search command; don't accept file-completion cmds.
866 */
867 flags |= ECF_NOCOMPLETE;
868 #endif
869
870 action = editchar(c, flags);
871
872 switch (action)
873 {
874 case A_NOACTION:
875 return (CC_OK);
876 case EC_RIGHT:
877 not_in_completion();
878 return (cmd_right());
879 case EC_LEFT:
880 not_in_completion();
881 return (cmd_left());
882 case EC_W_RIGHT:
883 not_in_completion();
884 while (*cp != '\0' && *cp != ' ')
885 cmd_right();
886 while (*cp == ' ')
887 cmd_right();
888 return (CC_OK);
889 case EC_W_LEFT:
890 not_in_completion();
891 while (cp > cmdbuf && cp[-1] == ' ')
892 cmd_left();
893 while (cp > cmdbuf && cp[-1] != ' ')
894 cmd_left();
895 return (CC_OK);
896 case EC_HOME:
897 not_in_completion();
898 cmd_offset = 0;
899 cmd_home();
900 cmd_repaint(cp);
901 return (CC_OK);
902 case EC_END:
903 not_in_completion();
904 while (*cp != '\0')
905 cmd_right();
906 return (CC_OK);
907 case EC_INSERT:
908 not_in_completion();
909 return (CC_OK);
910 case EC_BACKSPACE:
911 not_in_completion();
912 return (cmd_erase());
913 case EC_LINEKILL:
914 not_in_completion();
915 return (cmd_kill());
916 case EC_ABORT:
917 not_in_completion();
918 (void) cmd_kill();
919 return (CC_QUIT);
920 case EC_W_BACKSPACE:
921 not_in_completion();
922 return (cmd_werase());
923 case EC_DELETE:
924 not_in_completion();
925 return (cmd_delete());
926 case EC_W_DELETE:
927 not_in_completion();
928 return (cmd_wdelete());
929 case EC_LITERAL:
930 literal = 1;
931 return (CC_OK);
932 #if CMD_HISTORY
933 case EC_UP:
934 case EC_DOWN:
935 not_in_completion();
936 return (cmd_updown(action));
937 #endif
938 #if TAB_COMPLETE_FILENAME
939 case EC_F_COMPLETE:
940 case EC_B_COMPLETE:
941 case EC_EXPAND:
942 return (cmd_complete(action));
943 #endif
944 default:
945 not_in_completion();
946 return (CC_PASS);
947 }
948 }
949
950 #if TAB_COMPLETE_FILENAME
951 /*
952 * Insert a string into the command buffer, at the current position.
953 */
954 static int
cmd_istr(str)955 cmd_istr(str)
956 char *str;
957 {
958 char *s;
959 int action;
960 char *endline = str + strlen(str);
961
962 for (s = str; *s != '\0'; )
963 {
964 char *os = s;
965 step_char(&s, +1, endline);
966 action = cmd_ichar(os, s - os);
967 if (action != CC_OK)
968 return (action);
969 }
970 return (CC_OK);
971 }
972
973 /*
974 * Find the beginning and end of the "current" word.
975 * This is the word which the cursor (cp) is inside or at the end of.
976 * Return pointer to the beginning of the word and put the
977 * cursor at the end of the word.
978 */
979 static char *
delimit_word(VOID_PARAM)980 delimit_word(VOID_PARAM)
981 {
982 char *word;
983 #if SPACES_IN_FILENAMES
984 char *p;
985 int delim_quoted = 0;
986 int meta_quoted = 0;
987 constant char *esc = get_meta_escape();
988 int esclen = (int) strlen(esc);
989 #endif
990
991 /*
992 * Move cursor to end of word.
993 */
994 if (*cp != ' ' && *cp != '\0')
995 {
996 /*
997 * Cursor is on a nonspace.
998 * Move cursor right to the next space.
999 */
1000 while (*cp != ' ' && *cp != '\0')
1001 cmd_right();
1002 } else if (cp > cmdbuf && cp[-1] != ' ')
1003 {
1004 /*
1005 * Cursor is on a space, and char to the left is a nonspace.
1006 * We're already at the end of the word.
1007 */
1008 ;
1009 #if 0
1010 } else
1011 {
1012 /*
1013 * Cursor is on a space and char to the left is a space.
1014 * Huh? There's no word here.
1015 */
1016 return (NULL);
1017 #endif
1018 }
1019 /*
1020 * Find the beginning of the word which the cursor is in.
1021 */
1022 if (cp == cmdbuf)
1023 return (NULL);
1024 #if SPACES_IN_FILENAMES
1025 /*
1026 * If we have an unbalanced quote (that is, an open quote
1027 * without a corresponding close quote), we return everything
1028 * from the open quote, including spaces.
1029 */
1030 for (word = cmdbuf; word < cp; word++)
1031 if (*word != ' ')
1032 break;
1033 if (word >= cp)
1034 return (cp);
1035 for (p = cmdbuf; p < cp; p++)
1036 {
1037 if (meta_quoted)
1038 {
1039 meta_quoted = 0;
1040 } else if (esclen > 0 && p + esclen < cp &&
1041 strncmp(p, esc, esclen) == 0)
1042 {
1043 meta_quoted = 1;
1044 p += esclen - 1;
1045 } else if (delim_quoted)
1046 {
1047 if (*p == closequote)
1048 delim_quoted = 0;
1049 } else /* (!delim_quoted) */
1050 {
1051 if (*p == openquote)
1052 delim_quoted = 1;
1053 else if (*p == ' ')
1054 word = p+1;
1055 }
1056 }
1057 #endif
1058 return (word);
1059 }
1060
1061 /*
1062 * Set things up to enter completion mode.
1063 * Expand the word under the cursor into a list of filenames
1064 * which start with that word, and set tk_text to that list.
1065 */
1066 static void
init_compl(VOID_PARAM)1067 init_compl(VOID_PARAM)
1068 {
1069 char *word;
1070 char c;
1071
1072 /*
1073 * Get rid of any previous tk_text.
1074 */
1075 if (tk_text != NULL)
1076 {
1077 free(tk_text);
1078 tk_text = NULL;
1079 }
1080 /*
1081 * Find the original (uncompleted) word in the command buffer.
1082 */
1083 word = delimit_word();
1084 if (word == NULL)
1085 return;
1086 /*
1087 * Set the insertion point to the point in the command buffer
1088 * where the original (uncompleted) word now sits.
1089 */
1090 tk_ipoint = word;
1091 /*
1092 * Save the original (uncompleted) word
1093 */
1094 if (tk_original != NULL)
1095 free(tk_original);
1096 tk_original = (char *) ecalloc(cp-word+1, sizeof(char));
1097 strncpy(tk_original, word, cp-word);
1098 /*
1099 * Get the expanded filename.
1100 * This may result in a single filename, or
1101 * a blank-separated list of filenames.
1102 */
1103 c = *cp;
1104 *cp = '\0';
1105 if (*word != openquote)
1106 {
1107 tk_text = fcomplete(word);
1108 } else
1109 {
1110 #if MSDOS_COMPILER
1111 char *qword = NULL;
1112 #else
1113 char *qword = shell_quote(word+1);
1114 #endif
1115 if (qword == NULL)
1116 tk_text = fcomplete(word+1);
1117 else
1118 {
1119 tk_text = fcomplete(qword);
1120 free(qword);
1121 }
1122 }
1123 *cp = c;
1124 }
1125
1126 /*
1127 * Return the next word in the current completion list.
1128 */
1129 static char *
next_compl(action,prev)1130 next_compl(action, prev)
1131 int action;
1132 char *prev;
1133 {
1134 switch (action)
1135 {
1136 case EC_F_COMPLETE:
1137 return (forw_textlist(&tk_tlist, prev));
1138 case EC_B_COMPLETE:
1139 return (back_textlist(&tk_tlist, prev));
1140 }
1141 /* Cannot happen */
1142 return ("?");
1143 }
1144
1145 /*
1146 * Complete the filename before (or under) the cursor.
1147 * cmd_complete may be called multiple times. The global in_completion
1148 * remembers whether this call is the first time (create the list),
1149 * or a subsequent time (step thru the list).
1150 */
1151 static int
cmd_complete(action)1152 cmd_complete(action)
1153 int action;
1154 {
1155 char *s;
1156
1157 if (!in_completion || action == EC_EXPAND)
1158 {
1159 /*
1160 * Expand the word under the cursor and
1161 * use the first word in the expansion
1162 * (or the entire expansion if we're doing EC_EXPAND).
1163 */
1164 init_compl();
1165 if (tk_text == NULL)
1166 {
1167 bell();
1168 return (CC_OK);
1169 }
1170 if (action == EC_EXPAND)
1171 {
1172 /*
1173 * Use the whole list.
1174 */
1175 tk_trial = tk_text;
1176 } else
1177 {
1178 /*
1179 * Use the first filename in the list.
1180 */
1181 in_completion = 1;
1182 init_textlist(&tk_tlist, tk_text);
1183 tk_trial = next_compl(action, (char*)NULL);
1184 }
1185 } else
1186 {
1187 /*
1188 * We already have a completion list.
1189 * Use the next/previous filename from the list.
1190 */
1191 tk_trial = next_compl(action, tk_trial);
1192 }
1193
1194 /*
1195 * Remove the original word, or the previous trial completion.
1196 */
1197 while (cp > tk_ipoint)
1198 (void) cmd_erase();
1199
1200 if (tk_trial == NULL)
1201 {
1202 /*
1203 * There are no more trial completions.
1204 * Insert the original (uncompleted) filename.
1205 */
1206 in_completion = 0;
1207 if (cmd_istr(tk_original) != CC_OK)
1208 goto fail;
1209 } else
1210 {
1211 /*
1212 * Insert trial completion.
1213 */
1214 if (cmd_istr(tk_trial) != CC_OK)
1215 goto fail;
1216 /*
1217 * If it is a directory, append a slash.
1218 */
1219 if (is_dir(tk_trial))
1220 {
1221 if (cp > cmdbuf && cp[-1] == closequote)
1222 (void) cmd_erase();
1223 s = lgetenv("LESSSEPARATOR");
1224 if (s == NULL)
1225 s = PATHNAME_SEP;
1226 if (cmd_istr(s) != CC_OK)
1227 goto fail;
1228 }
1229 }
1230
1231 return (CC_OK);
1232
1233 fail:
1234 in_completion = 0;
1235 bell();
1236 return (CC_OK);
1237 }
1238
1239 #endif /* TAB_COMPLETE_FILENAME */
1240
1241 /*
1242 * Process a single character of a multi-character command, such as
1243 * a number, or the pattern of a search command.
1244 * Returns:
1245 * CC_OK The char was accepted.
1246 * CC_QUIT The char requests the command to be aborted.
1247 * CC_ERROR The char could not be accepted due to an error.
1248 */
1249 public int
cmd_char(c)1250 cmd_char(c)
1251 int c;
1252 {
1253 int action;
1254 int len;
1255
1256 if (!utf_mode)
1257 {
1258 cmd_mbc_buf[0] = c;
1259 len = 1;
1260 } else
1261 {
1262 /* Perform strict validation in all possible cases. */
1263 if (cmd_mbc_buf_len == 0)
1264 {
1265 retry:
1266 cmd_mbc_buf_index = 1;
1267 *cmd_mbc_buf = c;
1268 if (IS_ASCII_OCTET(c))
1269 cmd_mbc_buf_len = 1;
1270 #if MSDOS_COMPILER || OS2
1271 else if (c == (unsigned char) '\340' && IS_ASCII_OCTET(peekcc()))
1272 {
1273 /* Assume a special key. */
1274 cmd_mbc_buf_len = 1;
1275 }
1276 #endif
1277 else if (IS_UTF8_LEAD(c))
1278 {
1279 cmd_mbc_buf_len = utf_len(c);
1280 return (CC_OK);
1281 } else
1282 {
1283 /* UTF8_INVALID or stray UTF8_TRAIL */
1284 bell();
1285 return (CC_ERROR);
1286 }
1287 } else if (IS_UTF8_TRAIL(c))
1288 {
1289 cmd_mbc_buf[cmd_mbc_buf_index++] = c;
1290 if (cmd_mbc_buf_index < cmd_mbc_buf_len)
1291 return (CC_OK);
1292 if (!is_utf8_well_formed(cmd_mbc_buf, cmd_mbc_buf_index))
1293 {
1294 /* complete, but not well formed (non-shortest form), sequence */
1295 cmd_mbc_buf_len = 0;
1296 bell();
1297 return (CC_ERROR);
1298 }
1299 } else
1300 {
1301 /* Flush incomplete (truncated) sequence. */
1302 cmd_mbc_buf_len = 0;
1303 bell();
1304 /* Handle new char. */
1305 goto retry;
1306 }
1307
1308 len = cmd_mbc_buf_len;
1309 cmd_mbc_buf_len = 0;
1310 }
1311
1312 if (literal)
1313 {
1314 /*
1315 * Insert the char, even if it is a line-editing char.
1316 */
1317 literal = 0;
1318 return (cmd_ichar(cmd_mbc_buf, len));
1319 }
1320
1321 /*
1322 * See if it is a line-editing character.
1323 */
1324 if (in_mca() && len == 1)
1325 {
1326 action = cmd_edit(c);
1327 switch (action)
1328 {
1329 case CC_OK:
1330 case CC_QUIT:
1331 return (action);
1332 case CC_PASS:
1333 break;
1334 }
1335 }
1336
1337 /*
1338 * Insert the char into the command buffer.
1339 */
1340 return (cmd_ichar(cmd_mbc_buf, len));
1341 }
1342
1343 /*
1344 * Return the number currently in the command buffer.
1345 */
1346 public LINENUM
cmd_int(frac)1347 cmd_int(frac)
1348 long *frac;
1349 {
1350 char *p;
1351 LINENUM n = 0;
1352 int err;
1353
1354 for (p = cmdbuf; *p >= '0' && *p <= '9'; p++)
1355 n = (n * 10) + (*p - '0');
1356 *frac = 0;
1357 if (*p++ == '.')
1358 {
1359 *frac = getfraction(&p, NULL, &err);
1360 /* {{ do something if err is set? }} */
1361 }
1362 return (n);
1363 }
1364
1365 /*
1366 * Return a pointer to the command buffer.
1367 */
1368 public char *
get_cmdbuf(VOID_PARAM)1369 get_cmdbuf(VOID_PARAM)
1370 {
1371 return (cmdbuf);
1372 }
1373
1374 #if CMD_HISTORY
1375 /*
1376 * Return the last (most recent) string in the current command history.
1377 */
1378 public char *
cmd_lastpattern(VOID_PARAM)1379 cmd_lastpattern(VOID_PARAM)
1380 {
1381 if (curr_mlist == NULL)
1382 return (NULL);
1383 return (curr_mlist->curr_mp->prev->string);
1384 }
1385 #endif
1386
1387 #if CMD_HISTORY
1388 /*
1389 */
1390 static int
mlist_size(ml)1391 mlist_size(ml)
1392 struct mlist *ml;
1393 {
1394 int size = 0;
1395 for (ml = ml->next; ml->string != NULL; ml = ml->next)
1396 ++size;
1397 return size;
1398 }
1399
1400 /*
1401 * Get the name of the history file.
1402 */
1403 static char *
histfile_name(must_exist)1404 histfile_name(must_exist)
1405 int must_exist;
1406 {
1407 char *home;
1408 char *xdg;
1409 char *name;
1410
1411 /* See if filename is explicitly specified by $LESSHISTFILE. */
1412 name = lgetenv("LESSHISTFILE");
1413 if (!isnullenv(name))
1414 {
1415 if (strcmp(name, "-") == 0 || strcmp(name, "/dev/null") == 0)
1416 /* $LESSHISTFILE == "-" means don't use a history file. */
1417 return (NULL);
1418 return (save(name));
1419 }
1420
1421 /* See if history file is disabled in the build. */
1422 if (strcmp(LESSHISTFILE, "") == 0 || strcmp(LESSHISTFILE, "-") == 0)
1423 return (NULL);
1424
1425 /* Try in $XDG_DATA_HOME first, then in $HOME. */
1426 xdg = lgetenv("XDG_DATA_HOME");
1427 home = lgetenv("HOME");
1428 #if OS2
1429 if (isnullenv(home))
1430 home = lgetenv("INIT");
1431 #endif
1432 name = NULL;
1433 if (!must_exist)
1434 {
1435 /* If we're writing the file and the file already exists, use it. */
1436 name = dirfile(xdg, &LESSHISTFILE[1], 1);
1437 if (name == NULL)
1438 name = dirfile(home, LESSHISTFILE, 1);
1439 }
1440 if (name == NULL)
1441 name = dirfile(xdg, &LESSHISTFILE[1], must_exist);
1442 if (name == NULL)
1443 name = dirfile(home, LESSHISTFILE, must_exist);
1444 return (name);
1445 }
1446
1447 /*
1448 * Read a .lesshst file and call a callback for each line in the file.
1449 */
1450 static void
1451 read_cmdhist2(action, uparam, skip_search, skip_shell)
1452 void (*action)(void*,struct mlist*,char*);
1453 void *uparam;
1454 int skip_search;
1455 int skip_shell;
1456 {
1457 struct mlist *ml = NULL;
1458 char line[CMDBUF_SIZE];
1459 char *filename;
1460 FILE *f;
1461 char *p;
1462 int *skip = NULL;
1463
1464 filename = histfile_name(1);
1465 if (filename == NULL)
1466 return;
1467 f = fopen(filename, "r");
1468 free(filename);
1469 if (f == NULL)
1470 return;
1471 if (fgets(line, sizeof(line), f) == NULL ||
1472 strncmp(line, HISTFILE_FIRST_LINE, strlen(HISTFILE_FIRST_LINE)) != 0)
1473 {
1474 fclose(f);
1475 return;
1476 }
1477 while (fgets(line, sizeof(line), f) != NULL)
1478 {
1479 for (p = line; *p != '\0'; p++)
1480 {
1481 if (*p == '\n' || *p == '\r')
1482 {
1483 *p = '\0';
1484 break;
1485 }
1486 }
1487 if (strcmp(line, HISTFILE_SEARCH_SECTION) == 0)
1488 {
1489 ml = &mlist_search;
1490 skip = &skip_search;
1491 } else if (strcmp(line, HISTFILE_SHELL_SECTION) == 0)
1492 {
1493 #if SHELL_ESCAPE || PIPEC
1494 ml = &mlist_shell;
1495 skip = &skip_shell;
1496 #else
1497 ml = NULL;
1498 skip = NULL;
1499 #endif
1500 } else if (strcmp(line, HISTFILE_MARK_SECTION) == 0)
1501 {
1502 ml = NULL;
1503 } else if (*line == '"')
1504 {
1505 if (ml != NULL)
1506 {
1507 if (skip != NULL && *skip > 0)
1508 --(*skip);
1509 else
1510 (*action)(uparam, ml, line+1);
1511 }
1512 } else if (*line == 'm')
1513 {
1514 (*action)(uparam, NULL, line);
1515 }
1516 }
1517 fclose(f);
1518 }
1519
1520 static void
1521 read_cmdhist(action, uparam, skip_search, skip_shell)
1522 void (*action)(void*,struct mlist*,char*);
1523 void *uparam;
1524 int skip_search;
1525 int skip_shell;
1526 {
1527 read_cmdhist2(action, uparam, skip_search, skip_shell);
1528 (*action)(uparam, NULL, NULL); /* signal end of file */
1529 }
1530
1531 static void
addhist_init(void * uparam,struct mlist * ml,char * string)1532 addhist_init(void *uparam, struct mlist *ml, char *string)
1533 {
1534 if (ml != NULL)
1535 cmd_addhist(ml, string, 0);
1536 else if (string != NULL)
1537 restore_mark(string);
1538 }
1539 #endif /* CMD_HISTORY */
1540
1541 /*
1542 * Initialize history from a .lesshist file.
1543 */
1544 public void
init_cmdhist(VOID_PARAM)1545 init_cmdhist(VOID_PARAM)
1546 {
1547 #if CMD_HISTORY
1548 read_cmdhist(&addhist_init, NULL, 0, 0);
1549 #endif /* CMD_HISTORY */
1550 }
1551
1552 /*
1553 * Write the header for a section of the history file.
1554 */
1555 #if CMD_HISTORY
1556 static void
write_mlist_header(ml,f)1557 write_mlist_header(ml, f)
1558 struct mlist *ml;
1559 FILE *f;
1560 {
1561 if (ml == &mlist_search)
1562 fprintf(f, "%s\n", HISTFILE_SEARCH_SECTION);
1563 #if SHELL_ESCAPE || PIPEC
1564 else if (ml == &mlist_shell)
1565 fprintf(f, "%s\n", HISTFILE_SHELL_SECTION);
1566 #endif
1567 }
1568
1569 /*
1570 * Write all modified entries in an mlist to the history file.
1571 */
1572 static void
write_mlist(ml,f)1573 write_mlist(ml, f)
1574 struct mlist *ml;
1575 FILE *f;
1576 {
1577 for (ml = ml->next; ml->string != NULL; ml = ml->next)
1578 {
1579 if (!ml->modified)
1580 continue;
1581 fprintf(f, "\"%s\n", ml->string);
1582 ml->modified = 0;
1583 }
1584 ml->modified = 0; /* entire mlist is now unmodified */
1585 }
1586
1587 /*
1588 * Make a temp name in the same directory as filename.
1589 */
1590 static char *
make_tempname(filename)1591 make_tempname(filename)
1592 char *filename;
1593 {
1594 char lastch;
1595 char *tempname = ecalloc(1, strlen(filename)+1);
1596 strcpy(tempname, filename);
1597 lastch = tempname[strlen(tempname)-1];
1598 tempname[strlen(tempname)-1] = (lastch == 'Q') ? 'Z' : 'Q';
1599 return tempname;
1600 }
1601
1602 struct save_ctx
1603 {
1604 struct mlist *mlist;
1605 FILE *fout;
1606 };
1607
1608 /*
1609 * Copy entries from the saved history file to a new file.
1610 * At the end of each mlist, append any new entries
1611 * created during this session.
1612 */
1613 static void
copy_hist(void * uparam,struct mlist * ml,char * string)1614 copy_hist(void *uparam, struct mlist *ml, char *string)
1615 {
1616 struct save_ctx *ctx = (struct save_ctx *) uparam;
1617
1618 if (ml != NULL && ml != ctx->mlist) {
1619 /* We're changing mlists. */
1620 if (ctx->mlist)
1621 /* Append any new entries to the end of the current mlist. */
1622 write_mlist(ctx->mlist, ctx->fout);
1623 /* Write the header for the new mlist. */
1624 ctx->mlist = ml;
1625 write_mlist_header(ctx->mlist, ctx->fout);
1626 }
1627
1628 if (string == NULL) /* End of file */
1629 {
1630 /* Write any sections that were not in the original file. */
1631 if (mlist_search.modified)
1632 {
1633 write_mlist_header(&mlist_search, ctx->fout);
1634 write_mlist(&mlist_search, ctx->fout);
1635 }
1636 #if SHELL_ESCAPE || PIPEC
1637 if (mlist_shell.modified)
1638 {
1639 write_mlist_header(&mlist_shell, ctx->fout);
1640 write_mlist(&mlist_shell, ctx->fout);
1641 }
1642 #endif
1643 } else if (ml != NULL)
1644 {
1645 /* Copy mlist entry. */
1646 fprintf(ctx->fout, "\"%s\n", string);
1647 }
1648 /* Skip marks */
1649 }
1650 #endif /* CMD_HISTORY */
1651
1652 /*
1653 * Make a file readable only by its owner.
1654 */
1655 static void
make_file_private(f)1656 make_file_private(f)
1657 FILE *f;
1658 {
1659 #if HAVE_FCHMOD
1660 int do_chmod = 1;
1661 #if HAVE_STAT
1662 struct stat statbuf;
1663 int r = fstat(fileno(f), &statbuf);
1664 if (r < 0 || !S_ISREG(statbuf.st_mode))
1665 /* Don't chmod if not a regular file. */
1666 do_chmod = 0;
1667 #endif
1668 if (do_chmod)
1669 fchmod(fileno(f), 0600);
1670 #endif
1671 }
1672
1673 /*
1674 * Does the history file need to be updated?
1675 */
1676 static int
histfile_modified(VOID_PARAM)1677 histfile_modified(VOID_PARAM)
1678 {
1679 if (mlist_search.modified)
1680 return 1;
1681 #if SHELL_ESCAPE || PIPEC
1682 if (mlist_shell.modified)
1683 return 1;
1684 #endif
1685 #if CMD_HISTORY
1686 if (marks_modified)
1687 return 1;
1688 #endif
1689 return 0;
1690 }
1691
1692 /*
1693 * Update the .lesshst file.
1694 */
1695 public void
save_cmdhist(VOID_PARAM)1696 save_cmdhist(VOID_PARAM)
1697 {
1698 #if CMD_HISTORY
1699 char *histname;
1700 char *tempname;
1701 int skip_search;
1702 int skip_shell;
1703 struct save_ctx ctx;
1704 char *s;
1705 FILE *fout = NULL;
1706 int histsize = 0;
1707
1708 if (!histfile_modified())
1709 return;
1710 histname = histfile_name(0);
1711 if (histname == NULL)
1712 return;
1713 tempname = make_tempname(histname);
1714 fout = fopen(tempname, "w");
1715 if (fout != NULL)
1716 {
1717 make_file_private(fout);
1718 s = lgetenv("LESSHISTSIZE");
1719 if (s != NULL)
1720 histsize = atoi(s);
1721 if (histsize <= 0)
1722 histsize = 100;
1723 skip_search = mlist_size(&mlist_search) - histsize;
1724 #if SHELL_ESCAPE || PIPEC
1725 skip_shell = mlist_size(&mlist_shell) - histsize;
1726 #endif
1727 fprintf(fout, "%s\n", HISTFILE_FIRST_LINE);
1728 ctx.fout = fout;
1729 ctx.mlist = NULL;
1730 read_cmdhist(©_hist, &ctx, skip_search, skip_shell);
1731 save_marks(fout, HISTFILE_MARK_SECTION);
1732 fclose(fout);
1733 #if MSDOS_COMPILER==WIN32C
1734 /*
1735 * Windows rename doesn't remove an existing file,
1736 * making it useless for atomic operations. Sigh.
1737 */
1738 remove(histname);
1739 #endif
1740 rename(tempname, histname);
1741 }
1742 free(tempname);
1743 free(histname);
1744 #endif /* CMD_HISTORY */
1745 }
1746