pmemstat 3.0.1__tar.gz → 3.0.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pmemstat
3
- Version: 3.0.1
3
+ Version: 3.0.2
4
4
  Summary: Report Proportional Memory on Linux
5
5
  Keywords: proportional memory,RAM,htop,top,monitor
6
6
  Author-email: Joe Defen <joedef@duck.com>
@@ -9,7 +9,9 @@ Description-Content-Type: text/markdown
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: POSIX :: Linux
12
- Requires-Dist: psutil>5.9
12
+ License-File: LICENSE.txt
13
+ Requires-Dist: psutil > 5.9
14
+ Requires-Dist: console-window >= 1.0.0
13
15
  Project-URL: Bug Tracker, https://github.com/joedefen/pmemstat/issues
14
16
  Project-URL: Homepage, https://github.com/joedefen/pmemstat
15
17
 
@@ -50,7 +50,7 @@ import curses
50
50
  from types import SimpleNamespace
51
51
  from io import StringIO
52
52
  from datetime import datetime, timedelta
53
- from pmemstat.Window import Window, OptionSpinner
53
+ from console_window import ConsoleWindow, OptionSpinner
54
54
  from pmemstat.KillThem import KillThem
55
55
  from pmemstat.CpuSmooth import CpuSmooth, SysStat
56
56
 
@@ -1200,7 +1200,7 @@ class PmemStat:
1200
1200
  prompt='Set search string, then Enter', obj=self.opts)
1201
1201
 
1202
1202
  keys_we_handle = [ord('K'), curses.KEY_ENTER, 10] + list(self.spin.keys)
1203
- self.window = Window(head_line=True, keys=keys_we_handle)
1203
+ self.window = ConsoleWindow(head_line=True, keys=keys_we_handle)
1204
1204
  is_first = True
1205
1205
  was_groupby, regroup = self.opts.groupby, True
1206
1206
  was_others = self.opts.others
@@ -1331,7 +1331,7 @@ def run():
1331
1331
  except KeyboardInterrupt:
1332
1332
  pass
1333
1333
  except Exception as exce:
1334
- Window.stop_curses()
1334
+ ConsoleWindow.stop_curses()
1335
1335
  print("exception:", str(exce))
1336
1336
  print(traceback.format_exc())
1337
1337
 
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "pmemstat"
7
- version = "3.0.1"
7
+ version = "3.0.2"
8
8
  description = "Report Proportional Memory on Linux"
