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

110 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 

19import sys 

20from contextlib import contextmanager 

21 

22import urwid 

23 

24from hods.tui.base.view import ViewPlaceholder 

25 

26logger = logging.getLogger(__name__) 

27 

28 

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' 

34 

35 

36class FocusKeyFrame(urwid.Frame): 

37 """A frame widget with focusable header and footer.""" 

38 

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

40 """Initialize frame widget. 

41 

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) 

52 

53 def keypress(self, size, key): 

54 """Key event callback. 

55 

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 

67 

68 

69class BaseApp: 

70 """Base urwid application.""" 

71 

72 loop_class = urwid.MainLoop 

73 

74 exit_keys = ('q', 'Q', 'f10', 'esc') 

75 

76 menu_keys = ('f2',) 

77 

78 pause_keys = ('f12',) 

79 

80 def __init__(self, enable_unicode=None): 

81 """Initialize app. 

82 

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 

90 

91 def unhandled_input(self, key): 

92 """Key event callback. 

93 

94 This is called when no other widget handled the key. 

95 

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) 

104 

105 def end(self): 

106 """Stop the main loop and exit the application.""" 

107 raise urwid.ExitMainLoop() 

108 

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

113 

114 def init(self): 

115 """Initialize the main loop object.""" 

116 self.loop = self.get_loop() 

117 

118 def run(self): 

119 """Initialize and start the main loop.""" 

120 self.init() 

121 self.loop.run() 

122 return self.exitcode 

123 

124 def get_loop_class(self): 

125 """Return the class to built the main loop.""" 

126 return self.loop_class 

127 

128 def get_widget(self): 

129 """Return the main widget to display.""" 

130 return NotImplemented 

131 

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 ) 

141 

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

148 

149 try: 

150 yield 

151 finally: 

152 if started: 

153 self.loop.start() 

154 self.loop.screen_size = None 

155 self.loop.draw_screen() 

156 

157 def get_screen(self): 

158 """Return the screen object to display the app.""" 

159 from urwid.raw_display import Screen 

160 return Screen() 

161 

162 def get_palette(self): 

163 """Return the main color attribute palette.""" 

164 return [] 

165 

166 

167class ViewApp(BaseApp): 

168 """A base app to switch between views.""" 

169 

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

171 """Initialize app. 

172 

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) 

179 

180 # set when showing a view without a parent 

181 self.root = self.view 

182 

183 @property 

184 def view(self): 

185 """Get the currently displayed view.""" 

186 return self.placeholder.original_widget 

187 

188 @view.setter 

189 def view(self, value): 

190 """Set the displayed view.""" 

191 self.placeholder.original_widget = value 

192 

193 def show(self, new): 

194 """Replace the current view widget with the given one. 

195 

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

207 

208 def refresh(self): 

209 """Refresh widgets and their content.""" 

210 self.view.refresh() 

211 

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 

219 

220 def alarm(loop, user_data=None): 

221 call(*args, **kwargs) 

222 self.loop.set_alarm_in(seconds, alarm) 

223 

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)