| 1 | #!/usr/bin/python |
|---|
| 2 | # |
|---|
| 3 | # Urwid example lazy text editor suitable for tabbed and format=flowed text |
|---|
| 4 | # Copyright (C) 2004-2009 Ian Ward |
|---|
| 5 | # |
|---|
| 6 | # This library is free software; you can redistribute it and/or |
|---|
| 7 | # modify it under the terms of the GNU Lesser General Public |
|---|
| 8 | # License as published by the Free Software Foundation; either |
|---|
| 9 | # version 2.1 of the License, or (at your option) any later version. |
|---|
| 10 | # |
|---|
| 11 | # This library is distributed in the hope that it will be useful, |
|---|
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|---|
| 14 | # Lesser General Public License for more details. |
|---|
| 15 | # |
|---|
| 16 | # You should have received a copy of the GNU Lesser General Public |
|---|
| 17 | # License along with this library; if not, write to the Free Software |
|---|
| 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 19 | # |
|---|
| 20 | # Urwid web site: http://excess.org/urwid/ |
|---|
| 21 | |
|---|
| 22 | """ |
|---|
| 23 | Urwid example lazy text editor suitable for tabbed and flowing text |
|---|
| 24 | |
|---|
| 25 | Features: |
|---|
| 26 | - custom list walker for lazily loading text file |
|---|
| 27 | |
|---|
| 28 | Usage: |
|---|
| 29 | edit.py <filename> |
|---|
| 30 | |
|---|
| 31 | """ |
|---|
| 32 | |
|---|
| 33 | import sys |
|---|
| 34 | |
|---|
| 35 | import urwid |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | class LineWalker(urwid.ListWalker): |
|---|
| 39 | """ListWalker-compatible class for lazily reading file contents.""" |
|---|
| 40 | |
|---|
| 41 | def __init__(self, name): |
|---|
| 42 | self.file = open(name) |
|---|
| 43 | self.lines = [] |
|---|
| 44 | self.focus = 0 |
|---|
| 45 | |
|---|
| 46 | def get_focus(self): |
|---|
| 47 | return self._get_at_pos(self.focus) |
|---|
| 48 | |
|---|
| 49 | def set_focus(self, focus): |
|---|
| 50 | self.focus = focus |
|---|
| 51 | self._modified() |
|---|
| 52 | |
|---|
| 53 | def get_next(self, start_from): |
|---|
| 54 | return self._get_at_pos(start_from + 1) |
|---|
| 55 | |
|---|
| 56 | def get_prev(self, start_from): |
|---|
| 57 | return self._get_at_pos(start_from - 1) |
|---|
| 58 | |
|---|
| 59 | def read_next_line(self): |
|---|
| 60 | """Read another line from the file.""" |
|---|
| 61 | |
|---|
| 62 | next_line = self.file.readline() |
|---|
| 63 | |
|---|
| 64 | if not next_line or next_line[-1:] != '\n': |
|---|
| 65 | # no newline on last line of file |
|---|
| 66 | self.file = None |
|---|
| 67 | else: |
|---|
| 68 | # trim newline characters |
|---|
| 69 | next_line = next_line[:-1] |
|---|
| 70 | |
|---|
| 71 | expanded = next_line.expandtabs() |
|---|
| 72 | |
|---|
| 73 | edit = urwid.Edit("", expanded, allow_tab=True) |
|---|
| 74 | edit.set_edit_pos(0) |
|---|
| 75 | edit.original_text = next_line |
|---|
| 76 | self.lines.append(edit) |
|---|
| 77 | |
|---|
| 78 | return next_line |
|---|
| 79 | |
|---|
| 80 | |
|---|
| 81 | def _get_at_pos(self, pos): |
|---|
| 82 | """Return a widget for the line number passed.""" |
|---|
| 83 | |
|---|
| 84 | if pos < 0: |
|---|
| 85 | # line 0 is the start of the file, no more above |
|---|
| 86 | return None, None |
|---|
| 87 | |
|---|
| 88 | if len(self.lines) > pos: |
|---|
| 89 | # we have that line so return it |
|---|
| 90 | return self.lines[pos], pos |
|---|
| 91 | |
|---|
| 92 | if self.file is None: |
|---|
| 93 | # file is closed, so there are no more lines |
|---|
| 94 | return None, None |
|---|
| 95 | |
|---|
| 96 | assert pos == len(self.lines), "out of order request?" |
|---|
| 97 | |
|---|
| 98 | self.read_next_line() |
|---|
| 99 | |
|---|
| 100 | return self.lines[-1], pos |
|---|
| 101 | |
|---|
| 102 | def split_focus(self): |
|---|
| 103 | """Divide the focus edit widget at the cursor location.""" |
|---|
| 104 | |
|---|
| 105 | focus = self.lines[self.focus] |
|---|
| 106 | pos = focus.edit_pos |
|---|
| 107 | edit = urwid.Edit("",focus.edit_text[pos:], allow_tab=True) |
|---|
| 108 | edit.original_text = "" |
|---|
| 109 | focus.set_edit_text(focus.edit_text[:pos]) |
|---|
| 110 | edit.set_edit_pos(0) |
|---|
| 111 | self.lines.insert(self.focus+1, edit) |
|---|
| 112 | |
|---|
| 113 | def combine_focus_with_prev(self): |
|---|
| 114 | """Combine the focus edit widget with the one above.""" |
|---|
| 115 | |
|---|
| 116 | above, ignore = self.get_prev(self.focus) |
|---|
| 117 | if above is None: |
|---|
| 118 | # already at the top |
|---|
| 119 | return |
|---|
| 120 | |
|---|
| 121 | focus = self.lines[self.focus] |
|---|
| 122 | above.set_edit_pos(len(above.edit_text)) |
|---|
| 123 | above.set_edit_text(above.edit_text + focus.edit_text) |
|---|
| 124 | del self.lines[self.focus] |
|---|
| 125 | self.focus -= 1 |
|---|
| 126 | |
|---|
| 127 | def combine_focus_with_next(self): |
|---|
| 128 | """Combine the focus edit widget with the one below.""" |
|---|
| 129 | |
|---|
| 130 | below, ignore = self.get_next(self.focus) |
|---|
| 131 | if below is None: |
|---|
| 132 | # already at bottom |
|---|
| 133 | return |
|---|
| 134 | |
|---|
| 135 | focus = self.lines[self.focus] |
|---|
| 136 | focus.set_edit_text(focus.edit_text + below.edit_text) |
|---|
| 137 | del self.lines[self.focus+1] |
|---|
| 138 | |
|---|
| 139 | |
|---|
| 140 | class EditDisplay: |
|---|
| 141 | palette = [ |
|---|
| 142 | ('body','default', 'default'), |
|---|
| 143 | ('foot','dark cyan', 'dark blue', 'bold'), |
|---|
| 144 | ('key','light cyan', 'dark blue', 'underline'), |
|---|
| 145 | ] |
|---|
| 146 | |
|---|
| 147 | footer_text = ('foot', [ |
|---|
| 148 | "Text Editor ", |
|---|
| 149 | ('key', "F5"), " save ", |
|---|
| 150 | ('key', "F8"), " quit", |
|---|
| 151 | ]) |
|---|
| 152 | |
|---|
| 153 | def __init__(self, name): |
|---|
| 154 | self.save_name = name |
|---|
| 155 | self.walker = LineWalker(name) |
|---|
| 156 | self.listbox = urwid.ListBox(self.walker) |
|---|
| 157 | self.footer = urwid.AttrWrap(urwid.Text(self.footer_text), |
|---|
| 158 | "foot") |
|---|
| 159 | self.view = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'), |
|---|
| 160 | footer=self.footer) |
|---|
| 161 | |
|---|
| 162 | def main(self): |
|---|
| 163 | self.loop = urwid.MainLoop(self.view, self.palette, |
|---|
| 164 | unhandled_input=self.unhandled_keypress) |
|---|
| 165 | self.loop.run() |
|---|
| 166 | |
|---|
| 167 | def unhandled_keypress(self, k): |
|---|
| 168 | """Last resort for keypresses.""" |
|---|
| 169 | |
|---|
| 170 | if k == "f5": |
|---|
| 171 | self.save_file() |
|---|
| 172 | elif k == "f8": |
|---|
| 173 | raise urwid.ExitMainLoop() |
|---|
| 174 | elif k == "delete": |
|---|
| 175 | # delete at end of line |
|---|
| 176 | self.walker.combine_focus_with_next() |
|---|
| 177 | elif k == "backspace": |
|---|
| 178 | # backspace at beginning of line |
|---|
| 179 | self.walker.combine_focus_with_prev() |
|---|
| 180 | elif k == "enter": |
|---|
| 181 | # start new line |
|---|
| 182 | self.walker.split_focus() |
|---|
| 183 | # move the cursor to the new line and reset pref_col |
|---|
| 184 | self.loop.process_input(["down", "home"]) |
|---|
| 185 | elif k == "right": |
|---|
| 186 | w, pos = self.walker.get_focus() |
|---|
| 187 | w, pos = self.walker.get_next(pos) |
|---|
| 188 | if w: |
|---|
| 189 | self.listbox.set_focus(pos, 'above') |
|---|
| 190 | self.loop.process_input(["home"]) |
|---|
| 191 | elif k == "left": |
|---|
| 192 | w, pos = self.walker.get_focus() |
|---|
| 193 | w, pos = self.walker.get_prev(pos) |
|---|
| 194 | if w: |
|---|
| 195 | self.listbox.set_focus(pos, 'below') |
|---|
| 196 | self.loop.process_input(["end"]) |
|---|
| 197 | else: |
|---|
| 198 | return |
|---|
| 199 | return True |
|---|
| 200 | |
|---|
| 201 | |
|---|
| 202 | def save_file(self): |
|---|
| 203 | """Write the file out to disk.""" |
|---|
| 204 | |
|---|
| 205 | l = [] |
|---|
| 206 | walk = self.walker |
|---|
| 207 | for edit in walk.lines: |
|---|
| 208 | # collect the text already stored in edit widgets |
|---|
| 209 | if edit.original_text.expandtabs() == edit.edit_text: |
|---|
| 210 | l.append(edit.original_text) |
|---|
| 211 | else: |
|---|
| 212 | l.append(re_tab(edit.edit_text)) |
|---|
| 213 | |
|---|
| 214 | # then the rest |
|---|
| 215 | while walk.file is not None: |
|---|
| 216 | l.append(walk.read_next_line()) |
|---|
| 217 | |
|---|
| 218 | # write back to disk |
|---|
| 219 | outfile = open(self.save_name, "w") |
|---|
| 220 | |
|---|
| 221 | prefix = "" |
|---|
| 222 | for line in l: |
|---|
| 223 | outfile.write(prefix + line) |
|---|
| 224 | prefix = "\n" |
|---|
| 225 | |
|---|
| 226 | def re_tab(s): |
|---|
| 227 | """Return a tabbed string from an expanded one.""" |
|---|
| 228 | l = [] |
|---|
| 229 | p = 0 |
|---|
| 230 | for i in range(8, len(s), 8): |
|---|
| 231 | if s[i-2:i] == " ": |
|---|
| 232 | # collapse two or more spaces into a tab |
|---|
| 233 | l.append(s[p:i].rstrip() + "\t") |
|---|
| 234 | p = i |
|---|
| 235 | |
|---|
| 236 | if p == 0: |
|---|
| 237 | return s |
|---|
| 238 | else: |
|---|
| 239 | l.append(s[p:]) |
|---|
| 240 | return "".join(l) |
|---|
| 241 | |
|---|
| 242 | |
|---|
| 243 | |
|---|
| 244 | def main(): |
|---|
| 245 | try: |
|---|
| 246 | name = sys.argv[1] |
|---|
| 247 | assert open(name, "a") |
|---|
| 248 | except: |
|---|
| 249 | sys.stderr.write(__doc__) |
|---|
| 250 | return |
|---|
| 251 | EditDisplay(name).main() |
|---|
| 252 | |
|---|
| 253 | |
|---|
| 254 | if __name__=="__main__": |
|---|
| 255 | main() |
|---|