diff -uNr a/yrc/NEWS b/yrc/NEWS --- a/yrc/NEWS b47716297df92a443029a4e5186581a9556badfc467f7697f287bd7b93c01edcc07a2b3deca7b4810f41681c6869dd5952ce2d119af3ab2213e58c96c5f33e03 +++ b/yrc/NEWS a4e52a601f96abd15017767703388220a2855b8be122d7495359f377adba84ff2985093de383407d9b0da9d27d55ddd976a1618bddbc2fce0ad5b9914aa97a58 @@ -1,3 +1,30 @@ +96K (2020-06-08) +================ + +New script: +- yrc2local, filters a yrc log file converting timestamps to expanded local time + +Usability: +- Smooth/linear scrolling of chat window by wrapped rather than source lines +- Focus returns to the last selected window rather than the neighbor on close +- Emacs style scrolling keys M-< and M-> added to prompt keymap as they don't conflict with other usage, in the vein of C-v and M-v. + +Bugfix: +- Stray text on first line of newly opened empty window due to incomplete erase + +Compatibility: +- Replace int.bit_length() for Python 2.6. + +Style: +- Switch to tab-based indent and reduce hard-wrapped comments + +Documentation: +- Some improved comments +- Broken upstream references switched to Fixpoint library +- Manual updated for changes and contact info + +Thanks to Diana Coman ( http://ossasepia.com/ ) for testing and feedback leading to this release. + 97K (2019-06-05) ================ diff -uNr a/yrc/README.txt b/yrc/README.txt --- a/yrc/README.txt ab08d43b0754f25bae7f43b5ed9f2d88ebf71605d2426ce6a78369699f5239464ab23a91d4a1aa950d109f7f1b7ad0f598d5a2028c70b6d1ec22e3985540cd74 +++ b/yrc/README.txt a0925ad7a545437d5c9b7266d1732e3b48b9d46012b7d6315526c338d9df3778a674ec8a707306c6f76e93dfb92a6daa52075ec5d5df8772215934b03ce1666f @@ -2,7 +2,7 @@ INSTALLATION - The dependencies of yrc are Python 2.7 (on a POSIX system) and VT100-style terminal. (Earlier Pythons may work as well but are presently untested.) + The dependencies of yrc are Python 2.6 or 2.7 (on a POSIX system) and VT100-style terminal. Python 2.5 may work as well but is presently untested. It can be run directly from the source tree: diff -uNr a/yrc/codemap.txt b/yrc/codemap.txt --- a/yrc/codemap.txt 6d9b991a507a3a8cb7cfdf6830017901a1519d6860cfb29f38a29f0a1a416cb191da6a7f39a793536b763ec542f80e4fa431029bca8df0c6740f520760592ef1 +++ b/yrc/codemap.txt 5f2f130c0194f6f03a2eebdceca4f4936875cc449c1e9040adb318037f1d0ee6ca75ebc85302053197bb3394e7ad0a198f262135e26db0f7c09145a47af41db7 @@ -21,7 +21,7 @@ - name: str - parent: buf - title: str -- vscroll: int +- vscroll: (origin: int, offset: int) - lines: list of str - num_read: int - at_end: bool @@ -129,7 +129,6 @@ variant_args(val) -> list matcher(vtype, cases: iterable of (constructor, receiver: function of (*args -> x))) -> function of (val: vtype -> x) sequence(*thunks) -> thunk -flatten(iterable of iterable) -> iterable char_range(pair) -> str partition(list, pred) -> (left: list, right: list) split_pair(str, sep=' ': str) -> [str, str] @@ -147,11 +146,18 @@ parse_address(addr) -> (host: str, port: int) / ValueError format_address((host: str, port: int)) -> str int_of_bytes(str) -> int +bit_length(int) -> int heap_peek(heap: list) -> (key, value) / IndexError safe_filename(name) -> bool config_lines(text) -> non-empty list of non-empty str | None format_buf_msg(val: message) -> str -render_lines(lines: list of str, width: int, start: int, row_limit: int) -> (list of list, int) +buf_width() -> int +buf_height() -> int +render_line(str) -> list of str +buf_max_vscroll(buf) -> (int, int) +add_scroll_coords(lines: list of str, coords: (int, int), delta: int) -> (int, int) +sub_scroll_coords(lines: list of str, coords: (int, int), delta: int) -> (int, int) +render_lines(lines: list of str, vscroll: (int, int), row_limit: int) -> (list, int) build_msg(prefix, cmd, params) -> str max_param_len(cmd, prefix=None) -> int parse_msg(msg) -> (prefix: str | None, cmd: str, params: list of str) / ProtocolError diff -uNr a/yrc/manifest b/yrc/manifest --- a/yrc/manifest 8ef173b8984b3b7d5356ed70caeab06943c95d6d569bc2cd3137652b6477a167ca7e8b69767678b232de7228ce79d980cdd5bfcd5745f2a613510e10c8eecaba +++ b/yrc/manifest ae6bd2e7be742bf1e83941f2a8d0a18a600c992ff7c99bdd1df0d82c9c81f700e29dee073f6fb118348877ad7b2e6d2d85811867d09a4017c86d22ae1836add5 @@ -1 +1,2 @@ 632836 yrc_subdir_genesis jfw Initial V release of yrc, version 97K (regrind of yrc_genesis to follow the project subdir and manifest naming conventions and indent with tabs rather than spaces) +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. diff -uNr a/yrc/manual.txt b/yrc/manual.txt --- a/yrc/manual.txt 41380b168aeac2af439580b38a51b022d63d1212427d0392bc39ac97059ff25f5ec098eec856705b9bc96aaa1a69333208264d6dc43da62cd2d69d2445b96263 +++ b/yrc/manual.txt 063396f1f2d133868b5992658367a82b2558f957657140effb5bc582df27e7ed17f096ec03615ebcd46d5f892fee432709a4a57e0425ee4ab2cfeed8fb9100b2 @@ -18,7 +18,8 @@ 4.2 Logging 4.3 Example 5 Roadmap - 6 Notes + 6 News and contact + Footnotes 1. About yrc @@ -60,9 +61,9 @@ 8. Chat logging -It is written in Python 2.7 (though this is an implementation detail and subject to change). Some of its more reusable terminal interface code is included as a separate "yterm" module. The yrc+yterm code weighs in around 2360 raw lines, compressing to 18KB. +It is written in Python 2.6 (though this is an implementation detail and subject to change). Some of its more reusable terminal interface code is included as a separate "yterm" module. The yrc+yterm code weighs in around 2390 raw lines, compressing to 19KB. -While source code is published freely, the project does not accept code contributions from unknown entities. +yrc is written, published and maintained by JWRD Computing ( http://jwrd.net/ ). 2. Display @@ -269,7 +270,12 @@ C-v, PgDn: scroll-down-page M-v, PgUp: scroll-up-page -Scrolls the active scrollback window (see 3.2.4). +Scrolls the active scrollback window. + + M-<: scroll-top + M->: scroll-bottom + +Scrolls the active scrollback window to the top or bottom. 3.2.4. In the scrollback window @@ -282,12 +288,12 @@ C-v, C-f, f, PgDn, Space: scroll-down-page M-v, C-b, b, PgUp: scroll-up-page -Scrolls the window down or up by a page. The current implementation is fairly stupid and scrolls by half a page worth of messages, ignorant of wrapping. +Scrolls the window down or up by a page. C-n, j, Down: scroll-down-line C-p, k, Up: scroll-up-line -Scrolls the window down or up by one message. +Scrolls the window down or up by one wrapped line. M-<, g, Home: scroll-top M->, G, End: scroll-bottom @@ -364,7 +370,6 @@ - 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 based on rendered lines rather than messages - Scrolling + more efficient navigation of window list Nice to have: @@ -379,7 +384,13 @@ - Channel join status indicators - Finish config fields [?] -6. Notes +6. News and contact + +News can be found in the yrc category on the author's blog: http://fixpoint.welshcomputing.com/category/software/yrc/ + +Questions/comments can be left on articles there or by visiting #fixpoint on Freenode. + +Footnotes [1] Inspired by DJB software, http://cr.yp.to/. diff -uNr a/yrc/pylintrc b/yrc/pylintrc --- a/yrc/pylintrc 13cc8d8884d9d0d48877f5e4d43c26556763ed55e2d1ef5cd943f2e742168b6c9cc3282164251ae9e9f66c6f7c3c7929e77f7b7b8fd6e4b8be9443793e03ed56 +++ b/yrc/pylintrc 433827f3391fd161df7a85eb3480e5a5c706fd916d02298c2483b3594bb19feedb9c005da92a46430a90fa6eb7154dc0a443c36fc64d2828f08fa0f8cbcaba01 @@ -4,7 +4,7 @@ [MESSAGES CONTROL] -disable=global-statement,missing-docstring,bad-builtin +disable=global-statement,missing-docstring,bad-builtin,bad-continuation [REPORTS] @@ -26,6 +26,7 @@ const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|([a-z_][a-z0-9_]{2,30}))$ [FORMAT] +indent-string = "\t" max-line-length = 80 max-module-lines = 10000 diff -uNr a/yrc/setup.py b/yrc/setup.py --- a/yrc/setup.py bc991e62d46adb82f669327a05bc6b24009d61b88a24162009ebc99999dfff141001b0aa16b1b6296aac593cce0a3f0b26570f3bdbe2c662ce28ad40f56c84f9 +++ b/yrc/setup.py 1e75beb3ae11ab60465d7fb851e0ffdbbef8cd130dde4f9372e0237ec3d3e941c645d4bb2690e3bb598862709ac7373526f34ae8aeb4cd1cc545cda4bf766c00 @@ -5,5 +5,5 @@ version='97K', description='A Unix IRC client', py_modules=['yrc', 'yterm'], - scripts=['yrc'], + scripts=['yrc', 'yrc2local'], ) diff -uNr a/yrc/yrc.py b/yrc/yrc.py --- a/yrc/yrc.py 92cf8fe956976c0aac193657ed7a86b1d0d338c132766cd952e7123140badcea77bc51261819a30c23767e3bc5a518a76b511ea218e70d07dada6fc402275836 +++ b/yrc/yrc.py 27a0dab1a22aad54ce93d7805e682c8ad5a26655ee0b62bc9014fc557af97f3b123bad1a94dd0a25fe0167742b00ec6e08a699e50f016d80ea7b8ccc474a4366 @@ -4,7 +4,7 @@ February 2017 - June 2019 """ -__version__ = '97 Kelvin' +__version__ = '96 Kelvin' # Knobs that one might conceivably want to tweak DEFAULT_PORT = 6667 @@ -99,6 +99,10 @@ ctrl('V'): 'scroll-down-page', PGDN: 'scroll-down-page', + + meta('<'): 'scroll-top', + + meta('>'): 'scroll-bottom', } BUFLIST_KEYS = { @@ -255,11 +259,6 @@ f() return run -def flatten(iterables): - for it in iterables: - for val in it: - yield val - def char_range(pair): a, b = pair return ''.join(map(chr, range(ord(a), ord(b) + 1))) @@ -329,6 +328,7 @@ return clip_str(s, width) def wrap(line, width, sep=' '): + width = max(1, width) start = 0 lines = [] while start + width < len(line): @@ -364,11 +364,14 @@ int_of_bytes = lambda b: reduce(lambda acc, byte: acc*256 + ord(byte), b, 0) +# int.bit_length() added in Python 2.7 +bit_length = lambda i: len(bin(i).lstrip('-0b')) + def rand_int(n): """Get a random integer from 0 (inclusive) to n (exclusive) using the system's nonblocking entropy pool. More-or-less deterministic run time, at the cost of a small modulo bias.""" - nbytes = (n.bit_length() + 7) / 8 + nbytes = (bit_length(n) + 7) / 8 with open('/dev/urandom', 'rb') as f: return int_of_bytes(f.read(2*nbytes)) % n @@ -545,7 +548,8 @@ set_ping_draw = _setf(11) clr_ping_draw = _clrf(11) -new_buf = lambda name, parent, title: [name, parent, title, 0, [], 0, True] +new_buf = lambda name, parent, title: [name, parent, title, + (0, 0), [], 0, True] buf_name = lambda b: b[0] buf_parent = lambda b: b[1] @@ -557,7 +561,8 @@ buf_at_end = lambda b: b[6] buf_set_title = lambda b, title: b.__setitem__(2, title) -buf_set_vscroll = lambda b, i: b.__setitem__(3, clip_to(buf_lines(b), i)) +buf_set_vscroll = lambda b, coords: b.__setitem__(3, + (clip_to(buf_lines(b), coords[0]), coords[1])) buf_set_num_read = lambda b, n: b.__setitem__(5, max(buf_num_read(b), n)) buf_set_at_end = lambda b: b.__setitem__(6, True) buf_clr_at_end = lambda b: b.__setitem__(6, False) @@ -604,35 +609,58 @@ raise CommandError("can't send messages here") conn_privmsg(buf_registered_conn(buf), buf_name(buf), msg) -def render_lines(lines, width, start, row_limit): - """Returns list of wrapped line lists, and the number of *complete* source - lines in the output""" - rows = 0 - groups = [] - while rows < row_limit and start < len(lines): - msg = lines[start] - group = wrap(asciify(msg), width) - rows += len(group) - if rows > row_limit: - group = group[:row_limit - rows] - groups.append(group) - return groups, len(groups) - 1 - groups.append(group) - start += 1 - return groups, len(groups) +buf_width = lambda: scr_width - buflist_width() +buf_height = lambda: scr_height - 3 +render_line = lambda line: wrap(asciify(line), buf_width()) -def check_buf_at_end(): - width = scr_width - buflist_width() - height = scr_height - 3 - if height < 1 or width < 1: - return - lines = buf_lines(cur_buf) - scroll = buf_vscroll(cur_buf) - lines_shown = render_lines(lines, width, scroll, height)[1] - if scroll + lines_shown == len(lines): - buf_set_at_end(cur_buf) - else: - buf_clr_at_end(cur_buf) +def buf_max_vscroll(buf): + lines = buf_lines(buf) + if len(lines) == 0: + return 0, 0 + origin = len(lines) - 1 + return origin, len(render_line(lines[origin])) - 1 + +def add_scroll_coords(lines, coords, delta): + origin, offset = coords + if origin >= len(lines): + return max(1, len(lines)) - 1, 0 + delta += offset + while delta > 0: + n_rows = len(render_line(lines[origin])) + if n_rows > delta: + break + delta -= n_rows + origin += 1 + if origin == len(lines): # past the last line + return origin - 1, n_rows - 1 + return origin, delta + +def sub_scroll_coords(lines, coords, delta): + origin, offset = coords + if offset >= delta: + return origin, offset - delta + delta -= offset + while origin > 0: + origin -= 1 + n_rows = len(render_line(lines[origin])) + if n_rows >= delta: + return origin, n_rows - delta + delta -= n_rows + return 0, 0 + +def render_lines(lines, coords, row_limit): + origin, offset = coords + rows = [] + row_limit += offset + while len(rows) < row_limit and origin < len(lines): + group = render_line(lines[origin]) + space = row_limit - len(rows) + if len(group) > space: + group = group[:space] + else: + origin += 1 + rows.extend(group) + return rows[offset:], origin def buf_draw(buf): if scr_height < 2: @@ -642,34 +670,30 @@ write_out(t.render(( t.A_REVERSE, pad_or_clip_str(asciify(buf_title(buf)), scr_width)))) left_column = buflist_width() + 1 - width = scr_width - buflist_width() - top_row = 2 - height = scr_height - 3 - if height < 1 or width < 1: + w = buf_width() + h = buf_height() + if w < 1 or h < 1: return + top_row = 2 lines = buf_lines(buf) - while True: - scroll = buf_vscroll(buf) - groups, lines_shown = render_lines(lines, width, scroll, height) - lines_shown += scroll - if lines_shown == len(lines): - buf_set_at_end(buf) - break - if not buf_at_end(buf): - break - # Was at end until now: autoscroll - if scroll + 1 == len(lines): - break # Couldn't even fit one complete line - buf_set_vscroll(buf, max(scroll + 1, len(lines) - height)) - row = 0 - for row, text in enumerate(flatten(groups)): - write_out(t.cursor_pos(top_row + row, left_column) + text) - if len(text) != width: + rows, next_origin = render_lines(lines, buf_vscroll(buf), h) + if next_origin == len(lines): + buf_set_at_end(buf) + elif buf_at_end(buf): + # no longer at end: autoscroll + buf_set_vscroll(buf, + sub_scroll_coords(lines, buf_max_vscroll(buf), h - 1)) + rows, next_origin = render_lines(lines, buf_vscroll(buf), h) + buf_set_num_read(buf, next_origin) + n = -1 + for n, row in enumerate(rows): + write_out(t.cursor_pos(top_row + n, left_column)) + write_out(row) + if len(row) < w: write_out(t.ERASE_LINE_TO_END) - for row in range(row + 1, height): - write_out(t.cursor_pos(top_row + row, left_column)) + for n in range(n + 1, h): + write_out(t.cursor_pos(top_row + n, left_column)) write_out(t.ERASE_LINE_TO_END) - buf_set_num_read(buf, lines_shown) buf_draw_done() cur_buf = new_buf('yrc', None, 'yrc general messages') @@ -721,12 +745,10 @@ def close_buf(buf): i = find_buf(buf) assert i > 0 - prev = buffers[i - 1] - buflist_set_cursor(prev) last = buflist_last() - if last is buf: # not sure if this is possible but just in case - last = prev - buflist_select(prev) + if last is buf: + last = buffers[i - 1] + buflist_select(last) buflist_set_last(last) parent = buf_parent(buf) parent_name = buf_name(parent) if parent else None @@ -736,32 +758,41 @@ command('scroll-down-page')(sequence( schedule_buf_draw, schedule_buflist_draw, # update unread counts - lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) + (scr_height-3)/2))) + lambda: buf_set_vscroll(cur_buf, + add_scroll_coords(buf_lines(cur_buf), + buf_vscroll(cur_buf), buf_height() - 1)))) command('scroll-up-page')(sequence( schedule_buf_draw, - lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) - (scr_height-3)/2), - check_buf_at_end)) # stop autoscrolling if no longer at end + lambda: buf_set_vscroll(cur_buf, + sub_scroll_coords(buf_lines(cur_buf), + buf_vscroll(cur_buf), buf_height() - 1)), + # Stop autoscrolling, to be resumed by buf_draw if it turns out the last line is still on screen + lambda: buf_clr_at_end(cur_buf))) command('scroll-down-line')(sequence( schedule_buf_draw, schedule_buflist_draw, - lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) + 1))) + lambda: buf_set_vscroll(cur_buf, + add_scroll_coords(buf_lines(cur_buf), + buf_vscroll(cur_buf), 1)))) command('scroll-up-line')(sequence( schedule_buf_draw, - lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) - 1), - check_buf_at_end)) + lambda: buf_set_vscroll(cur_buf, + sub_scroll_coords(buf_lines(cur_buf), + buf_vscroll(cur_buf), 1)), + lambda: buf_clr_at_end(cur_buf))) command('scroll-bottom')(sequence( schedule_buf_draw, schedule_buflist_draw, - lambda: buf_set_vscroll(cur_buf, len(buf_lines(cur_buf))))) + lambda: buf_set_vscroll(cur_buf, buf_max_vscroll(cur_buf)))) command('scroll-top')(sequence( schedule_buf_draw, - lambda: buf_set_vscroll(cur_buf, 0), - check_buf_at_end)) + lambda: buf_set_vscroll(cur_buf, (0, 0)), + lambda: buf_clr_at_end(cur_buf))) def info(msg, buf=None): buf_log_msg(buf or buffers[0], m_client(msg)) @@ -940,12 +971,11 @@ def buflist_select(b): global cur_buf schedule_buflist_draw() - schedule_buf_draw() - schedule_status_draw() if b is not cur_buf: + schedule_buf_draw() + schedule_status_draw() buflist_set_last(cur_buf) - # don't autoscroll a newly selected window (at_end will get restored on - # draw if it's still at the end) + # Stop autoscrolling for newly selected window, to be resumed by buf_draw if it turns out the last line is still on screen buf_clr_at_end(b) buflist_set_cursor(b) cur_buf = b @@ -2029,7 +2059,14 @@ def handle_resize(*_): global scr_height, scr_width scr_height, scr_width = t.screen_size() + # Re-clip scrolling as last line may now fit above viewport + max_vsc = buf_max_vscroll(cur_buf) + if buf_vscroll(cur_buf) > max_vsc: + buf_set_vscroll(cur_buf, max_vsc) + # Stop autoscrolling, to be resumed by buf_draw if it turns out the last line is still on screen + buf_clr_at_end(cur_buf) schedule_redraw() + # Wake main event loop try: write(self_pipe_wr, '\0') except EnvironmentError, e: diff -uNr a/yrc/yrc2local b/yrc/yrc2local --- a/yrc/yrc2local false +++ b/yrc/yrc2local 211474f9f20cb062469bf344f38ed1e21b3393c242335205566a0ce5288380ac3793abff76b301bbeb4525b7b1adc417de538497f2ce259b42c36478fdfc570d @@ -0,0 +1,11 @@ +#!/usr/bin/python2 +import time +from sys import stdin, stdout + +try: + for line in stdin: + t, rest = line.split(' ', 1) + t = int(t)/100 + stdout.write(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t)) + ' ' + rest) +except IOError: + pass diff -uNr a/yrc/yterm.py b/yrc/yterm.py --- a/yrc/yterm.py eda12507121074ec6d06b138abf2b654a2a3e25b879e2d00ad7ec6bb6956f49d120e7b2ec8ec0efc245a379a11a52ddf6db065fddb420b19a27b4aeddfdb52c6 +++ b/yrc/yterm.py 6f9a7e8e49fc16c2ee668ffaaf9b046d5507d96df34b520a87a56fe21e66ba79bf866c80918899b1ae18237c6a268edf315b5497a0c5c650796528453771ec10 @@ -20,7 +20,7 @@ SI = chr(15) ESC = chr(27) -# VT100 http://vt100.net/docs/vt100-ug/chapter3.html +# VT100 http://fixpoint.welshcomputing.com/library/vt100-ug/chapter3.html RESET = ESC + 'c' CURSOR_SAVE = ESC + '7' CURSOR_RESTORE = ESC + '8' @@ -76,7 +76,7 @@ ATTR_BLINK = attrs(A_BLINK) ATTR_REVERSE = attrs(A_REVERSE) -# DEC special graphics characters http://vt100.net/docs/vt100-ug/table3-9.html +# DEC special graphics characters http://fixpoint.welshcomputing.com/library/vt100-ug/table3-9.html SGC_BLANK = '_' SGC_DIAMOND = '`' SGC_CHECKER = 'a' @@ -110,7 +110,7 @@ SGC_STERLING = '}' SGC_CENTERDOT = '~' -# XTerm http://invisible-island.net/xterm/ctlseqs/ctlseqs.html +# XTerm http://fixpoint.welshcomputing.com/library/xterm/ctlseqs/ctlseqs.html ALT_SCREEN_ON = set_priv_mode(47) ALT_SCREEN_OFF = rst_priv_mode(47) BRACKETED_PASTE_ON = set_priv_mode(2004)