Fix input_box refresh() out of order resize
[canto-curses.git] / canto_curses / screen.py
1 # -*- coding: utf-8 -*-
2 #Canto-curses - ncurses RSS reader
3 #   Copyright (C) 2016 Jack Miller <jack@codezen.org>
4 #
5 #   This program is free software; you can redistribute it and/or modify
6 #   it under the terms of the GNU General Public License version 2 as 
7 #   published by the Free Software Foundation.
8
9 from canto_next.plugins import Plugin
10 from canto_next.encoding import locale_enc
11 from canto_next.hooks import on_hook
12
13 from .command import CommandHandler, register_commands, register_arg_types
14 from .taglist import TagList
15 from .input import InputBox
16 from .text import InfoBox
17 from .widecurse import wsize, set_redisplay_callback, set_getc, raw_readline
18 from .locks import sync_lock
19
20 from threading import Lock
21 import traceback
22 import readline
23 import logging
24 import curses
25 import time
26 import os
27
28 log = logging.getLogger("SCREEN")
29
30 # The Screen class handles the layout of multiple sub-windows on the main
31 # curses window. It's also the top-level gui object, so it handles calls to
32 # refresh the screen, get input, and curses related console commands, like
33 # "color".
34
35 # There are two types of windows that the Screen class handles. The first are
36 # normal windows (in self.tiles). These windows are all tiled in a single
37 # layout (determined by self.layout and self.fill_layout()) and rendered first.
38
39 # The other types are floats that are rendered on top of the window layout.
40 # These floats are all independent of each other.
41
42 # The Screen class is also in charge of honoring the window specific
43 # configuration options. Like window.{maxwidth,maxheight,float}.
44
45 class ScreenPlugin(Plugin):
46     pass
47
48 class Screen(CommandHandler):
49     def __init__(self, callbacks, types = [InputBox, TagList]):
50         CommandHandler.__init__(self)
51
52         self.plugin_class = ScreenPlugin
53         self.update_plugin_lookups()
54
55         self.callbacks = callbacks
56         self.layout = "default"
57
58         self.window_types = types
59
60         self.stdscr = curses.initscr()
61         if self.curses_setup() < 0:
62             return -1
63
64         self.pseudo_input_box = curses.newpad(1,1)
65
66         self.pseudo_input_box.keypad(1)
67         self.pseudo_input_box.nodelay(1)
68         self.input_lock = Lock()
69
70         set_redisplay_callback(self.readline_redisplay)
71         set_getc(self.readline_getc)
72
73         # See Python bug 2675, readline + curses
74         os.unsetenv('LINES')
75         os.unsetenv('COLUMNS')
76
77         self.floats = []
78         self.tiles = []
79         self.windows = []
80
81         self.subwindows()
82
83         args = {
84             "color_name" : ("[color name] Either a pair number (0-255, >8 ignored on 8 color terminals), a default fore/background (deffg, defbg), or an arbitrary name to be used in themes (unread, pending, etc.)", self.type_color_name),
85             "fg-color" : ("[fg-color] Foreground color", self.type_color),
86             "bg-color" : ("[bg-color] Background color (optional)\n\nNamed colors: white black red yellow green blue magenta pink\nNumeric colors: 1-256", self.type_color),
87             "style" : ("[style] Curses style (normal, bold, dim, reverse, standout, underline)", self.type_style),
88         }
89
90         cmds = {
91             "color": (self.cmd_color, ["color_name", "fg-color", "bg-color"],
92 """Change the color palette.
93
94 Most like you want to use this to change a color used in the theme. For example,
95
96    :color unread green
97
98 Will change the color of unread items to green, with the default background. The list of names used in the default theme are:
99
100     unread
101     read
102     selected
103     marked
104     pending
105     error
106     reader_quote
107     reader_link
108     reader_image_link
109     reader_italics
110     enum_hints
111
112 You can also change the defaults
113
114     :color deffg blue
115     :color defbg white
116
117 Which will be used anywhere a color pair doesn't have an explicit foreground/background.
118
119 Lastly you can change the color pairs themselves. This isn't recommended, they're initialized so each available color is available with the default background. If you change these pairs, the named colors above may not make any sense (i.e. green really turns on the color pair set aside for green, so if you change that pair to actually be yellow, don't expect this command to figure it out).
120
121     :color 1 white red
122
123 Arguments:"""
124
125 ),
126         "style": (self.cmd_style, ["color_name", "style"],
127 """Change the curses style of a named color.
128
129 For example,
130
131     :style selected underline
132
133 The names used in the default theme are:
134
135     unread
136     read
137     selected
138     marked
139     pending
140     error
141     reader_quote
142     reader_link
143     reader_image_link
144     reader_italics
145     enum_hints
146
147 Changing other colors (numeric pairs or deffg/defbg) will have no effect as these are separate from the built in curses color system.
148
149 Arguments:"""),
150         }
151
152         register_arg_types(self, args)
153         register_commands(self, cmds, "Theme")
154
155         on_hook("curses_opt_change", self.screen_opt_change)
156
157     # Wrap curses.curs_set in exception handler
158     # because we don't really care if it's displayed
159     # on terminals that don't like it.
160
161     def curs_set(self, n):
162         try:
163             curses.curs_set(n)
164         except:
165             pass
166
167     # Do initial curses setup. This should only be done on init, or after
168     # endwin() (i.e. resize).
169
170     def curses_setup(self):
171         self.curs_set(0)
172
173         try:
174             curses.cbreak()
175             curses.noecho()
176             curses.start_color()
177             curses.use_default_colors()
178             curses.typeahead(-1)
179             curses.halfdelay(5)
180         except Exception as e:
181             log.error("Curses setup failed: %s" % e.msg)
182             return -1
183
184         self.height, self.width = self.stdscr.getmaxyx()
185         self.height = int(self.height)
186         self.width = int(self.width)
187
188         color_conf = self.callbacks["get_opt"]("color")
189
190         for i in range(0, curses.COLOR_PAIRS):
191             if str(i) not in color_conf:
192                 continue
193
194             color = color_conf[str(i)]
195
196             if type(color) == int:
197                 fg = color
198                 bg = color_conf['defbg']
199             else:
200                 if 'fg' in color:
201                     fg = color['fg']
202                 else:
203                     fg = color_conf['deffg']
204
205                 if 'bg' in color:
206                     bg = color['bg']
207                 else:
208                     bg = color_conf['defbg']
209
210             try:
211                 curses.init_pair(i, fg, bg)
212                 log.debug("color pair %s : %s %s", i, fg, bg)
213             except:
214                 log.debug("color pair failed!: %d fg: %d bg: %d", 
215                         i + 1, fg, bg)
216         return 0
217
218     def screen_opt_change(self, conf):
219         # Require resize even to re-init curses and colors.
220         if "color" in conf:
221             self.callbacks["set_var"]("needs_resize", True)
222
223         for key in list(conf.keys()):
224             if type(conf[key]) == dict and "window" in conf[key]:
225                 self.callbacks["set_var"]("needs_resize", True)
226                 break
227
228     # _subw_size functions enforce the height and width of windows.
229     # It returns the minimum of:
230     #       - The maximum size (given by layout)
231     #       - The requested size (given by the class)
232     #       - The configured size (given by the config)
233
234     def _subw_size_height(self, ci, height):
235         window_conf = self.callbacks["get_opt"](ci.get_opt_name() + ".window")
236
237         if not window_conf["maxheight"]:
238             window_conf["maxheight"] = height
239         req_height = ci.get_height(height)
240
241         return min(height, window_conf["maxheight"], req_height)
242
243     def _subw_size_width(self, ci, width):
244         window_conf = self.callbacks["get_opt"](ci.get_opt_name() + ".window")
245
246         if not window_conf["maxwidth"]:
247             window_conf["maxwidth"] = width
248         req_width = ci.get_width(width)
249
250         return min(width, window_conf["maxwidth"], req_width)
251
252     # _subw_layout_size will return the total size of layout
253     # in either height or width where layout is a list of curses
254     # pads, or sublists of curses pads.
255
256     def _subw_layout_size(self, layout, dim):
257
258         # Grab index into pad.getmaxyx()
259         if dim == "width":
260             idx = 1
261         elif dim == "height":
262             idx = 0
263         else:
264             raise Exception("Unknown dim: %s" % dim)
265
266         sizes = []
267         for x in layout:
268             if hasattr(x, "__iter__"):
269                 sizes.append(self._subw_layout_size(x, dim))
270             else:
271                 sizes.append(x.pad.getmaxyx()[idx] - 1)
272
273         return max(sizes)
274
275     # Translate the layout into a set of curses pads given
276     # a set of coordinates relating to how they're mapped to the screen.
277
278     def _subw_init(self, ci, top, left, height, width):
279
280         # Height - 1 because start + height = line after bottom.
281
282         bottom = top + (height - 1)
283         right = left + (width - 1)
284
285         # lambda this up so that subwindows truly have no idea where on the
286         # screen they are, only their dimensions, but can still selectively
287         # refresh their portion of the screen.
288
289         refcb = lambda : self.refresh_callback(ci, top, left, bottom, right)
290
291         # Callback to allow windows to know if they're floating. This is
292         # important because floating windows are only rendered up to their
293         # last cursor position, despite being given a maximal window.
294
295         floatcb = lambda : ci in self.floats
296
297         # Use coordinates and dimensions to determine where borders
298         # are needed. This is independent of whether there are actually
299         # windows there.
300
301         # NOTE: These should only be honored if the window is non-floating.
302         # Floating windows are, by design, given a window the size of the
303         # entire screen, but only actually written lines are drawn.
304
305         window_conf = self.callbacks["get_opt"](ci.get_opt_name() + ".window")
306
307         if window_conf['border'] == "smart":
308             top_border = top != 0
309             bottom_border = bottom != (self.height - 1)
310             left_border = left != 0
311             right_border = right != (self.width - 1)
312
313             if ci in self.floats:
314                 if "top" in window_conf['align']:
315                     bottom_border = True
316                 if "bottom" in window_conf['align']:
317                     top_border = True
318
319         elif window_conf['border'] == "full":
320             top_border, bottom_border, left_border, right_border = (True,) * 4
321
322         elif window_conf['border'] == "none":
323             top_border, bottom_border, left_border, right_border = (False,) * 4
324
325         bordercb = lambda : (top_border, left_border, bottom_border, right_border)
326
327         # Height + 1 to account for the last curses pad line
328         # not being fully writable.
329
330         log.debug("h: %s w: %s", self.height, self.width)
331         log.debug("h: %s w: %s", height, width)
332         pad = curses.newpad(height + 1, width)
333
334         # Pass on callbacks we were given from CantoCursesGui
335         # plus our own.
336
337         callbacks = self.callbacks.copy()
338         callbacks["refresh"] = refcb
339         callbacks["border"] = bordercb
340         callbacks["floating"] = floatcb
341         callbacks["input"] = self.input_callback
342         callbacks["die"] = self.die_callback
343         callbacks["pause_interface" ] = self.pause_interface_callback
344         callbacks["unpause_interface"] = self.unpause_interface_callback
345         callbacks["add_window"] = self.add_window_callback
346
347         ci.init(pad, callbacks)
348
349     # Layout some windows into the given space, stacking with
350     # orientation horizontally or vertically.
351
352     def _subw(self, layout, top, left, height, width, orientation):
353         immediates = []
354         cmplx = []
355         sizes = [0] * len(layout)
356
357         # Separate windows in to two categories:
358         # immediates that are defined as base classes and
359         # cmplx which are lists for further processing (iterables)
360
361         for i, unit in enumerate(layout):
362             if hasattr(unit, "__iter__"):
363                 cmplx.append((i, unit))
364             else:
365                 immediates.append((i,unit))
366
367         # Units are the number of windows we'll have
368         # to split the area with.
369
370         units = len(layout)
371
372         # Used, the amounts of space already used.
373         used = 0
374
375         for i, unit in immediates:
376             # Get the size of the window from the class.
377             # Each class is given, as a maximum, the largest
378             # possible slice we can *guarantee*.
379
380             if orientation == "horizontal":
381                 size = self._subw_size_width(unit, int((width - used) / units))
382             else:
383                 size = self._subw_size_height(unit, int((height - used) / units))
384
385             used += size
386
387             sizes[i] = size
388
389             # Subtract so that the next run only divides
390             # the remaining space by the number of units
391             # that don't have space allocated.
392
393             units -= 1
394
395         # All of the immediates have been allocated for.
396         # So now only the cmplxs are vying for space.
397
398         units = len(cmplx)
399
400         for i, unit in cmplx:
401             offset = sum(sizes[0:i])
402
403             # Recursives call this function, alternating
404             # the orientation, for the space we can guarantee
405             # this set of windows.
406
407             if orientation == "horizontal":
408                 available = int((width - used) / units)
409                 r = self._subw(unit, top, left + offset,\
410                         height, available, "vertical")
411                 sizes[i] = self._subw_layout_size(r, "width")
412             else:
413                 available = int((height - used) / units)
414                 r = self._subw(unit, top + offset, left,\
415                         available, width, "horizontal")
416                 sizes[i] = self._subw_layout_size(r, "height")
417
418             used += sizes[i]
419             units -= 1
420
421         # Now that we know the actual sizes (and thus locations) of
422         # the windows, we actually setup the immediates.
423
424         for i, ci in immediates:
425             offset = sum(sizes[0:i])
426             if orientation == "horizontal":
427                 self._subw_init(ci, top, left + offset,
428                         height, sizes[i])
429             else:
430                 self._subw_init(ci, top + offset, left,
431                         sizes[i], width)
432         return layout
433
434     # The fill_layout() function takes a list of active windows and generates a
435     # list based layout. The depth of a window in the list determines its
436     # orientation.
437     #
438     #   Example return: [ Window1, Window2 ]
439     #       - Window1 on top of Window2, each taking half of the vertical space.
440     #
441     #   Example return: [ [ Window1, Window2 ], Window 3 ]
442     #       - Window1 left of Window2 each taking half of the horizontal space,
443     #           and whatever vertical space left by Window3, because Window3 is
444     #           shallower than 1 or 2, so it's size is evaluated first and the
445     #           remaining given to the [ Window1, Window2 ] horizontal layout.
446     #
447     #   Example return: [ [ [ [ Window1 ] ], Window2 ], Window3 ]
448     #       - Same as above, except because Window1 is deeper than Window2 now,
449     #           Window2's size is evaluated first and Window1 is given all of 
450     #           the remaining space.
451     #
452     #   NOTE: Floating windows are not handled in the layout, this is solely for
453     #   the tiling bottom layer of windows.
454
455     def fill_layout(self, layout, windows):
456         inputs = [ w for w in windows if w.is_input() ]
457         if inputs:
458             self.input_box = inputs[0]
459         else:
460             self.input_box = None
461
462         # Simple stacking, even distribution between all windows.
463         if layout == "hstack":
464             return windows
465         elif layout == "vstack":
466             return [ windows ]
467         else:
468             aligns = { "top" : [], "bottom" : [], "left" : [], "right" : [],
469                             "neutral" : [] }
470
471             # Separate windows by alignment.
472             for w in windows:
473                 align = self.callbacks["get_opt"]\
474                         (w.get_opt_name() + ".window.align")
475
476                 # Move taglist deeper so that it absorbs any
477                 # extra space left in the rest of the layout.
478
479                 if w.get_opt_name() == "taglist":
480                     aligns[align].append([[w]])
481                 else:
482                     aligns[align].append(w)
483
484             horizontal = aligns["left"] + aligns["neutral"] + aligns["right"]
485             return aligns["top"] + [horizontal] + aligns["bottom"]
486
487     # subwindows() is the top level window generator. It handles both the bottom
488     # level tiled window layout as well as the floats.
489
490     def subwindows(self):
491
492         # Cleanup any window objects that will be destroyed.
493         for w in self.windows:
494             log.debug("die to %s", w)
495             w.die()
496
497         self.floats = []
498         self.tiles = []
499         self.windows = []
500
501         # Instantiate new windows, separating them into
502         # floating and tiling windows.
503
504         for wt in self.window_types:
505             w = wt()
506             optname = w.get_opt_name()
507             flt = self.callbacks["get_opt"](optname + ".window.float")
508             if flt:
509                 self.floats.append(w)
510             else:
511                 self.tiles.append(w)
512             self.windows.append(w)
513
514         # Focused window will no longer exist.
515         self.focused = None
516
517         # Init tiled windows.
518         l = self.fill_layout(self.layout, self.tiles)
519         self._subw(l, 0, 0, self.height, self.width, "vertical")
520
521         # Init floating windows.
522         for f in self.floats: 
523             align = self.callbacks["get_opt"]\
524                     (f.get_opt_name() + ".window.align")
525             height = self._subw_size_height(f, self.height)
526             width = self._subw_size_width(f, self.width)
527
528             top = 0
529             if align.startswith("bottom"):
530                 top = self.height - height
531
532             left = 0
533             if align.endswith("right"):
534                 left = self.width - width
535
536             self._subw_init(f, top, left, height, width)
537
538         # Default to giving first window focus.
539         self._focus_abs(0)
540
541     def refresh_callback(self, c, t, l, b, r):
542         if c in self.floats:
543             b = min(b, t + c.pad.getyx()[0])
544         c.pad.noutrefresh(0, 0, t, l, b, r)
545
546     def input_callback(self, prompt, completions=True):
547         # Setup subedit
548         self.curs_set(1)
549
550         self.callbacks["set_var"]("input_do_completions", completions)
551         self.callbacks["set_var"]("input_prompt", prompt)
552
553         self.input_box.reset()
554         self.input_box.refresh()
555         curses.doupdate()
556
557         self.pseudo_input_box.keypad(0)
558
559         r = raw_readline()
560         if not r:
561             r = ""
562
563         self.pseudo_input_box.keypad(1)
564
565         # Only add history for commands, not other prompts
566         if completions:
567             readline.add_history(r)
568
569         self.callbacks["set_var"]("input_prompt", "")
570         self.input_box.reset()
571         self.input_box.refresh()
572         curses.doupdate()
573
574         self.curs_set(0)
575         return r
576
577     def die_callback(self, window):
578         sync_lock.acquire_write()
579
580         # Remove window from both window_types and the general window list
581
582         try:
583             idx = self.windows.index(window)
584         except:
585             pass
586         else:
587             # Call the window's die function
588             window.die()
589
590             del self.windows[idx]
591             del self.window_types[idx]
592
593             # Regenerate layout with remaining windows.
594             self.subwindows()
595
596             self.refresh()
597
598             # Force a doupdate because refresh doesn't, but we have possibly
599             # uncovered part of the screen that isn't handled by any other window.
600
601             curses.doupdate()
602
603         sync_lock.release_write()
604
605     # The pause interface callback keeps the interface from updating. This is
606     # useful if we have to temporarily surrender the screen (i.e. text browser).
607
608     # NOTE: This does not affect signals so even while "paused", c-c continues
609     # to take things like SIGWINCH which will be interpreted on wakeup.
610
611     def pause_interface_callback(self):
612         log.debug("Pausing interface.")
613         sync_lock.acquire_write()
614
615         curses.raw()
616
617         self.input_lock.acquire()
618
619         curses.endwin()
620
621     def unpause_interface_callback(self):
622         log.debug("Unpausing interface.")
623         self.input_lock.release()
624
625         # All of our window information could be stale.
626         self.resize()
627         sync_lock.release_write()
628
629     def add_window_callback(self, cls):
630         sync_lock.acquire_write()
631
632         self.window_types.append(cls)
633
634         self.subwindows()
635
636         # Focus new window
637         self._focus_abs(0)
638
639         self.refresh()
640         self.redraw()
641
642         sync_lock.release_write()
643
644     def _readline_redisplay(self):
645         self.input_box.refresh()
646         curses.doupdate()
647
648     def _readline_getc(self):
649         do_comp = self.callbacks["get_var"]("input_do_completions")
650
651         # Don't flush, because readline loses keys.
652         r = self.get_key(False)
653
654         if r == curses.KEY_BACKSPACE:
655             r = ord("\b")
656         elif r == curses.KEY_RESIZE:
657             return
658         elif chr(r) == '\t' and do_comp:
659             self.input_box.rotate_completions()
660             return
661
662         comp = self.input_box.break_completion()
663         if comp:
664             log.debug("inserting: %s", comp)
665             readline.insert_text(comp)
666
667         log.debug("KEY: %s", r)
668         return r
669
670     def _exception_wrap(self, fn, *args):
671         r = None
672         try:
673             r = fn(*args)
674         except:
675             log.error("".join(traceback.format_exc()))
676         return r
677
678     def readline_redisplay(self, *args):
679         return self._exception_wrap(self._readline_redisplay, *args)
680     def readline_complete(self, *args):
681         return self._exception_wrap(self._readline_complete, *args)
682     def readline_display_matches(self, *args):
683         return self._exception_wrap(self._readline_display_matches, *args)
684     def readline_getc(self, *args):
685         return self._exception_wrap(self._readline_getc, *args)
686
687     # Refresh operates in order, which doesn't matter for top level tiled
688     # windows, but this ensures that floats are ordered such that the last
689     # floating window is rendered on top of all others.
690
691     def refresh(self):
692         for c in self.tiles + self.floats:
693             c.refresh()
694
695     def redraw(self):
696         for c in self.tiles + self.floats:
697             c.redraw()
698         curses.doupdate()
699
700     # Typical curses resize, endwin and re-setup.
701     def resize(self):
702         try:
703             curses.endwin()
704         except:
705             pass
706
707         self.pseudo_input_box.keypad(1)
708         self.pseudo_input_box.nodelay(1)
709         self.stdscr.refresh()
710
711         self.curses_setup()
712         self.subwindows()
713         self.refresh()
714         self.redraw()
715
716     def _focus_abs(self, idx):
717         focus_order = self.tiles + self.floats
718         focus_order.reverse()
719         l = len(focus_order)
720
721         if idx < 0:
722             idx = -1 * (idx % l)
723         else:
724             idx %= l
725
726         self._focus(focus_order[idx])
727
728     def _focus(self, win):
729         self.focused = win
730         log.debug("Focusing window (%s)", self.focused)
731
732     def type_color(self):
733         colors = {
734             'white' : curses.COLOR_WHITE,
735             'black' : curses.COLOR_BLACK,
736             'red' : curses.COLOR_RED,
737             'blue' : curses.COLOR_BLUE,
738             'green' : curses.COLOR_GREEN,
739             'yellow' : curses.COLOR_YELLOW,
740             'cyan' : curses.COLOR_CYAN,
741             'magenta' : curses.COLOR_MAGENTA,
742             'pink' : curses.COLOR_MAGENTA,
743             "default" : -1,
744         }
745         def c(x):
746             if x == '':
747                 return (True, -1)
748
749             if x in colors:
750                 return (True, colors[x])
751             try:
752                 x = int(x)
753                 if x in list(range(-1, 256)):
754                     return (True, x)
755                 return (False, None)
756             except:
757                 return (False, None)
758         return (list(colors.keys()), c)
759
760     def type_color_name(self):
761         color_conf = self.callbacks["get_opt"]("color")
762
763         completions = []
764         for key in color_conf:
765             try:
766                 pair = int(key)
767                 continue
768             except:
769                 completions.append(key)
770
771         return (completions, lambda x : (True, x))
772
773     def cmd_color(self, idx, fg, bg):
774         conf = self.callbacks["get_conf"]()
775
776         log.debug(":COLOR IDX: %s", idx)
777         if idx in ['deffg', 'defbg']:
778             conf["color"][idx] = fg  # Ignore second color pair
779         else:
780             try:
781                 pair = int(idx)
782                 color = {}
783                 if fg != -1:
784                     color['fg'] = fg
785                 if bg != -1:
786                     color['bg'] = bg
787                 conf["color"][idx] = color
788             except:
789                 conf["color"][idx] = fg + 1 # +1 since the color pairs are offset
790
791         log.debug("color %s set: %s", idx, conf["color"][idx])
792
793         self.callbacks["set_conf"](conf)
794
795         # Cause curses to be re-init'd
796         self.resize()
797
798     def type_style(self):
799         styles = [ "normal", "bold", "dim", "reverse", "standout", "underline" ]
800         return (styles, lambda x: (x in styles, x))
801
802     def cmd_style(self, name, style):
803         conf = self.callbacks["get_opt"]("style")
804
805         styles = {
806             "bold" : "%B",
807             "normal" : "",
808             "dim" : "%D",
809             "standout" : "%S",
810             "reverse" : "%R",
811             "underline" : "%U",
812         }
813
814         conf[name] = styles[style]
815
816         log.debug("style %s set: %s", name, style)
817
818         self.callbacks["set_opt"]("style", conf)
819
820         self.resize()
821
822     def get_focus_list(self):
823
824         # self.focused might be None, if this is getting called during a
825         # subwindows() call.
826
827         if self.focused:
828             return [ self, self.focused ]
829         return [ self ]
830
831     def get_key(self, flush=True):
832         while True:
833             self.input_lock.acquire()
834             try:
835                 r = self.pseudo_input_box.get_wch()
836             except Exception as e:
837                 r = self.pseudo_input_box.getch()
838             self.input_lock.release()
839             if r != -1:
840                 break
841
842         if flush and r != curses.KEY_RESIZE:
843             curses.flushinp()
844
845         if type(r) == str:
846             r = ord(r)
847         return r
848
849     def exit(self):
850         curses.endwin()
851
852     def get_opt_name(self):
853         return "screen"