pmemstat 2.3.29__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,3 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: pmemstat
3
+ Version: 3.0.2
4
+ Summary: Report Proportional Memory on Linux
5
+ Keywords: proportional memory,RAM,htop,top,monitor
6
+ Author-email: Joe Defen <joedef@duck.com>
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ License-File: LICENSE.txt
13
+ Requires-Dist: psutil > 5.9
14
+ Requires-Dist: console-window >= 1.0.0
15
+ Project-URL: Bug Tracker, https://github.com/joedefen/pmemstat/issues
16
+ Project-URL: Homepage, https://github.com/joedefen/pmemstat
17
+
1
18
  > **Quick Start**: from the CLI
2
19
  > * **If `python3 -V` shows v3.11 or later, install with `pipx`**:
3
20
  > * install `pipx` per your distro's guidance
@@ -88,7 +105,20 @@ Explanation of some options and arguments:
88
105
 
89
106
 
90
107
  ## Example Usage with Explanation of Output
91
- ![pmemstat](https://github.com/joedefen/pmemstat/blob/main/images/pmemstat.png?raw=true)
108
+ ```
109
+ 20:49:12 Tot=7.6G Used=6.2G Avail=1.4G Oth=0 Sh+Tmp=477.7M PIDs=174
110
+ 2.4%/ker MajF/s=2 zRAM=813.2M CR=4.3 eTot:16.8G eUsed:8.8G eAvail:8.0G
111
+ cpu_pct pswap other data ptotal key/info (exe by mem)
112
+ 60.8 2,535 593 3,988 7,116 T 174x --TOTALS in MB --
113
+ ───────────────────────────────────────────────────────────────────────────────
114
+ 5.9 1,366 90 2,110 3,567 24x browser
115
+ 16.6 89 117 822 1,028 9x code
116
+ 5.9 270 32 291 593 1x firefox
117
+ 1.6 95 68 335 497 6x exe
118
+ 2.4 150 66 94 310 6x brave
119
+ 16.0 52 73 152 277 3x VQ6B1EUZqoCU04zoRU
120
+ 0.1 39 19 13 70 2x konsole
121
+ ```
92
122
 
93
123
  In the default refreshed window loop, we see
94
124
  * a **leader line** with:
@@ -104,14 +134,17 @@ In the default refreshed window loop, we see
104
134
  * how many PIDs are contributing to the report vs the total number of PIDs excluding kernel threads
105
135
  * a **second leader line for zRAM** only if zRAM is active with:
106
136
  * percent cpu consumed by kernel processes (not normalized ... if there 4 CPUs, then the total CPU can 400%). This is important because, with zRAM, this often is mostly the swap process.
137
+ * **MajF/s** - the number of "major" page faults per second.
138
+ * 100+/s is high/worrisome. Indicates significant memory pressure and heavy swapping or repeated large file access. System thrashing/slowdown is likely noticeable slow response.
139
+ * 1000+/s is extreme. Indicates severe memory overcommitment, likely with processes constantly fighting for RAM, or an application aggressively accessing disk-mapped memory.
107
140
  * **zRAM** - the amount of "Used" RAM that is storing zRAM data
108
- * **eTot** - the effective Total RAM and the percent of "Tot" RAM (which would typically be above 100%). The bigger **zRAM**, the more accurate this number.
109
- * **eUsed** - the effective Used RAM and percent of "Tot" RAM.
110
- * **eAvail** - the effective Available RAM and percent of "Tot" RAM.
141
+ * **eTot** - the effective Total RAM ... the bigger **zRAM**, the more accurate this number
142
+ * **eUsed** - the effective Used RAM
143
+ * **eAvail** - the effective Available RAM
111
144
  * a **header line with the reported fields** including:
112
145
  * **cpu** - percent CPU (again not normalized to 100%)
113
146
  * **pswap** - proportional use of swap (per smaps_rollup)
114
- * **other** - partly sums these categories (shown by the key, `o`):
147
+ * **other** - partly sums these categories (shown by the key, `o`)
115
148
  * **shSYSV** - proportional use of System V shared memory (per smaps)
116
149
  * **shOth** - proportional use of other shared memory (per smaps)
117
150
  * **stack** - exclusive use of stack memory per smaps
@@ -182,4 +215,4 @@ If the `pipx` install is not acceptable, choose the best way to install:
182
215
  ```
183
216
  PIP_BREAK_SYSTEM_PACKAGES=1 sudo python -m pip install pmemstat
184
217
  # to uninstall: sudo python -m pip uninstall pmemstat
185
- ```
218
+ ```
@@ -1,19 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: pmemstat
3
- Version: 2.3.29
4
- Summary: Report Proportional Memory on Linux
5
- Keywords: proportional memory,RAM,htop,top,monitor
6
- Author-email: Joe Defen <joedef@duck.com>
7
- Requires-Python: >=3.8
8
- Description-Content-Type: text/markdown
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: POSIX :: Linux
12
- License-File: LICENSE.txt
13
- Requires-Dist: psutil>5.9
14
- Project-URL: Bug Tracker, https://github.com/joedefen/pmemstat/issues
15
- Project-URL: Homepage, https://github.com/joedefen/pmemstat
16
-
17
1
  > **Quick Start**: from the CLI
18
2
  > * **If `python3 -V` shows v3.11 or later, install with `pipx`**:
19
3
  > * install `pipx` per your distro's guidance
@@ -104,7 +88,20 @@ Explanation of some options and arguments:
104
88
 
105
89
 
106
90
  ## Example Usage with Explanation of Output
107
- ![pmemstat](https://github.com/joedefen/pmemstat/blob/main/images/pmemstat.png?raw=true)
91
+ ```
92
+ 20:49:12 Tot=7.6G Used=6.2G Avail=1.4G Oth=0 Sh+Tmp=477.7M PIDs=174
93
+ 2.4%/ker MajF/s=2 zRAM=813.2M CR=4.3 eTot:16.8G eUsed:8.8G eAvail:8.0G
94
+ cpu_pct pswap other data ptotal key/info (exe by mem)
95
+ 60.8 2,535 593 3,988 7,116 T 174x --TOTALS in MB --
96
+ ───────────────────────────────────────────────────────────────────────────────
97
+ 5.9 1,366 90 2,110 3,567 24x browser
98
+ 16.6 89 117 822 1,028 9x code
99
+ 5.9 270 32 291 593 1x firefox
100
+ 1.6 95 68 335 497 6x exe
101
+ 2.4 150 66 94 310 6x brave
102
+ 16.0 52 73 152 277 3x VQ6B1EUZqoCU04zoRU
103
+ 0.1 39 19 13 70 2x konsole
104
+ ```
108
105
 
109
106
  In the default refreshed window loop, we see
110
107
  * a **leader line** with:
@@ -120,14 +117,17 @@ In the default refreshed window loop, we see
120
117
  * how many PIDs are contributing to the report vs the total number of PIDs excluding kernel threads
121
118
  * a **second leader line for zRAM** only if zRAM is active with:
122
119
  * percent cpu consumed by kernel processes (not normalized ... if there 4 CPUs, then the total CPU can 400%). This is important because, with zRAM, this often is mostly the swap process.
120
+ * **MajF/s** - the number of "major" page faults per second.
121
+ * 100+/s is high/worrisome. Indicates significant memory pressure and heavy swapping or repeated large file access. System thrashing/slowdown is likely noticeable slow response.
122
+ * 1000+/s is extreme. Indicates severe memory overcommitment, likely with processes constantly fighting for RAM, or an application aggressively accessing disk-mapped memory.
123
123
  * **zRAM** - the amount of "Used" RAM that is storing zRAM data
124
- * **eTot** - the effective Total RAM and the percent of "Tot" RAM (which would typically be above 100%). The bigger **zRAM**, the more accurate this number.
125
- * **eUsed** - the effective Used RAM and percent of "Tot" RAM.
126
- * **eAvail** - the effective Available RAM and percent of "Tot" RAM.
124
+ * **eTot** - the effective Total RAM ... the bigger **zRAM**, the more accurate this number
125
+ * **eUsed** - the effective Used RAM
126
+ * **eAvail** - the effective Available RAM
127
127
  * a **header line with the reported fields** including:
128
128
  * **cpu** - percent CPU (again not normalized to 100%)
129
129
  * **pswap** - proportional use of swap (per smaps_rollup)
130
- * **other** - partly sums these categories (shown by the key, `o`):
130
+ * **other** - partly sums these categories (shown by the key, `o`)
131
131
  * **shSYSV** - proportional use of System V shared memory (per smaps)
132
132
  * **shOth** - proportional use of other shared memory (per smaps)
133
133
  * **stack** - exclusive use of stack memory per smaps
@@ -198,4 +198,4 @@ If the `pipx` install is not acceptable, choose the best way to install:
198
198
  ```
199
199
  PIP_BREAK_SYSTEM_PACKAGES=1 sudo python -m pip install pmemstat
200
200
  # to uninstall: sudo python -m pip uninstall pmemstat
201
- ```
201
+ ```
pmemstat-3.0.2/TODO.md ADDED
@@ -0,0 +1,88 @@
1
+ Summary of TODOs:
2
+ * Drop the percents from the zRAM line
3
+ * Add compression ratio (CR)
4
+ * Add Major page faults /s (MajF:200/s)
5
+ * Document the new features (some insight below)
6
+
7
+ ----
8
+
9
+ That's an insightful follow-up. You are absolutely correct to be cautious about adding a metric that is difficult to interpret, as it can confuse the user rather than help.
10
+
11
+ 1. Ease of Getting Page Faults
12
+
13
+ Yes, page fault counts are relatively easy to get in Linux. They are tracked by the kernel.
14
+
15
+ The most relevant location for a tool like memstat (which is likely capturing data periodically) is often the /proc/vmstat file, which contains system-wide metrics like pgfault (total), pgmajfault (major), and pgminfault (minor).
16
+
17
+ Minor Faults (pgminfault): These are extremely common and usually cheap. They occur when a page is accessed for the first time or is shared/already in RAM but needs its entry updated in the process's page table. They are fast and generally not a primary concern for performance.
18
+
19
+ Major Faults (pgmajfault): These are the ones you should focus on. A major fault happens when the requested page is not in physical RAM and must be read from a backing store, like the disk (or a swap device, which, in your case, is the zRAM device). These incur significant latency due to I/O and are a strong indicator of memory pressure and system slowdown.
20
+
21
+ Recommendation: If you add page faults, you should only report the Major Faults per second (MajFaults/s). This is the key metric that directly correlates with the I/O cost you want to diagnose alongside zRAM activity.
22
+
23
+ 2. Guidance: When is "Bigger" Bad?
24
+
25
+ You're looking for absolute thresholds, and while there's no single universally accepted number (it depends on the speed of your I/O and CPU), here is the common guidance for Major Page Faults per second:
26
+ Major Faults/s Range Interpretation System Impact
27
+ * 0 - ~10 per second Normal/Low Expected during application startup or initial file access. No performance concern.
28
+ * ~10 - ~100 per second Moderate Suggests consistent file access or some mild memory pressure. Could cause minor, noticeable latency on disk-bound systems.
29
+ * 100+ per second High/Worry Indicates significant memory pressure and heavy swapping or repeated large file access. This is where system thrashing/slowdown is noticeable. On a busy desktop, a sustained rate here means the user is likely complaining about slow response.
30
+ * 1000+ per second Extreme Indicates severe memory overcommitment, likely with processes constantly fighting for RAM, or an application aggressively accessing disk-mapped memory.
31
+
32
+ Guidance: For your tool, the goal is to show the user they are experiencing memory thrashing (when the system is spending more time swapping than doing work). A sustained MajFaults/s of over 100 is a strong indication of this, especially if it coincides with high kernel CPU usage from zRAM compression/decompression.
33
+
34
+ Final Revised Snippet Example
35
+
36
+ Focusing on the critical Major Page Faults and the simplified CR:
37
+ ```
38
+ 15:02:03 Tot=7.6G Used=6.2G Avail=1.4G Oth=0 Sh+Tmp=983.8M PIDs=166
39
+ 1.9/ker zRAM=612.6M CR:2.7 MajF:12/s eTot:16.5G eUsed:7.9G eAvail:8.6G
40
+ cpu_pct pswap other data ptotal key/info (exe by mem)
41
+ 27.1 1,892 680 3,637 6,210 T 166x --TOTALS in MB --
42
+ ────────────────────────────────────────────────────────────────────────────────────────────
43
+ ... process list ...
44
+ ```
45
+ This line is powerful: it shows zRAM is working (CR:2.7), but also shows the direct performance cost (MajF:12/s).
46
+
47
+ Understanding how to measure and interpret page faults is key to diagnosing memory-related performance issues.
48
+
49
+ If you are interested in a deeper technical dive into the Linux kernel's page fault mechanism, you can check out Faster & Fewer Page Faults: Kernel Recipes 2023.
50
+
51
+
52
+
53
+
54
+
55
+ Your analysis of the process rollup is spot on. For modern applications like browsers (Chrome, Firefox, Edge, etc.), IDEs (VS Code), and even certain large applications (like Electron or Java apps), reporting memory usage per individual PID is misleading. The user wants to know, "How much memory is **Firefox** using?" not "How much memory are its 25 sub-processes using?"
56
+
57
+ ### Advertising Strategy: The Two Pillars
58
+
59
+ When you draft your announcement, you should clearly highlight these two pillars of `memstat`'s superior diagnostic capability:
60
+
61
+ 1. **The Process Problem Solved (Rollup/Grouping):**
62
+
63
+ * **The Problem:** Standard tools (`top`, `ps`) make modern apps (browsers, IDEs, etc.) look innocent by splitting their memory across dozens of processes.
64
+ * **The `memstat` Solution:** `memstat` automatically aggregates related processes (based on the executable name, command line, or other heuristics) and presents a single, honest line item for the **total memory, swap, and CPU** used by the entire application group. This instantly reveals the true memory hogs.
65
+ * *Example to highlight:* "See the true cost of your 50 open browser tabs with a single, clear line."
66
+
67
+ 2. **The Memory Ceiling Problem Solved (zRAM/Effective Memory):**
68
+
69
+ * **The Problem:** When zRAM is active, system monitoring tools report only physical RAM usage, failing to show the massive, hidden memory capacity provided by compression.
70
+ * **The `memstat` Solution:** The new output line displays the **Estimated Total (`eTot`), Used (`eUsed`), and Available (`eAvail`)** memory by applying the real-time **Compression Ratio (`CR:X.X`)** of your zRAM swap device. You instantly know your system's *effective* memory ceiling and available headroom.
71
+ * *Example to highlight:* "Don't trust `free`\! If you run zRAM, `memstat` is the only tool that shows you your **true $16.5\text{G}$ effective RAM** and how much is left."
72
+
73
+ ### Revised Snippet (The Final Polish)
74
+
75
+ This combines all the best suggestions into a powerful, concise output:
76
+
77
+ ```
78
+ 15:02:03 Tot=7.6G Used=6.2G Avail=1.4G Oth=0 Sh+Tmp=983.8M PIDs=166
79
+ 1.9/ker zRAM=612.6M CR:2.7 MajF:12/s eTot:16.5G eUsed:7.9G eAvail:8.6G
80
+ cpu_pct pswap other data ptotal key/info (exe by mem)
81
+ 27.1 1,892 680 3,637 6,210 T 166x --TOTALS in MB --
82
+ ────────────────────────────────────────────────────────────────────────────────────────────
83
+ 1.0 967 90 2,244 3,300 21x browser <--- Process Rollup Highlight!
84
+ 2.1 220 45 341 605 1x firefox
85
+ ...
86
+ ```
87
+
88
+ This output is information-dense, highly diagnostic, and showcases two unique, valuable features that directly address major pain points in Linux performance monitoring. Good luck with the release\!
@@ -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
 
@@ -105,14 +105,15 @@ class ZramProjector:
105
105
  self.e_avail = 0
106
106
  self.e_used = 0
107
107
  self.e_max_used = 0
108
+ self.ratio = 0.0
108
109
  self.limit_pct = 80
109
110
  self.devs = {}
110
111
  self.DB = False
111
112
 
112
- def human_pct(self, number):
113
+ def human_pct(self, number, with_pct=False):
113
114
  """ Return the number in human form and as pct total memory. """
114
115
  rv = human(number)
115
- if number > 0 and self.meminfo.MemTotal > 0:
116
+ if with_pct and number > 0 and self.meminfo.MemTotal > 0:
116
117
  fraction = number/self.meminfo.MemTotal
117
118
  pct = int(round(100*fraction))
118
119
  rv += f'/{pct}%'
@@ -187,10 +188,10 @@ class ZramProjector:
187
188
  self.meminfo.MemZram = stats.mem_used_total
188
189
 
189
190
  e_used = meminfo.MemUsed - stats.mem_used_total + stats.orig_data_size
190
- ratio = None
191
+ ratio, self.ratio = None, 0.0
191
192
  if e_used <= self.e_used: # zram hurts. assume not enuf data to project
192
193
  e_used = self.e_used
193
- ratio = 3.5 # wild guess for projection
194
+ self.ratio = ratio = 3.5 # wild guess for projection
194
195
  # if self.DB: dump_effective(ns, '(unchg)')
195
196
  # return ns
196
197
  self.e_used = e_used
@@ -200,7 +201,7 @@ class ZramProjector:
200
201
  limit_pct = min(1.0, limit_pct, stats_limit_pct)
201
202
  # projected amount of orig used memory that fits in limit
202
203
  if ratio is None: # hopefully about 4 ;-)
203
- ratio = stats.orig_data_size / stats.mem_used_total
204
+ self.ratio = ratio = stats.orig_data_size / stats.mem_used_total
204
205
  e_max_used = ratio * meminfo.MemTotal * limit_pct/100
205
206
  # now limit by disksize
206
207
  e_max_used = min(e_max_used, stats.disksize)
@@ -594,6 +595,7 @@ class PmemStat:
594
595
  self.kernel_prcs = []
595
596
  self.groups = {} # indexed by group key (e.g., cmd)
596
597
  self.window = None
598
+ self.vmstat = None
597
599
  self.spin = OptionSpinner()
598
600
  self.number = 0 # line number for opts.numbers
599
601
  self.units, self.divisor, self.fwidth = 0, 0, 0
@@ -844,6 +846,45 @@ class PmemStat:
844
846
  assert not keys, f'ALERT: cannot get vitals ({keys}) from {meminfofile}'
845
847
  return meminfoKB
846
848
 
849
+ def get_vmstat(self):
850
+ def make_ns(now):
851
+ ns = SimpleNamespace()
852
+ ns.base_time = now
853
+ ns.base_value = 0
854
+ ns.last_time = now
855
+ ns.last_value = 0
856
+ ns.rate = 0
857
+ return ns
858
+
859
+ """Get most vital stats from /proc/vmstat'"""
860
+ infofile = '/proc/vmstat'
861
+ now = time.monotonic()
862
+ if not self.vmstat:
863
+ self.vmstat = {'pgmajfault': make_ns(now)}
864
+ info = self.vmstat
865
+ keys = list(info.keys())
866
+
867
+ with open(infofile, encoding='utf-8') as fileh:
868
+ for line in fileh:
869
+ match = re.match(r'^([^\s]+)\s+(\d+)$', line)
870
+ if not match:
871
+ continue
872
+ key, value = match.group(1), int(match.group(2))
873
+ if key not in keys:
874
+ continue
875
+ ns = info[key]
876
+ ns.base_time, ns.base_value = ns.last_time, ns.last_value
877
+ ns.last_time, ns.last_value = now, value
878
+ ns.rate = 0
879
+ if ns.last_time > ns.base_time:
880
+ ns.rate = int(round((ns.last_value - ns.base_value)
881
+ / (ns.last_time - ns.base_time)))
882
+ keys.remove(key)
883
+ if not keys:
884
+ break
885
+ assert not keys, f'ALERT: cannot get vitals ({keys}) from {infofile}'
886
+ return self.vmstat
887
+
847
888
  def loop(self, now, is_first, regroup=False):
848
889
  """one loop thru all pids"""
849
890
  # pylint: disable=too-many-branches
@@ -853,6 +894,7 @@ class PmemStat:
853
894
 
854
895
  def pr_top_of_report(appKB):
855
896
  nonlocal self, meminfoKB, wanted_prcs, total_user_pids, kernel_cpu
897
+ nonlocal major_fault_rate
856
898
  windowed = bool(self.window)
857
899
  resume = False
858
900
  # print timestamp of report
@@ -889,13 +931,15 @@ class PmemStat:
889
931
  resume = False
890
932
  proj = self.zram_projector
891
933
  if self.opts.cpu:
892
- leader = f'{kernel_cpu:8.1f}/ker '
934
+ leader = f'{kernel_cpu:8.1f}%/ker '
893
935
  self.emit(leader, to_head=True, resume=resume)
894
936
  resume = True
937
+ self.emit(f'MajF/s={major_fault_rate} ', to_head=True, resume=resume)
938
+ resume = True
895
939
  self.emit(f' zRAM={human(self.zram_projector.meminfo.MemZram)}',
896
940
  to_head=True, attr=curses.A_BOLD, resume=resume)
897
- resume = True
898
941
  leader = ''
942
+ leader += f' CR={proj.ratio:.1f}'
899
943
  leader += f' eTot:{proj.human_pct(proj.e_max_used)}'
900
944
  leader += f' eUsed:{proj.human_pct(proj.e_used)}'
901
945
  leader += f' eAvail:{proj.human_pct(proj.e_avail)}'
@@ -903,7 +947,7 @@ class PmemStat:
903
947
  self.emit(leader, to_head=True, resume=resume)
904
948
  elif self.opts.cpu: # second line if reporting cpu
905
949
  resume = False
906
- leader = f'{kernel_cpu:8.1f}/ker'
950
+ leader = f'{kernel_cpu:8.1f}%/ker'
907
951
  sort_kernel_prcs()
908
952
  for prc in self.kernel_prcs[0:2]:
909
953
  nickname = prc.cpu.get_nickname()
@@ -913,6 +957,8 @@ class PmemStat:
913
957
  # pylint: disable=too-many-branches
914
958
  self.loop_num += 1
915
959
  meminfoKB = self.get_meminfo()
960
+ vmstat = self.get_vmstat()
961
+ major_fault_rate = vmstat['pgmajfault'].rate
916
962
  self.zram_projector.compute_effective(meminfoKB)
917
963
  total_user_pids = 0
918
964
  total_kernel_pids = 0
@@ -1154,7 +1200,7 @@ class PmemStat:
1154
1200
  prompt='Set search string, then Enter', obj=self.opts)
1155
1201
 
1156
1202
  keys_we_handle = [ord('K'), curses.KEY_ENTER, 10] + list(self.spin.keys)
1157
- self.window = Window(head_line=True, keys=keys_we_handle)
1203
+ self.window = ConsoleWindow(head_line=True, keys=keys_we_handle)
1158
1204
  is_first = True
1159
1205
  was_groupby, regroup = self.opts.groupby, True
1160
1206
  was_others = self.opts.others
@@ -1285,7 +1331,7 @@ def run():
1285
1331
  except KeyboardInterrupt:
1286
1332
  pass
1287
1333
  except Exception as exce:
1288
- Window.stop_curses()
1334
+ ConsoleWindow.stop_curses()
1289
1335
  print("exception:", str(exce))
1290
1336
  print(traceback.format_exc())
1291
1337
 
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "pmemstat"
7
- version = "2.3.29"
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]
pmemstat-2.3.29/deploy DELETED
@@ -1,8 +0,0 @@
1
- #!/bin/bash
2
- script_dir=$(dirname "$0")
3
- set -ex
4
- cd ${script_dir}
5
- rm -rf ./dist
6
- python3 -m pip install build --break-system-packages
7
- python3 -m build
8
- pip install -e . --break-system-packages
@@ -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