9
9
  authors = [
10
10
  {name = "Joe Defen", email = "joedef@duck.com"},
@@ -19,7 +19,8 @@ classifiers = [
19
19
  "Operating System :: POSIX :: Linux"
20
20
  ]
21
21
  dependencies = [
22
- 'psutil>5.9',
22
+ 'psutil > 5.9',
23
+ 'console-window >= 1.0.0',
23
24
  ]
24
25
 
25
26
  [project.urls]
@@ -1,647 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Custom Wrapper for python curses.
5
- """
6
- # pylint: disable=too-many-instance-attributes,too-many-arguments
7
- # pylint: disable=invalid-name,broad-except,too-many-branches
8
-
9
- import traceback
10
- import atexit
11
- import time
12
- import curses
13
- import textwrap
14
- from types import SimpleNamespace
15
- from curses.textpad import rectangle, Textbox
16
- dump_str = None
17
-
18
- class OptionSpinner:
19
- """Manage a bunch of options where the value is rotate thru
20
- a fixed set of values pressing a key."""
21
- def __init__(self):
22
- """Give the object with the attribute to change its
23
- value (e.g., options from argparse or "self" from
24
- the object managing the window).
25
-
26
- And array of specs like:
27
- ['a - allow auto suggestions', 'allow_auto', True, False],
28
- ['/ - filter pattern', 'filter_str', self.filter_str],
29
- A spec can have a trailing None + more comments shown after
30
- the value.
31
- """
32
- self.options, self.keys = [], []
33
- self.margin = 4 # + actual width (1st column right pos)
34
- self.align = self.margin # + actual width (1st column right pos)
35
- self.default_obj = SimpleNamespace() # if not given one
36
- self.attr_to_option = {} # given an attribute, find its option ns
37
- self.key_to_option = {} # given key, options namespace
38
- self.keys = set()
39
-
40
- @staticmethod
41
- def _make_option_ns():
42
- return SimpleNamespace(
43
- keys=[],
44
- descr='',
45
- obj=None,
46
- attr='',
47
- vals=None,
48
- prompt=None,
49
- comments=[],
50
- )
51
-
52
- def get_value(self, attr, coerce=False):
53
- """Get the value of the given attribute."""
54
- ns = self.attr_to_option.get(attr, None)
55
- obj = ns.obj if ns else None
56
- value = getattr(obj, attr, None) if obj else None
57
- if value is None and obj and coerce:
58
- if ns.vals:
59
- if value not in ns.vals:
60
- value = ns.vals[0]
61
- setattr(obj, attr, value)
62
- else:
63
- if value is None:
64
- value = ''
65
- setattr(ns.obj, ns.attr, '')
66
- return value
67
-
68
- def _register(self, ns):
69
- """ Create the mappings needed"""
70
- assert ns.attr not in self.attr_to_option
71
- self.attr_to_option[ns.attr] = ns
72
- for key in ns.keys:
73
- assert key not in self.key_to_option, f'key ({chr(key)}, {key}) already used'
74
- self.key_to_option[key] = ns
75
- self.keys.add(key)
76
- self.options.append(ns)
77
- self.align = max(self.align, self.margin+len(ns.descr))
78
- self.get_value(ns.attr, coerce=True)
79
-
80
- def add(self, obj, specs):
81
- """ Compatibility Method."""
82
- for spec in specs:
83
- ns = self._make_option_ns()
84
- ns.descr = spec[0]
85
- ns.obj = obj
86
- ns.attr = spec[1]
87
- ns.vals=spec[2:]
88
- if None in ns.vals:
89
- idx = ns.vals.index(None)
90
- ns.vals = ns.vals[:idx]
91
- ns.comments = ns.vals[idx+1:]
92
- ns.keys = [ord(ns.descr[0])]
93
- self._register(ns)
94
-
95
- def add_key(self, attr, descr, obj=None, vals=None, prompt=None, keys=None, comments=None):
96
- """ Standard method"""
97
- ns = self._make_option_ns()
98
- if keys:
99
- ns.keys = list(keys) if isinstance(keys, (list, tuple, set)) else [keys]
100
- else:
101
- ns.keys = [ord(descr[0])]
102
- if comments is None:
103
- ns.comments = []
104
- else:
105
- ns.comments = list(comments) if isinstance(keys, (list, tuple)) else [comments]
106
- ns.descr = descr
107
- ns.attr = attr
108
- ns.obj = obj if obj else self.default_obj
109
- ns.vals, ns.prompt = vals, prompt
110
- assert bool(ns.vals) ^ bool(ns.prompt)
111
- self._register(ns)
112
-
113
- def show_help_nav_keys(self, win):
114
- """ Get/present standard verbiage for the navigation keys"""
115
- for line in Window.get_nav_keys_blurb().splitlines():
116
- if line:
117
- win.add_header(line)
118
-
119
- def show_help_body(self, win):
120
- """ Write the help page section."""
121
- win.add_body('Type keys to alter choice:', curses.A_UNDERLINE)
122
-
123
- for ns in self.options:
124
- # get / coerce the current value
125
- value = self.get_value(ns.attr)
126
- assert value is not None, f'cannot get value of {repr(ns.attr)}'
127
- choices = ns.vals if ns.vals else [value]
128
-
129
- win.add_body(f'{ns.descr:>{self.align}}: ')
130
-
131
- for choice in choices:
132
- shown = f'{choice}'
133
- if isinstance(choice, bool):
134
- shown = "ON" if choice else "off"
135
- win.add_body(' ', resume=True)
136
- win.add_body(shown, resume=True,
137
- attr=curses.A_REVERSE if choice == value else None)
138
-
139
- for comment in ns.comments:
140
- win.add_body(f'{"":>{self.align}}: {comment}')
141
-
142
- def do_key(self, key, win):
143
- """Do the automated processing of a key."""
144
- ns = self.key_to_option.get(key, None)
145
- if ns is None:
146
- return None
147
- value = self.get_value(ns.attr)
148
- if ns.vals:
149
- idx = ns.vals.index(value) if value in ns.vals else -1
150
- value = ns.vals[(idx+1) % len(ns.vals)] # choose next
151
- else:
152
- value = win.answer(prompt=ns.prompt, seed=value)
153
- setattr(ns.obj, ns.attr, value)
154
- return value
155
-
156
- class Window:
157
- """ Layer above curses to encapsulate what we need """
158
- timeout_ms = 200
159
- static_scr = None
160
- nav_keys = """
161
- Navigation: H/M/L: top/middle/end-of-page
162
- k, UP: up one row 0, HOME: first row
163
- j, DOWN: down one row $, END: last row
164
- Ctrl-u: half-page up Ctrl-b, PPAGE: page up
165
- Ctrl-d: half-page down Ctrl-f, NPAGE: page down
166
- """
167
- def __init__(self, head_line=True, head_rows=50, body_rows=200,
168
- body_cols=200, keys=None, pick_mode=False, pick_size=1):
169
- self.scr = self._start_curses()
170
-
171
- self.head = SimpleNamespace(
172
- pad=curses.newpad(head_rows, body_cols),
173
- rows=head_rows,
174
- cols=body_cols,
175
- row_cnt=0, # no. head rows added
176
- texts = [],
177
- view_cnt=0, # no. head rows viewable (NOT in body)
178
- )
179
- self.body = SimpleNamespace(
180
- pad = curses.newpad(body_rows, body_cols),
181
- rows= body_rows,
182
- cols=body_cols,
183
- row_cnt = 0,
184
- texts = []
185
- )
186
- self.hor_line_cnt = 1 if head_line else 0 # no. h-lines in header
187
- self.scroll_pos = 0 # how far down into body are we?
188
- self.max_scroll_pos = 0
189
- self.pick_pos = 0 # in highlight mode, where are we?
190
- self.last_pick_pos = -1 # last highlighted position
191
- self.pick_mode = pick_mode # whether in highlight mode
192
- self.pick_size = pick_size # whether in highlight mode
193
- self.rows, self.cols = 0, 0
194
- self.scroll_view_size = 0 # no. viewable lines of the body
195
- self.handled_keys = set(keys) if isinstance(keys, (set, list)) else []
196
- self._set_screen_dims()
197
- self.calc()
198
-
199
- @staticmethod
200
- def get_nav_keys_blurb():
201
- """For a help screen, describe the nav keys"""
202
- return textwrap.dedent(Window.nav_keys)
203
-
204
- def _set_screen_dims(self):
205
- """Recalculate dimensions ... return True if geometry changed."""
206
- rows, cols = self.scr.getmaxyx()
207
- same = bool(rows == self.rows and cols == self.cols)
208
- self.rows, self.cols = rows, cols
209
- return same
210
-
211
- @staticmethod
212
- def _start_curses():
213
- """ Curses initial setup. Note: not using curses.wrapper because we
214
- don't wish to change the colors. """
215
- atexit.register(Window.stop_curses)
216
- Window.static_scr = scr = curses.initscr()
217
- curses.noecho()
218
- curses.cbreak()
219
- curses.curs_set(0)
220
- scr.keypad(1)
221
- scr.timeout(Window.timeout_ms)
222
- scr.clear()
223
- return scr
224
-
225
- def set_pick_mode(self, on=True, pick_size=1):
226
- """Set whether in highlight mode."""
227
- was_on, was_size = self.pick_mode, self.pick_size
228
- self.pick_mode = bool(on)
229
- self.pick_size = max(pick_size, 1)
230
- if self.pick_mode and (not was_on or was_size != self.pick_size):
231
- self.last_pick_pos = -2 # indicates need to clear them all
232
-
233
- @staticmethod
234
- def stop_curses():
235
- """ Curses shutdown (registered to be called on exit). """
236
- if Window.static_scr:
237
- curses.nocbreak()
238
- curses.echo()
239
- Window.static_scr.keypad(0)
240
- curses.endwin()
241
- Window.static_scr = None
242
-
243
- def calc(self):
244
- """Recalculate dimensions ... return True if geometry changed."""
245
- same = self._set_screen_dims()
246
- self.head.view_cnt = min(self.rows - self.hor_line_cnt, self.head.row_cnt)
247
- self.scroll_view_size = self.rows - self.head.view_cnt - self.hor_line_cnt
248
- self.max_scroll_pos = max(self.body.row_cnt - self.scroll_view_size, 0)
249
- self.body_base = self.head.view_cnt + self.hor_line_cnt
250
- return not same
251
-
252
- def _add(self, ns, text, attr=None, resume=False):
253
- """ Add text to head/body pad using its namespace"""
254
- is_body = bool(id(ns) == id(self.body))
255
- if ns.row_cnt < ns.rows:
256
- row = max(ns.row_cnt - (1 if resume else 0), 0)
257
- if (is_body and self.pick_mode) or attr is None:
258
- attr = curses.A_NORMAL
259
- if resume:
260
- ns.pad.addstr(text, attr)
261
- ns.texts[row] += text
262
- else:
263
- ns.pad.addstr(row, 0, text, attr)
264
- ns.texts.append(text) # text only history
265
- ns.row_cnt += 1
266
-
267
- def add_header(self, text, attr=None, resume=False):
268
- """Add text to header"""
269
- self._add(self.head, text, attr, resume)
270
-
271
- def add_body(self, text, attr=None, resume=False):
272
- """ Add text to body (below header and header line)"""
273
- self._add(self.body, text, attr, resume)
274
-
275
- def draw(self, y, x, text, text_attr=None, width=None, leftpad=False, header=False):
276
- """Draws the given text (as utf-8 or unicode) at position (row=y,col=x)
277
- with optional text attributes and width.
278
- This is more compatible with my older, simpler Window class.
279
- """
280
- ns = self.head if header else self.body
281
- text_attr = text_attr if text_attr else curses.A_NORMAL
282
- if y < 0 or y >= ns.rows or x < 0 or x >= ns.cols:
283
- return # nada if out of bounds
284
- if y+1 >= ns.row_cnt:
285
- ns.row_cnt = y+1
286
-
287
-
288
- uni = text if isinstance(text, str) else text.decode('utf-8')
289
-
290
- if width is not None:
291
- width = min(width, self.cols - x)
292
- if width <= 0:
293
- return
294
- padlen = width - len(uni)
295
- if padlen > 0:
296
- if leftpad:
297
- uni = padlen * ' ' + uni
298
- else: # rightpad
299
- uni += padlen * ' '
300
- text = uni[:width].encode('utf-8')
301
- else:
302
- text = uni.encode('utf-8')
303
-
304
- try:
305
- while y >= len(ns.texts):
306
- ns.texts.append('')
307
- ns.texts[y] = ns.texts[y][:x].ljust(x) + uni + ns.texts[y][x+len(uni):]
308
- ns.pad.addstr(y, x, text, text_attr)
309
- except curses.error:
310
- # this sucks, but curses returns an error if drawing the last character
311
- # on the screen always. this can happen if resizing screen even if
312
- # special care is taken. So, we just ignore errors. Anyhow, you cannot
313
- # get decent error handling.
314
- pass
315
-
316
- def highlight_picked(self):
317
- """Highlight the current pick and un-highlight the previous pick."""
318
- if not self.pick_mode:
319
- return
320
- pos0, pos1 = self.last_pick_pos, self.pick_pos
321
- if pos0 == -2: # special flag to clear all formatting
322
- for row in range(self.body.row_cnt):
323
- self.body.pad.addstr(row, 0, self.body.texts[row], curses.A_NORMAL)
324
- if pos0 != pos1:
325
- if 0 <= pos0 < self.body.row_cnt:
326
- for i in range(self.pick_size):
327
- self.body.pad.addstr(pos0+i, 0, self.body.texts[pos0+i], curses.A_NORMAL)
328
- if 0 <= pos1 < self.body.row_cnt:
329
- for i in range(self.pick_size):
330
- string = self.body.texts[pos1+i]
331
- self.body.pad.addstr(pos1+i, 0, string, curses.A_REVERSE)
332
- self.last_pick_pos = pos1
333
-
334
- def _scroll_indicator_row(self):
335
- """ Compute the absolute scroll indicator row:
336
- - We want the top to be only when scroll_pos==0
337
- - We want the bottom to be only when scroll_pos=max_scroll_pos-1
338
- """
339
- if self.max_scroll_pos <= 1:
340
- return self.body_base
341
- y2, y1 = self.scroll_view_size-1, 1
342
- x2, x1 = self.max_scroll_pos, 1
343
- x = self.scroll_pos
344
- pos = y1 + (y2-y1)*(x-x1)/(x2-x1)
345
- return min(self.body_base + int(max(pos, 0)), self.rows-1)
346
-
347
- def _scroll_indicator_col(self):
348
- """ Compute the absolute scroll indicator col:
349
- - We want the left to be only when scroll_pos==0
350
- - We want the right to be only when scroll_pos=max_scroll_pos-1
351
- """
352
- if self.pick_mode:
353
- return self._calc_indicator(
354
- self.pick_pos, 0, self.body.row_cnt-1, 0, self.cols-1)
355
- return self._calc_indicator(
356
- self.scroll_pos, 0, self.max_scroll_pos, 0, self.cols-1)
357
-
358
- def _calc_indicator(self, pos, pos0, pos9, ind0, ind9):
359
- if self.max_scroll_pos <= 0:
360
- return -1 # not scrollable
361
- if pos9 - pos0 <= 0:
362
- return -1 # not scrollable
363
- if pos <= pos0:
364
- return ind0
365
- if pos >= pos9:
366
- return ind9
367
- ind = int(round(ind0 + (ind9-ind0+1)*(pos-pos0)/(pos9-pos0+1)))
368
- return min(max(ind, ind0+1), ind9-1)
369
-
370
- def render(self):
371
- """Draw everything added. In a loop cuz curses is a
372
- piece of shit."""
373
- for _ in range(128):
374
- try:
375
- self.render_once()
376
- return
377
- except curses.error:
378
- time.sleep(0.16)
379
- self._set_screen_dims()
380
- continue
381
- try:
382
- self.render_once()
383
- except Exception:
384
- Window.stop_curses()
385
- print(f"""curses err:
386
- head.row_cnt={self.head.row_cnt}
387
- head.view_cnt={self.head.view_cnt}
388
- hor_line_cnt={self.hor_line_cnt}
389
- body.row_cnt={self.body.row_cnt}
390
- scroll_pos={self.scroll_pos}
391
- max_scroll_pos={self.max_scroll_pos}
392
- pick_pos={self.pick_pos}
393
- last_pick_pos={self.last_pick_pos}
394
- pick_mode={self.pick_mode}
395
- pick_size={self.pick_size}
396
- rows={self.rows}
397
- cols={self.cols}
398
- """)
399
- raise
400
-
401
-
402
- def render_once(self):
403
- """Draw everything added."""
404
- self.calc()
405
- # if self.scroll_view_size <= 0:
406
- # self.scr.refresh()
407
- indent = 0
408
- if self.body_base < self.rows:
409
- ind_pos = 0 if self.pick_mode else self._scroll_indicator_row()
410
- if self.pick_mode:
411
- self.pick_pos = max(self.pick_pos, 0)
412
- self.pick_pos = min(self.pick_pos, self.body.row_cnt-1)
413
- if self.pick_pos >= 0:
414
- self.pick_pos -= (self.pick_pos % self.pick_size)
415
- if self.pick_pos < 0:
416
- self.scroll_pos = 0
417
- elif self.scroll_pos > self.pick_pos:
418
- # light position is below body bottom
419
- self.scroll_pos = self.pick_pos
420
- elif self.scroll_pos < self.pick_pos - (self.scroll_view_size - self.pick_size):
421
- # light position is above body top
422
- self.scroll_pos = self.pick_pos - (self.scroll_view_size - self.pick_size)
423
- self.scroll_pos = max(self.scroll_pos, 0)
424
- self.scroll_pos = min(self.scroll_pos, self.max_scroll_pos)
425
- indent = 1
426
- else:
427
- self.scroll_pos = max(self.scroll_pos, 0)
428
- self.scroll_pos = min(self.scroll_pos, self.max_scroll_pos)
429
- self.pick_pos = self.scroll_pos + ind_pos - self.body_base
430
- # indent = 1 if self.body.row_cnt > self.scroll_view_size else 0
431
-
432
- if indent > 0 and self.pick_mode:
433
- self.scr.vline(self.body_base, 0, ' ', self.scroll_view_size)
434
- if self.pick_pos >= 0:
435
- pos = self.pick_pos - self.scroll_pos + self.body_base
436
- self.scr.addstr(pos, 0, '>', curses.A_REVERSE)
437
-
438
- if self.head.view_cnt < self.rows:
439
- self.scr.hline(self.head.view_cnt, 0, curses.ACS_HLINE, self.cols)
440
- ind_pos = self._scroll_indicator_col()
441
- if ind_pos >= 0:
442
- bot, cnt = ind_pos, 1
443
- if 0 < ind_pos < self.cols-1:
444
- width = self.scroll_view_size/self.body.row_cnt * self.cols
445
- bot = max(int(round(ind_pos-width/2)), 1)
446
- top = min(int(round(ind_pos+width/2)), self.cols-1)
447
- cnt = top - bot
448
- # self.scr.addstr(self.head.view_cnt, bot, '-'*cnt, curses.A_REVERSE)
449
- # self.scr.hline(self.head.view_cnt, bot, curses.ACS_HLINE, curses.A_REVERSE, cnt)
450
- for idx in range(bot, bot+cnt):
451
- self.scr.addch(self.head.view_cnt, idx, curses.ACS_HLINE, curses.A_REVERSE)
452
-
453
- self.scr.refresh()
454
-
455
- if self.rows > 0:
456
- last_row = min(self.head.view_cnt, self.rows)-1
457
- if last_row >= 0:
458
- self.head.pad.refresh(0, 0, 0, indent, last_row, self.cols-1)
459
-
460
- if self.body_base < self.rows:
461
- if self.pick_mode:
462
- self.highlight_picked()
463
- self.body.pad.refresh(self.scroll_pos, 0,
464
- self.body_base, indent, self.rows-1, self.cols-1)
465
-
466
-
467
- def answer(self, prompt='Type string [then Enter]', seed='', width=80):
468
- """Popup"""
469
- def mod_key(key):
470
- return 7 if key == 10 else key
471
-
472
- # need 3 extra cols for rectangle (so we don't draw in southeast corner)
473
- # and 3 rows (top/prompt/bottom)
474
- # +Prompt--- -----------------+
475
- # | Seed-for-answer |
476
- # +---------Press ENTER to submit+
477
- if self.rows < 3 or self.cols < 30:
478
- return seed
479
- width = min(width, self.cols-3) # max text width
480
- row0, row9 = self.rows//2 - 1, self.rows//2 + 1
481
- col0 = (self.cols - (width+2)) // 2
482
- col9 = col0 + width + 2 - 1
483
-
484
- self.scr.clear()
485
- win = curses.newwin(1, width, row0+1, col0+1) # input window
486
- rectangle(self.scr, row0, col0, row9, col9)
487
- self.scr.addstr(row0, col0+1, prompt[0:width])
488
- win.addstr(seed[0:width-1])
489
- ending = 'Press ENTER to submit'[:width]
490
- self.scr.addstr(row9, col0+1+width-len(ending), ending)
491
- self.scr.refresh()
492
- curses.curs_set(2)
493
- answer = Textbox(win).edit(mod_key).strip()
494
- curses.curs_set(0)
495
- return answer
496
-
497
- def alert(self, title='ALERT', message='', height=1, width=80):
498
- """Alert box"""
499
- def mod_key(key):
500
- return 7 if key == 10 else key
501
-
502
- # need 3 extra cols for rectangle (so we don't draw in southeast corner)
503
- # and 3 rows (top/prompt/bottom)
504
- # +Prompt--- -----------------+
505
- # | First line for message... |
506
- # | Last line for message. |
507
- # +-----------Press ENTER to ack+
508
- if self.rows < 2+height or self.cols < 30:
509
- return
510
- width = min(width, self.cols-3) # max text width
511
- row0 = (self.rows+height-1)//2 - 1
512
- row9 = row0 + height + 1
513
- col0 = (self.cols - (width+2)) // 2
514
- col9 = col0 + width + 2 - 1
515
-
516
- self.scr.clear()
517
- for row in range(self.rows):
518
- self.scr.insstr(row, 0, ' '*self.cols, curses.A_REVERSE)
519
- pad = curses.newpad(20, 200)
520
- win = curses.newwin(1, 1, row9-1, col9-2) # input window
521
- rectangle(self.scr, row0, col0, row9, col9)
522
- self.scr.addstr(row0, col0+1, title[0:width], curses.A_REVERSE)
523
- pad.addstr(message)
524
- ending = 'Press ENTER to ack'[:width]
525
- self.scr.addstr(row9, col0+1+width-len(ending), ending)
526
- self.scr.refresh()
527
- pad.refresh(0, 0, row0+1, col0+1, row9-1, col9-1)
528
- Textbox(win).edit(mod_key).strip()
529
- return
530
-
531
- def clear(self):
532
- """Clear in prep for new screen"""
533
- self.scr.clear()
534
- self.head.pad.clear()
535
- self.body.pad.clear()
536
- self.head.texts, self.body.texts, self.last_pick_pos = [], [], -1
537
- self.head.row_cnt = self.body.row_cnt = 0
538
-
539
- def prompt(self, seconds=1.0):
540
- """Here is where we sleep waiting for commands or timeout"""
541
- ctl_b, ctl_d, ctl_f, ctl_u = 2, 4, 6, 21
542
- elapsed = 0.0
543
- while elapsed < seconds:
544
- key = self.scr.getch()
545
- if key == curses.ERR:
546
- elapsed += self.timeout_ms / 1000
547
- continue
548
- if key in (curses.KEY_RESIZE, ) or curses.is_term_resized(self.rows, self.cols):
549
- # self.scr.erase()
550
- self._set_screen_dims()
551
- # self.render()
552
- break
553
-
554
- # App keys...
555
- if key in self.handled_keys:
556
- return key # return for handling
557
-
558
- # Navigation Keys...
559
- pos = self.pick_pos if self.pick_mode else self.scroll_pos
560
- delta = self.pick_size if self.pick_mode else 1
561
- was_pos = pos
562
- if key in (ord('k'), curses.KEY_UP):
563
- pos -= delta
564
- elif key in (ord('j'), curses.KEY_DOWN):
565
- pos += delta
566
- elif key in (ctl_b, curses.KEY_PPAGE):
567
- pos -= self.scroll_view_size
568
- elif key in (ctl_u, ):
569
- pos -= self.scroll_view_size//2
570
- elif key in (ctl_f, curses.KEY_NPAGE):
571
- pos += self.scroll_view_size
572
- elif key in (ctl_d, ):
573
- pos += self.scroll_view_size//2
574
- elif key in (ord('0'), curses.KEY_HOME):
575
- pos = 0
576
- elif key in (ord('$'), curses.KEY_END):
577
- pos = self.body.row_cnt - 1
578
- elif key in (ord('H'), ):
579
- pos = self.scroll_pos
580
- elif key in (ord('M'), ):
581
- pos = self.scroll_pos + self.scroll_view_size//2
582
- elif key in (ord('L'), ):
583
- pos = self.scroll_pos + self.scroll_view_size-1
584
-
585
- if self.pick_mode:
586
- self.pick_pos = pos
587
- else:
588
- self.scroll_pos = pos
589
- self.pick_pos = pos
590
-
591
- if pos != was_pos:
592
- self.render()
593
- # ignore unhandled keys
594
- return None
595
-
596
- def no_runner():
597
- """Appease sbrun"""
598
-
599
- if __name__ == '__main__':
600
- def main():
601
- """Test program"""
602
- def do_key(key):
603
- nonlocal spin, win, opts
604
- value = spin.do_key(key, win)
605
- if key in (ord('p'), ord('s')):
606
- win.set_pick_mode(on=opts.pick_mode, pick_size=opts.pick_size)
607
- elif key == ord('n'):
608
- win.alert(title='Info', message=f'got: {value}')
609
- return value
610
-
611
- spin = OptionSpinner()
612
- spin.add_key('help_mode', '? - toggle help screen', vals=[False, True])
613
- spin.add_key('pick_mode', 'p - toggle pick mode', vals=[False, True])
614
- spin.add_key('pick_size', 's - #rows in pick', vals=[1, 2, 3])
615
- spin.add_key('name', 'n - select name', prompt='Provide Your Name:')
616
- spin.add_key('mult', 'm - row multiplier', vals=[0.5, 0.9, 1.0, 1.1, 2, 4, 16])
617
- opts = spin.default_obj
618
-
619
- win = Window(head_line=True, keys=spin.keys)
620
- opts.name = "[hit 'n' to enter name]"
621
- for loop in range(100000000000):
622
- body_size = int(round(win.scroll_view_size*opts.mult))
623
- if opts.help_mode:
624
- win.set_pick_mode(False)
625
- spin.show_help_nav_keys(win)
626
- spin.show_help_body(win)
627
- else:
628
- win.set_pick_mode(opts.pick_mode, opts.pick_size)
629
- win.add_header(f'Header: {loop} "{opts.name}"')
630
- for idx, line in enumerate(range(body_size//opts.pick_size)):
631
- win.add_body(f'Main pick: {loop}.{line}')
632
- for num in range(1, opts.pick_size):
633
- win.draw(num+idx*opts.pick_size, 0, f' addon: {loop}.{line}')
634
- win.render()
635
- do_key(win.prompt(seconds=5))
636
- win.clear()
637
-
638
- try:
639
- main()
640
- except KeyboardInterrupt:
641
- pass
642
- except Exception as exce:
643
- Window.stop_curses()
644
- print("exception:", str(exce))
645
- print(traceback.format_exc())
646
- if dump_str:
647
- print(dump_str)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes