diff -uNr a/yrc/codemap.txt b/yrc/codemap.txt --- a/yrc/codemap.txt e17fe3855e0266ce8a34fcd51a3c38da6ef09219e932222ee8e844a40ec1e218796f8be809eb442dc0cf0c9246ad389c6806d6ee424fdf8c77deb7d1627c530b +++ b/yrc/codemap.txt f168745fe71becbdcc6ff376d2afb22492b11aa45d4c44d58203bcef49c57f8b1c634b8fa659a4a273ed64cc89066f0bdfce4a23a8d87e3ea0bf978900a7ae10 @@ -50,6 +50,10 @@ - prompt - ping_draw +ring +- start: int +- list: list + Enumerations ============ @@ -95,6 +99,8 @@ out_buf: bytearray kbd_accum: bytearray kbd_state: function +history_pos: int | None +history_stash: str | None Collections =========== @@ -105,6 +111,7 @@ open_conns: (fileno: int) => conn network_conns: (network: str) => conn : all enabled networks, whether TCP alive or not schedule: min-heap of (time: float) => thunk +history_ring: ring of str Functions ========= @@ -172,6 +179,8 @@ empty2 lterr / TypeError format_log_msg(val: message except (m_chantopic | m_mode | m_client)) -> str +ring_index(ring, int) -> int +ring_get(ring of t, int) -> t | None Pure I/O: write_all(blocking fd, str|bytearray) / EnvironmentError @@ -210,6 +219,9 @@ close_buf(buf) prompt_insert(chr) prompt_submit +prompt_end +history_prev +history_next prompt_backspace prompt_delete info(msg: str, buf=None) : buf_log_msg to buf or buffers[0] @@ -242,6 +254,9 @@ handle_resize mono_time -> float run_in(seconds, thunk) +ring_set(ring of t, int, t) +ring_rotate(ring, int) +ring_append(ring of t, t) Slash commands: quit_cmd diff -uNr a/yrc/manifest b/yrc/manifest --- a/yrc/manifest 562aeaf072a99dfa4950c8215b3a74da57a2d743d4bcc1a25d51a8c85f4b22e1c3c1d705a5cead9b97e232dcb701251b05b18b27bd4e8667505ca3d63396e4ec +++ b/yrc/manifest 2463d27af64f5b54459d437aa1b1f9e387e65a3d13e4fa4faed466f3f55462f6c54876d08efbbcbec53d5f918152e3bb6c7c05f9f7e2e2d1679864417d53f283 @@ -2,3 +2,4 @@ 633652 yrc_linear_scroll_etc jfw Version 96K with smooth/linear scrolling, Python 2.6 compat, yrc2local log formatter, bugfixes and more: see NEWS for details. 768770 yrc_minor_refactors_reorders jfw Move prompt_clear to a higher layer by making it use the prompt_chars accessor: it now deletes entries from a fixed character list container rather than replacing the list as a whole. Expand prompt_backspace not to stack on top of prompt_delete, coming out only slightly longer. Tighten some prompt commands (such as backspace/delete) to skip unnecessary redraw. In codemap.txt, reclassify commands dict as a quasiconstant since it doesn't change after startup. 768773 yrc_minor_command_reorder jfw Move prompt_backspace and prompt_delete implementations to match the more sensible order in keymap and manual. +768773 yrc_input_history jfw Implement input history, by use of a new data structure for rings (cyclic lists). diff -uNr a/yrc/yrc.py b/yrc/yrc.py --- a/yrc/yrc.py 40e09cc21c0e83b9dd8610577fe63c82f5bca1b384e0a37dd791482081f4ecc30efcd2eb9a706e2bb954e0d99bc77e6d115b34696aa2506854b941c22066f052 +++ b/yrc/yrc.py 6d38419516970f97781c49217aabffa72ddb540773b30b479a0c25c7dc503083f95ac4c99d9cfef2762f2abd146ae677c1aaf1421b8e791c06c35d4e62ddb9ba @@ -7,6 +7,8 @@ __version__ = '96 Kelvin' # Knobs that one might conceivably want to tweak +KILL_RING_SIZE = 8 +HISTORY_RING_SIZE = 1024 DEFAULT_PORT = 6667 RECONN_DELAY_MIN = 4 # seconds RECONN_DELAY_MAX = 256 @@ -88,6 +90,12 @@ END: 'prompt-end', LNX_END: 'prompt-end', + ctrl('P'): 'history-prev', + UP: 'history-prev', + + ctrl('N'): 'history-next', + DOWN: 'history-next', + ctrl('H'): 'prompt-backspace', ctrl('?'): 'prompt-backspace', @@ -426,6 +434,24 @@ while h: yield heap_extract(h)[0] +# Rings (cyclic lists) for input history and kills + +new_ring = lambda size: [0, [None]*size] +ring_start = lambda r: r[0] +ring_list = lambda r: r[1] +ring_set_start = lambda r, k: r.__setitem__(0, k) + +ring_len = lambda r: len(ring_list(r)) +ring_index = lambda r, k: (ring_start(r) + k) % len(ring_list(r)) +ring_get = lambda r, k: ring_list(r)[ring_index(r, k)] +ring_set = lambda r, k, v: ring_list(r).__setitem__(ring_index(r, k), v) +ring_rotate = lambda r, k: ring_set_start(r, ring_index(r, k)) + +def ring_append(r, v): + # By analogy to an appended list, the idea is that the virtual indices 0 to N-1 address the entries oldest to newest (equivalently, -N to -1). The analogy breaks in that the "low" entries start out as None (uninitialized), with the appended items appearing to shift in from high to low, 0 being the last entry filled. + ring_set(r, 0, v) + ring_rotate(r, 1) + # # Config # @@ -827,6 +853,10 @@ prompt_set_cursor(0) prompt_set_hscroll(0) +history_ring = new_ring(HISTORY_RING_SIZE) +history_pos = None +history_stash = None + def prompt_insert(char): schedule_prompt_draw() c = prompt_cursor() @@ -835,9 +865,13 @@ @command('prompt-submit') def prompt_submit(): + global history_pos, history_stash line = ''.join(prompt_chars()) if len(line) == 0: return + ring_append(history_ring, line) + history_pos = None + history_stash = None prompt_clear() schedule_prompt_draw() try: @@ -855,7 +889,48 @@ command('prompt-forward')(lambda: prompt_set_cursor(prompt_cursor() + 1)) command('prompt-backward')(lambda: prompt_set_cursor(prompt_cursor() - 1)) command('prompt-start')(lambda: prompt_set_cursor(0)) -command('prompt-end')(lambda: prompt_set_cursor(len(prompt_chars()))) + +@command('prompt-end') +def prompt_end(): + prompt_set_cursor(len(prompt_chars())) + +@command('history-prev') +def history_prev(): + global history_pos, history_stash + if ring_get(history_ring, -1) is None: + # Empty history + return + if history_pos is None: + # Editing a fresh line: stash it and enter history + history_stash = ''.join(prompt_chars()) + history_pos = ring_len(history_ring) + elif history_pos == 0 or ring_get(history_ring, history_pos-1) is None: + # Already at oldest initialized position + return + history_pos -= 1 + # Copy into the current prompt input. The string from history is immutable as it should be! (GNU Readline fails here) + prompt_clear() + prompt_chars().extend(ring_get(history_ring, history_pos)) + prompt_end() + schedule_prompt_draw() + +@command('history-next') +def history_next(): + global history_pos, history_stash + if history_pos is None: + # Editing a fresh line: not in history + return + history_pos += 1 + prompt_clear() + if history_pos < ring_len(history_ring): + prompt_chars().extend(ring_get(history_ring, history_pos)) + else: + # Past the end: exit history and restore from stash (pdksh fails here) + history_pos = None + prompt_chars().extend(history_stash) + history_stash = None + prompt_end() + schedule_prompt_draw() @command('prompt-backspace') def prompt_backspace():