forked from saulpw/visidata
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmainloop.py
307 lines (228 loc) · 8.44 KB
/
mainloop.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import builtins
import contextlib
import os
import curses
import signal
import threading
import time
from visidata import vd, VisiData, colors, ESC, options, BaseSheet, AttrDict
__all__ = ['ReturnValue', 'run']
vd.curses_timeout = 100 # curses timeout in ms
vd.timeouts_before_idle = 10
vd.min_draw_ms = 100 # draw_all at least this often, even if keystrokes are pending
vd._lastDrawTime = 0 # last time drawn (from time.time())
class ReturnValue(BaseException):
'raise ReturnValue(ret) to exit from an inner runresult() with its result.'
pass
@VisiData.api
def drawSheet(self, scr, sheet):
'Erase *scr* and draw *sheet* on it, including status bars and sidebar.'
sheet.ensureLoaded()
scr.erase() # clear screen before every re-draw
scr.bkgd(' ', colors.color_default)
sheet._scr = scr
try:
sheet.draw(scr)
except Exception as e:
self.exceptionCaught(e)
self.drawLeftStatus(scr, sheet)
self.drawRightStatus(scr, sheet) # visible during this getkeystroke
self.drawSidebar(scr, sheet)
vd.windowConfig = dict(pct=0, n=0, h=0, w=0) # n=top line of bottom window; h=height of bottom window; w=width of screen
vd.winTop = None
vd.scrMenu = None
vd.scrFull = None
@VisiData.api
def setWindows(vd, scr, pct=None):
'Assign winTop, winBottom, win1 and win2 according to options.disp_splitwin_pct.'
if pct is None:
pct = options.disp_splitwin_pct # percent of window for secondary sheet (negative means bottom)
disp_menu = getattr(vd, 'menuRunning', None) or vd.options.disp_menu
topmenulines = 1 if disp_menu else 0
h, w = scr.getmaxyx()
n = 0
if pct:
# on 100 line screen, pct = 25 means second window on lines 75-100. pct -25 -> lines 0-25
n = abs(pct)*h//100
n = min(n, h-topmenulines-3)
n = max(3, n)
desiredConfig = dict(pct=pct, n=n, h=h-topmenulines, w=w)
if vd.scrFull is not scr or vd.windowConfig != desiredConfig:
if not topmenulines:
vd.scrMenu = None
elif not vd.scrMenu:
vd.scrMenu = vd.subwindow(scr, 0, 0, w, h)
vd.scrMenu.keypad(1)
vd.winTop = vd.subwindow(scr, 0, topmenulines, w, n)
vd.winTop.keypad(1)
vd.winBottom = vd.subwindow(scr, 0, n+topmenulines, w, h-n-topmenulines)
vd.winBottom.keypad(1)
if pct == 0 or pct >= 100: # no second pane
vd.win1 = vd.winBottom
# drawing to 0-line window causes problems
vd.win2 = None
elif pct > 0: # pane 2 from line n to bottom
vd.win1 = vd.winTop
vd.win2 = vd.winBottom
elif pct < 0: # pane 2 from line 0 to n
vd.win1 = vd.winBottom
vd.win2 = vd.winTop
for vs in vd.sheetstack(1)[0:1]+vd.sheetstack(2)[0:1]:
vs.refresh()
vd.windowConfig = desiredConfig
vd.scrFull = scr
return True
@VisiData.api
def draw_all(vd):
'Draw all sheets in all windows.'
vd.clearCaches()
ss1 = vd.sheetstack(1)
ss2 = vd.sheetstack(2)
if ss1 and not ss2:
vd.activePane = 1
vd.setWindows(vd.scrFull)
vd.drawSheet(vd.win1, ss1[0])
if vd.win2:
vd.win2.erase()
elif not ss1 and ss2:
vd.activePane = 2
vd.setWindows(vd.scrFull)
vd.drawSheet(vd.win2, ss2[0])
if vd.win1:
vd.win1.erase()
elif ss1 and ss2 and vd.win2:
vd.drawSheet(vd.win1, ss1[0])
vd.drawSheet(vd.win2, ss2[0])
elif ss1 and ss2 and not vd.win2:
vd.drawSheet(vd.win1, vd.sheetstack(vd.activePane)[0])
vd.setWindows(vd.scrFull)
if vd.scrMenu:
vd.drawMenu(vd.scrMenu, vd.activeSheet)
if vd.win1:
vd.win1.refresh()
if vd.win2:
vd.win2.refresh()
if vd.scrMenu:
vd.scrMenu.refresh()
@VisiData.api
def runresult(vd):
try:
err = vd.mainloop(vd.scrFull)
if err:
raise Exception(err)
except ReturnValue as e:
return e.args[0]
@VisiData.api
def mainloop(self, scr):
'Manage execution of keystrokes and subsequent redrawing of screen.'
nonidle_timeout = vd.curses_timeout
scr.timeout(vd.curses_timeout)
with contextlib.suppress(curses.error):
curses.curs_set(0)
numTimeouts = 0
prefixWaiting = False
vd.scrFull = scr
self.keystrokes = ''
while True:
if not self.stackedSheets and self.currentReplay is None:
return
sheet = self.activeSheet
if not sheet:
continue # waiting for replay to push sheet
threading.current_thread().sheet = sheet
vd.drawThread = threading.current_thread()
vd.setWindows(vd.scrFull)
if not self.drainPendingKeys(scr) or time.time() - self._lastDrawTime > self.min_draw_ms/1000: #1459
self.draw_all()
self._lastDrawTime = time.time()
if vd._nextCommands:
sheet.execCommand(vd._nextCommands.pop(0), keystrokes=self.keystrokes)
continue
keystroke = self.getkeystroke(scr, sheet)
if not keystroke and prefixWaiting and "Alt+" in self.keystrokes: # timeout ESC
self.keystrokes = ''
if keystroke: # wait until next keystroke to clear statuses and previous keystrokes
numTimeouts = 0
if not prefixWaiting:
self.keystrokes = ''
self.statuses.clear()
if keystroke == 'KEY_MOUSE':
try:
keystroke = vd.handleMouse(sheet) # if it was handled, don't handle again as a regular keystroke
except Exception as e:
self.exceptionCaught(e)
if keystroke and keystroke in vd.allPrefixes and keystroke in vd.keystrokes[:-1]:
vd.warning('duplicate prefix: ' + keystroke)
self.keystrokes = ''
else:
keystroke = self.prettykeys(keystroke)
self.keystrokes += keystroke
self.drawRightStatus(sheet._scr, sheet) # visible for commands that wait for input
if not keystroke: # timeout instead of keypress
pass
elif keystroke == 'Ctrl+Q':
return self.lastErrors and '\n'.join(self.lastErrors[-1])
elif vd.bindkeys._get(self.keystrokes):
sheet.execCommand(self.keystrokes, keystrokes=self.keystrokes)
prefixWaiting = False
elif keystroke in self.allPrefixes:
prefixWaiting = True
else:
vd.status('no command for "%s"' % (self.keystrokes))
prefixWaiting = False
self.checkForFinishedThreads()
sheet.checkCursorNoExceptions()
# no idle redraw unless background threads are running
time.sleep(0) # yield to other threads which may not have started yet
if vd.unfinishedThreads:
vd.curses_timeout = nonidle_timeout
else:
numTimeouts += 1
if vd.timeouts_before_idle >= 0 and numTimeouts > vd.timeouts_before_idle:
vd.curses_timeout = -1
else:
vd.curses_timeout = nonidle_timeout
scr.timeout(vd.curses_timeout)
@VisiData.api
def initCurses(vd):
# reduce ESC timeout to 25ms. http://en.chys.info/2009/09/esdelay-ncurses/
os.putenv('ESCDELAY', '25')
curses.use_env(True)
scr = curses.initscr()
curses.start_color()
colors.setup()
curses.noecho()
curses.raw() # get control keys instead of signals
curses.meta(1) # allow "8-bit chars"
scr.keypad(1)
curses.def_prog_mode()
return scr
def wrapper(f, *args, **kwargs):
try:
scr = vd.initCurses()
return f(scr, *args, **kwargs)
finally:
curses.endwin()
@VisiData.global_api
def run(vd, *sheetlist):
'Main entry point; launches vdtui with the given sheets already pushed (last one is visible)'
scr = None
try:
# Populate VisiData object with sheets from a given list.
for vs in sheetlist:
vd.push(vs, load=False)
scr = vd.initCurses()
ret = vd.mainloop(scr)
except curses.error as e:
vd.fail(str(e))
finally:
if scr:
curses.endwin()
vd.cancelThread(*[t for t in vd.unfinishedThreads if not t.name.startswith('save_')])
if ret:
builtins.print(ret)
@VisiData.api
def addCommand(vd, *args, **kwargs):
return BaseSheet.addCommand(*args, **kwargs)
import sys
vd.addGlobals({k:getattr(sys.modules[__name__], k) for k in __all__})