pipeforge 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,555 @@
1
+ #!/bin/env python
2
+ # vim: colorcolumn=101 textwidth=100
3
+
4
+ import os
5
+ import sys
6
+ import textwrap
7
+ import random
8
+
9
+ try:
10
+ import curses
11
+ import curses.textpad
12
+ except Exception:
13
+ print("")
14
+ print("ERROR: 'curses' package not found")
15
+ print("ERROR: If you are on Windows, try installing it with this command:")
16
+ print("ERROR:")
17
+ print("ERROR: > python -m pip install -i https://pypi.org/project windows-curses")
18
+ print("ERROR:")
19
+ print("")
20
+ sys.exit(1)
21
+
22
+
23
+ try:
24
+ from .._internal import database # Local module
25
+ from .._internal import drawing # Local module
26
+ except Exception:
27
+ from _internal import database # Local module
28
+ from _internal import drawing # Local module
29
+
30
+
31
+
32
+ ####################################################################################################
33
+ # API
34
+ ####################################################################################################
35
+
36
+ class TUI:
37
+ """
38
+ Terminnal user interface for querying the pipelines database
39
+ """
40
+
41
+ def __init__(self, database_url):
42
+
43
+ self._database_url = database_url
44
+ self._all_pipelines = None
45
+ self._current_pipeline = None
46
+
47
+ self._refresh_list_of_all_pipelines()
48
+
49
+ # Save all non-running pipelines into the cache
50
+ #
51
+ self._cache = {}
52
+
53
+ for i, pipeline in enumerate(self._all_pipelines):
54
+ if pipeline.metadata.current_state not in [ "RUNNING", "WAITING" ]:
55
+ self._current_pipeline_index = i
56
+ self._refresh_current_pipeline()
57
+
58
+ self._current_pipeline_index = 0
59
+ self._refresh_current_pipeline()
60
+
61
+ self._current_job_index = 0
62
+ self._focus = "pipelines"
63
+
64
+ self._help = 0
65
+
66
+
67
+ def _refresh_list_of_all_pipelines(self):
68
+ # This triggers a DB query
69
+ #
70
+ self._all_pipelines = [
71
+ x for x in
72
+ database.Pipeline.objects().order_by('-metadata__start_time')[0:10]
73
+ ]
74
+
75
+
76
+ def _refresh_current_pipeline(self):
77
+ # This triggers a DB query if not already in the cache
78
+ #
79
+ if len(self._all_pipelines) == 0:
80
+ return
81
+
82
+ pipeline_id = str(self._all_pipelines[self._current_pipeline_index].id)
83
+
84
+ if pipeline_id in self._cache:
85
+ self._current_pipeline = self._cache[pipeline_id]
86
+ else:
87
+ self._current_pipeline = database.DBProxy(self._database_url,
88
+ str(self._all_pipelines[self._current_pipeline_index].id))
89
+
90
+ if self._current_pipeline.pipeline.metadata.current_state not in ["RUNNING", "WAITING"]:
91
+ # This pipeline is not going to change state anymore.
92
+ # We can add it to the cache.
93
+ #
94
+ self._cache[pipeline_id] = self._current_pipeline
95
+
96
+
97
+ def _draw(self,
98
+ stdscr,
99
+ top_pane_geometry,
100
+ middle_top_pane_geometry,
101
+ middle_bottom_pane_geometry,
102
+ bottom_pane_geometry):
103
+
104
+ top = stdscr.subwin(*top_pane_geometry)
105
+ middle_top = stdscr.subwin(*middle_top_pane_geometry)
106
+ middle_bottom = stdscr.subwin(*middle_bottom_pane_geometry)
107
+ #raise Exception(f"ERROR: {bottom_pane_geometry}")
108
+ bottom = stdscr.subwin(*bottom_pane_geometry)
109
+
110
+
111
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
112
+ # Top pane: pipeline selector
113
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
114
+
115
+ top.addstr(0, 0,
116
+ f"{'#':>2} "
117
+ f"{textwrap.shorten('pipeline id', width=28, placeholder='...'):28}"
118
+ f"{textwrap.shorten('started', width=23, placeholder='...'):23}"
119
+ f"{textwrap.shorten('from', width=25, placeholder='...'):25}"
120
+ f"{textwrap.shorten('current state', width=20, placeholder='...'):20}"
121
+ )
122
+ top.addstr(1, 0, "-"*100)
123
+
124
+ if len(self._all_pipelines) == 0:
125
+ # Nothing to draw
126
+ #
127
+ return
128
+
129
+ parent_of_currently_selected = \
130
+ self._all_pipelines[self._current_pipeline_index].metadata.parent
131
+
132
+ child_of_currently_selected = None
133
+ for i, pipeline in enumerate(self._all_pipelines):
134
+ if pipeline.metadata.parent == self._all_pipelines[self._current_pipeline_index]:
135
+ child_of_currently_selected = pipeline
136
+ break
137
+
138
+ for i, pipeline in enumerate(self._all_pipelines):
139
+
140
+ id_str = str(pipeline.id)
141
+
142
+ if i == self._current_pipeline_index :
143
+
144
+ if pipeline.metadata.current_state in [ "RUNNING", "WAITING" ]:
145
+ pipeline.reload()
146
+
147
+ if self._focus == "pipelines":
148
+ attr = curses.A_REVERSE
149
+ else:
150
+ attr = curses.A_UNDERLINE
151
+ else:
152
+ attr = 0
153
+
154
+ if pipeline.metadata.start_time:
155
+ start_time = pipeline.metadata.start_time.isoformat(' ', 'seconds')
156
+ else:
157
+ start_time = "?"
158
+
159
+ if pipeline.metadata.current_state:
160
+ current_state = pipeline.metadata.current_state
161
+ else:
162
+ current_state = "?"
163
+
164
+ if pipeline == parent_of_currently_selected:
165
+ indicator = "<-"
166
+ elif pipeline == child_of_currently_selected:
167
+ indicator = "->"
168
+ else:
169
+ indicator = ""
170
+
171
+ try:
172
+ top.addstr(i+2, 0,
173
+ f"{i:>2} "
174
+ f"{textwrap.shorten(id_str, width=28, placeholder='...'):28}"
175
+ f"{textwrap.shorten(start_time, width=23, placeholder='...'):23}"
176
+ f"{textwrap.shorten(os.path.basename(pipeline.file_path), width=25, placeholder='...'):25}"
177
+ f"{textwrap.shorten(current_state, width=20, placeholder='...'):20}{indicator}",
178
+ attr
179
+ )
180
+ except Exception:
181
+ # TODO: Implement scroll
182
+ pass
183
+
184
+
185
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
186
+ # Top pane: keys
187
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
188
+
189
+ if self._help == 1:
190
+ top.addstr( 0, top_pane_geometry[1]-96, r""" . . """)
191
+ top.addstr( 1, top_pane_geometry[1]-96, r""" |\_|\ """)
192
+ top.addstr( 2, top_pane_geometry[1]-96, r""" | a_a\ """)
193
+ top.addstr( 3, top_pane_geometry[1]-96, r""" | | "] """)
194
+ top.addstr( 4, top_pane_geometry[1]-96, r""" ____| '-\___ """)
195
+ top.addstr( 5, top_pane_geometry[1]-96, r""" /.----.___.-'\ """)
196
+ top.addstr( 6, top_pane_geometry[1]-96, r""" // _ \ """)
197
+ top.addstr( 7, top_pane_geometry[1]-96, r""" // .-. (~v~) /| """)
198
+ top.addstr( 8, top_pane_geometry[1]-96, r""" |'| /\: .-- / \ """)
199
+ top.addstr( 9, top_pane_geometry[1]-96, r""" // |-/ \_/____/\/~| """)
200
+ top.addstr(10, top_pane_geometry[1]-96, r"""|/ \ | []_|_|_] \ | """)
201
+ top.addstr(11, top_pane_geometry[1]-96, r"""| \ | \ |___ _\ ]_} """)
202
+ elif self._help == 2:
203
+ top.addstr( 0, top_pane_geometry[1]-96, r""" .=., """)
204
+ top.addstr( 1, top_pane_geometry[1]-96, r""" ;c =\ """)
205
+ top.addstr( 2, top_pane_geometry[1]-96, r""" __| _/ """)
206
+ top.addstr( 3, top_pane_geometry[1]-96, r""" .'-'-._/-'-._ """)
207
+ top.addstr( 4, top_pane_geometry[1]-96, r""" /.. ____ \ """)
208
+ top.addstr( 5, top_pane_geometry[1]-96, r""" /' _ [<_->] ) \ """)
209
+ top.addstr( 6, top_pane_geometry[1]-96, r""" ( / \--\_>/-/'._ ) """)
210
+ top.addstr( 7, top_pane_geometry[1]-96, r""" \-;_/\__;__/ _/ _/ """)
211
+ top.addstr( 8, top_pane_geometry[1]-96, r""" '._}|==o==\{_\/ """)
212
+ top.addstr( 9, top_pane_geometry[1]-96, r""" / /-._.--\ \_ """)
213
+ top.addstr(10, top_pane_geometry[1]-96, r""" // / /| \ \ \ """)
214
+ top.addstr(11, top_pane_geometry[1]-96, r""" / | | | \; | \ \ """)
215
+ elif self._help == 3:
216
+ top.addstr( 0, top_pane_geometry[1]-96, r""" .-'-. """)
217
+ top.addstr( 1, top_pane_geometry[1]-96, r""" /` |__ """)
218
+ top.addstr( 2, top_pane_geometry[1]-96, r""" /` _.--`-,-` """)
219
+ top.addstr( 3, top_pane_geometry[1]-96, r""" '-|` a '<-. [] """)
220
+ top.addstr( 4, top_pane_geometry[1]-96, r""" \ _\__) \=` """)
221
+ top.addstr( 5, top_pane_geometry[1]-96, r""" C_ ` ,_/ """)
222
+ top.addstr( 6, top_pane_geometry[1]-96, r""" | ;----' """)
223
+ top.addstr( 7, top_pane_geometry[1]-96, r""" _.---| |--._ """)
224
+ top.addstr( 8, top_pane_geometry[1]-96, r""" .' _./' '\._ '. """)
225
+ top.addstr( 9, top_pane_geometry[1]-96, r""" /--'` `-.-` `'-\ """)
226
+ top.addstr(10, top_pane_geometry[1]-96, r""" | o \ """)
227
+ top.addstr(11, top_pane_geometry[1]-96, r""" |__ . / ) """)
228
+ elif self._help == 4:
229
+ top.addstr( 0, top_pane_geometry[1]-96, r""" ,___ .-;' """)
230
+ top.addstr( 1, top_pane_geometry[1]-96, r""" `"-.`\_...._/`.` """)
231
+ top.addstr( 2, top_pane_geometry[1]-96, r""" , \ / """)
232
+ top.addstr( 3, top_pane_geometry[1]-96, r""" .-' ', / () ()\ """)
233
+ top.addstr( 4, top_pane_geometry[1]-96, r"""`'._ \ /() . (| """)
234
+ top.addstr( 5, top_pane_geometry[1]-96, r""" > .' ;, -'- / """)
235
+ top.addstr( 6, top_pane_geometry[1]-96, r""" / < |;, __.; """)
236
+ top.addstr( 7, top_pane_geometry[1]-96, r""" '-.'-.| , \ , \ """)
237
+ top.addstr( 8, top_pane_geometry[1]-96, r""" `>.|;, \_) \_) """)
238
+ top.addstr( 9, top_pane_geometry[1]-96, r""" `-; , / """)
239
+ top.addstr(10, top_pane_geometry[1]-96, r""" \ / < """)
240
+ top.addstr(11, top_pane_geometry[1]-96, r""" '. <`'-,_) """)
241
+ elif self._help == 5:
242
+ top.addstr( 0, top_pane_geometry[1]-96, r""" """)
243
+ top.addstr( 1, top_pane_geometry[1]-96, r""" \WWW/ """)
244
+ top.addstr( 2, top_pane_geometry[1]-96, r""" / \ """)
245
+ top.addstr( 3, top_pane_geometry[1]-96, r""" /wwwww\ """)
246
+ top.addstr( 4, top_pane_geometry[1]-96, r""" _| o_o |_ """)
247
+ top.addstr( 5, top_pane_geometry[1]-96, r""" \WWWWWWW/ (_ / \ _)""")
248
+ top.addstr( 6, top_pane_geometry[1]-96, r""" _.'` o_o `'._ | \_/ | """)
249
+ top.addstr( 7, top_pane_geometry[1]-96, r"""(_ (_) _): ~~~~~ : """)
250
+ top.addstr( 8, top_pane_geometry[1]-96, r""" '.'-...-'.' \_____/ """)
251
+ top.addstr( 9, top_pane_geometry[1]-96, r""" (`'---'`) [ ] """)
252
+ top.addstr(10, top_pane_geometry[1]-96, r""" `""'""` `""'""` """)
253
+ top.addstr(11, top_pane_geometry[1]-96, r""" """)
254
+
255
+ top.addstr( 1, top_pane_geometry[1]-67, "== Keys =======================================================")
256
+ top.addstr( 2, top_pane_geometry[1]-67, "| |")
257
+ top.addstr( 3, top_pane_geometry[1]-67, "| <tab> : toggle between pipelines and jobs |")
258
+ top.addstr( 4, top_pane_geometry[1]-67, "| j/k : select new pipeline/job |")
259
+ top.addstr( 5, top_pane_geometry[1]-67, "| r : re-query DB for new recently triggered pipelines |")
260
+ top.addstr( 6, top_pane_geometry[1]-67, "| h : ask for help |")
261
+ top.addstr( 7, top_pane_geometry[1]-67, "| q : exit |")
262
+ top.addstr( 8, top_pane_geometry[1]-67, "| |")
263
+ top.addstr( 9, top_pane_geometry[1]-67, "===============================================================")
264
+
265
+
266
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
267
+ # Bottom pane: pipeline timeline
268
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
269
+
270
+ timeline = drawing.draw_timeline(
271
+ self._current_pipeline,
272
+ mode=f"unicode:{bottom_pane_geometry[1]-2}:buffer:auto")
273
+
274
+ i = 0
275
+ legend = None
276
+ buffer = []
277
+
278
+ # Extract legend and save lines to print to a tmp buffer to see how many lines there are
279
+ #
280
+ if timeline:
281
+ for line in timeline.split("\n"):
282
+
283
+ if "LEGEND" in line:
284
+ legend = {}
285
+ continue
286
+
287
+ if legend is None:
288
+ buffer.append(line)
289
+ else:
290
+ # Don't print anything else, just save the legend aliases for later
291
+ #
292
+ if ":" in line:
293
+ alias, full_name = line.split(":")
294
+ legend[full_name.strip()] = alias.strip()
295
+
296
+ # Print
297
+ #
298
+ i = 0
299
+ for line in buffer:
300
+ if i < bottom_pane_geometry[0]:
301
+ bottom.addstr(i, 0, line)
302
+ i += 1
303
+
304
+
305
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
306
+ # Middle top pane: pipeline jobs data
307
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
308
+
309
+ middle_top.addstr(0, 0,
310
+ f"{textwrap.shorten('alias', width=7, placeholder='...'):7}"
311
+ f"{textwrap.shorten('name', width=30, placeholder='...'):30}"
312
+ f"{textwrap.shorten('script', width=25, placeholder='...'):25}"
313
+ f"{textwrap.shorten('runner', width=20, placeholder='...'):20}"
314
+ f"{textwrap.shorten('detached', width=12, placeholder='...'):12}"
315
+ f"{textwrap.shorten('timeout', width=14, placeholder='...'):14}"
316
+ f"{textwrap.shorten('retries', width=11, placeholder='...'):11}"
317
+ f"{textwrap.shorten('on failure', width=12, placeholder='...'):18}"
318
+ f"{textwrap.shorten('on input error', width=16, placeholder='...'):18}"
319
+ f"{textwrap.shorten('current state', width=14, placeholder='...'):14}"
320
+ )
321
+ middle_top.addstr(1, 0, "-"*168)
322
+
323
+ i = 2
324
+ for job in self._current_pipeline.jobs:
325
+
326
+ if legend:
327
+ if job.name in legend.keys():
328
+ alias = legend[job.name]
329
+ else:
330
+ alias = "-"
331
+ else:
332
+ alias = job.name
333
+
334
+ if i-2 == self._current_job_index:
335
+
336
+ if self._focus == "jobs":
337
+ attr = curses.A_REVERSE
338
+ else:
339
+ attr = 0
340
+ else:
341
+ attr = 0
342
+
343
+ try:
344
+ middle_top.addstr(i, 0,
345
+ f"{textwrap.shorten(alias, width=7, placeholder='...'):7}"
346
+ f"{textwrap.shorten(job.name, width=30, placeholder='...'):30}"
347
+ f"{textwrap.shorten(job.script, width=25, placeholder='...'):25}"
348
+ f"{textwrap.shorten(job.runner, width=20, placeholder='...'):20}"
349
+ f"{textwrap.shorten(job.detached, width=12, placeholder='...'):12}"
350
+ f"{textwrap.shorten(job.timeout, width=14, placeholder='...'):14}"
351
+ f"{textwrap.shorten(job.retries, width=11, placeholder='...'):11}"
352
+ f"{textwrap.shorten(job.on_failure, width=18, placeholder='...'):18}"
353
+ f"{textwrap.shorten(job.on_input_err, width=18, placeholder='...'):18}"
354
+ f"{textwrap.shorten(job.metadata.current_state, width=14, placeholder='...'):14}",
355
+ attr
356
+ )
357
+ except Exception:
358
+ # TODO: Implement scroll
359
+ pass
360
+
361
+ i +=1
362
+
363
+
364
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
365
+ # Middle bottom pane: job input/output parameters
366
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
367
+
368
+ if self._focus == "jobs":
369
+
370
+ middle_bottom.addstr(0, 0,
371
+ f"{textwrap.shorten('input', width=100, placeholder='...'):100}"
372
+ f"{textwrap.shorten('output', width=100, placeholder='...'):100}"
373
+ )
374
+ middle_bottom.addstr(1, 0, "-"*(bottom_pane_geometry[1]-1))
375
+
376
+ for i, job in enumerate(self._current_pipeline.jobs):
377
+ if i == self._current_job_index:
378
+ break
379
+ else:
380
+ return
381
+
382
+ input_params = [ f"{k}={v}" for k,v in job.input.items()]
383
+ output_params = [ f"{k}={v}" for k,v in job.output.items()]
384
+
385
+ total_rows = max(len(input_params), len(output_params))
386
+
387
+ input_params += [""] * (total_rows-len(input_params))
388
+ output_params += [""] * (total_rows-len(output_params))
389
+
390
+ if input_params == []: input_params = [""]
391
+ if output_params == []: output_params = [""]
392
+
393
+ i = 2
394
+ for param_index in range(total_rows):
395
+
396
+ try:
397
+ middle_bottom.addstr(i, 0,
398
+ f"{textwrap.shorten(input_params[param_index], width=100, placeholder='...'):100}"
399
+ f"{textwrap.shorten(output_params[param_index], width=100, placeholder='...'):100}"
400
+ )
401
+ except Exception:
402
+ # TODO: Implement scroll
403
+ pass
404
+
405
+ i +=1
406
+
407
+
408
+ def _process_input(self, key, stdscr):
409
+ """
410
+ Change the currently selected item
411
+ """
412
+
413
+ if key == "j" or key == "KEY_DOWN":
414
+ if self._focus == "pipelines":
415
+ if self._current_pipeline_index < len(self._all_pipelines)-1:
416
+ self._current_pipeline_index += 1
417
+ self._refresh_current_pipeline()
418
+ else:
419
+ if self._current_job_index < len(self._current_pipeline.jobs)-1:
420
+ self._current_job_index += 1
421
+
422
+ elif key == "k" or key == "KEY_UP":
423
+ if self._focus == "pipelines":
424
+ if self._current_pipeline_index > 0:
425
+ self._current_pipeline_index -= 1
426
+ self._refresh_current_pipeline()
427
+ else:
428
+ if self._current_job_index > 0:
429
+ self._current_job_index -= 1
430
+
431
+ elif key == "r":
432
+ old_pipelines = len(self._all_pipelines)
433
+ self._refresh_list_of_all_pipelines()
434
+ new_pipelines = len(self._all_pipelines)
435
+
436
+ if new_pipelines != old_pipelines:
437
+ # Reset selection
438
+ #
439
+ self._current_pipeline_index = 0
440
+ self._refresh_current_pipeline()
441
+
442
+ elif key == chr(9): # TAB key
443
+ if self._focus == "pipelines":
444
+ self._focus = "jobs"
445
+ self._current_job_index = 0
446
+ else:
447
+ self._focus = "pipelines"
448
+
449
+ elif key == chr(10): # ENTER key
450
+ if self._focus == "pipelines":
451
+ self._focus = "jobs"
452
+ self._current_job_index = 0
453
+ else:
454
+ self._focus = "pipelines"
455
+
456
+ elif key == "h":
457
+ if self._help == 0:
458
+ self._help = random.randint(1,5)
459
+ else:
460
+ self._help = 0
461
+
462
+ elif key == "q":
463
+ raise Exception("Quit")
464
+
465
+
466
+ def loop(self, stdscr):
467
+ """
468
+ Processing loop that takes input and updates the display.
469
+ """
470
+
471
+ # Hide cursor
472
+ #
473
+ curses.curs_set(0)
474
+
475
+
476
+ # Geometry
477
+ #
478
+ # -----------------------------
479
+ # | top | 1/4
480
+ # |---------------------------|
481
+ # | middle top | 3/8
482
+ # |---------------------------|
483
+ # | middle bottom | 1/4 (-2 rows)
484
+ # |---------------------------|
485
+ # | bottom | 1/8 (+ 2 rows)
486
+ # -----------------------------
487
+ #
488
+ BORDER = 2
489
+
490
+ top_pane_geometry = (
491
+ ((curses.LINES - BORDER)*2)//8, # height
492
+ (curses.COLS - BORDER), # width
493
+ BORDER, # y_off
494
+ BORDER # x_off
495
+ )
496
+
497
+ middle_top_pane_geometry = (
498
+ ((curses.LINES - BORDER)*3)//8, # height
499
+ (curses.COLS - BORDER), # width
500
+ BORDER + ((curses.LINES - BORDER)*2)//8, # y_off
501
+ BORDER # x_off
502
+ )
503
+
504
+ middle_bottom_pane_geometry = (
505
+ ((curses.LINES - BORDER)*2)//8 - 2, # height
506
+ (curses.COLS - BORDER), # width
507
+ BORDER + ((curses.LINES - BORDER)*5)//8, # y_off
508
+ BORDER # x_off
509
+ )
510
+
511
+ bottom_pane_geometry = (
512
+ ((curses.LINES - BORDER)*1)//8 + 2, # height
513
+ (curses.COLS - BORDER), # width
514
+ BORDER + ((curses.LINES - BORDER)*7)//8 - 2, # y_off
515
+ BORDER # x_off
516
+ )
517
+
518
+
519
+ # Main loop (draw + process input + draw + ...)
520
+ #
521
+ stdscr.timeout(500) # If there is no input from the user in 0.5 seconds, getkey() will raise
522
+ # an exception
523
+
524
+ while True:
525
+ stdscr.clear()
526
+
527
+ self._draw(
528
+ stdscr,
529
+ top_pane_geometry,
530
+ middle_top_pane_geometry,
531
+ middle_bottom_pane_geometry,
532
+ bottom_pane_geometry,
533
+ )
534
+
535
+ while True:
536
+ try:
537
+ key = stdscr.getkey()
538
+ self._process_input(key, stdscr)
539
+ break
540
+
541
+ except Exception as e:
542
+ if str(e) == "Quit":
543
+ return ""
544
+ elif str(e).startswith("ERROR:"):
545
+ raise e
546
+
547
+ # Time out! Refresh current pipeline and jobs if needed
548
+ #
549
+ if len(self._all_pipelines) > 0:
550
+ if self._current_pipeline.pipeline.metadata.current_state in [ "RUNNING", "WAITING" ]:
551
+ self._refresh_current_pipeline()
552
+ break
553
+
554
+ stdscr.refresh()
555
+