Coverage for src/hods/tui/base/app.py: 100.00%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""hods - home directory synchronization.
3Copyright (C) 2016-2020 Mathias Stelzer <knoppo@rolln.de>
5hods is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
10hods is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
15You should have received a copy of the GNU General Public License
16along with this program. If not, see <http://www.gnu.org/licenses/>.
17"""
18import logging
19import sys
20from contextlib import contextmanager
22import urwid
24from hods.tui.base.view import ViewPlaceholder
26logger = logging.getLogger(__name__)
29def detect_unicode_support():
30 """Return a bool whether the current terminal supports unicode."""
31 logger.debug(sys.stdout.isatty())
32 logger.debug(sys.stdout.encoding.lower())
33 return sys.stdout.isatty() and sys.stdout.encoding.lower() == 'utf-8'
36class FocusKeyFrame(urwid.Frame):
37 """A frame widget with focusable header and footer."""
39 def __init__(self, *args, **kwargs):
40 """Initialize frame widget.
42 :param args: Pass to `urwid.frame`.
43 :param header_focus_keys: iterable - list of keys to focus the header
44 when pressed.
45 :param footer_focus_keys: iterable - list of keys to focus the footer
46 when pressed.
47 :param kwargs: Pass to `urwid.frame`.
48 """
49 self.header_focus_keys = kwargs.pop('header_focus_keys', ())
50 self.footer_focus_keys = kwargs.pop('footer_focus_keys', ())
51 super().__init__(*args, **kwargs)
53 def keypress(self, size, key):
54 """Key event callback.
56 Check if the key is in any `_focus_keys` and change focus.
57 """
58 logger.debug('FocusKeyFrame %s: %s.keypress(%s, %s)', self, self.__class__.__name__, repr(size), repr(key))
59 key = super().keypress(size, key)
60 if key in self.header_focus_keys:
61 if self.focus_position == 'header':
62 self.focus_position = 'body'
63 else:
64 self.focus_position = 'header'
65 return
66 return key
69class BaseApp:
70 """Base urwid application."""
72 loop_class = urwid.MainLoop
74 exit_keys = ('q', 'Q', 'f10', 'esc')
76 menu_keys = ('f2',)
78 pause_keys = ('f12',)
80 def __init__(self, enable_unicode=None):
81 """Initialize app.
83 :param enable_unicode: `bool` - Allow unicode characters.
84 """
85 if enable_unicode is None:
86 enable_unicode = detect_unicode_support()
87 self.enable_unicode = enable_unicode
88 self.loop = None
89 self.exitcode = 0
91 def unhandled_input(self, key):
92 """Key event callback.
94 This is called when no other widget handled the key.
96 :param key: `str` - The pressed key.
97 """
98 if key in self.pause_keys:
99 with self.pause_loop():
100 return
101 if key in self.exit_keys:
102 self.end()
103 logger.debug('unhandled input: %s', key)
105 def end(self):
106 """Stop the main loop and exit the application."""
107 raise urwid.ExitMainLoop()
109 def end_error(self, code=1):
110 """Stop the main loop and exit the application with an error."""
111 self.exitcode = code
112 self.end()
114 def init(self):
115 """Initialize the main loop object."""
116 self.loop = self.get_loop()
118 def run(self):
119 """Initialize and start the main loop."""
120 self.init()
121 self.loop.run()
122 return self.exitcode
124 def get_loop_class(self):
125 """Return the class to built the main loop."""
126 return self.loop_class
128 def get_widget(self):
129 """Return the main widget to display."""
130 return NotImplemented
132 def get_loop(self):
133 """Built and return the main loop object."""
134 cls = self.get_loop_class()
135 return cls(
136 self.get_widget(),
137 screen=self.get_screen(),
138 palette=self.get_palette(),
139 unhandled_input=self.unhandled_input,
140 )
142 @contextmanager
143 def pause_loop(self):
144 """Context manager to hide and show the TUI."""
145 started = hasattr(self.loop, 'idle_handle')
146 if started:
147 self.loop.stop()
149 try:
150 yield
151 finally:
152 if started:
153 self.loop.start()
154 self.loop.screen_size = None
155 self.loop.draw_screen()
157 def get_screen(self):
158 """Return the screen object to display the app."""
159 from urwid.raw_display import Screen
160 return Screen()
162 def get_palette(self):
163 """Return the main color attribute palette."""
164 return []
167class ViewApp(BaseApp):
168 """A base app to switch between views."""
170 def __init__(self, *args, **kwargs):
171 """Initialize app.
173 :param args: Pass to `hods.tui.base.BaseApp`
174 :param kwargs: Pass to `hods.tui.base.BaseApp`
175 """
176 self.defer_multiplier = kwargs.pop('defer_multiplier', 1)
177 super().__init__(*args, **kwargs)
178 self.placeholder = ViewPlaceholder(self)
180 # set when showing a view without a parent
181 self.root = self.view
183 @property
184 def view(self):
185 """Get the currently displayed view."""
186 return self.placeholder.original_widget
188 @view.setter
189 def view(self, value):
190 """Set the displayed view."""
191 self.placeholder.original_widget = value
193 def show(self, new):
194 """Replace the current view widget with the given one.
196 Call *on_close* on the current widget before switching and
197 *on_open* on the new one after switching to it.
198 """
199 view = self.view
200 if new.parent is None:
201 self.root = new
202 while view is not new.parent:
203 view.on_close()
204 view = view.parent
205 self.view = new
206 self.view.on_open()
208 def refresh(self):
209 """Refresh widgets and their content."""
210 self.view.refresh()
212 def defer(self, call, *args, seconds=0.1, **kwargs):
213 """Queue given function and arguments in given seconds."""
214 if seconds < 0.1:
215 raise ValueError('seconds argument must be greater than or equal to 0.1') # pragma: no cover
216 if self.defer_multiplier < 1:
217 raise ValueError('{}.defer_multiplier must be greater than or equal 1') # pragma: no cover
218 seconds *= self.defer_multiplier
220 def alarm(loop, user_data=None):
221 call(*args, **kwargs)
222 self.loop.set_alarm_in(seconds, alarm)
224 def check_defer(self, call, *args, defer=True, **kwargs):
225 """Call given callback or defer it."""
226 if callable(call):
227 if defer:
228 self.defer(call, *args, **kwargs)
229 else:
230 call(*args, **kwargs)