Projects : yrc : genesis

yrc.py

Dir - Raw

1"""
2yrc: the Unix IRC client
3Jacob Welsh
4February 2017 - June 2019
5"""
6
7__version__ = '97 Kelvin'
8
9# Knobs that one might conceivably want to tweak
10DEFAULT_PORT = 6667
11RECONN_DELAY_MIN = 4 # seconds
12RECONN_DELAY_MAX = 256
13PING_INTERVAL = 120
14PING_TIMEOUT = 240
15BUFLIST_WIDTH_MIN = len('* =#a$|')
16BUFLIST_WIDTH_STEP = 4
17BUFLIST_WIDTH_START = BUFLIST_WIDTH_MIN + 3*BUFLIST_WIDTH_STEP
18format_time = lambda tt: '%02d:%02d:%02d' % (tt.tm_hour, tt.tm_min, tt.tm_sec)
19
20from os import (
21 O_APPEND, O_CREAT, O_NONBLOCK, O_WRONLY, chdir, close, getenv, mkdir,
22 open as os_open, pipe, read, strerror, write
23)
24from time import localtime, time
25from errno import EAGAIN, EWOULDBLOCK, EEXIST, EINPROGRESS, EINTR, ENOENT
26from select import select, error as SelectError
27from signal import signal, SIG_DFL, SIGINT, SIGWINCH
28from socket import socket, SOL_SOCKET, SO_ERROR, SHUT_RDWR, error as SockError
29
30from fcntl import fcntl, F_GETFL, F_SETFL
31
32import yterm as t
33from yterm import NUL, BS, LF, CR, SO, SI, ESC, CSI, cseq
34STDIN = 0
35STDOUT = 1
36self_pipe_rd, self_pipe_wr = pipe()
37
38is_ctrl = lambda c: c < '\x20' or c == '\x7f'
39ctrl = lambda c: chr(ord(c) ^ 0x40)
40
41meta = lambda c: ESC + c
42
43BEL = chr(7)
44CRLF = CR + LF
45C_X = ctrl('X')
46C_G = ctrl('G') # aka BEL
47
48# Cursor control keys
49LEFT = t.CURSOR_BACK1
50RIGHT = t.CURSOR_FORWARD1
51UP = t.CURSOR_UP1
52DOWN = t.CURSOR_DOWN1
53HOME = t.CURSOR_HOME
54# Not sure where these originate; vt220 perhaps? Seen on xterm, linux, screen
55END = cseq('F')
56INS = cseq('~', 2)
57DEL = cseq('~', 3)
58PGUP = cseq('~', 5)
59PGDN = cseq('~', 6)
60# Likewise; so far seen on linux, screen
61LNX_HOME = cseq('~', 1)
62LNX_END = cseq('~', 4)
63
64GLOBAL_KEYS = {
65 C_X + 'n': 'buflist-switch-next',
66 C_X + 'p': 'buflist-switch-prev',
67 C_X + 'l': 'buflist-last-selected',
68 C_X + 'w': 'buflist-enter',
69 ctrl('L'): 'redraw',
70}
71
72PROMPT_KEYS = {
73 CR: 'prompt-submit',
74
75 INS: 'prompt-exit',
76
77 ctrl('F'): 'prompt-forward',
78 RIGHT: 'prompt-forward',
79
80 ctrl('B'): 'prompt-backward',
81 LEFT: 'prompt-backward',
82
83 ctrl('A'): 'prompt-start',
84 HOME: 'prompt-start',
85 LNX_HOME: 'prompt-start',
86
87 ctrl('E'): 'prompt-end',
88 END: 'prompt-end',
89 LNX_END: 'prompt-end',
90
91 ctrl('H'): 'prompt-backspace',
92 ctrl('?'): 'prompt-backspace',
93
94 ctrl('D'): 'prompt-delete',
95 DEL: 'prompt-delete',
96
97 meta('v'): 'scroll-up-page',
98 PGUP: 'scroll-up-page',
99
100 ctrl('V'): 'scroll-down-page',
101 PGDN: 'scroll-down-page',
102}
103
104BUFLIST_KEYS = {
105 CR: 'buflist-submit',
106
107 ctrl('N'): 'buflist-next',
108 DOWN: 'buflist-next',
109 'j': 'buflist-next',
110
111 ctrl('P'): 'buflist-prev',
112 UP: 'buflist-prev',
113 'k': 'buflist-prev',
114
115 meta('<'): 'buflist-top',
116 HOME: 'buflist-top',
117 LNX_HOME: 'buflist-top',
118 'g': 'buflist-top',
119
120 meta('>'): 'buflist-bottom',
121 END: 'buflist-bottom',
122 LNX_END: 'buflist-bottom',
123 'G': 'buflist-bottom',
124
125 'h': 'buflist-shrink',
126 LEFT: 'buflist-shrink',
127
128 'l': 'buflist-grow',
129 RIGHT: 'buflist-grow',
130}
131
132BUF_KEYS = {
133 'i': 'prompt-enter',
134 INS: 'prompt-enter',
135
136 meta('v'): 'scroll-up-page',
137 PGUP: 'scroll-up-page',
138 ctrl('B'): 'scroll-up-page',
139 'b': 'scroll-up-page',
140
141 ctrl('V'): 'scroll-down-page',
142 PGDN: 'scroll-down-page',
143 ctrl('F'): 'scroll-down-page',
144 'f': 'scroll-down-page',
145 ' ': 'scroll-down-page',
146
147 ctrl('P'): 'scroll-up-line',
148 UP: 'scroll-up-line',
149 'k': 'scroll-up-line',
150
151 ctrl('N'): 'scroll-down-line',
152 DOWN: 'scroll-down-line',
153 'j': 'scroll-down-line',
154
155 meta('<'): 'scroll-top',
156 HOME: 'scroll-top',
157 LNX_HOME: 'scroll-top',
158 'g': 'scroll-top',
159
160 meta('>'): 'scroll-bottom',
161 END: 'scroll-bottom',
162 LNX_END: 'scroll-bottom',
163 'G': 'scroll-bottom',
164}
165
166CMD_ABBREVS = {
167 'c': 'connect',
168 'd': 'disconnect',
169 'j': 'join',
170 'k': 'kick',
171 'l': 'list',
172 'm': 'mode',
173 'n': 'nick',
174 'nam': 'names',
175 'p': 'part',
176 'q': 'quit',
177 's': 'send',
178 't': 'topic',
179 'st': 'set-topic',
180 'w': 'whois',
181 'ww': 'whowas',
182 'x': 'close',
183 'xn': 'close-net',
184}
185
186# file.write can barf up EINTR and unclear how to retry due to buffering. Nuts.
187def write_all(fd, s):
188 n = 0
189 while n < len(s):
190 try:
191 n += write(fd, s[n:])
192 except EnvironmentError, e:
193 if e.errno != EINTR:
194 raise
195
196out_buf = bytearray()
197write_out = out_buf.extend
198def flush_out():
199 write_all(STDOUT, out_buf)
200 del out_buf[:]
201
202# MLtronics
203def variant(vtype, name, nargs):
204 tag = (vtype, len(vtype), name)
205 def cons(*args):
206 if len(args) != nargs:
207 raise TypeError('%s takes %d args (%d given)'
208 % (name, nargs, len(args)))
209 return (tag, args)
210 vtype.append((name, cons))
211 return cons
212
213variant_name = lambda val: val[0][2]
214variant_args = lambda val: val[1]
215
216def matcher(vtype, cases):
217 def receiver(name, cons):
218 for case, recv in cases:
219 if case is cons:
220 return recv
221 raise TypeError('missing case %s' % name)
222 tbl = [receiver(name, cons) for name, cons in vtype]
223 def match(val):
224 tag, args = val
225 if tag[0] is not vtype:
226 raise TypeError
227 return tbl[tag[1]](*args)
228 return match
229
230message = []
231m_privmsg = variant(message, 'PRIVMSG', 2)
232m_notice = variant(message, 'NOTICE', 2)
233m_join = variant(message, 'JOIN', 2)
234m_part = variant(message, 'PART', 3)
235m_quit = variant(message, 'QUIT', 2)
236m_nick = variant(message, 'NICK', 2)
237m_kick = variant(message, 'KICK', 4) # kick of specified user
238m_kicked = variant(message, 'KICKED', 3) # kick of self
239m_topic = variant(message, 'TOPIC', 2) # unifies TOPIC and RPL_(NO)TOPIC
240m_chantopic = variant(message, 'CHANTOPIC', 3) # for unjoined channels
241m_mode = variant(message, 'MODE', 2)
242m_chanmode = variant(message, 'CHANMODE', 3)
243m_names = variant(message, 'NAMES', 3)
244m_endnames = variant(message, 'ENDNAMES', 2)
245m_error = variant(message, 'ERROR', 2) # ERROR from server
246m_client = variant(message, 'CLIENT', 1) # generated by client, not logged
247m_server = variant(message, 'SERVER', 2) # catch-all for other server msgs
248
249scr_height = None
250scr_width = None
251
252def sequence(*thunks):
253 def run():
254 for f in thunks:
255 f()
256 return run
257
258def flatten(iterables):
259 for it in iterables:
260 for val in it:
261 yield val
262
263def char_range(pair):
264 a, b = pair
265 return ''.join(map(chr, range(ord(a), ord(b) + 1)))
266
267def partition(l, pred):
268 left = []
269 right = []
270 for elem in l:
271 if pred(elem):
272 left.append(elem)
273 else:
274 right.append(elem)
275 return left, right
276
277def split_pair(s, sep=' '):
278 pair = s.split(sep, 1)
279 if len(pair) == 1:
280 pair.append('')
281 return pair
282
283def make_encoder(f):
284 tbl = [f(i) for i in range(0x100)]
285 return lambda s: ''.join(tbl[ord(c)] for c in s)
286
287# Non-invertible terminal sanitizing
288asciify = make_encoder(lambda i:
289 '^' + ctrl(chr(i)) if i < 0x20 or i == 0x7f
290 else chr(i) if 0x20 <= i <= 0x7e
291 else '\\x%X' % i)
292
293# Uglier, injective encoding for file names
294fs_encode = make_encoder(lambda i:
295 chr(i) if 0x20 <= i <= 0x7e and chr(i) not in './%'
296 else '%' + ('%02X' % i))
297
298make_casemapper = lambda upper_bound: make_encoder(lambda i:
299 chr(i + 0x20) if 0x41 <= i <= upper_bound
300 else chr(i))
301
302# We currently support only the original IRC case mapping (as implemented, not
303# as specified; one of many howlers in the RFC). The RPL_ISUPPORT reply that
304# some servers use to advertise alternate mappings was specified in a draft
305# long since expired
306# (http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt). Besides,
307# a case-insensitive protocol that can't agree on what "case" means is
308# preposterous. This is an IRC client; if a server does not behave like an IRC
309# server, it doesn't fall to us to decipher its nonsense.
310
311IRC_CASEMAP = {'rfc1459': make_casemapper(0x5e)}
312
313casemap_ascii = make_casemapper(0x5a)
314
315clip = lambda min_, max_, x: max(min_, min(max_, x))
316clip_to = lambda l, i: clip(0, len(l) - 1, i)
317get_clipped = lambda l, i: l[clip_to(l, i)]
318
319def clip_str(s, width):
320 if width == 0:
321 return ''
322 if len(s) > width:
323 return '%s$' % s[:width - 1]
324 return s
325
326def pad_or_clip_str(s, width, pad=' '):
327 if len(s) < width:
328 return s + pad*(width - len(s))
329 return clip_str(s, width)
330
331def wrap(line, width, sep=' '):
332 start = 0
333 lines = []
334 while start + width < len(line):
335 end = start + width
336 cut = line.rfind(sep, start, end + 1)
337 if cut == -1:
338 lines.append(line[start:end])
339 start = end
340 else:
341 lines.append(line[start:cut])
342 start = cut + 1
343 lines.append(line[start:])
344 return lines
345
346def is_digit(c):
347 return '0' <= c <= '9'
348
349def parse_address(addr):
350 addr = addr.rsplit(':', 1)
351 host = addr[0]
352 if len(addr) == 1:
353 port = DEFAULT_PORT
354 else:
355 port = addr[1]
356 if not all(map(is_digit, port)):
357 raise ValueError('port not a positive integer: %r' % port)
358 port = int(port)
359 if port >= 2**16:
360 raise ValueError('port out of range: %d' % port)
361 return host, port
362
363format_address = lambda host_port: '%s:%d' % host_port
364
365int_of_bytes = lambda b: reduce(lambda acc, byte: acc*256 + ord(byte), b, 0)
366
367def rand_int(n):
368 """Get a random integer from 0 (inclusive) to n (exclusive) using the
369 system's nonblocking entropy pool. More-or-less deterministic run time, at
370 the cost of a small modulo bias."""
371 nbytes = (n.bit_length() + 7) / 8
372 with open('/dev/urandom', 'rb') as f:
373 return int_of_bytes(f.read(2*nbytes)) % n
374
375#
376# Binary min-heap, used as priority queue for scheduling
377#
378
379def heap_insert(h, key, value):
380 h.append((key, value))
381 # Percolate up
382 i = len(h) - 1
383 while i > 0:
384 i_parent = (i - 1)/2
385 item = h[i]
386 parent = h[i_parent]
387 if parent[0] <= item[0]:
388 break
389 h[i] = parent
390 h[i_parent] = item
391 i = i_parent
392
393heap_peek = lambda h: h[0]
394
395def heap_extract(h):
396 if len(h) == 1:
397 return h.pop()
398 result = h[0]
399 h[0] = h.pop()
400 # Percolate down
401 i = 0
402 while True:
403 i_child = 2*i + 1
404 if i_child >= len(h):
405 break
406 i_right = i_child + 1
407 if i_right < len(h) and h[i_right][0] < h[i_child][0]:
408 i_child = i_right
409 item = h[i]
410 child = h[i_child]
411 if item[0] <= child[0]:
412 break
413 h[i] = child
414 h[i_child] = item
415 i = i_child
416 return result
417
418# purely for testing
419def heapsort(iterable):
420 h = []
421 for item in iterable:
422 heap_insert(h, item, None)
423 while h:
424 yield heap_extract(h)[0]
425
426#
427# Config
428#
429
430def safe_filename(name):
431 return '/' not in name and name not in ('', '.', '..')
432
433def get_config(key, paths=(()), default=None):
434 assert safe_filename(key)
435 for path in paths:
436 assert all(map(safe_filename, path))
437 fspath = []
438 fspath.extend(path)
439 fspath.append(key)
440 try:
441 with open('/'.join(fspath), 'rb') as f:
442 return f.read().rstrip('\n')
443 except EnvironmentError, e:
444 if e.errno != ENOENT:
445 error(e)
446 return default
447
448def config_lines(text):
449 if text is None:
450 return None
451 lines = text.split('\n')
452 lines = [l.strip() for l in lines if not l.startswith('#')]
453 lines = [l for l in lines if len(l) > 0]
454 return lines if lines else None
455
456#
457# UI stuff
458#
459
460class CommandError(Exception):
461 pass
462
463commands = {}
464def command(name, min_args=0, max_args=None, extended_arg=False):
465 if max_args is None:
466 max_args = min_args
467 def register(func):
468 if name in commands:
469 raise ValueError('duplicate command %s' % name)
470 def run(arg_str=''):
471 args = []
472 while len(args) != max_args:
473 arg, arg_str = split_pair(arg_str.lstrip())
474 if arg == '':
475 break
476 args.append(arg)
477 arg_str = arg_str.lstrip()
478 if extended_arg:
479 args.append(arg_str)
480 elif arg_str:
481 raise CommandError('%s: too many arguments' % name)
482 if len(args) < min_args:
483 raise CommandError('%s: too few arguments' % name)
484 return func(*args)
485 commands[name] = run
486 return func
487 return register
488
489def check_command_dicts(dicts):
490 for d in dicts:
491 for cmd in d.itervalues():
492 assert cmd in commands, cmd
493
494def run_command(line):
495 cmd, args = split_pair(line)
496 cmd = CMD_ABBREVS.get(cmd, cmd)
497 func = commands.get(cmd)
498 if func is None:
499 raise CommandError('bad command: %s' % cmd)
500 return func(args)
501
502flags = [False]*12
503_getf = lambda i: (lambda: flags[i])
504_setf = lambda i: (lambda: flags.__setitem__(i, True))
505_clrf = lambda i: (lambda: flags.__setitem__(i, False))
506
507refresh_flag = _getf(0)
508schedule_refresh = _setf(0)
509refresh_done = _clrf(0)
510
511redraw_flag = _getf(1)
512schedule_redraw = _setf(1)
513redraw_done = _clrf(1)
514
515buflist_draw_flag = _getf(2)
516schedule_buflist_draw = _setf(2)
517buflist_draw_done = _clrf(2)
518
519buf_draw_flag = _getf(3)
520schedule_buf_draw = _setf(3)
521buf_draw_done = _clrf(3)
522
523prompt_draw_flag = _getf(4)
524schedule_prompt_draw = _setf(4)
525prompt_draw_done = _clrf(4)
526
527status_draw_flag = _getf(5)
528schedule_status_draw = _setf(5)
529status_draw_done = _clrf(5)
530
531quit_flag = _getf(6)
532schedule_quit = _setf(6)
533
534# 7-8: available
535
536buflist_flag = _getf(9)
537buflist_enter = _setf(9)
538buflist_exit = _clrf(9)
539
540prompt_flag = _getf(10)
541prompt_enter = _setf(10)
542prompt_exit = _clrf(10)
543
544ping_draw_flag = _getf(11)
545set_ping_draw = _setf(11)
546clr_ping_draw = _clrf(11)
547
548new_buf = lambda name, parent, title: [name, parent, title, 0, [], 0, True]
549
550buf_name = lambda b: b[0]
551buf_parent = lambda b: b[1]
552buf_title = lambda b: b[2]
553buf_vscroll = lambda b: b[3]
554buf_lines = lambda b: b[4]
555buf_num_read = lambda b: b[5]
556buf_unread = lambda b: len(buf_lines(b)) - buf_num_read(b)
557buf_at_end = lambda b: b[6]
558
559buf_set_title = lambda b, title: b.__setitem__(2, title)
560buf_set_vscroll = lambda b, i: b.__setitem__(3, clip_to(buf_lines(b), i))
561buf_set_num_read = lambda b, n: b.__setitem__(5, max(buf_num_read(b), n))
562buf_set_at_end = lambda b: b.__setitem__(6, True)
563buf_clr_at_end = lambda b: b.__setitem__(6, False)
564
565format_buf_msg = matcher(message, (
566 (m_privmsg, lambda sender, msg: '<%s> %s' % (sender, msg)),
567 (m_server, lambda sender, msg: '<%s> %s' % (sender, msg)),
568 (m_notice, lambda sender, msg: '<-%s-> %s' % (sender, msg)),
569 (m_join, lambda sender, chan: '%s joins %s' % (sender, chan)),
570 (m_part, lambda sender, chan, msg: '%s parts %s [%s]'
571 % (sender, chan, msg)),
572 (m_quit, lambda sender, msg: '%s quits [%s]' % (sender, msg)),
573 (m_nick, lambda sender, nick: '%s is now known as %s' % (sender, nick)),
574 (m_kick, lambda sender, chan, name, msg: '%s kicked from %s by %s [%s]'
575 % (name, chan, sender, msg)),
576 (m_kicked, lambda sender, chan, msg: 'kicked from %s by %s [%s]'
577 % (chan, sender, msg)),
578 (m_topic, lambda sender, topic:
579 '%s sets topic to %s' % (sender, topic)
580 if topic else
581 '%s removes topic' % sender),
582 (m_chantopic, lambda sender, chan, topic:
583 '<%s> Topic for %s: %s' % (sender, chan, topic)
584 if topic is not None else
585 '<%s> No topic for %s' % (sender, chan)),
586 (m_mode, lambda sender, modes: 'MODE %s by %s' % (modes, sender)),
587 (m_chanmode, lambda sender, chan, modes: 'MODE %s on %s by %s'
588 % (modes, chan, sender)),
589 (m_names, lambda sender, chan, names: '<%s> NAMES in %s: %s'
590 % (sender, chan, names)),
591 (m_endnames, lambda sender, chan: '<%s> end NAMES in %s'
592 % (sender, chan)),
593 (m_error, lambda sender, msg: '<%s> server error: %s' % (sender, msg)),
594 (m_client, lambda msg: msg)))
595
596def buf_log_msg(buf, m):
597 buf_lines(buf).append(format_time(localtime()) + ' ' + format_buf_msg(m))
598 schedule_buflist_draw() # for unread counts
599 if buf is cur_buf and buf_at_end(buf):
600 schedule_buf_draw()
601
602def buf_privmsg(buf, msg):
603 if buf_parent(buf) is None:
604 raise CommandError("can't send messages here")
605 conn_privmsg(buf_registered_conn(buf), buf_name(buf), msg)
606
607def render_lines(lines, width, start, row_limit):
608 """Returns list of wrapped line lists, and the number of *complete* source
609 lines in the output"""
610 rows = 0
611 groups = []
612 while rows < row_limit and start < len(lines):
613 msg = lines[start]
614 group = wrap(asciify(msg), width)
615 rows += len(group)
616 if rows > row_limit:
617 group = group[:row_limit - rows]
618 groups.append(group)
619 return groups, len(groups) - 1
620 groups.append(group)
621 start += 1
622 return groups, len(groups)
623
624def check_buf_at_end():
625 width = scr_width - buflist_width()
626 height = scr_height - 3
627 if height < 1 or width < 1:
628 return
629 lines = buf_lines(cur_buf)
630 scroll = buf_vscroll(cur_buf)
631 lines_shown = render_lines(lines, width, scroll, height)[1]
632 if scroll + lines_shown == len(lines):
633 buf_set_at_end(cur_buf)
634 else:
635 buf_clr_at_end(cur_buf)
636
637def buf_draw(buf):
638 if scr_height < 2:
639 return
640 schedule_refresh()
641 write_out(t.CURSOR_HOME)
642 write_out(t.render((t.A_REVERSE,
643 pad_or_clip_str(asciify(buf_title(buf)), scr_width))))
644 left_column = buflist_width() + 1
645 width = scr_width - buflist_width()
646 top_row = 2
647 height = scr_height - 3
648 if height < 1 or width < 1:
649 return
650 lines = buf_lines(buf)
651 while True:
652 scroll = buf_vscroll(buf)
653 groups, lines_shown = render_lines(lines, width, scroll, height)
654 lines_shown += scroll
655 if lines_shown == len(lines):
656 buf_set_at_end(buf)
657 break
658 if not buf_at_end(buf):
659 break
660 # Was at end until now: autoscroll
661 if scroll + 1 == len(lines):
662 break # Couldn't even fit one complete line
663 buf_set_vscroll(buf, max(scroll + 1, len(lines) - height))
664 row = 0
665 for row, text in enumerate(flatten(groups)):
666 write_out(t.cursor_pos(top_row + row, left_column) + text)
667 if len(text) != width:
668 write_out(t.ERASE_LINE_TO_END)
669 for row in range(row + 1, height):
670 write_out(t.cursor_pos(top_row + row, left_column))
671 write_out(t.ERASE_LINE_TO_END)
672 buf_set_num_read(buf, lines_shown)
673 buf_draw_done()
674
675cur_buf = new_buf('yrc', None, 'yrc general messages')
676buffers = [cur_buf]
677buffer_index = {}
678
679is_child_of = lambda parent: lambda buf: buf_parent(buf) is parent
680
681def sort_buffers():
682 global buffers
683 schedule_buflist_draw()
684 # "yrc" is always first
685 acc, buffers = buffers[:1], buffers[1:]
686 roots, buffers = partition(buffers, is_child_of(None))
687 roots.sort(key=buf_name)
688 for root in roots:
689 children, buffers = partition(buffers, is_child_of(root))
690 children.sort(key=buf_name)
691 acc.append(root)
692 acc.extend(children)
693 buffers = acc
694 buffer_index.clear()
695 for b in buffers:
696 parent = buf_parent(b)
697 parent_name = buf_name(parent) if parent else None
698 buffer_index[(buf_name(b), parent_name)] = b
699
700def get_buf(name, parent_name):
701 try:
702 return buffer_index[(name, parent_name)]
703 except KeyError:
704 if parent_name is None:
705 parent = None
706 title = 'Network messages: %s' % name
707 else:
708 parent = get_buf(parent_name, None)
709 title = name if is_chan(name) else 'Private messages: %s' % name
710 b = new_buf(name, parent, title)
711 buffers.append(b)
712 sort_buffers()
713 return b
714
715def find_buf(buf):
716 for i, b in enumerate(buffers):
717 if b is buf:
718 return i
719 raise ValueError("not in buffer list")
720
721def close_buf(buf):
722 i = find_buf(buf)
723 assert i > 0
724 prev = buffers[i - 1]
725 buflist_set_cursor(prev)
726 last = buflist_last()
727 if last is buf: # not sure if this is possible but just in case
728 last = prev
729 buflist_select(prev)
730 buflist_set_last(last)
731 parent = buf_parent(buf)
732 parent_name = buf_name(parent) if parent else None
733 del buffer_index[(buf_name(buf), parent_name)]
734 buffers.pop(i)
735
736command('scroll-down-page')(sequence(
737 schedule_buf_draw,
738 schedule_buflist_draw, # update unread counts
739 lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) + (scr_height-3)/2)))
740
741command('scroll-up-page')(sequence(
742 schedule_buf_draw,
743 lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) - (scr_height-3)/2),
744 check_buf_at_end)) # stop autoscrolling if no longer at end
745
746command('scroll-down-line')(sequence(
747 schedule_buf_draw,
748 schedule_buflist_draw,
749 lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) + 1)))
750
751command('scroll-up-line')(sequence(
752 schedule_buf_draw,
753 lambda: buf_set_vscroll(cur_buf, buf_vscroll(cur_buf) - 1),
754 check_buf_at_end))
755
756command('scroll-bottom')(sequence(
757 schedule_buf_draw,
758 schedule_buflist_draw,
759 lambda: buf_set_vscroll(cur_buf, len(buf_lines(cur_buf)))))
760
761command('scroll-top')(sequence(
762 schedule_buf_draw,
763 lambda: buf_set_vscroll(cur_buf, 0),
764 check_buf_at_end))
765
766def info(msg, buf=None):
767 buf_log_msg(buf or buffers[0], m_client(msg))
768
769def error(msg_or_exc, buf=None):
770 buf_log_msg(buf or buffers[0], m_client('ERROR: ' + str(msg_or_exc)))
771
772prompt = [[], 0, 0, 1]
773
774prompt_chars = lambda: prompt[0]
775prompt_cursor = lambda: prompt[1]
776prompt_hscroll = lambda: prompt[2]
777prompt_cursor_column = lambda: prompt[3]
778
779def prompt_clear():
780 schedule_prompt_draw()
781 prompt[0] = []
782 prompt_set_cursor(0)
783 prompt_set_hscroll(0)
784
785def prompt_set_cursor(c):
786 schedule_prompt_draw()
787 if 0 <= c <= len(prompt_chars()):
788 prompt[1] = c
789 return True
790 return False
791
792def prompt_set_hscroll(s):
793 schedule_prompt_draw()
794 prompt[2] = s
795
796def prompt_set_cursor_column(c):
797 prompt[3] = c
798
799def prompt_insert(char):
800 schedule_prompt_draw()
801 c = prompt_cursor()
802 prompt_chars().insert(c, char)
803 prompt_set_cursor(c + 1)
804
805@command('prompt-delete')
806def prompt_delete():
807 schedule_prompt_draw()
808 c = prompt_cursor()
809 chars = prompt_chars()
810 if c < len(chars):
811 chars.pop(c)
812
813@command('prompt-backspace')
814def prompt_backspace():
815 schedule_prompt_draw()
816 c = prompt_cursor()
817 if prompt_set_cursor(c - 1):
818 prompt_delete()
819
820@command('prompt-submit')
821def prompt_submit():
822 schedule_prompt_draw()
823 line = ''.join(prompt_chars())
824 prompt_clear()
825 if len(line) == 0:
826 return
827 try:
828 if line.startswith('/'):
829 line = line[1:]
830 if not line:
831 raise CommandError('empty command')
832 if not line.startswith('/'):
833 run_command(line)
834 return
835 buf_privmsg(cur_buf, line)
836 except CommandError, e:
837 error(e, cur_buf)
838
839command('prompt-forward')(lambda: prompt_set_cursor(prompt_cursor() + 1))
840command('prompt-backward')(lambda: prompt_set_cursor(prompt_cursor() - 1))
841command('prompt-start')(lambda: prompt_set_cursor(0))
842command('prompt-end')(lambda: prompt_set_cursor(len(prompt_chars())))
843
844def prompt_draw():
845 if scr_height < 1:
846 return
847 write_out(t.cursor_pos(scr_height, 1))
848 if scr_width < 4:
849 write_out(t.ERASE_LINE_TO_END)
850 return
851 prompt_str = '> '
852 write_out(prompt_str)
853 chars = prompt_chars()
854 cursor = prompt_cursor()
855 hscr = prompt_hscroll()
856 before_cursor = asciify(chars[:cursor])
857 cursor_offset = len(before_cursor) - hscr
858 max_width = scr_width - len(prompt_str)
859 if not 0 <= cursor_offset < max_width:
860 hscr = max(0, hscr + cursor_offset - max_width/2)
861 prompt_set_hscroll(hscr)
862 cursor_offset = len(before_cursor) - hscr
863 prompt_set_cursor_column(1 + len(prompt_str) + cursor_offset)
864 printable = (before_cursor[hscr:] +
865 asciify(chars[cursor:])[:max_width - cursor_offset])
866 write_out(printable)
867 write_out(t.ERASE_LINE_TO_END)
868 schedule_refresh()
869 prompt_draw_done()
870
871def conn_ping_status(c):
872 t0 = conn_ping_ts(c)
873 t1 = conn_pong_ts(c)
874 if t1 < t0:
875 if not ping_draw_flag():
876 set_ping_draw()
877 run_in(5, sequence(clr_ping_draw, status_draw))
878 delta = int(mono_time() - t0)
879 return '%d...' % (delta - delta%5)
880 return '%.3f' % (t1 - t0)
881
882def status_draw():
883 row = scr_height - 1
884 if row < 1:
885 return
886 if kbd_state is ks_esc:
887 status = 'ESC-'
888 elif kbd_state is ks_cx:
889 status = 'C-x-'
890 elif kbd_state in (ks_cseq, ks_cs_intermed):
891 status = ' '.join(['CSI'] + list(str(kbd_accum[len(CSI):]))) + '-'
892 elif cur_buf is buffers[0]:
893 status = ''
894 else:
895 parent = buf_parent(cur_buf)
896 network = buf_name(cur_buf if parent is None else parent)
897 status = asciify(network)
898 c = network_conns.get(network)
899 if c:
900 status = asciify(conn_nick(c) + '@') + status
901 if parent:
902 venue = buf_name(cur_buf)
903 status += ' | ' + asciify(venue)
904 if c and conn_sock(c):
905 if not conn_registered(c):
906 status += ' | unregistered'
907 elif parent and is_chan(venue) and venue not in conn_channels(c):
908 status += ' | unjoined'
909 status += ' | ping: %s' % conn_ping_status(c)
910 else:
911 status += ' | offline'
912 write_out(t.cursor_pos(row, 1))
913 write_out(t.ATTR_REVERSE)
914 write_out(pad_or_clip_str(status, scr_width))
915 write_out(t.ATTR_NONE)
916 schedule_refresh()
917 status_draw_done()
918
919# buflist singleton: lists the open buffers in a left-hand pane
920
921buflist = [0, BUFLIST_WIDTH_START, cur_buf, cur_buf, cur_buf]
922buflist_vscroll = lambda: buflist[0]
923buflist_width = lambda: buflist[1]
924buflist_cursor = lambda: buflist[2]
925buflist_last = lambda: buflist[3]
926
927def buflist_set_width(w):
928 schedule_redraw()
929 if w > scr_width:
930 w = scr_width - (scr_width - w) % BUFLIST_WIDTH_STEP
931 buflist[1] = max(BUFLIST_WIDTH_MIN, w)
932
933def buflist_set_cursor(b):
934 schedule_buflist_draw()
935 buflist[2] = b
936
937def buflist_set_last(b):
938 buflist[3] = b
939
940def buflist_select(b):
941 global cur_buf
942 schedule_buflist_draw()
943 schedule_buf_draw()
944 schedule_status_draw()
945 if b is not cur_buf:
946 buflist_set_last(cur_buf)
947 # don't autoscroll a newly selected window (at_end will get restored on
948 # draw if it's still at the end)
949 buf_clr_at_end(b)
950 buflist_set_cursor(b)
951 cur_buf = b
952
953command('buflist-switch-next')(
954 lambda: buflist_select(
955 get_clipped(buffers, find_buf(cur_buf) + 1)))
956command('buflist-switch-prev')(
957 lambda: buflist_select(
958 get_clipped(buffers, find_buf(cur_buf) - 1)))
959command('buflist-last-selected')(lambda: buflist_select(buflist_last()))
960
961command('buflist-next')(
962 lambda: buflist_set_cursor(
963 get_clipped(buffers, find_buf(buflist_cursor()) + 1)))
964command('buflist-prev')(
965 lambda: buflist_set_cursor(
966 get_clipped(buffers, find_buf(buflist_cursor()) - 1)))
967command('buflist-top')(lambda: buflist_set_cursor(buffers[0]))
968command('buflist-bottom')(lambda: buflist_set_cursor(buffers[-1]))
969command('buflist-shrink')(lambda:
970 buflist_set_width(buflist_width() - BUFLIST_WIDTH_STEP))
971command('buflist-grow')(lambda:
972 buflist_set_width(buflist_width() + BUFLIST_WIDTH_STEP))
973
974@command('buflist-submit')
975def buflist_submit():
976 buflist_exit()
977 buflist_select(buflist_cursor())
978
979def buflist_draw():
980 schedule_refresh()
981 top_row = 2
982 h = scr_height - 3
983 w = min(scr_width, buflist_width() - 1)
984 cursor = buflist_cursor()
985 def network_status(network):
986 c = network_conns.get(network)
987 if c is None: # not connected
988 return ' '
989 sock = conn_sock(c)
990 if sock is None: # down, awaiting reconnection
991 return '~'
992 fd = conn_sock(c).fileno()
993 if fd in opening_conns: # DNS / TCP handshake
994 return '~'
995 if conn_registered(c):
996 return '='
997 else: # TCP connected but unregistered (no WELCOME yet)
998 return '-'
999 def get_label(buf):
1000 parts = []
1001 if buf is cur_buf:
1002 parts.append('*')
1003 elif buf is buflist_last():
1004 parts.append('-')
1005 else:
1006 parts.append(' ')
1007 if buf_parent(buf) is None:
1008 if buf is buffers[0]:
1009 parts.append(' ')
1010 else:
1011 parts.append(network_status(buf_name(buf)))
1012 else: # channel/pm
1013 parts.append(' ')
1014 parts.append(asciify(buf_name(buf)))
1015 unread = buf_unread(buf)
1016 if unread > 0:
1017 parts.append(' +%d' % unread)
1018 label = ''.join(parts)
1019 if buf is cursor and buflist_flag():
1020 return t.render((t.A_REVERSE, pad_or_clip_str(label, w)))
1021 else:
1022 return clip_str(label, w)
1023 write_out(t.cursor_pos(2, 1))
1024 scroll = buflist_vscroll()
1025 for row in range(h):
1026 write_out(t.cursor_pos(top_row + row, w) + t.ERASE_LINE_FROM_START)
1027 i = scroll + row
1028 if i < len(buffers):
1029 write_out(CR + get_label(buffers[i]))
1030 buflist_draw_done()
1031
1032def buflist_vline_draw():
1033 top_row = 2
1034 height = scr_height - 3
1035 column = buflist_width()
1036 if column > scr_width:
1037 return
1038 move_down = LF if column == scr_width else BS + LF
1039 write_out(t.cursor_pos(top_row, column))
1040 write_out(SO)
1041 write_out(move_down.join(t.SGC_VBAR*height))
1042 write_out(SI)
1043
1044command('prompt-enter')(sequence(schedule_refresh, prompt_enter))
1045command('prompt-exit')(sequence(schedule_refresh, prompt_exit))
1046command('buflist-enter')(sequence(schedule_buflist_draw, buflist_enter))
1047command('redraw')(schedule_redraw)
1048
1049# Terminal input state machine
1050
1051# Only valid control sequences per ECMA-48 5ed. sec. 5.4 are accepted.
1052# Esc always cancels any sequence in progress and moves to ks_esc, to avoid
1053# control sequences leaking through as text input. C-g always cancels and
1054# returns to ks_start.
1055
1056kbd_accum = bytearray()
1057kaccum = kbd_accum.extend
1058
1059def kaccept(sym=''):
1060 kaccum(sym)
1061 seq = str(kbd_accum)
1062 ktrans(ks_start)
1063 def try_keymap(km):
1064 cmd = km.get(seq)
1065 if cmd is None:
1066 return False
1067 run_command(cmd)
1068 return True
1069 try:
1070 if try_keymap(GLOBAL_KEYS):
1071 return
1072 elif buflist_flag():
1073 try_keymap(BUFLIST_KEYS)
1074 elif prompt_flag():
1075 if not try_keymap(PROMPT_KEYS):
1076 if len(seq) == 1 and not is_ctrl(seq):
1077 prompt_insert(seq)
1078 else:
1079 try_keymap(BUF_KEYS)
1080 except CommandError, e:
1081 error(e, cur_buf)
1082
1083def ktrans(state):
1084 global kbd_state
1085 kbd_state = state
1086 schedule_status_draw()
1087 if state in (ks_start, ks_esc):
1088 del kbd_accum[:]
1089 elif state is ks_cseq:
1090 del kbd_accum[:]
1091 kaccum(CSI)
1092
1093# States
1094
1095def ks_start(sym):
1096 if sym == C_X:
1097 kaccum(C_X)
1098 ktrans(ks_cx)
1099 elif sym == ESC:
1100 ktrans(ks_esc)
1101 else:
1102 kaccept(sym)
1103
1104def ks_cx(sym):
1105 if sym == C_G:
1106 ktrans(ks_start)
1107 elif sym == ESC:
1108 ktrans(ks_esc)
1109 else:
1110 kaccept(casemap_ascii(ctrl(sym)) if is_ctrl(sym) else sym)
1111
1112def ks_esc(sym):
1113 if sym == C_G:
1114 ktrans(ks_start)
1115 elif sym == ESC:
1116 pass
1117 elif sym == '[':
1118 ktrans(ks_cseq)
1119 else:
1120 kaccept(meta(sym))
1121
1122def ks_cseq(sym):
1123 if sym == ESC:
1124 ktrans(ks_esc)
1125 elif '\x20' <= sym <= '\x2F':
1126 kaccum(sym)
1127 ktrans(ks_cs_intermed)
1128 elif '\x30' <= sym <= '\x3F':
1129 kaccum(sym)
1130 schedule_status_draw()
1131 elif '\x40' <= sym <= '\x7E':
1132 kaccept(sym)
1133 else:
1134 ktrans(ks_start)
1135
1136def ks_cs_intermed(sym):
1137 if sym == ESC:
1138 ktrans(ks_esc)
1139 elif '\x20' <= sym <= '\x2F':
1140 kaccum(sym)
1141 schedule_status_draw()
1142 elif '\x40' <= sym <= '\x7E':
1143 kaccept(sym)
1144 else:
1145 ktrans(ks_start)
1146
1147kbd_state = ks_start
1148
1149def buf_conn(buf):
1150 if buf is buffers[0]:
1151 raise CommandError('this window not associated with a network')
1152 parent = buf_parent(buf)
1153 network = buf_name(buf if parent is None else parent)
1154 try:
1155 return network_conns[network]
1156 except KeyError:
1157 raise CommandError('not connected to %s' % network)
1158
1159def buf_registered_conn(buf):
1160 c = buf_conn(buf)
1161 if conn_sock(c) is None:
1162 raise CommandError('connection to %s is down' % conn_network(c))
1163 if not conn_registered(c):
1164 raise CommandError('connection to %s not registered' % conn_network(c))
1165 return c
1166
1167@command('connect', 1, 4)
1168def connect_cmd(*args):
1169 net = args[0]
1170 if net in network_conns:
1171 raise CommandError('connect: connection already active for %s' % net)
1172
1173 if not safe_filename(net):
1174 raise CommandError('connect: bad network name: %s' % net)
1175 conf_paths = (('nets', net), ())
1176
1177 if len(args) > 1:
1178 addrs = [args[1]]
1179 else:
1180 addrs = config_lines(get_config('addrs', conf_paths))
1181 if addrs is None:
1182 raise CommandError('connect: no addrs for network %s' % net)
1183 try:
1184 addrs = map(parse_address, addrs)
1185 except ValueError, e:
1186 raise CommandError('connect: %s' % e)
1187
1188 if len(args) > 2:
1189 nick = args[2]
1190 else:
1191 nick = get_config('nick', conf_paths)
1192 if nick is None:
1193 raise CommandError('connect: no nick for %s' % net)
1194 if not valid_nick(nick):
1195 raise CommandError('connect: bad nick: %s' % nick)
1196
1197 if len(args) > 3:
1198 pw = args[3]
1199 else:
1200 pw = get_config('pass', conf_paths)
1201 if not valid_password(pw):
1202 raise CommandError(
1203 'connect: illegal character in password for %s' % net)
1204
1205 conn_start(new_conn(net, addrs, nick, pw))
1206
1207@command('disconnect', 0, 1)
1208def disconnect_cmd(net=None):
1209 if net is None:
1210 c = buf_conn(cur_buf)
1211 net = conn_network(c)
1212 else:
1213 try:
1214 c = network_conns[net]
1215 except KeyError:
1216 raise CommandError('no connection for network %s' % net)
1217 del network_conns[net]
1218 conn_info(c, 'disconnected')
1219 conn_close(c)
1220
1221@command('join', 1, 2)
1222def join_cmd(chan, key=None):
1223 if not valid_chan(chan):
1224 raise CommandError('join: bad channel name: %s' % chan)
1225 conn_join(buf_registered_conn(cur_buf), chan, key)
1226
1227@command('kick', 1, extended_arg=True)
1228def kick_cmd(user, msg=''):
1229 chan = buf_name(cur_buf)
1230 if buf_parent(cur_buf) is None or not is_chan(chan):
1231 raise CommandError('kick: this window not a channel')
1232 conn_send(buf_registered_conn(cur_buf), 'KICK', [chan, user, msg])
1233
1234@command('list', 0, 1)
1235def list_cmd(*args):
1236 conn_send(buf_registered_conn(cur_buf), 'LIST', args)
1237
1238@command('me', extended_arg=True)
1239def me_cmd(msg=''):
1240 buf_privmsg(cur_buf, '\x01ACTION %s\x01' % msg)
1241
1242@command('mode', 1, 3)
1243def mode_cmd(*args):
1244 conn_send(buf_registered_conn(cur_buf), 'MODE', args)
1245
1246@command('msg', 1, extended_arg=True)
1247def msg_cmd(target, msg=None):
1248 c = buf_registered_conn(cur_buf)
1249 if is_chan(target) and not valid_chan(target):
1250 raise CommandError('msg: bad channel name: %s' % target)
1251 if msg:
1252 conn_privmsg(c, target, msg)
1253 buflist_select(get_buf(target, conn_network(c)))
1254
1255@command('names', 0, 1)
1256def names_cmd(*args):
1257 conn_send(buf_registered_conn(cur_buf), 'NAMES', args)
1258
1259@command('nick', 1)
1260def nick_cmd(nick):
1261 c = buf_conn(cur_buf)
1262 if conn_sock(c) is None:
1263 schedule_status_draw()
1264 conn_set_nick(c, nick)
1265 conn_info(c, 'nick changed to %s for next reconnection' % nick)
1266 else:
1267 if not conn_registered(c):
1268 schedule_status_draw()
1269 conn_set_nick(c, nick)
1270 conn_info(c, 'nick changed to %s' % nick)
1271 conn_set_ping_ts(c, mono_time())
1272 conn_run_in(c, PING_TIMEOUT, conn_reg_timeout)
1273 conn_send(c, 'NICK', [nick])
1274
1275@command('part', extended_arg=True)
1276def part_cmd(msg=''):
1277 chan = buf_name(cur_buf)
1278 if buf_parent(cur_buf) is None or not is_chan(chan):
1279 raise CommandError('part: this window not a channel')
1280 conn_send(buf_registered_conn(cur_buf), 'PART', [chan, msg])
1281
1282@command('quit', extended_arg=True)
1283def quit_cmd(*msg):
1284 schedule_quit()
1285 for c in open_conns.itervalues():
1286 conn_send(c, 'QUIT', msg)
1287
1288@command('send', extended_arg=True)
1289def send_cmd(line):
1290 if len(line) > MAX_MSG_LEN:
1291 raise CommandError('send: line too long')
1292 conn_write(buf_registered_conn(cur_buf), line)
1293
1294@command('topic', 0, 1)
1295def topic_cmd(chan=None):
1296 if chan is None:
1297 chan = buf_name(cur_buf)
1298 if buf_parent(cur_buf) is None or not is_chan(chan):
1299 raise CommandError(
1300 'topic: this window not a channel and none specified')
1301 conn_send(buf_registered_conn(cur_buf), 'TOPIC', [chan])
1302
1303@command('set-topic', extended_arg=True)
1304def set_topic_cmd(topic=''):
1305 chan = buf_name(cur_buf)
1306 if buf_parent(cur_buf) is None or not is_chan(chan):
1307 raise CommandError('set-topic: this window not a channel')
1308 conn_send(buf_registered_conn(cur_buf), 'TOPIC', [chan, topic])
1309
1310@command('whois', 1)
1311def whois_cmd(nick):
1312 conn_send(buf_registered_conn(cur_buf), 'WHOIS', [nick])
1313
1314@command('whowas', 1)
1315def whowas_cmd(nick):
1316 conn_send(buf_registered_conn(cur_buf), 'WHOWAS', [nick])
1317
1318@command('close')
1319def close_cmd():
1320 parent = buf_parent(cur_buf)
1321 if parent is None:
1322 raise CommandError(
1323 "close: won't close a top-level window (try close-net)")
1324 venue = buf_name(cur_buf)
1325 try:
1326 c = buf_registered_conn(cur_buf)
1327 except CommandError:
1328 pass
1329 else:
1330 if venue in conn_channels(c):
1331 conn_send(c, 'PART', [venue, ''])
1332 del conn_channels(c)[venue]
1333 close_buf(cur_buf)
1334
1335@command('close-net')
1336def close_net_cmd():
1337 raise CommandError('stub') # TODO
1338
1339def place_cursor():
1340 if buflist_flag():
1341 write_out(t.cursor_pos(max(1, scr_height - 2),
1342 clip(1, scr_width, buflist_width() - 1)))
1343 elif prompt_flag():
1344 write_out(t.cursor_pos(scr_height, prompt_cursor_column()))
1345 else:
1346 write_out(t.cursor_pos(max(1, scr_height - 2), max(1, scr_width)))
1347
1348def refresh_if_needed():
1349 if redraw_flag():
1350 write_out(t.ERASE_FULL)
1351 buf_draw(cur_buf)
1352 buflist_vline_draw()
1353 buflist_draw()
1354 status_draw()
1355 prompt_draw()
1356 redraw_done()
1357 else:
1358 if buf_draw_flag():
1359 buf_draw(cur_buf)
1360 if buflist_draw_flag():
1361 buflist_draw()
1362 if status_draw_flag():
1363 status_draw()
1364 if prompt_draw_flag():
1365 prompt_draw()
1366 if refresh_flag():
1367 place_cursor()
1368 flush_out()
1369 refresh_done()
1370
1371#
1372# IRC stuff
1373#
1374
1375RPL_WELCOME = 1
1376RPL_NOTOPIC = 331
1377RPL_TOPIC = 332
1378RPL_NAMREPLY = 353
1379RPL_ENDOFNAMES = 366
1380ERR_NICKNAMEINUSE = 433
1381ERR_NICKCOLLISION = 436
1382ERR_NOTREGISTERED = 451
1383
1384MAX_MSG_LEN = 510
1385MAX_CHAN_LEN = 50
1386MAX_NICK_LEN = 31
1387
1388IRC_ILLEGAL = NUL + CR + LF
1389CHAN_ILLEGAL = IRC_ILLEGAL + BEL + ' ,:'
1390CHAN_START = '&#+!'
1391IRC_SPECIAL = '[]\\`_^{|}'
1392
1393LETTER = char_range('AZ') + char_range('az')
1394DIGIT = char_range('09')
1395NICK_START = LETTER + IRC_SPECIAL
1396NICK_BODY = NICK_START + DIGIT + '-'
1397
1398class ProtocolError(Exception):
1399 pass
1400
1401def build_msg(prefix, cmd, params):
1402 """Build an IRC wire message.
1403
1404 Conditions caller must enforce:
1405 * No args may contain NUL, CR, or LF
1406 * Only last param may be empty, contain spaces, or start with :
1407 * Valid cmd
1408 * 15 parameters max
1409 """
1410 parts = []
1411 if prefix is not None:
1412 parts.append(':' + prefix)
1413 parts.append(cmd)
1414 if len(params):
1415 parts.extend(params[:-1])
1416 parts.append(':' + params[-1])
1417 return ' '.join(parts)
1418
1419def max_param_len(cmd, prefix=None):
1420 # colon before prefix + space after prefix
1421 prefix_len = 0 if prefix is None else len(prefix) + 2
1422 # space after cmd + colon before last param
1423 return MAX_MSG_LEN - prefix_len - len(cmd) - 2
1424
1425def parse_msg(msg):
1426 if any(c in msg for c in IRC_ILLEGAL):
1427 raise ProtocolError('illegal character in message')
1428 start = 0
1429 end = len(msg)
1430 def next_word():
1431 cut = msg.find(' ', start)
1432 if cut == -1:
1433 cut = end
1434 return cut + 1, msg[start:cut]
1435 if msg.startswith(':'):
1436 start = 1
1437 start, prefix = next_word()
1438 else:
1439 prefix = None
1440 start, cmd = next_word()
1441 if not cmd:
1442 raise ProtocolError('message with no command')
1443 params = []
1444 while start < end:
1445 if msg[start] == ':':
1446 params.append(msg[start+1:])
1447 break
1448 start, param = next_word()
1449 params.append(param)
1450 return prefix, casemap_ascii(cmd), params
1451
1452is_chan = lambda n: len(n) > 0 and n[0] in CHAN_START
1453
1454def valid_chan(n):
1455 return is_chan(n) and len(n) <= MAX_CHAN_LEN \
1456 and not any(c in CHAN_ILLEGAL for c in n)
1457
1458def valid_nick(n):
1459 return 0 < len(n) <= MAX_NICK_LEN \
1460 and n[0] in NICK_START \
1461 and all(c in NICK_BODY for c in n[1:])
1462
1463def valid_password(p):
1464 return p is None or (
1465 len(p) <= max_param_len('PASS')
1466 and not any(c in IRC_ILLEGAL for c in p))
1467
1468sender_nick = lambda s: s.split('!', 1)[0]
1469
1470#
1471# Networking / main loop
1472#
1473
1474set_nonblock = lambda fd: fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK)
1475
1476def read_all(fd):
1477 chunks = []
1478 try:
1479 chunk = read(fd, 4096)
1480 if not chunk:
1481 raise EOFError
1482 while chunk:
1483 chunks.append(chunk)
1484 chunk = read(fd, 4096)
1485 except EnvironmentError, e:
1486 if e.errno not in (EAGAIN, EWOULDBLOCK):
1487 raise
1488 return ''.join(chunks)
1489
1490def new_conn(network, addrs, nick, password=None):
1491 i = rand_int(len(addrs))
1492 addrs = addrs[i:] + addrs[:i]
1493 return [network, None, '', '', addrs, nick, password, False, dict(),
1494 IRC_CASEMAP['rfc1459'], RECONN_DELAY_MIN, 0, 0, 0]
1495
1496conn_network = lambda c: c[0]
1497conn_sock = lambda c: c[1]
1498conn_rdbuf = lambda c: c[2]
1499conn_wrbuf = lambda c: c[3]
1500conn_addrs = lambda c: c[4]
1501conn_nick = lambda c: c[5]
1502conn_password = lambda c: c[6]
1503conn_registered = lambda c: c[7]
1504conn_channels = lambda c: c[8]
1505conn_casemap = lambda c, s: c[9](s)
1506conn_count = lambda c: c[11]
1507conn_ping_ts = lambda c: c[12]
1508conn_pong_ts = lambda c: c[13]
1509
1510conn_set_sock = lambda c, s: c.__setitem__(1, s)
1511conn_rdbuf_add = lambda c, b: c.__setitem__(2, c[2] + b)
1512conn_rdbuf_rm = lambda c, n: c.__setitem__(2, c[2][n:])
1513conn_wrbuf_add = lambda c, b: c.__setitem__(3, c[3] + b)
1514conn_wrbuf_rm = lambda c, n: c.__setitem__(3, c[3][n:])
1515conn_set_nick = lambda c, n: c.__setitem__(5, n)
1516conn_set_registered = lambda c: c.__setitem__(7, True)
1517conn_clr_registered = lambda c: c.__setitem__(7, False)
1518conn_reset_reconn_delay = lambda c: c.__setitem__(10, RECONN_DELAY_MIN)
1519conn_set_count = lambda c, n: c.__setitem__(11, n)
1520conn_set_ping_ts = lambda c, t: c.__setitem__(12, t)
1521conn_set_pong_ts = lambda c, t: c.__setitem__(13, t)
1522
1523def conn_reconn_delay(c):
1524 # limited exponential backoff
1525 d = c[10]
1526 c[10] = min(2*d, RECONN_DELAY_MAX)
1527 return d
1528
1529conn_nick_lc = lambda c: conn_casemap(c, conn_nick(c))
1530
1531def conn_run_in(c, delay, method, run_if_down=False):
1532 count = conn_count(c)
1533 def run():
1534 # Drop leftover tasks from old connections
1535 if c is network_conns.get(conn_network(c)) and conn_count(c) == count \
1536 and (run_if_down or conn_sock(c) is not None):
1537 method(c)
1538 run_in(delay, run)
1539
1540def conn_log_msg(c, venue, m):
1541 network = conn_network(c)
1542 if venue is None:
1543 buf = get_buf(network, None)
1544 else:
1545 buf = get_buf(venue, network)
1546 buf_log_msg(buf, m)
1547 file_log_msg(network, venue, m)
1548
1549def conn_info(c, msg):
1550 conn_log_msg(c, None, m_client(msg))
1551
1552def conn_error(c, msg):
1553 conn_log_msg(c, None, m_client('ERROR: ' + msg))
1554
1555opening_conns = {}
1556network_conns = {}
1557def conn_start(c):
1558 schedule_buflist_draw()
1559 schedule_status_draw()
1560 net = conn_network(c)
1561 assert conn_sock(c) is None, 'socket exists when starting connection'
1562 sock = socket()
1563 set_nonblock(sock.fileno())
1564 conn_set_sock(c, sock)
1565 conn_set_count(c, conn_count(c) + 1)
1566 addrs = conn_addrs(c)
1567 addrs.append(addrs.pop(0))
1568 conn_info(c, 'connecting to %s' % format_address(addrs[0]))
1569 network_conns[net] = opening_conns[sock.fileno()] = c
1570 try:
1571 sock.connect(addrs[0]) # TODO async DNS
1572 except SockError, e:
1573 if e.errno != EINPROGRESS:
1574 del opening_conns[sock.fileno()]
1575 conn_error(c, e.strerror)
1576 else:
1577 conn_handle_connected(c)
1578
1579def conn_write(c, msg):
1580 if len(msg) > MAX_MSG_LEN:
1581 msg = msg[:MAX_MSG_LEN]
1582 conn_error(c, 'outbound message truncated')
1583 conn_wrbuf_add(c, msg + CRLF)
1584
1585def conn_send(c, cmd, params, prefix=None):
1586 conn_write(c, build_msg(prefix, cmd, params))
1587
1588open_conns = {}
1589def conn_handle_connected(c):
1590 n = conn_nick(c)
1591 p = conn_password(c)
1592 s = conn_sock(c)
1593 e = s.getsockopt(SOL_SOCKET, SO_ERROR)
1594 if e == EINPROGRESS:
1595 return
1596 schedule_buflist_draw()
1597 schedule_status_draw()
1598 del opening_conns[s.fileno()]
1599 if e != 0:
1600 conn_error(c, strerror(e))
1601 conn_close(c)
1602 return
1603 conn_reset_reconn_delay(c)
1604 open_conns[s.fileno()] = c
1605 conn_info(c, 'connection established')
1606 conn_set_ping_ts(c, mono_time())
1607 conn_run_in(c, PING_TIMEOUT, conn_reg_timeout)
1608 if p is not None:
1609 conn_send(c, 'PASS', [p])
1610 conn_send(c, 'NICK', [n])
1611 conn_send(c, 'USER', [n, '0', '*', n])
1612
1613def conn_close(c):
1614 sock = conn_sock(c)
1615 if sock is None:
1616 return
1617 schedule_buflist_draw()
1618 schedule_status_draw()
1619 fd = sock.fileno()
1620 if fd in open_conns:
1621 del open_conns[fd]
1622 elif fd in opening_conns:
1623 del opening_conns[fd]
1624 try:
1625 sock.shutdown(SHUT_RDWR)
1626 except SockError:
1627 pass
1628 sock.close()
1629 conn_set_sock(c, None)
1630 conn_rdbuf_rm(c, len(conn_rdbuf(c)))
1631 conn_wrbuf_rm(c, len(conn_wrbuf(c)))
1632 conn_clr_registered(c)
1633 conn_info(c, 'connection closed')
1634 if conn_network(c) in network_conns:
1635 delay = conn_reconn_delay(c)
1636 conn_run_in(c, delay, conn_start, True)
1637 conn_info(c, 'reconnecting in %d seconds' % delay)
1638
1639def conn_handle_data(c, data):
1640 conn_rdbuf_add(c, data)
1641 data = conn_rdbuf(c)
1642 start = 0
1643 while start < len(data):
1644 end = data.find(CRLF, start)
1645 if end == -1:
1646 if len(data) - start >= MAX_MSG_LEN:
1647 conn_error(c, 'received oversize message')
1648 conn_close(c)
1649 return
1650 break
1651 if end > start:
1652 try:
1653 conn_handle_msg(c, data[start:end])
1654 except ProtocolError, e:
1655 conn_error(c, e)
1656 start = end + 2
1657 conn_rdbuf_rm(c, start)
1658
1659def conn_handle_msg(c, msg):
1660 #pylint: disable=unbalanced-tuple-unpacking,too-many-locals
1661 prefix, cmd, params = parse_msg(msg)
1662
1663 def welcome():
1664 if destination != conn_nick(c): # for pre-welcome nick change
1665 schedule_status_draw()
1666 conn_info(c, 'nick changed to %s' % destination)
1667 conn_set_nick(c, destination)
1668 conn_log_msg(c, None, m_server(prefix or '', ' '.join(params)))
1669 if not conn_registered(c):
1670 schedule_buflist_draw()
1671 conn_set_registered(c)
1672 pong()
1673 for chan in conn_channels(c):
1674 conn_join(c, chan)
1675 conn_channels(c).clear()
1676
1677 def names_reply():
1678 if len(params) != 3:
1679 conn_error(c, 'RPL_NAMREPLY with bad parameter count: %s' % msg)
1680 return
1681 _, chan, names = params
1682 chan_lc = conn_casemap(c, chan)
1683 members = conn_channels(c).get(chan_lc)
1684 conn_log_msg(c, None if members is None else chan_lc,
1685 m_names(prefix or '', chan, names))
1686 if members is not None:
1687 for nick in names.split(' '):
1688 if not nick:
1689 conn_error(c, 'RPL_NAMREPLY with empty nick')
1690 break
1691 if nick[0] in '@+':
1692 nick = nick[1:]
1693 members.add(conn_casemap(c, nick))
1694
1695 def end_names():
1696 if len(params) != 2:
1697 conn_error(c, 'RPL_ENDOFNAMES with bad parameter count: %s' % msg)
1698 return
1699 chan = params[0]
1700 chan_lc = conn_casemap(c, chan)
1701 conn_log_msg(c, chan_lc if chan_lc in conn_channels(c) else None,
1702 m_endnames(prefix or '', chan))
1703
1704 def error_msg():
1705 if len(params) != 1:
1706 conn_error(c, 'ERROR with bad parameter count: %s' % msg)
1707 return
1708 conn_log_msg(c, None, m_error(prefix or '', params[0]))
1709
1710 def ping():
1711 conn_send(c, 'PONG', params)
1712
1713 def pong():
1714 schedule_status_draw()
1715 conn_set_pong_ts(c, mono_time())
1716 conn_run_in(c, PING_INTERVAL, conn_ping)
1717
1718 def privmsg():
1719 if len(params) != 2:
1720 conn_error(c, 'message with bad parameter count: %s' % msg)
1721 return
1722 target, content = params
1723 target_lc = conn_casemap(c, target)
1724 if prefix is None:
1725 conn_error(c, 'message without sender: %s' % msg)
1726 return
1727 sender = sender_nick(prefix)
1728 if target_lc == conn_nick_lc(c): # PM
1729 venue = conn_casemap(c, sender)
1730 elif valid_chan(target_lc):
1731 if target_lc in conn_channels(c):
1732 venue = target_lc
1733 else:
1734 return # drop messages to unknown channels
1735 elif target_lc == '*': # not sure if standard but freenode does this
1736 venue = None
1737 else:
1738 conn_error(c, 'message to unknown target: %s' % msg)
1739 return
1740 conn_log_msg(c, venue,
1741 (m_notice if cmd == 'notice' else m_privmsg)(sender, content))
1742
1743 def join():
1744 if len(params) != 1:
1745 conn_error(c, 'JOIN with bad parameter count: %s' % msg)
1746 return
1747 chan, = params
1748 chan_lc = conn_casemap(c, chan)
1749 if prefix is None:
1750 conn_error(c, 'JOIN without sender: %s' % msg)
1751 return
1752 sender_lc = conn_casemap(c, sender_nick(prefix))
1753 channels_dict = conn_channels(c)
1754 if sender_lc == conn_nick_lc(c):
1755 if chan_lc in channels_dict:
1756 conn_error(c, 'JOIN to already joined channel %s' % chan)
1757 return
1758 channels_dict[chan_lc] = set()
1759 else:
1760 if chan_lc not in channels_dict:
1761 conn_error(c, 'JOIN %s to unknown channel %s' % (prefix, chan))
1762 return
1763 channels_dict[chan_lc].add(sender_lc)
1764 conn_log_msg(c, chan_lc, m_join(prefix, chan))
1765
1766 def mode():
1767 if len(params) < 1:
1768 conn_error(c, 'MODE with bad parameter count: %s' % msg)
1769 return
1770 target = params[0]
1771 modes = ' '.join(params[1:])
1772 target_lc = conn_casemap(c, target)
1773 if prefix is None:
1774 conn_error(c, 'MODE without sender: %s' % msg)
1775 return
1776 if is_chan(target_lc):
1777 if target_lc not in conn_channels(c):
1778 conn_error(c, 'MODE to unknown channel: %s' % msg)
1779 return
1780 conn_log_msg(c, target_lc, m_chanmode(prefix, target, modes))
1781 else:
1782 if not target_lc == prefix == conn_nick(c):
1783 conn_error(c, 'MODE to unknown target: %s' % msg)
1784 return
1785 conn_log_msg(c, None, m_mode(prefix, modes))
1786
1787 def part():
1788 if len(params) == 1:
1789 comment = ''
1790 elif len(params) == 2:
1791 comment = params[1]
1792 else:
1793 conn_error(c, 'PART with bad parameter count: %s' % msg)
1794 return
1795 parted_chans = params[0].split(',')
1796 if prefix is None:
1797 conn_error(c, 'PART without sender: %s' % msg)
1798 return
1799 sender_lc = conn_casemap(c, sender_nick(prefix))
1800 channels_dict = conn_channels(c)
1801 me = (sender_lc == conn_nick_lc(c))
1802 for chan in parted_chans:
1803 chan_lc = conn_casemap(c, chan)
1804 if chan_lc not in channels_dict:
1805 # drop PARTs from unknown channels (e.g. closed window)
1806 continue
1807 try:
1808 if me:
1809 del channels_dict[chan_lc]
1810 conn_info(c, 'parted %s' % chan)
1811 schedule_status_draw()
1812 else:
1813 channels_dict[chan_lc].remove(sender_lc)
1814 except KeyError:
1815 conn_error(c, 'PART non-member %s from %s' % (prefix, chan))
1816 continue
1817 conn_log_msg(c, chan_lc, m_part(prefix, chan, comment))
1818
1819 def quit_msg():
1820 if len(params) != 1:
1821 conn_error(c, 'QUIT with bad parameter count: %s' % msg)
1822 return
1823 quit_msg, = params
1824 if prefix is None:
1825 conn_error(c, 'QUIT without sender [%s]' % quit_msg)
1826 return
1827 sender_lc = conn_casemap(c, sender_nick(prefix))
1828 for chan_lc, members in conn_channels(c).items():
1829 if sender_lc in members:
1830 conn_log_msg(c, chan_lc, m_quit(prefix, quit_msg))
1831 members.remove(sender_lc)
1832
1833 def kick():
1834 if len(params) < 2 or len(params) > 3:
1835 conn_error(c, 'KICK with bad parameter count: %s' % msg)
1836 return
1837 if prefix is None:
1838 conn_error(c, 'KICK without sender: %s' % msg)
1839 return
1840 chan = params[0]
1841 chan_lc = conn_casemap(c, chan)
1842 kicked_user = params[1]
1843 kicked_user_lc = conn_casemap(c, kicked_user)
1844 comment = params[2] if len(params) == 3 else ''
1845 channels_dict = conn_channels(c)
1846 if kicked_user_lc == conn_nick_lc(c):
1847 try:
1848 del channels_dict[chan_lc]
1849 except KeyError:
1850 conn_error(c, 'KICK from unknown channel %s by %s [%s]'
1851 % (chan, prefix, comment))
1852 return
1853 schedule_status_draw()
1854 conn_log_msg(c, chan_lc, m_kicked(prefix, chan, comment))
1855 else:
1856 if chan_lc not in channels_dict:
1857 conn_error(c, 'KICK %s from unknown channel %s by %s [%s]'
1858 % (kicked_user, chan, prefix, comment))
1859 return
1860 try:
1861 channels_dict[chan_lc].remove(kicked_user_lc)
1862 except KeyError:
1863 conn_error(c, 'KICK non-member %s from %s by %s [%s]'
1864 % (kicked_user, chan, prefix, comment))
1865 return
1866 conn_log_msg(c, chan_lc,
1867 m_kick(prefix, chan, kicked_user, comment))
1868
1869 def nick():
1870 if len(params) != 1:
1871 conn_error(c, 'NICK with bad parameter count: %s' % msg)
1872 return
1873 new_nick, = params
1874 if prefix is None:
1875 conn_error(c, 'NICK without sender: %s' % msg)
1876 return
1877 sender = sender_nick(prefix)
1878 new_nick_lc = conn_casemap(c, new_nick)
1879 sender_lc = conn_casemap(c, sender)
1880 if sender_lc == conn_nick_lc(c):
1881 conn_info(c, 'nick changed to %s' % new_nick)
1882 conn_set_nick(c, new_nick)
1883 schedule_status_draw()
1884 for chan_lc, members in conn_channels(c).items():
1885 if sender_lc in members:
1886 members.remove(sender_lc)
1887 members.add(new_nick_lc)
1888 conn_log_msg(c, chan_lc, m_nick(sender, new_nick))
1889
1890 def topic():
1891 if len(params) != 2:
1892 conn_error(c, '(RPL_(NO))TOPIC with bad parameter count: %s' % msg)
1893 chan = params[0]
1894 topic = None
1895 if cmd != str(RPL_NOTOPIC):
1896 topic = params[1]
1897 chan_lc = conn_casemap(c, chan)
1898 if chan_lc in conn_channels(c):
1899 buf_set_title(get_buf(chan_lc, conn_network(c)), topic or chan_lc)
1900 conn_log_msg(c, chan_lc, m_topic(prefix or '', topic))
1901 else:
1902 conn_log_msg(c, None, m_chantopic(prefix or '', chan, topic))
1903
1904 def unknown_command():
1905 conn_info(c, 'unknown command from server: %s' % msg)
1906
1907 def unknown_reply():
1908 conn_log_msg(c, None, m_server(prefix or '', ' '.join([cmd] + params)))
1909
1910 if len(cmd) == 3 and all(map(is_digit, cmd)):
1911 # Replies
1912 if not params:
1913 conn_error(c, 'reply %s with no destination' % cmd)
1914 return
1915 destination = params.pop(0)
1916 {
1917 RPL_WELCOME: welcome,
1918 RPL_NAMREPLY: names_reply,
1919 RPL_ENDOFNAMES: end_names,
1920 RPL_TOPIC: topic,
1921 RPL_NOTOPIC: topic,
1922 #ERR_NICKNAMEINUSE:
1923 #ERR_NICKCOLLISION:
1924 #ERR_NOTREGISTERED:
1925 }.get(int(cmd), unknown_reply)()
1926 else:
1927 {
1928 'error': error_msg,
1929 'ping': ping,
1930 'pong': pong,
1931 'privmsg': privmsg,
1932 'notice': privmsg,
1933 'join': join,
1934 'mode': mode,
1935 'part': part,
1936 'quit': quit_msg,
1937 'kick': kick,
1938 'nick': nick,
1939 'topic': topic,
1940 }.get(cmd, unknown_command)()
1941
1942def conn_join(c, chan, key=None):
1943 conn_info(c, 'joining %s' % chan)
1944 conn_send(c, 'JOIN', [chan] if key is None else [chan, key])
1945
1946def conn_privmsg(c, target, msg):
1947 # There is NO SANE WAY to deduce the max privmsg length, go figure.
1948 for line in wrap(msg, 400):
1949 conn_log_msg(c, target, m_privmsg(conn_nick(c), line))
1950 conn_send(c, 'PRIVMSG', [target, line])
1951
1952def conn_ping(c):
1953 conn_set_ping_ts(c, mono_time())
1954 conn_send(c, 'PING', [conn_nick(c)])
1955 conn_run_in(c, PING_TIMEOUT, conn_timeout)
1956
1957def conn_timeout(c):
1958 if mono_time() - conn_pong_ts(c) > PING_TIMEOUT:
1959 conn_error(c, 'ping timed out')
1960 conn_close(c)
1961
1962def conn_reg_timeout(c):
1963 if mono_time() - max(conn_pong_ts(c), conn_ping_ts(c)) > PING_TIMEOUT:
1964 conn_error(c, 'registration timed out')
1965 conn_close(c)
1966
1967# Log file message formatting is simpler than UI as the message type and
1968# sender are always shown explicitly.
1969arg2 = lambda a, b: b
1970arg3 = lambda a, b, c: c
1971empty2 = lambda a, b: ''
1972def lterr(*_):
1973 raise TypeError('bad log message type')
1974
1975format_log_msg = matcher(message, (
1976 (m_privmsg, arg2),
1977 (m_notice, arg2),
1978 (m_join, empty2),
1979 (m_part, arg3),
1980 (m_quit, arg2),
1981 (m_nick, arg2),
1982 (m_kick, lambda sender, chan, name, msg: name + ' ' + msg),
1983 (m_kicked, arg3),
1984 (m_topic, lambda sender, topic: topic or ''),
1985 (m_chantopic, lterr),
1986 (m_mode, lterr),
1987 (m_chanmode, arg3),
1988 (m_names, arg3),
1989 (m_endnames, empty2),
1990 (m_error, arg2),
1991 (m_client, lterr),
1992 (m_server, arg2)))
1993
1994def file_log_msg(network, venue, m):
1995 if venue is None or len(venue) == 0:
1996 return
1997 path = ('logs', network, fs_encode(venue) + '.log')
1998 # Possible optimization: cache log FDs and check freshness by comparing
1999 # (dev, inode) with stat() on each write. Exceeding the max FDs rlimit
2000 # would need to be handled, e.g. by LRU eviction using a minheap with
2001 # global write counter.
2002 def open_log():
2003 return os_open('/'.join(path), O_WRONLY | O_APPEND | O_CREAT, 0666)
2004 try:
2005 try:
2006 fd = open_log()
2007 except EnvironmentError, e:
2008 if e.errno != ENOENT:
2009 raise
2010 try:
2011 mkdir(path[0])
2012 except EnvironmentError, e:
2013 if e.errno != EEXIST:
2014 raise
2015 mkdir('/'.join(path[:2]))
2016 fd = open_log()
2017 try:
2018 write_all(fd, ''.join((
2019 str(int(time()*100)),
2020 ' ', variant_name(m),
2021 ' <', variant_args(m)[0], '> ', format_log_msg(m), LF)))
2022 except EnvironmentError, e:
2023 error(e)
2024 close(fd)
2025 except EnvironmentError, e:
2026 error(e)
2027
2028def handle_resize(*_):
2029 global scr_height, scr_width
2030 scr_height, scr_width = t.screen_size()
2031 schedule_redraw()
2032 try:
2033 write(self_pipe_wr, '\0')
2034 except EnvironmentError, e:
2035 if e.errno not in (EAGAIN, EWOULDBLOCK):
2036 raise
2037
2038# Poor man's monotonic clock. Doesn't give real time, but at least avoids
2039# overly delayed events if the clock is stepped backward.
2040mono_last = mono_offset = 0
2041def mono_time(time_=time):
2042 global mono_last, mono_offset
2043 now = time_() + mono_offset
2044 if now < mono_last:
2045 mono_offset += mono_last - now
2046 now = mono_last
2047 mono_last = now
2048 return now
2049
2050schedule = []
2051run_in = lambda seconds, thunk: heap_insert(schedule, mono_time() + seconds,
2052 thunk)
2053
2054def main():
2055 yrc_home = getenv('HOME', '') + '/.yrc'
2056 try:
2057 chdir(yrc_home)
2058 except EnvironmentError:
2059 mkdir(yrc_home)
2060 chdir(yrc_home)
2061
2062 set_nonblock(self_pipe_rd)
2063 set_nonblock(self_pipe_wr)
2064 signal(SIGINT, SIG_DFL)
2065 signal(SIGWINCH, handle_resize)
2066 handle_resize()
2067
2068 info('Welcome to yrc: the Unix IRC client')
2069 info('Version: %s' % __version__)
2070 info('Documentation is included in the distribution: refer to README and '
2071 + "manual.txt to get started, or NEWS for what's new in this release")
2072 info('Type /quit<Enter> to exit')
2073 prompt_enter()
2074
2075 while not quit_flag():
2076 refresh_if_needed()
2077 timeout = None
2078 if schedule:
2079 timeout = max(0, heap_peek(schedule)[0] - mono_time())
2080 try:
2081 readers, writers, _ = select(
2082 [STDIN, self_pipe_rd] + open_conns.keys(),
2083 opening_conns.keys() +
2084 [fd for fd, c in open_conns.items() if conn_wrbuf(c)],
2085 (), timeout)
2086 except SelectError, e:
2087 if e[0] == EINTR:
2088 continue
2089 raise
2090 for fd in readers:
2091 if fd == STDIN:
2092 try:
2093 data = read_all(fd)
2094 except (EOFError, EnvironmentError):
2095 schedule_quit()
2096 for c in data:
2097 kbd_state(c)
2098 elif fd == self_pipe_rd:
2099 read_all(self_pipe_rd)
2100 elif fd in open_conns:
2101 c = open_conns[fd]
2102 try:
2103 data = read_all(fd)
2104 except EOFError:
2105 conn_info(c, 'connection closed by server')
2106 conn_close(c)
2107 except EnvironmentError, e:
2108 conn_error(c, str(e))
2109 conn_close(c)
2110 else:
2111 conn_handle_data(c, data)
2112 for fd in writers:
2113 if fd in opening_conns:
2114 conn_handle_connected(opening_conns[fd])
2115 elif fd in open_conns:
2116 c = open_conns[fd]
2117 try:
2118 n = write(fd, conn_wrbuf(c))
2119 except EnvironmentError, e:
2120 if e.errno not in (EAGAIN, EWOULDBLOCK):
2121 conn_error(c, str(e))
2122 conn_close(c)
2123 else:
2124 conn_wrbuf_rm(c, n)
2125 while schedule and heap_peek(schedule)[0] < mono_time():
2126 heap_extract(schedule)[1]()
2127
2128def crash_handler():
2129 cleanup = t.init(nonblock_read=True)
2130 try:
2131 main()
2132 finally:
2133 cleanup()
2134
2135check_command_dicts((
2136 GLOBAL_KEYS,
2137 PROMPT_KEYS,
2138 BUFLIST_KEYS,
2139 BUF_KEYS,
2140 CMD_ABBREVS
2141))