Coverage for src/hods/tui/base/view.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
20import urwid
22logger = logging.getLogger(__name__)
25class ViewPlaceholder(urwid.WidgetPlaceholder):
26 """A widget placeholder to switch between `View` instances."""
28 def __init__(self, app):
29 """Initialize placeholder.
31 :param app: `hods.tui.base.app.ViewApp`
32 :param original_widget: optional `urwid.Widget` to show when
33 no view is loaded
34 """
35 self.app = app
36 super().__init__(BlankView(app))
38 def keypress(self, size, key):
39 """Callback to close the window by a click on the background."""
40 logger.debug('ViewPlaceholder %s: %s.keypress(%s, %s)', self, self.__class__.__name__, repr(size), repr(key))
42 key = super().keypress(size, key)
44 # TODO: click is not propagated to this!? # noqa
45 if isinstance(key, tuple): # pragma: no cover
46 # close on click in the background
47 action, _, x, y = key
48 if action == 'mouse press':
49 from hods.tui.base import Window
50 if isinstance(self.app.view, Window):
51 self.app.view.close()
52 logger.debug('KEY %s', key)
53 return
55 return key
57 def selectable(self):
58 """Return ``True`` to make this widget selectable."""
59 return True
62class View:
63 """Base view to used with `hods.__main__.TUI.show`.
65 Use this as a mixin with a box widget to make it a closable fullscreen window widget.
66 """
68 def __init__(self, app, *args, **kwargs):
69 """Initialize view.
71 #:param app: `hods.__main__.TUI` instance
72 :param args: Passed to parent if used as mixin.
73 :param on_close: Optional function to call when terminating the view.
74 :param kwargs: Passed to parent if used as mixin.
75 """
76 self.app = app
77 self.parent = kwargs.pop('parent', None)
78 self._on_close = kwargs.pop('on_close', None)
79 self._defer_close = kwargs.pop('defer_close', True)
80 self._refresh_on_open = kwargs.pop('refresh_on_open', True)
81 super().__init__(*args, **kwargs)
83 def show(self):
84 """Open the window in its app."""
85 self.app.show(self)
87 def close(self):
88 """Close the window, switch back to the parent."""
89 self.app.show(self.parent)
91 def on_open(self):
92 """Called by the app after switching to this widget."""
93 if self._refresh_on_open: # pragma: no branch
94 self.app.refresh()
96 def on_close(self):
97 """Called by the app before switching to another widget."""
98 self.app.check_defer(self._on_close, defer=self._defer_close)
100 def refresh(self):
101 """Screen refresh callback."""
102 pass
105class BlankView(View, urwid.SolidFill):
106 """A view filled with a single char."""
108 def keypress(self, size, key): # pragma: no cover
109 """Callback logging the key when it reaches the blank screen."""
110 logger.debug('BlankView %s: %s.keypress(%s, %s)', self, self.__class__.__name__, repr(size), repr(key))
111 return key
114class Window(View, urwid.Overlay):
115 """A bordered widget on top of another."""
117 close_on_escape = True
118 close_on_enter = False
120 def __init__(self, app, **kwargs):
121 """Initialize the widget, its decorations and the *body*.
123 :param app: `hods.app.App` instance
124 :param kwargs: passed to `urwid.Overlay`
125 """
126 self.app = app
127 body = self.get_body()
128 title = kwargs.pop('title', '')
129 attr = kwargs.pop('attr', 'frame')
131 padding = kwargs.pop('padding', 1)
132 if padding:
133 body = urwid.Padding(body, left=padding, right=padding)
135 if app.enable_unicode:
136 linebox = urwid.LineBox(body, title=title) # pragma: no cover
137 else:
138 linebox = urwid.LineBox(
139 body,
140 title=title,
141 tlcorner='|', tline='-', trcorner='|',
142 lline='|', rline='|',
143 blcorner='|', bline='-', brcorner='|',
144 )
145 self.linebox = linebox
146 self.linebox_map = urwid.AttrMap(self.linebox, attr)
148 kwargs.setdefault('parent', app.root)
149 kwargs.setdefault('align', urwid.CENTER)
150 kwargs.setdefault('width', ('relative', 90))
151 kwargs.setdefault('valign', urwid.MIDDLE)
152 kwargs.setdefault('height', ('relative', 90))
153 super().__init__(app, self.linebox_map, kwargs['parent'], **kwargs)
155 def get_body(self):
156 """Overwrite and return the main widget."""
157 raise NotImplementedError
159 def keypress(self, size, key):
160 """Key was pressed.
162 Close the window if the key is *escape* and
163 `close_on_escape` is True.
164 """
165 logger.debug('Window %s: %s.keypress(%s, %s)', self, self.__class__.__name__, repr(size), repr(key))
167 key = super().keypress(size, key)
168 if key == 'esc' and self.close_on_escape:
169 self.close()
170 return
171 if key == 'enter' and self.close_on_enter:
172 self.close()
173 return
175 return key
178class ListBoxWindow(Window):
179 """Window containing a `urwid.ListBox`."""
181 def get_body(self):
182 """Return the ListBox widget as body."""
183 return self.get_listbox()
185 def get_listbox(self):
186 """Return the ListBox widget with an empty walker."""
187 self._walker = urwid.SimpleListWalker([])
188 self._listbox = urwid.ListBox(self._walker)
189 return self._listbox
191 def refresh(self):
192 """Reset walker items."""
193 self._walker[:] = list(self.get_items())
195 def get_items(self):
196 """Overwrite and return a list of items or yield them."""
197 return []
200class TextWindow(ListBoxWindow):
201 """Window to display a multiline string."""
203 close_on_enter = True
205 def __init__(self, app, text, center=False, **kwargs):
206 """Initialize window."""
207 lines = text.splitlines()
208 lines_len = len(lines)
210 if lines_len < 5:
211 kwargs.setdefault('height', lines_len + 2)
213 title = kwargs.get('title', '')
214 max_line_len = max(map(len, lines))
215 width = max([len(title) + 4, max_line_len]) + 4
217 kwargs.setdefault('width', width)
218 super().__init__(app, **kwargs)
219 align = urwid.CENTER if center else urwid.LEFT
220 self.line_texts = [
221 urwid.Text(line, align=align) for line in lines
222 ]
224 def get_items(self):
225 """Yield a text widget for each line."""
226 yield from self.line_texts
229class SuccessWindow(TextWindow):
230 """Window to display a success message."""
232 def __init__(self, *args, **kwargs):
233 """Initialize the window with success-style decoration.
235 :param args: Passed to `TextWindow`
236 :param kwargs: Passed to `TextWindow`
237 """
238 kwargs.setdefault('title', 'Success')
239 kwargs.setdefault('attr', 'success window')
240 kwargs.setdefault('center', True)
241 super().__init__(*args, **kwargs)
244class WarningWindow(TextWindow):
245 """Window to display a warning message."""
247 def __init__(self, *args, **kwargs):
248 """Initialize the window with warning-style decoration.
250 :param args: Passed to `TextWindow`
251 :param kwargs: Passed to `TextWindow`
252 """
253 kwargs.setdefault('title', 'Warning')
254 kwargs.setdefault('attr', 'warning window')
255 kwargs.setdefault('center', True)
256 super().__init__(*args, **kwargs)
259class ErrorWindow(TextWindow):
260 """Window to display an error."""
262 def __init__(self, *args, **kwargs):
263 """Initialize the window with error-style decoration.
265 :param args: Passed to `TextWindow`
266 :param kwargs: Passed to `TextWindow`
267 """
268 kwargs.setdefault('title', 'Error')
269 kwargs.setdefault('attr', 'error window')
270 kwargs.setdefault('center', True)
271 super().__init__(*args, **kwargs)
274class ProcessErrorWindow(ErrorWindow):
275 """A window to display a `subprocess.CalledProcessError`."""
277 def __init__(self, app, exception, message='', name='Command', **kwargs):
278 """Initialize window."""
279 if message: # pragma: no branch
280 message += '\n'
281 message += '{} returned non-zero exit status {}'.format(name, exception.returncode)
283 stdout = exception.stdout
284 if stdout:
285 if isinstance(stdout, bytes): # pragma: no cover
286 stdout = stdout.decode()
287 message += ':\n{}'.format(stdout)
289 kwargs.setdefault('title', 'Sub-Process Error')
290 super().__init__(app, message, center=False, **kwargs)