#!/usr/bin/env python """ status.py - Emano StatusWindow Class Author: Sean B. Palmer, inamidst.com Source: http://inamidst.com/emano/ """ import curses import events, window class Template(object): def __init__(self, template): self.tokens = [] self.parse(template) def parse(self, template): for tok in template.split(): tok = tok.strip() if tok: self.tokens.append(tok) class StatusWindow(window.Window): def __init__(self, editor): self.editor = editor self.mappings = { events.LEFT: self.moveLeft, events.RIGHT: self.moveRight, events.HOME: self.startOfLine, events.END: self.endOfLine, events.DELETE: self.delChar, events.BACKSPACE: self.deleteChar, events.CTRL_C: self.cancel, events.RETURN: self.collectInput } self.templates = { 'default': Template(""" Emano $. $modchar > $. $uri $. $help $exp $. Line $. $line , $. Pos $. $pos $. $codepoint $. (File $. $doc / $docs ) """), 'query': Template("$message : $. $input $. (Emano)"), 'notice': Template("$message $exp $. (Emano)") } self.persist = 0 self.input = None self.inputStart = 0 # Some defaults that don't really need setting self.template = 'default' self.args = {} # Draw the default template now self.show('default') # The following correspond with self.mappings # @@ StatusWindow.mappings[events.RIGHT] = moveRight? def moveLeft(self): pos = self.position() # cf. self.deleteChar... y, x = self.editor.screen.getyx() if self.inputStart and x == self.inputRange[0]: self.inputStart -= 1 self.refresh() elif x > self.inputRange[0]: self.editor.moveCursor(y, x - 1) def moveRight(self): pos = self.position() y, x = self.editor.screen.getyx() maxPos = self.inputRange[0] + len(self.input) inputRangeEnd = self.inputRange[1] - 1 if x < min(maxPos, inputRangeEnd): self.editor.moveCursor(y, x + 1) elif pos < len(self.input): self.inputStart += 1 self.refresh() def startOfLine(self): y, x = self.editor.screen.getyx() self.editor.moveCursor(y, self.inputRange[0]) self.inputStart = 0 self.refresh() def endOfLine(self): y, x = self.editor.screen.getyx() inputWidth = (self.inputRange[1] - self.inputRange[0]) if not len(self.input) > inputWidth: self.editor.moveCursor(y, self.inputRange[0] + len(self.input)) else: self.editor.moveCursor(y, self.inputRange[1] - 1) start = len(self.input) - inputWidth + 1 if start > 0: self.inputStart = start self.refresh() def delChar(self): pos = self.position() if pos < len(self.input): self.input = self.input[:pos] + self.input[pos + 1:] if self.stream: self.dispatch(self.input) self.refresh() def deleteChar(self): pos = self.position() self.input = self.input[:pos - 1] + self.input[pos:] y, x = self.editor.screen.getyx() if self.inputStart: self.inputStart -= 1 elif x > self.inputRange[0]: self.editor.moveCursor(y, x - 1) if self.stream: self.dispatch(self.input) self.refresh() def cancel(self): self.input = '' self.inputRange = None self.editor.setFocus(self.editor.mainwin) if self.args and self.args.has_key('message'): message = 'Cancelled "%s"...' % self.args['message'].lower() else: message = 'Cancelled...' self.notice(message) def collectInput(self): input = self.input self.input = '' self.inputRange = None if not self.stream: self.dispatch(input) else: self.stream = False self.dispatch = False if self.editor.focus is self: self.editor.setFocus(self.savedFocus) self.savedFocus = None # End of self.mappings correspondences def display(self): template = self.templates[self.template] self.args = self.args or {} savedScreenPosition = self.editor.screen.getyx() # Create all the variables args = {'$': '$', '.': ' '} y, x, q, p = self.editor.mainwin.position() def line(): return str(q + 1) args['line'] = line def pos(): return str(p + 1) args['pos'] = pos def modchar(): if len(self.editor.documents) and self.editor.doc.modified(): return '-' return '=' args['modchar'] = modchar def help(): if not self.editor.mainwin.typed: return '(Help? Ctrl+H)' return False args['help'] = help def doc(): if self.editor.documents: return str(self.editor.documents.index(self.editor.doc) + 1) return str(0) args['doc'] = doc def docs(): return str(len(self.editor.documents)) args['docs'] = docs def codepoint(): if not self.editor.documents: return False try: char = self.editor.doc.line(q)[p] except IndexError: pass else: cp = ord(char) if isinstance(char, unicode): if cp > 0x7F: return '{U+%04X}' % cp elif cp == 0x0D: return '{U+000D}' elif cp > 0x7F: return r'{\x%02X}' % cp elif cp == 0x0D: return r'{\x0D}' return False args['codepoint'] = codepoint special = frozenset(['uri', 'input', 'message', 'exp']) # uri -> middle truncate ([...]) # input -> front truncate # message -> end truncate ([...]) # exp -> expand # Substitute the normal args, and user args minus special tokens = template.tokens[:] for (i, token) in enumerate(tokens): if not token.startswith('$'): continue token = token[1:] if args.has_key(token): thing = args[token] if isinstance(thing, basestring): tokens[i] = thing else: tokens[i] = thing() or '' elif self.args.has_key(token): tokens[i] = self.args[token] elif token not in special: tokens[i] = '?' maxy = self.editor.maxy maxx = self.editor.maxx def remaining(tokens): length = 0 for token in tokens: if not token.startswith('$'): length += len(token) return maxx - length # Special if '$uri' in tokens: if self.editor.documents and self.editor.doc.uri: uri = self.editor.doc.uri else: uri = 'Unsaved File' i = remaining(tokens) if len(uri) > i: # The awesome sliding filename code uri = uri[:7] + '[...]' + uri[- i + 12:] tokens[tokens.index('$uri')] = uri if '$input' in tokens: input = self.input i = remaining(tokens) before = len(''.join(tokens[:tokens.index('$input')])) after = len(''.join(tokens[tokens.index('$input') + 1:])) self.inputRange = (before, self.editor.maxx - after) if self.inputStart: input = '<' + input[self.inputStart + 1:] if len(input) > i: if i < 0: i = 0 input = input[:-(len(input) - i + 1)] + '>' else: input += ' ' * (i - len(input)) tokens[tokens.index('$input')] = input else: self.inputRange = None if '$exp' in tokens: exp = ' ' * remaining(tokens) tokens[tokens.index('$exp')] = exp for (i, token) in enumerate(tokens): if token.startswith('$'): if token[1:] in special: tokens[i] = '@@' + token[1:] # Hack to make curses print to the bottom right hand corner # Cf. http://swhack.com/logs/2005-03-06#T01-37-04 self.editor.screen.addch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE) self.editor.screen.insch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE) status = ''.join(tokens)[:self.editor.maxx] self.editor.screen.addstr(maxy + 1, 0, status, curses.A_REVERSE) self.editor.screen.refresh() # Since we'll be drawing this when, e.g., in the main editor window # we need to make sure we restore the cursor to where it was. self.editor.moveCursor(*savedScreenPosition) def show(self, name, args=None, persist=None, force=False): if self.persist and (not force) and (self.template != name): persist = self.persist - 1 self.show(self.template, args=self.args, persist=persist) if persist is not None: self.persist = persist self.template = name self.args = args self.display() def overlay(self, augment, align=None): if align == 'right': savedScreenPosition = self.editor.screen.getyx() y = self.editor.maxy + 1 x = self.editor.maxx - len(augment) maxy = self.editor.maxy maxx = self.editor.maxx self.editor.screen.addch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE) self.editor.screen.insch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE) self.editor.screen.addstr(y, x, augment, curses.A_REVERSE) self.editor.moveCursor(*savedScreenPosition) else: raise ValueError("align must be right, for now") def refresh(self, augment=None): self.show(self.template, args=self.args) if augment is not None: self.overlay(augment, align='right') # Legacy methods def dialogue(self, message, method, stream=False): self.input = '' self.query(message) self.savedFocus = self.editor.focus self.editor.setFocus(self) self.editor.moveCursor(self.editor.maxy + 1, self.inputRange[0]) # that will set self.inputRange self.overlay(str(self.inputRange), align='right') self.dispatch = method self.stream = stream def setInput(self, input): self.input = input self.refresh() def default(self, augment=None): self.show('default') if augment is not None: self.overlay(augment, align='right') def query(self, message=None, persist=None): message = message.rstrip(': ') self.show('query', {'message': message}, persist, force=True) def notice(self, message=None, persist=None): self.show('notice', {'message': message}, persist, force=True) # Input and events def position(self): y, x = self.editor.screen.getyx() return x - self.inputRange[0] + self.inputStart def insertChar(self, c): pos = self.position() self.input = self.input[:pos] + chr(c) + self.input[pos:] y, x = self.editor.screen.getyx() if x < self.inputRange[1] - 1: self.editor.moveCursor(y, x + 1) else: self.inputStart += 1 if self.stream: self.dispatch(self.input) self.refresh() def event(self, e): if self.mappings.has_key(e): self.mappings[e]() return True else: return False if __name__=="__main__": print __doc__