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