1import threading 2import traceback 3 4import time 5 6import workspace.sys 7from workspace import ui 8 9 10COLOR = (0x44, 0x44, 0x44) 11 12 13class ShellWindow(ui.Window): 14 def __init__(self, parent, argv=None): 15 super().__init__( 16 parent, "Shell", menu=True, maximizable=False, color=COLOR 17 ) 18 if argv is None or len(argv) == 0: 19 argv = ["CLI"] 20 21 col = ui.Column() 22 self.add(col) 23 # col.add(ui.TitleSeparator(self)) 24 self.canvas = ShellWidget(self, argv=argv) 25 self.set_background_color(ui.Color(*COLOR)) 26 col.add(self.canvas, left=12, top=6, right=12, bottom=6) 27 28 self.closed.connect(self.__on_closed) 29 30 def __on_closed(self): 31 self.canvas.terminate_process() 32 33 34class ShellWidget(ui.Canvas): 35 36 output = ui.Signal() 37 exited = ui.Signal() 38 39 def __init__(self, parent, *, argv, columns=80, rows=24): 40 super().__init__(parent) 41 self.font = ui.Font("Roboto Mono", 14) 42 self.set_background_color(ui.Color(*COLOR)) 43 self.text_color = ui.Color(0xFF, 0xFF, 0xFF) 44 45 self.input_thread = None 46 self.output_thread = None 47 self.output.connect(self.__on_output) 48 49 self._completed = False 50 self.exited.connect(self.__on_exited) 51 52 self.line_buffer = [] 53 self.lines = [] 54 # for _ in range(25): 55 self._new_line() 56 self.cx = 0 57 self.cy = 0 58 59 # self.execute(["version"]) 60 # self.execute(["python", "-i"]) 61 self.process = None 62 self.execute(argv) 63 64 # FIXME: Using Qt directly 65 from fsui.qt import Qt, QPainter, QPixmap 66 67 self._widget.setFocusPolicy(Qt.StrongFocus) 68 69 painter = QPainter() 70 pixmap = QPixmap(100, 100) 71 painter.begin(pixmap) 72 painter.setFont(self.font.qfont()) 73 size = painter.boundingRect( 74 0, 0, 10000, 1000, Qt.AlignLeft | Qt.AlignTop, " " * columns 75 ) 76 painter.end() 77 print(size) 78 self.line_width = size.width() 79 self.line_height = size.height() 80 81 self.set_min_size((self.line_width, self.line_height * rows)) 82 83 def execute(self, args): 84 self.process = workspace.sys.workspace_exec(args) 85 print(self.process) 86 self.input_thread = InputThread(self, self.process) 87 self.input_thread.start() 88 self.output_thread = OutputThread(self, self.process) 89 self.output_thread.start() 90 91 def terminate_process(self): 92 self.input_thread.stop() 93 self.output_thread.stop() 94 self.process.kill() 95 96 def _new_line(self): 97 self.lines = self.lines[-24:] 98 self.lines.append([" "] * 80) 99 self.cx = 0 100 self.cy = len(self.lines) - 1 101 102 def __on_output(self): 103 print("on_output") 104 # This runs in the main thread 105 data = self.output_thread.data() 106 print(data) 107 for byte in data: 108 self._print_byte(byte) 109 110 def __on_exited(self): 111 self._complete() 112 113 def _complete(self): 114 if not self._completed: 115 self._print_text("\n[Process completed]") 116 self.input_thread.stop() 117 self.output_thread.stop() 118 self._completed = True 119 120 def _print_text(self, text): 121 for char in text: 122 self._print_char(char) 123 124 def _print_byte(self, byte): 125 # FIXME: UTF-8... 126 char = byte.decode("ISO-8859-1") 127 self._print_char(char) 128 129 def _print_char(self, char): 130 if char == "\n": 131 self._new_line() 132 return 133 if char == "\r": 134 self.cx = 0 135 return 136 # print(self.cy, self.cx) 137 self.lines[self.cy][self.cx] = char 138 # line = self.lines[-1] 139 self.cx += 1 140 if self.cx == 80: 141 # self.cy += 1 142 # self.cx = 0 143 144 # if self.cy == 25: 145 # self.lines = self.lines[:24] 146 147 self._new_line() 148 149 # if len(line) < 80: 150 # self.lines[-1] = line + char 151 # else: 152 # line = char 153 # self.lines.append(line) 154 # if len(self.lines) > 25: 155 # self.lines = self.lines[-25:] 156 self.refresh() 157 158 def _erase_char(self): 159 # FIXME: TODO: multi-line erase.. 160 if self.cx > 0: 161 self.cx -= 1 162 self.lines[self.cy][self.cx] = " " 163 self.refresh() 164 165 def on_paint(self): 166 painter = ui.Painter(self) 167 painter.set_font(self.font) 168 if hasattr(self, "text_color"): 169 painter.set_text_color(self.text_color) 170 # lines = self.buffer() 171 x, y = 0, 0 172 for line_data in self.lines: 173 line = "".join(line_data) 174 # tw, th = painter.measure_text(line) 175 painter.draw_text(line, x, y) 176 # y += th 177 y += self.line_height 178 179 def on_key_press(self, event): 180 print("on_key_press", event) 181 if self._completed: 182 print("process is completed, ignoring") 183 return 184 # FIXME: using a QKeyEvent directly 185 from fsui.qt import QKeyEvent 186 187 assert isinstance(event, QKeyEvent) 188 char = event.text() 189 print(repr(char)) 190 if not char: 191 print("char was", repr(char)) 192 return 193 if char == "\x08": 194 if len(self.line_buffer) > 0: 195 self.line_buffer = self.line_buffer[:-1] 196 self._erase_char() 197 return 198 if char == "\x04": 199 self.input_thread.add_byte(b"\x04") 200 # FIXME: Close stdin if/when you type Ctrl+D? 201 # self._p.stdin.close() 202 return 203 if char == "\r": 204 # Qt peculiarity... 205 char = "\n" 206 self.line_buffer.append(char) 207 self._new_line() 208 bytes = [] 209 for char in self.line_buffer: 210 bytes.append(char.encode("ISO-8859-1")) 211 self.input_thread.add_bytes(bytes) 212 self.line_buffer.clear() 213 return 214 else: 215 self.line_buffer.append(char) 216 self._print_char(char) 217 # byte = char.encode("ISO-8859-1") 218 # self.input_thread.add_byte(byte) 219 220 221class OutputThread(threading.Thread): 222 def __init__(self, widget, p): 223 super().__init__() 224 self._widget = widget 225 self._p = p 226 self._data = [] 227 self._available = False 228 self._lock = threading.Lock() 229 self.stop_flag = False 230 231 def data(self): 232 with self._lock: 233 data = self._data 234 self._data = [] 235 self._available = False 236 return data 237 238 def run(self): 239 print("OutputThread.run") 240 try: 241 self._run() 242 except Exception: 243 traceback.print_exc() 244 self._widget = None 245 246 def stop(self): 247 self.stop_flag = True 248 self._widget = None 249 250 def _run(self): 251 while True: 252 byte = self._p.stdout.read(1) 253 if not byte: 254 print("no more data, ending output thread") 255 if self._widget is not None: 256 self._widget.exited.emit() 257 return 258 with self._lock: 259 self._data.append(byte) 260 if not self._available: 261 self._available = True 262 print("emit time") 263 if self._widget is not None: 264 self._widget.output.emit() 265 266 267class InputThread(threading.Thread): 268 """Handles data sent to child process""" 269 270 def __init__(self, widget, p): 271 super().__init__() 272 self._widget = widget 273 self._p = p 274 self._data = [] 275 # self._available = False 276 self._lock = threading.Lock() 277 self.stop_flag = False 278 279 def add_byte(self, byte): 280 with self._lock: 281 self._data.append(byte) 282 283 def add_bytes(self, bytes): 284 with self._lock: 285 self._data.extend(bytes) 286 287 def run(self): 288 print("InputThread.run") 289 try: 290 self._run() 291 except Exception: 292 traceback.print_exc() 293 self._widget = None 294 295 def stop(self): 296 self.stop_flag = True 297 self._widget = None 298 299 def _run(self): 300 while not self.stop_flag: 301 # FIXME: conditions instead 302 time.sleep(0.01) 303 with self._lock: 304 if len(self._data) == 0: 305 continue 306 data = b"".join(self._data) 307 self._data = [] 308 print("writing", data) 309 self._p.stdin.write(data) 310 self._p.stdin.flush() 311