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

120 statements  

1"""hods - home directory synchronization. 

2 

3Copyright (C) 2016-2020 Mathias Stelzer <knoppo@rolln.de> 

4 

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. 

9 

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. 

14 

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 

19 

20import urwid 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25class ViewPlaceholder(urwid.WidgetPlaceholder): 

26 """A widget placeholder to switch between `View` instances.""" 

27 

28 def __init__(self, app): 

29 """Initialize placeholder. 

30 

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)) 

37 

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)) 

41 

42 key = super().keypress(size, key) 

43 

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 

54 

55 return key 

56 

57 def selectable(self): 

58 """Return ``True`` to make this widget selectable.""" 

59 return True 

60 

61 

62class View: 

63 """Base view to used with `hods.__main__.TUI.show`. 

64 

65 Use this as a mixin with a box widget to make it a closable fullscreen window widget. 

66 """ 

67 

68 def __init__(self, app, *args, **kwargs): 

69 """Initialize view. 

70 

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) 

82 

83 def show(self): 

84 """Open the window in its app.""" 

85 self.app.show(self) 

86 

87 def close(self): 

88 """Close the window, switch back to the parent.""" 

89 self.app.show(self.parent) 

90 

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() 

95 

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) 

99 

100 def refresh(self): 

101 """Screen refresh callback.""" 

102 pass 

103 

104 

105class BlankView(View, urwid.SolidFill): 

106 """A view filled with a single char.""" 

107 

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 

112 

113 

114class Window(View, urwid.Overlay): 

115 """A bordered widget on top of another.""" 

116 

117 close_on_escape = True 

118 close_on_enter = False 

119 

120 def __init__(self, app, **kwargs): 

121 """Initialize the widget, its decorations and the *body*. 

122 

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') 

130 

131 padding = kwargs.pop('padding', 1) 

132 if padding: 

133 body = urwid.Padding(body, left=padding, right=padding) 

134 

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) 

147 

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) 

154 

155 def get_body(self): 

156 """Overwrite and return the main widget.""" 

157 raise NotImplementedError 

158 

159 def keypress(self, size, key): 

160 """Key was pressed. 

161 

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)) 

166 

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 

174 

175 return key 

176 

177 

178class ListBoxWindow(Window): 

179 """Window containing a `urwid.ListBox`.""" 

180 

181 def get_body(self): 

182 """Return the ListBox widget as body.""" 

183 return self.get_listbox() 

184 

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 

190 

191 def refresh(self): 

192 """Reset walker items.""" 

193 self._walker[:] = list(self.get_items()) 

194 

195 def get_items(self): 

196 """Overwrite and return a list of items or yield them.""" 

197 return [] 

198 

199 

200class TextWindow(ListBoxWindow): 

201 """Window to display a multiline string.""" 

202 

203 close_on_enter = True 

204 

205 def __init__(self, app, text, center=False, **kwargs): 

206 """Initialize window.""" 

207 lines = text.splitlines() 

208 lines_len = len(lines) 

209 

210 if lines_len < 5: 

211 kwargs.setdefault('height', lines_len + 2) 

212 

213 title = kwargs.get('title', '') 

214 max_line_len = max(map(len, lines)) 

215 width = max([len(title) + 4, max_line_len]) + 4 

216 

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 ] 

223 

224 def get_items(self): 

225 """Yield a text widget for each line.""" 

226 yield from self.line_texts 

227 

228 

229class SuccessWindow(TextWindow): 

230 """Window to display a success message.""" 

231 

232 def __init__(self, *args, **kwargs): 

233 """Initialize the window with success-style decoration. 

234 

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) 

242 

243 

244class WarningWindow(TextWindow): 

245 """Window to display a warning message.""" 

246 

247 def __init__(self, *args, **kwargs): 

248 """Initialize the window with warning-style decoration. 

249 

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) 

257 

258 

259class ErrorWindow(TextWindow): 

260 """Window to display an error.""" 

261 

262 def __init__(self, *args, **kwargs): 

263 """Initialize the window with error-style decoration. 

264 

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) 

272 

273 

274class ProcessErrorWindow(ErrorWindow): 

275 """A window to display a `subprocess.CalledProcessError`.""" 

276 

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) 

282 

283 stdout = exception.stdout 

284 if stdout: 

285 if isinstance(stdout, bytes): # pragma: no cover 

286 stdout = stdout.decode() 

287 message += ':\n{}'.format(stdout) 

288 

289 kwargs.setdefault('title', 'Sub-Process Error') 

290 super().__init__(app, message, center=False, **kwargs)