diff -uNr a/yrc/codemap.txt b/yrc/codemap.txt --- a/yrc/codemap.txt f168745fe71becbdcc6ff376d2afb22492b11aa45d4c44d58203bcef49c57f8b1c634b8fa659a4a273ed64cc89066f0bdfce4a23a8d87e3ea0bf978900a7ae10 +++ b/yrc/codemap.txt e08134a15620d6666db39851a414802e45319626b0e321af9c8df72bec0e604db9f668b085a4d603aa00d953c8ee1887b8818c5275ba4849e48aae005fb52416 @@ -101,6 +101,9 @@ kbd_state: function history_pos: int | None history_stash: str | None +kill_pos: int | None +yank_start_pos: int | None +yank_end_pos: int | None Collections =========== @@ -112,6 +115,7 @@ 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 +kill_ring: ring of str Functions ========= @@ -217,6 +221,7 @@ sort_buffers : sorts buffers & generates buffer_index get_buf(name, parent_name) -> buf : buffer_index lookup but creates if not found close_buf(buf) +exit_yank prompt_insert(chr) prompt_submit prompt_end @@ -224,6 +229,10 @@ history_next prompt_backspace prompt_delete +kill_start +kill_end +yank +yank_next info(msg: str, buf=None) : buf_log_msg to buf or buffers[0] error(msg_or_exc: str | Exception, buf=None) : buf_log_msg to buf or buffers[0] kaccum(str) diff -uNr a/yrc/manifest b/yrc/manifest --- a/yrc/manifest 2463d27af64f5b54459d437aa1b1f9e387e65a3d13e4fa4faed466f3f55462f6c54876d08efbbcbec53d5f918152e3bb6c7c05f9f7e2e2d1679864417d53f283 +++ b/yrc/manifest e2db0fe5b97891eb86eb3421146dd0505861fa5d457a129c6a0deae8a838787ec6b436a901fbbb67cee946ee128b2bd57da05414dc65a2f95f8b76a9efd747cd @@ -3,3 +3,4 @@ 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). +768775 yrc_kill_yank jfw Implement the basic kill and yank commands. Also: rename prompt-backward command to prompt-back, to avert confusion with the anticipated prompt-back-word; update manual for the new commands plus clarifications & updated priorities. diff -uNr a/yrc/manual.txt b/yrc/manual.txt --- a/yrc/manual.txt 063396f1f2d133868b5992658367a82b2558f957657140effb5bc582df27e7ed17f096ec03615ebcd46d5f892fee432709a4a57e0425ee4ab2cfeed8fb9100b2 +++ b/yrc/manual.txt 208ab21d9080f17919659ac71137084c4cbb8ff013fc4efac4dbfcbf713ea94d55ecbe864d3e2d194ce83b1074abe2eb8db2c97909dd1748db36f375eb1d5595 @@ -247,7 +247,7 @@ Moves the cursor forward by one character. - C-b, Left: prompt-backward + C-b, Left: prompt-back Moves the cursor backward by one character. @@ -259,6 +259,14 @@ Moves the cursor to the end of the line. + C-p, Up: history-prev + +Recalls a previous line from input history. (History size is currently fixed at 1024 most recent entries.) + + C-n, Down: history-next + +Recalls a subsequent line from input history, discarding any unsubmitted edits to a previous line. + Backspace (C-h, C-?): prompt-backspace Deletes backward by one character. @@ -267,6 +275,22 @@ Deletes forward by one character. + C-u: kill-start + +Deletes text from start of line to cursor, saving it to the kill ring. (Kill ring size is currently fixed at 8 entries.) + + C-k: kill-end + +Deletes text from cursor to end of line, saving it to the kill ring. + + C-y: yank + +Inserts ("yanks" or "pastes") the most recent kill ring entry at cursor position. + + M-y: yank-next + +Following a yank, replaces the just-inserted text with the next older entry in the kill ring. + C-v, PgDn: scroll-down-page M-v, PgUp: scroll-up-page @@ -364,16 +388,18 @@ High-priority missing features: - - Prompt history, kill/yank commands - - Indication of nick mentions (ring bell + bold the window name?) + - Word-level cursor motion and kill commands - Tab completion (nicks, slash commands) - - Formatting for displayed messages (e.g. bold sender, highlight own nick) - Finish mode/op and close-net commands - - Filtering of inbound PMs (/ignore or similar) - - Scrolling + more efficient navigation of window list + - Scrolling in window list if it exceeds available screen rows + - More accurate derivation of message length limit to reduce overly-conservative wrapping Nice to have: + - Indication of nick mentions (ring bell + bold the window name?) + - Formatting for displayed messages (e.g. bold sender, highlight own nick) + - Filtering of inbound PMs (/ignore or similar) + - More efficient navigation of window list - Search in scrollback - Nickname fallback - Online /help command diff -uNr a/yrc/yrc.py b/yrc/yrc.py --- a/yrc/yrc.py 6d38419516970f97781c49217aabffa72ddb540773b30b479a0c25c7dc503083f95ac4c99d9cfef2762f2abd146ae677c1aaf1421b8e791c06c35d4e62ddb9ba +++ b/yrc/yrc.py 59b7e716e692e29ef4e5a39ed30eb333bb6b6e225c8e55210eb627ed7cbb6ad671f9e9c2475ddf6bbde41bb12739156439e03920ae8f28ba91d2d657750e5eb5 @@ -79,8 +79,8 @@ ctrl('F'): 'prompt-forward', RIGHT: 'prompt-forward', - ctrl('B'): 'prompt-backward', - LEFT: 'prompt-backward', + ctrl('B'): 'prompt-back', + LEFT: 'prompt-back', ctrl('A'): 'prompt-start', HOME: 'prompt-start', @@ -102,6 +102,14 @@ ctrl('D'): 'prompt-delete', DEL: 'prompt-delete', + ctrl('U'): 'kill-start', + + ctrl('K'): 'kill-end', + + ctrl('Y'): 'yank', + + meta('y'): 'yank-next', + meta('v'): 'scroll-up-page', PGUP: 'scroll-up-page', @@ -826,6 +834,7 @@ def error(msg_or_exc, buf=None): buf_log_msg(buf or buffers[0], m_client('ERROR: ' + str(msg_or_exc))) +# The prompt is a one-line text input field at the bottom of the screen, spanning its width. This record tracks its state. prompt = [[], 0, 0, 1] prompt_chars = lambda: prompt[0] @@ -857,6 +866,27 @@ history_pos = None history_stash = None +kill_ring = new_ring(KILL_RING_SIZE) +kill_pos = None +yank_start_pos = None +yank_end_pos = None + +def exit_yank(): + global kill_pos, yank_start_pos, yank_end_pos + kill_pos = None + yank_start_pos = None + yank_end_pos = None + +# Commands from the prompt keymap known to not require resetting the yank state, i.e. those that can't change the prompt's contents or move its cursor. +YANK_SAFE_COMMANDS = set(( + 'prompt-exit', + 'yank-next', + 'scroll-up-page', + 'scroll-down-page', + 'scroll-top', + 'scroll-bottom', +)) + def prompt_insert(char): schedule_prompt_draw() c = prompt_cursor() @@ -887,7 +917,7 @@ error(e, cur_buf) command('prompt-forward')(lambda: prompt_set_cursor(prompt_cursor() + 1)) -command('prompt-backward')(lambda: prompt_set_cursor(prompt_cursor() - 1)) +command('prompt-back')(lambda: prompt_set_cursor(prompt_cursor() - 1)) command('prompt-start')(lambda: prompt_set_cursor(0)) @command('prompt-end') @@ -948,6 +978,66 @@ chars.pop(c) schedule_prompt_draw() +@command('kill-start') +def kill_start(): + c = prompt_cursor() + if c > 0: # Don't send empty strings to the kill ring + chars = prompt_chars() + ring_append(kill_ring, ''.join(chars[:c])) + prompt_set_cursor(0) + del chars[:c] + schedule_prompt_draw() + +@command('kill-end') +def kill_end(): + c = prompt_cursor() + chars = prompt_chars() + if c < len(chars): # Don't send empty strings to the kill ring + ring_append(kill_ring, ''.join(chars[c:])) + del chars[c:] + schedule_prompt_draw() + +@command('yank') +def yank(): + global kill_pos, yank_start_pos, yank_end_pos + # Always start from most recent kill (pdksh and GNU Readline disagree here; I favor the former's way, as it matches history behavior and reduces hidden state) + saved = ring_get(kill_ring, -1) + if saved is None: + # Empty kill ring + return + kill_pos = ring_len(kill_ring) - 1 + yank_start_pos = prompt_cursor() + yank_end_pos = yank_start_pos + len(saved) + chars = prompt_chars() + tail = chars[yank_start_pos:] + del chars[yank_start_pos:] + chars.extend(saved) + chars.extend(tail) + prompt_set_cursor(yank_end_pos) + schedule_prompt_draw() + +@command('yank-next') +def yank_next(): + global kill_pos, yank_end_pos + if kill_pos is None: + # Nothing yanked yet + return + if kill_pos == 0 or ring_get(kill_ring, kill_pos-1) is None: + # Past oldest initialized kill slot: wrap back to newest + kill_pos = ring_len(kill_ring) + kill_pos -= 1 + saved = ring_get(kill_ring, kill_pos) + chars = prompt_chars() + assert yank_end_pos == prompt_cursor() + # ^ something changed and we failed to call exit_yank(), so the positions are no longer valid + tail = chars[yank_end_pos:] + del chars[yank_start_pos:] + chars.extend(saved) + chars.extend(tail) + yank_end_pos = yank_start_pos + len(saved) + prompt_set_cursor(yank_end_pos) + schedule_prompt_draw() + def prompt_draw(): if scr_height < 1: return @@ -1178,9 +1268,16 @@ elif buflist_flag(): try_keymap(BUFLIST_KEYS) elif prompt_flag(): - if not try_keymap(PROMPT_KEYS): + # Prompt has the focus. A bit of an awkward special case: any editing action other than yank-next needs to reset yank state, if any. Rather than littering every command handler with exit_yank calls, we peek inside the box here at the common ancestor. + cmd = PROMPT_KEYS.get(seq) + if cmd is None: if len(seq) == 1 and not is_ctrl(seq): + exit_yank() prompt_insert(seq) + else: + if cmd not in YANK_SAFE_COMMANDS: + exit_yank() + run_command(cmd) else: try_keymap(BUF_KEYS) except CommandError, e: