3 # bed - the BTUI editor
5 # This is a simple example program demonstrating a minimal text editor
6 # implemented with the Python BTUI bindings.
8 # Usage: ./bed.py [-d|--debug] <file>
9 # You can move around with arrow keys, PgUp/PgDn and make basic file edits.
10 # Save with Ctrl-S, Quit with Ctrl-Q or Ctrl-C
14 from collections import namedtuple
16 Vec2 = namedtuple('Vec2', 'x y')
18 def clamp(x, low, high):
19 if x < low: return low
20 elif x > high: return high
24 def __init__(self, bt, lines, pos=Vec2(0,0), size=None):
28 self.numlen = len(str(len(self.lines)))
30 self.size = size if size else Vec2(self.bt.width-pos.x, self.bt.height-pos.y)
32 self._cursor = Vec2(0, 0)
42 y = clamp(y, 0, len(self.lines)-1)
43 x = clamp(x, 0, len(self.lines[y]))
44 self._cursor = Vec2(x, y)
46 def update_term_cursor(self):
47 self.bt.move(self.pos.x + self.numlen + 1 + self.cursor.x,
48 self.pos.y + self.cursor.y - self.scroll)
50 def set_scroll(self, newscroll):
51 newscroll = clamp(newscroll, 0, len(self.lines)-1 - (self.size.y-1))
52 if newscroll == self.scroll: return
53 delta = newscroll - self.scroll
54 self.bt.scroll(self.pos.y, self.pos.y + self.size.y - 1, delta)
55 self.scroll = newscroll
56 self.drawn &= set(range(self.scroll, self.scroll + (self.size.y-1)+1))
57 if self.cursor.y < newscroll:
58 self.cursor = Vec2(self.cursor.x, newscroll)
59 elif self.cursor.y > newscroll + (self.size.y-1):
60 self.cursor = Vec2(self.cursor.x, newscroll + (self.size.y-1))
62 def handle_input(self, key, mx=None, my=None):
63 if key == 'Space': key = ' '
64 if key == 'Tab': key = ' '
67 self.cursor = Vec2(self.cursor.x - 1, self.cursor.y)
69 self.cursor = Vec2(self.cursor.x + 1, self.cursor.y)
71 self.cursor = Vec2(self.cursor.x, self.cursor.y - 1)
72 if self.cursor.y < self.scroll:
73 self.set_scroll(self.cursor.y)
75 self.cursor = Vec2(self.cursor.x, self.cursor.y + 1)
76 newscroll = clamp(self.scroll, self.cursor.y - (self.size.y-1), self.cursor.y)
77 if self.cursor.y > self.scroll + (self.size.y-1):
78 self.set_scroll(self.cursor.y - (self.size.y-1))
79 elif key == 'Home' or key == 'Ctrl-a':
80 self.cursor = Vec2(0, self.cursor.y)
81 elif key == 'End' or key == 'Ctrl-e':
82 self.cursor = Vec2(len(self.lines[self.cursor.y]), self.cursor.y)
83 elif key == 'Page Down' or key == 'Ctrl-d':
84 self.set_scroll(self.scroll + self.bt.height // 2)
85 elif key == 'Page Up' or key == 'Ctrl-u':
86 self.set_scroll(self.scroll - self.bt.height // 2)
88 line = self.lines[self.cursor.y]
90 if i == len(line) and self.cursor.y < len(self.lines)-1:
91 line.extend(self.lines.pop(self.cursor.y+1))
92 self.drawn &= {d for d in self.drawn if d < self.cursor.y or d >= len(self.lines)+1}
96 self.drawn.discard(self.cursor.y)
98 elif key == 'Backspace':
99 line = self.lines[self.cursor.y]
101 if i == 0 and self.cursor.y > 0:
102 self.lines.pop(self.cursor.y)
103 prevline = self.lines[self.cursor.y - 1]
104 self.cursor = Vec2(len(prevline), self.cursor.y-1)
105 prevline.extend(line)
106 self.drawn &= {d for d in self.drawn if d < self.cursor.y or d >= len(self.lines)+1}
110 self.drawn.discard(self.cursor.y)
111 self.cursor = Vec2(self.cursor.x - 1, self.cursor.y)
114 line = self.lines[self.cursor.y]
115 before, after = line[:self.cursor.x], line[self.cursor.x:]
116 self.lines[self.cursor.y] = before
117 self.lines.insert(self.cursor.y+1, after)
118 self.drawn &= {d for d in self.drawn if d < self.cursor.y or d >= len(self.lines)}
119 self.cursor = Vec2(0, self.cursor.y + 1)
121 elif key == 'Left release':
122 self.cursor = Vec2(mx-(self.numlen + 1), my - 1 + self.scroll)
123 elif key == 'Mouse wheel down':
124 self.set_scroll(self.scroll + 3)
125 elif key == 'Mouse wheel up':
126 self.set_scroll(self.scroll - 3)
127 elif key and (len(key) == 1 or key == ' '):
128 line = self.lines[self.cursor.y]
130 line[i:i] = bytearray(key, 'utf8')
131 self.cursor = Vec2(self.cursor.x + len(key), self.cursor.y)
132 self.drawn.discard(self.cursor.y)
136 self.bt.hide_cursor()
137 for i in range(self.size.y):
138 lineno = self.scroll + i
139 if lineno in self.drawn: continue
143 if lineno >= len(self.lines):
144 self.bt.clear(btui.ClearType.LINE)
145 self.drawn.add(lineno)
147 with self.bt.attributes("faint"):
148 self.bt.write(("{:>"+str(self.numlen)+"}").format(lineno + 1))
151 self.bt.write_bytes(bytes(self.lines[lineno]))
152 self.bt.clear(btui.ClearType.RIGHT)
153 self.drawn.add(lineno)
156 def __init__(self, bt, filename):
158 self.filename = filename
160 lines = [bytearray(line.rstrip(b'\n')) for line in open(filename, 'rb').readlines()]
161 except FileNotFoundError:
162 lines = [bytearray(b'')]
163 self.file = BedFile(bt, lines, Vec2(0, 1), Vec2(bt.width, bt.height-2))
168 self.bt.set_cursor(btui.CursorType.BLINKING_BAR)
170 key, mx, my = self.bt.getkey()
174 elif key == 'Ctrl-q':
175 if self.file.unsaved:
178 elif key == 'Ctrl-s':
181 self.file.handle_input(key, mx, my)
183 key, mx, my = self.bt.getkey()
185 def confirm_save(self):
186 self.bt.move(0, self.bt.height)
187 with self.bt.attributes('bold'):
188 self.bt.write(f"Do you want to save {self.filename}? [y/n]")
189 key, mx, my = self.bt.getkey()
190 if key in ('y', 'Y'):
193 elif key in ('n', 'N', 'Ctrl-c', 'Escape', 'Ctrl-q'):
197 with open(self.filename, 'wb') as f:
198 for line in self.file.lines:
201 self.file.unsaved = False
206 k = 'title-modified' if self.file.unsaved else 'title'
207 if k not in self.drawn:
208 self.drawn -= {'title', 'title-modified'}
210 with self.bt.attributes("bold"):
211 self.bt.write(self.filename)
212 if self.file.unsaved:
213 with self.bt.attributes("dim"):
214 self.bt.write(" (modified)")
215 self.bt.clear(btui.ClearType.RIGHT)
220 # Just redraw this every time:
221 with self.bt.attributes("bold"):
222 self.bt.move(0, self.bt.height-1)
223 self.bt.write("Ctrl-Q to quit, Ctrl-S to save")
224 self.bt.clear(btui.ClearType.RIGHT)
225 with self.bt.attributes("faint"):
226 s = f"Line {self.file.cursor.y}, Col {self.file.cursor.x}, {self.renders} redraws"
227 self.bt.move(self.bt.width-len(s), self.bt.height-1)
230 self.file.update_term_cursor()
231 self.bt.show_cursor()
234 if __name__ == '__main__':
237 if args and args[0] in ('-d', '--debug'):
242 print("Usage: bed.py [-d|--debug] file")
246 with btui.open(debug=debug) as bt:
247 bed = BED(bt, filename)