1#!/usr/local/bin/python3.8
2#
3# PLASMA : Generate an indented asm code (pseudo-C) with colored syntax.
4# Copyright (C) 2015    Joel
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program 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
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.    If not, see <http://www.gnu.org/licenses/>.
18#
19
20import curses
21from curses import A_UNDERLINE, color_pair, A_REVERSE
22
23from plasma.lib.custom_colors import *
24from plasma.lib.consts import *
25from plasma.lib.ui.widget import Widget
26
27
28ALPHANUM = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
29
30
31def check_match_word(line, idx, word):
32    if idx != 0 and line[idx - 1] in ALPHANUM:
33        return False
34    if idx + len(word) != len(line) and line[idx + len(word)] in ALPHANUM:
35        return False
36    return True
37
38
39class Listbox(Widget):
40    def __init__(self, x, y, w, h, output):
41        Widget.__init__(self, x, y, w, h)
42
43        # Coordinates of the cursor inside the box
44        self.win_y = 0
45        self.cursor_y = 0
46        self.cursor_x = 0
47
48        if output is not None:
49            self.output = output
50            self.token_lines = output.token_lines
51
52        self.search_hi = []
53        self.search_bin = None
54        self.word_accepted_chars = ["_", "@", ".", "$", ":", "?"]
55
56        # Note: all these functions should return a boolean. The value is true
57        # if the screen must be refreshed (not re-drawn, in this case call
58        # explictly self.draw or self.reload_asm if the output changed).
59
60        self.mapping = {
61            b"\x1b\x5b\x44": self.k_left,
62            b"\x1b\x5b\x43": self.k_right,
63            b"\x1b\x5b\x41": self.k_up,
64            b"\x1b\x5b\x42": self.k_down,
65            b"\x1b\x5b\x35\x7e": self.k_pageup,
66            b"\x1b\x5b\x36\x7e": self.k_pagedown,
67            b"g": self.k_top,
68            b"G": self.k_bottom,
69            b"\x01": self.k_home, # ctrl-a
70            b"\x05": self.k_end, # ctrl-e
71            b"\x1b\x5b\x37\x7e": self.k_home,
72            b"\x1b\x5b\x38\x7e": self.k_end,
73            b" ": self.cmd_highlight_current_word,
74            b"\x0b": self.cmd_highlight_clear, # ctrl-k
75            b"\x1b\x5b\x31\x3b\x35\x44": self.k_ctrl_left,
76            b"\x1b\x5b\x31\x3b\x35\x43": self.k_ctrl_right,
77            b"\n": self.k_enter,
78            b"q": self.k_q,
79            b"\x1b": self.k_q,
80        }
81
82        self.cursor_position_utf8 = {
83            0: "█",
84            1: "▇",
85            2: "▆",
86            3: "▅",
87            4: "▄",
88            5: "▃",
89            6: "▂",
90            7: "▁",
91        }
92
93
94    def is_tok_var(self):
95        num_line = self.win_y + self.cursor_y
96        tokens = self.output.token_lines[num_line]
97
98        x = self.cursor_x
99        if x >= len(self.output.lines[num_line]):
100            return None
101
102        i = 0
103        for (s, col, _) in tokens:
104            i += len(s)
105            if x < i:
106                return col == COLOR_VAR.val
107
108        return False
109
110
111    def get_word_under_cursor(self):
112        num_line = self.win_y + self.cursor_y
113        line = self.output.lines[num_line]
114
115        if len(line) == 0:
116            return None
117
118        x = self.cursor_x
119        if x >= len(line):
120            return None
121
122        if not line[x].isalnum() and not line[x] in self.word_accepted_chars:
123            return None
124
125        curr = []
126        while x >= 0 and (line[x].isalnum() or line[x] in self.word_accepted_chars):
127            x -= 1
128        x += 1
129
130        while x < len(line) and (line[x].isalnum() or \
131                line[x] in self.word_accepted_chars):
132            curr.append(line[x])
133            x += 1
134
135        if curr[-1] == ":":
136            return "".join(curr[:-1])
137
138        if curr:
139            return "".join(curr)
140        return None
141
142
143    def goto_line(self, new_line):
144        curr_line = self.win_y + self.cursor_y
145        diff = new_line - curr_line
146        if diff > 0:
147            self.scroll_down(diff, False)
148        elif diff < 0:
149            self.scroll_up(-diff, False)
150
151
152    def draw(self):
153        i = 0
154        while i < self.height:
155            if self.win_y + i < len(self.token_lines):
156                self.print_line(i)
157            else:
158                # force to clear the entire line
159                self.screen.move(i, 0)
160            self.screen.clrtoeol()
161            i += 1
162
163        # Print the scroll cursor on the right. It uses utf-8 block characters.
164
165        y = self.get_y_scroll()
166        i = y % 8
167        y = y // 8
168
169        self.screen.insstr(y, self.width - 1,
170            self.cursor_position_utf8[i],
171            color_pair(COLOR_SCROLL_CURSOR))
172
173        if i != 0 and y + 1 < self.height:
174            self.screen.insstr(y + 1, self.width - 1,
175                self.cursor_position_utf8[i],
176                color_pair(COLOR_SCROLL_CURSOR) | A_REVERSE)
177
178
179    def draw_cursor(self):
180        size_line = len(self.output.lines[self.win_y + self.cursor_y])
181        if size_line == 0:
182            x = 0
183        elif self.cursor_x >= size_line:
184            x = size_line - 1
185        else:
186            x = self.cursor_x
187
188        self.screen.move(self.cursor_y, x)
189
190
191    def print_line(self, i):
192        num_line = self.win_y + i
193        is_current_line = self.cursor_y == i and self.has_focus
194        force_exit = False
195        x = 0
196
197        for (string, col, is_bold) in self.token_lines[num_line]:
198            if x + len(string) >= self.width - 1:
199                string = string[:self.width - x - 1]
200                force_exit = True
201
202            c = color_pair(col)
203
204            if is_current_line:
205                c |= A_UNDERLINE
206
207            if is_bold:
208                c |= curses.A_BOLD
209
210            self.screen.addstr(i, x, string, c)
211
212            x += len(string)
213            if force_exit:
214                break
215
216        if is_current_line and not force_exit:
217            n = self.width - x - 1
218            self.screen.addstr(i, x, " " * n, color_pair(0) | A_UNDERLINE)
219            x += n
220
221        self.highlight_search(i)
222        self.screen.move(i, x)
223
224
225    def get_y_scroll(self):
226        # Because the scroll can have 8 states
227        h8 = self.height * 8
228        if len(self.token_lines) <= self.height:
229            return 0
230        y = self.win_y * h8 // (len(self.token_lines) - self.height)
231        if y >= h8 - 8:
232            return h8 - 8
233        return y
234
235
236    def callback_mouse_left(self, x, y):
237        self.cursor_x = x
238        self.goto_line(self.win_y + y)
239        self.cmd_highlight_current_word(True)
240        self.check_cursor_x()
241
242
243    def callback_mouse_up(self):
244        self.scroll_up(3, True)
245
246
247    def callback_mouse_down(self):
248        self.scroll_down(3, True)
249
250
251    def callback_mouse_double_left(self):
252        return False
253
254
255    def highlight_search(self, i):
256        if not self.search_hi:
257            return
258        num_line = self.win_y + i
259        start = 0
260        for word in self.search_hi:
261            while 1:
262                idx = self.output.lines[num_line].find(word, start)
263                if idx == -1 or idx >= self.width:
264                    break
265                if check_match_word(self.output.lines[num_line], idx, word):
266                    self.screen.chgat(i, idx, len(word), curses.color_pair(1))
267                start = idx + 1
268
269
270    def scroll_up(self, n, do_page_scroll):
271        if do_page_scroll:
272            wy = self.win_y - n
273            y = self.cursor_y + n
274            line = self.win_y + self.cursor_y
275
276            wy = self.dump_update_up(wy)
277
278            if wy >= 0:
279                self.win_y = wy
280                if y <= self.height - 3:
281                    if line != len(self.token_lines):
282                        self.cursor_y = y
283                else:
284                    self.cursor_y = self.height - 4
285            else:
286                self.win_y = 0
287        else:
288            # TODO: find another way
289            for i in range(n):
290                self.dump_update_up(self.win_y)
291
292                if self.win_y == 0:
293                    if self.cursor_y == 0:
294                        break
295                    self.cursor_y -= 1
296                else:
297                    if self.cursor_y == 3:
298                        self.win_y -= 1
299                    else:
300                        self.cursor_y -= 1
301
302
303    def scroll_down(self, n, do_page_scroll):
304        if do_page_scroll:
305            wy = self.win_y + n
306            y = self.cursor_y - n
307
308            self.dump_update_bottom(wy)
309
310            if wy > len(self.token_lines) - self.height:
311                if wy < len(self.token_lines) - 3:
312                    self.win_y = wy
313                else:
314                    self.win_y = len(self.token_lines) - 3 - 1
315                if y >= 3:
316                    self.cursor_y = y
317                else:
318                    self.cursor_y = 3
319            else:
320                self.win_y = wy
321                if y >= 3:
322                    self.cursor_y = y
323                else:
324                    self.cursor_y = 3
325        else:
326            # TODO: find another way
327            for i in range(n):
328                self.dump_update_bottom(self.win_y)
329
330                if self.win_y >= len(self.token_lines) - self.height:
331                    if self.win_y + self.cursor_y == len(self.token_lines) - 1:
332                        break
333                    self.cursor_y += 1
334                else:
335                    if self.cursor_y == self.height - 4:
336                        self.win_y += 1
337                    else:
338                        self.cursor_y += 1
339
340
341    def dump_update_up(self, wy):
342        return wy
343
344
345    def dump_update_bottom(self, wy):
346        return
347
348
349    def check_cursor_x(self):
350        size_line = len(self.output.lines[self.win_y + self.cursor_y])
351        if size_line == 0:
352            self.cursor_x = 0
353        elif self.cursor_x >= size_line:
354            self.cursor_x = size_line - 1
355
356
357    # Commands / Mapping keys
358
359
360    def k_left(self):
361        self.check_cursor_x()
362        if self.cursor_x > 0:
363            self.cursor_x -= 1
364        return False
365
366    def k_right(self):
367        self.cursor_x += 1
368        self.check_cursor_x()
369        return False
370
371    def k_down(self):
372        self.scroll_down(1, False)
373        return True
374
375    def k_up(self):
376        self.scroll_up(1, False)
377        return True
378
379    def k_pageup(self):
380        self.scroll_up(self.height - 1, True)
381        return True
382
383    def k_pagedown(self):
384        self.scroll_down(self.height - 1, True)
385        return True
386
387    def k_enter(self):
388        self.should_stop = True
389        self.value_selected = True
390        return False
391
392
393    def k_q(self):
394        self.should_stop = True
395        self.value_selected = False
396        return False
397
398
399    def k_home(self):
400        # TODO: fix self.cursor_x >= w
401        if self.cursor_x == 0:
402            line = self.output.lines[self.win_y + self.cursor_y]
403            while self.cursor_x < len(line):
404                if line[self.cursor_x] != " ":
405                    break
406                self.cursor_x += 1
407        else:
408            self.cursor_x = 0
409        return False
410
411    def k_end(self):
412        # TODO: fix self.cursor_x >= w
413        size_line = len(self.output.lines[self.win_y + self.cursor_y])
414        if size_line >= self.width:
415            self.cursor_x = self.width - 1
416        elif size_line > 0:
417            self.cursor_x = size_line - 1
418        else:
419            self.cursor_x = 0
420        return False
421
422    def k_ctrl_right(self):
423        self.check_cursor_x()
424        # TODO: fix self.cursor_x >= w
425        line = self.output.lines[self.win_y + self.cursor_y]
426        x = self.cursor_x
427        while x < len(line) and line[x] == " " and x < self.width:
428            x += 1
429        while x < len(line) and line[x] != " " and x < self.width:
430            x += 1
431        self.cursor_x = x
432
433    def k_ctrl_left(self):
434        self.check_cursor_x()
435        line = self.output.lines[self.win_y + self.cursor_y]
436        x = self.cursor_x
437        if x == 0:
438            return
439        x -= 1
440        while x > 0 and line[x] == " ":
441            x -= 1
442        while x > 0 and line[x] != " ":
443            x -= 1
444        if x != 0:
445            x += 1
446        self.cursor_x = x
447
448    def k_prev_paragraph(self):
449        l = self.win_y + self.cursor_y - 1
450        while l > 0 and len(self.output.lines[l]) != 0:
451            l -= 1
452        if l >= 0:
453            self.goto_line(l)
454            self.check_cursor_x()
455        return True
456
457    def k_next_paragraph(self):
458        l = self.win_y + self.cursor_y + 1
459        while l < len(self.output.lines)-1 and len(self.output.lines[l]) != 0:
460            l += 1
461        if l < len(self.output.lines):
462            self.goto_line(l)
463            self.check_cursor_x()
464        return True
465
466
467    def k_top(self):
468        self.cursor_y = 0
469        self.win_y = 0
470        self.cursor_x = 0
471        return True
472
473
474    def k_bottom(self):
475        self.cursor_x = 0
476        if self.win_y >= len(self.token_lines) - self.height:
477            self.cursor_y += len(self.token_lines) - \
478                             self.win_y - self.cursor_y - 1
479        else:
480            self.cursor_y = self.height - 1
481            self.win_y = len(self.token_lines) - self.height
482        return True
483
484
485    def cmd_highlight_current_word(self, from_mouse_event=False):
486        # When we click on a word with the mouse, we must be explicitly
487        # on the word.
488        if not from_mouse_event:
489            num_line = self.win_y + self.cursor_y
490            line = self.output.lines[num_line]
491            if self.cursor_x >= len(line):
492                self.cursor_x = len(line) - 1
493
494        w = self.get_word_under_cursor()
495        if w is None:
496            return False
497        self.search_hi = [w]
498        return True
499
500
501    def cmd_highlight_clear(self):
502        self.search_hi.clear()
503        return True
504