monitorify 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.
Files changed (37) hide show
  1. monitorify/__init__.py +3 -0
  2. monitorify/__main__.py +4 -0
  3. monitorify/collector/__init__.py +5 -0
  4. monitorify/collector/manager.py +48 -0
  5. monitorify/collector/schema.py +22 -0
  6. monitorify/collector/worker.py +44 -0
  7. monitorify/config.py +26 -0
  8. monitorify/daemon.py +32 -0
  9. monitorify/main.py +53 -0
  10. monitorify/stats/DiskStats.py +108 -0
  11. monitorify/stats/__init__.py +1 -0
  12. monitorify/stats/cpuStats.py +46 -0
  13. monitorify/stats/networkStats.py +69 -0
  14. monitorify/stats/programmList.py +272 -0
  15. monitorify/stats/ramStats.py +18 -0
  16. monitorify/storage/__init__.py +3 -0
  17. monitorify/storage/db.py +154 -0
  18. monitorify/ui/__init__.py +1 -0
  19. monitorify/ui/tui/__init__.py +1 -0
  20. monitorify/ui/tui/components/menu.py +180 -0
  21. monitorify/ui/tui/components/widgetManager.py +43 -0
  22. monitorify/ui/tui/css/tui.css +168 -0
  23. monitorify/ui/tui/tui.py +187 -0
  24. monitorify/ui/tui/widgets/__init__.py +1 -0
  25. monitorify/ui/tui/widgets/brailleGraph.py +261 -0
  26. monitorify/ui/tui/widgets/cpuWidget.py +46 -0
  27. monitorify/ui/tui/widgets/diskWidget.py +207 -0
  28. monitorify/ui/tui/widgets/networkWidget.py +98 -0
  29. monitorify/ui/tui/widgets/procInfoWidget.py +44 -0
  30. monitorify/ui/tui/widgets/programmListWidget.py +202 -0
  31. monitorify/ui/tui/widgets/ramWidget.py +36 -0
  32. monitorify/ui/tui/widgets/statusWidget.py +45 -0
  33. monitorify-1.0.0.dist-info/METADATA +135 -0
  34. monitorify-1.0.0.dist-info/RECORD +37 -0
  35. monitorify-1.0.0.dist-info/WHEEL +5 -0
  36. monitorify-1.0.0.dist-info/entry_points.txt +3 -0
  37. monitorify-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,207 @@
1
+ # pyrefly: ignore [missing-import]
2
+ from textual.widgets import Static
3
+ # pyrefly: ignore [missing-import]
4
+ from textual.containers import Container, Vertical
5
+ from monitorify.ui.tui.widgets.brailleGraph import BrailleGraph, make_time_grid
6
+
7
+
8
+ def format_bps(bps: float) -> str:
9
+ if bps >= 1024 * 1024 * 1024:
10
+ return f"{bps / (1024 ** 3):.1f} GB/s"
11
+ elif bps >= 1024 * 1024:
12
+ return f"{bps / (1024 * 1024):.1f} MB/s"
13
+ elif bps >= 1024:
14
+ return f"{bps / 1024:.1f} KB/s"
15
+ else:
16
+ return f"{bps:.0f} B/s"
17
+
18
+
19
+ def format_bytes(b: float) -> str:
20
+ if b >= 1024 ** 4:
21
+ return f"{b / (1024 ** 4):.1f} TB"
22
+ elif b >= 1024 ** 3:
23
+ return f"{b / (1024 ** 3):.1f} GB"
24
+ elif b >= 1024 ** 2:
25
+ return f"{b / (1024 ** 2):.1f} MB"
26
+ elif b >= 1024:
27
+ return f"{b / 1024:.1f} KB"
28
+ else:
29
+ return f"{b:.0f} B"
30
+
31
+
32
+ def format_storage_summary(disk_storage: dict | None) -> str:
33
+ if not disk_storage or not isinstance(disk_storage, dict):
34
+ return "Storage: –"
35
+
36
+ total = sum(d.get("total", 0) for d in disk_storage.values() if isinstance(d, dict))
37
+ used = sum(d.get("used", 0) for d in disk_storage.values() if isinstance(d, dict))
38
+ pct = (used / total * 100) if total > 0 else 0
39
+ return f"{format_bytes(used)}/{format_bytes(total)} ({pct:.0f}%)"
40
+
41
+
42
+ class DiskRatePoint:
43
+ def __init__(self, timestamp: float, read_bps: float, write_bps: float):
44
+ self.timestamp = timestamp
45
+ self.read_bps = read_bps
46
+ self.write_bps = write_bps
47
+
48
+
49
+ def compute_disk_rates(snapshots: list) -> list[DiskRatePoint]:
50
+ points = []
51
+ prev_ts = None
52
+ prev_read = None
53
+ prev_write = None
54
+
55
+ for s in snapshots:
56
+ if s.disk_io is None or not isinstance(s.disk_io, dict):
57
+ prev_ts = None
58
+ continue
59
+
60
+ curr_read = sum(d.get("read_bytes", 0) for d in s.disk_io.values() if isinstance(d, dict))
61
+ curr_write = sum(d.get("write_bytes", 0) for d in s.disk_io.values() if isinstance(d, dict))
62
+
63
+ if prev_ts is not None:
64
+ dt = s.timestamp - prev_ts
65
+ if 0 < dt <= 10.0:
66
+ r_bps = max(0.0, curr_read - prev_read) / dt
67
+ w_bps = max(0.0, curr_write - prev_write) / dt
68
+ points.append(DiskRatePoint(s.timestamp, r_bps, w_bps))
69
+ else:
70
+ points.append(DiskRatePoint(s.timestamp, 0.0, 0.0))
71
+ else:
72
+ points.append(DiskRatePoint(s.timestamp, 0.0, 0.0))
73
+
74
+ prev_ts = s.timestamp
75
+ prev_read = curr_read
76
+ prev_write = curr_write
77
+
78
+ return points
79
+
80
+
81
+ class DiskInfo(Container):
82
+ def compose(self):
83
+ self.write_current = Static("▲ 0 B/s", classes="tx_text")
84
+ self.write_top = Static("▲ Top: 0 B/s", classes="tx_text")
85
+
86
+ self.read_current = Static("▼ 0 B/s", classes="rx_text")
87
+ self.read_top = Static("▼ Top: 0 B/s", classes="rx_text")
88
+
89
+ self.storage_label = Static("Storage: –", classes="storage_text")
90
+
91
+ yield self.write_current
92
+ yield self.write_top
93
+ yield Static("") # Spacer
94
+ yield self.read_current
95
+ yield self.read_top
96
+ yield Static("") # Spacer
97
+ yield self.storage_label
98
+
99
+
100
+ class DiskWidget(Container):
101
+ BORDER_TITLE = "DISK"
102
+
103
+ def __init__(self, **kwargs):
104
+ super().__init__(**kwargs)
105
+ self.top_read: float = 0.0
106
+ self.top_write: float = 0.0
107
+ self.current_read: float = 0.0
108
+ self.current_write: float = 0.0
109
+ self.last_timestamp: float | None = None
110
+ self.last_read_bytes: float = 0.0
111
+ self.last_write_bytes: float = 0.0
112
+
113
+ def compose(self):
114
+ self.disk_graph = BrailleGraph(
115
+ data_lines=2,
116
+ color1="#c678dd",
117
+ color2="#61afef",
118
+ show_scale=True,
119
+ scale_fmt=format_bps,
120
+ show_timeline=True,
121
+ id="disk_graph",
122
+ )
123
+ yield self.disk_graph
124
+
125
+ with Vertical(id="disk_info_wrapper"):
126
+ self.disk_info = DiskInfo(id="disk_info")
127
+ self.disk_info.border_title = "write"
128
+ self.disk_info.border_subtitle = "read"
129
+ yield self.disk_info
130
+
131
+ def _calculate_rates(self, snapshot) -> tuple[float, float]:
132
+ if snapshot.disk_io is None or not isinstance(snapshot.disk_io, dict):
133
+ return 0.0, 0.0
134
+
135
+ total_read = sum(d.get("read_bytes", 0) for d in snapshot.disk_io.values() if isinstance(d, dict))
136
+ total_write = sum(d.get("write_bytes", 0) for d in snapshot.disk_io.values() if isinstance(d, dict))
137
+
138
+ if self.last_timestamp is None:
139
+ self.last_timestamp = snapshot.timestamp
140
+ self.last_read_bytes = total_read
141
+ self.last_write_bytes = total_write
142
+ return 0.0, 0.0
143
+
144
+ if snapshot.timestamp <= self.last_timestamp:
145
+ return self.current_read, self.current_write
146
+
147
+ dt = snapshot.timestamp - self.last_timestamp
148
+ if 0 < dt <= 10.0:
149
+ read_bps = max(0.0, total_read - self.last_read_bytes) / dt
150
+ write_bps = max(0.0, total_write - self.last_write_bytes) / dt
151
+ else:
152
+ read_bps = 0.0
153
+ write_bps = 0.0
154
+
155
+ self.last_timestamp = snapshot.timestamp
156
+ self.last_read_bytes = total_read
157
+ self.last_write_bytes = total_write
158
+ self.current_read = read_bps
159
+ self.current_write = write_bps
160
+
161
+ return read_bps, write_bps
162
+
163
+ def update_disk(self, snapshot) -> None:
164
+ """Update labels and append one live data point to the graph."""
165
+ read_bps, write_bps = self._calculate_rates(snapshot)
166
+ self.top_read = max(self.top_read, read_bps)
167
+ self.top_write = max(self.top_write, write_bps)
168
+ self._refresh_labels(read_bps, write_bps, snapshot.disk_storage)
169
+ self.disk_graph.update_value(write_bps, read_bps)
170
+
171
+ def update_labels(self, snapshot) -> None:
172
+ """Update only the text labels — does not touch the graph."""
173
+ read_bps, write_bps = self._calculate_rates(snapshot)
174
+ self.top_read = max(self.top_read, read_bps)
175
+ self.top_write = max(self.top_write, write_bps)
176
+ self._refresh_labels(read_bps, write_bps, snapshot.disk_storage)
177
+
178
+ def _refresh_labels(self, read_bps: float, write_bps: float, disk_storage: dict | None) -> None:
179
+ info = self.disk_info
180
+ info.write_current.update(f"▲ {format_bps(write_bps)}")
181
+ info.write_top.update(f"▲ Top: {format_bps(self.top_write)}")
182
+ info.read_current.update(f"▼ {format_bps(read_bps)}")
183
+ info.read_top.update(f"▼ Top: {format_bps(self.top_read)}")
184
+ info.storage_label.update(format_storage_summary(disk_storage))
185
+
186
+ def load_history(self, snapshots: list, time_span: float = 0.0) -> None:
187
+ """Bulk-load disk I/O history, time-positioned to show real gaps."""
188
+ w = self.disk_graph.size.width * 2 or 400
189
+ rate_points = compute_disk_rates(snapshots)
190
+ read_vals = make_time_grid(rate_points, lambda p: p.read_bps, time_span, w)
191
+ write_vals = make_time_grid(rate_points, lambda p: p.write_bps, time_span, w)
192
+
193
+ real_read = [v for v in read_vals if v is not None]
194
+ real_write = [v for v in write_vals if v is not None]
195
+ if real_read:
196
+ self.top_read = max(real_read)
197
+ if real_write:
198
+ self.top_write = max(real_write)
199
+
200
+ if snapshots:
201
+ last = snapshots[-1]
202
+ if last.disk_io and isinstance(last.disk_io, dict):
203
+ self.last_timestamp = last.timestamp
204
+ self.last_read_bytes = sum(d.get("read_bytes", 0) for d in last.disk_io.values() if isinstance(d, dict))
205
+ self.last_write_bytes = sum(d.get("write_bytes", 0) for d in last.disk_io.values() if isinstance(d, dict))
206
+
207
+ self.disk_graph.load_history(write_vals, read_vals, timeline_seconds=time_span)
@@ -0,0 +1,98 @@
1
+ # pyrefly: ignore [missing-import]
2
+ from textual.widgets import Static
3
+ # pyrefly: ignore [missing-import]
4
+ from textual.containers import Container, Vertical
5
+ from monitorify.ui.tui.widgets.brailleGraph import BrailleGraph, make_time_grid
6
+
7
+
8
+ def format_bps(bps):
9
+ if bps >= 1024 * 1024:
10
+ return f"{bps / (1024 * 1024):.1f} MB/s"
11
+ elif bps >= 1024:
12
+ return f"{bps / 1024:.1f} KB/s"
13
+ else:
14
+ return f"{bps:.0f} B/s"
15
+
16
+ def format_bytes(b):
17
+ if b >= 1024 ** 3:
18
+ return f"{b / (1024 ** 3):.1f} GB"
19
+ elif b >= 1024 ** 2:
20
+ return f"{b / (1024 ** 2):.1f} MB"
21
+ elif b >= 1024:
22
+ return f"{b / 1024:.1f} KB"
23
+ else:
24
+ return f"{b:.0f} B"
25
+
26
+
27
+ class NetworkInfo(Container):
28
+ def compose(self):
29
+ self.rx_current = Static("▼ 0 B/s", classes="rx_text")
30
+ self.rx_top = Static("▼ Top: 0 B/s", classes="rx_text")
31
+
32
+ self.tx_current = Static("▲ 0 B/s", classes="tx_text")
33
+ self.tx_top = Static("▲ Top: 0 B/s", classes="tx_text")
34
+
35
+ yield self.rx_current
36
+ yield self.rx_top
37
+ yield Static("") # Spacer
38
+ yield self.tx_current
39
+ yield self.tx_top
40
+
41
+
42
+ class NetworkWidget(Container):
43
+ BORDER_TITLE = "NET"
44
+
45
+ def __init__(self, **kwargs):
46
+ super().__init__(**kwargs)
47
+ self.top_rx = 0
48
+ self.top_tx = 0
49
+
50
+ def compose(self):
51
+ self.network_graph = BrailleGraph(
52
+ data_lines=2, color1="#61afef", color2="#c678dd",
53
+ show_scale=True, scale_fmt=format_bps,
54
+ show_timeline=True,
55
+ id="network_graph"
56
+ )
57
+ yield self.network_graph
58
+
59
+ with Vertical(id="network_info_wrapper"):
60
+ self.network_info = NetworkInfo(id="network_info")
61
+ self.network_info.border_title = "download"
62
+ self.network_info.border_subtitle = "upload"
63
+ yield self.network_info
64
+
65
+ def update_network(self, snapshot) -> None:
66
+ """Update labels and append one live data point to the graph."""
67
+ rx_bps, tx_bps = snapshot.network
68
+ self.top_rx = max(self.top_rx, rx_bps)
69
+ self.top_tx = max(self.top_tx, tx_bps)
70
+ self._refresh_labels(rx_bps, tx_bps)
71
+ self.network_graph.update_value(rx_bps, tx_bps)
72
+
73
+ def update_labels(self, snapshot) -> None:
74
+ """Update only the text labels — does not touch the graph."""
75
+ rx_bps, tx_bps = snapshot.network
76
+ self.top_rx = max(self.top_rx, rx_bps)
77
+ self.top_tx = max(self.top_tx, tx_bps)
78
+ self._refresh_labels(rx_bps, tx_bps)
79
+
80
+ def _refresh_labels(self, rx_bps: float, tx_bps: float) -> None:
81
+ info = self.network_info
82
+ info.rx_current.update(f"▼ {format_bps(rx_bps)}")
83
+ info.rx_top.update(f"▼ Top: {format_bps(self.top_rx)}")
84
+ info.tx_current.update(f"▲ {format_bps(tx_bps)}")
85
+ info.tx_top.update(f"▲ Top: {format_bps(self.top_tx)}")
86
+
87
+ def load_history(self, snapshots: list, time_span: float = 0.0) -> None:
88
+ """Bulk-load network history, time-positioned to show real gaps."""
89
+ w = self.network_graph.size.width * 2 or 400
90
+ rx_vals = make_time_grid(snapshots, lambda s: s.network[0], time_span, w)
91
+ tx_vals = make_time_grid(snapshots, lambda s: s.network[1], time_span, w)
92
+ real_rx = [v for v in rx_vals if v is not None]
93
+ real_tx = [v for v in tx_vals if v is not None]
94
+ if real_rx:
95
+ self.top_rx = max(real_rx)
96
+ if real_tx:
97
+ self.top_tx = max(real_tx)
98
+ self.network_graph.load_history(rx_vals, tx_vals, timeline_seconds=time_span)
@@ -0,0 +1,44 @@
1
+ # pyrefly: ignore [missing-import]
2
+ from textual.containers import Container
3
+ # pyrefly: ignore [missing-import]
4
+ from textual.widgets import Static
5
+ from monitorify.stats.programmList import get_process_state, get_process_running_time
6
+
7
+
8
+ class ProcInfoWidget(Container):
9
+ BORDER_TITLE = "Process"
10
+
11
+ def compose(self):
12
+ self.pid_label = Static("PID: -")
13
+ yield self.pid_label
14
+
15
+ self.name_label = Static("Name: -")
16
+ yield self.name_label
17
+
18
+ self.state_label = Static("State: -")
19
+ yield self.state_label
20
+
21
+ self.cpu_label = Static("CPU: -")
22
+ yield self.cpu_label
23
+
24
+ self.ram_label = Static("RAM: -")
25
+ yield self.ram_label
26
+
27
+ self.uptime_label = Static("Uptime: -")
28
+ yield self.uptime_label
29
+
30
+ def update_process(self, d: dict):
31
+ pid = d.get("pid", 0)
32
+ name = d.get("name", "Unknown")
33
+ state = d.get("state") or (get_process_state(pid) if pid else "Unknown")
34
+ uptime = d.get("running_time") or (get_process_running_time(pid) if pid else "Unknown")
35
+ cpu = d.get("cpu_ussage", 0.0)
36
+ ram = d.get("ram_ussage", 0.0)
37
+
38
+ self.border_title = f"PID: {pid}"
39
+ self.pid_label.update(f"PID: {pid}")
40
+ self.name_label.update(f"Name: {name}")
41
+ self.state_label.update(f"State: {state}")
42
+ self.cpu_label.update(f"CPU: {cpu:.1f}%")
43
+ self.ram_label.update(f"RAM: {ram:.1f} MB")
44
+ self.uptime_label.update(f"Uptime: {uptime}")
@@ -0,0 +1,202 @@
1
+ # pyrefly: ignore [missing-import]
2
+ from textual.containers import Container
3
+ # pyrefly: ignore [missing-import]
4
+ from textual.widgets import ListItem, ListView, Static
5
+ from textual import on
6
+ from textual.events import Click
7
+ from monitorify.stats.programmList import (
8
+ get_latest_process_items_data,
9
+ start_process_cache_thread,
10
+ _format_process_item,
11
+ )
12
+ from monitorify.ui.tui.widgets.procInfoWidget import ProcInfoWidget
13
+
14
+
15
+ class FastListView(ListView):
16
+ """ListView subclass with instant reactive callbacks on scroll or index changes."""
17
+
18
+ def watch_scroll_y(self, old_value: float, new_value: float) -> None:
19
+ super().watch_scroll_y(old_value, new_value)
20
+ if hasattr(self, "on_scroll_cb") and self.on_scroll_cb:
21
+ self.on_scroll_cb()
22
+
23
+ def watch_index(self, old_value: int | None, new_value: int | None) -> None:
24
+ super().watch_index(old_value, new_value)
25
+ if hasattr(self, "on_scroll_cb") and self.on_scroll_cb:
26
+ self.on_scroll_cb()
27
+ if hasattr(self, "on_highlight_cb") and self.on_highlight_cb:
28
+ self.on_highlight_cb()
29
+
30
+
31
+ class ProgrammListWidget(Container):
32
+ BORDER_TITLE = "PROC"
33
+
34
+ def __init__(self, **kwargs):
35
+ super().__init__(**kwargs)
36
+ self.last_cache_version = -1
37
+ self.rendered_limit = 0
38
+ self.sort_by = "cpu_ussage"
39
+ self.sort_reverse = True
40
+ self.current_sorted_data = []
41
+ self.total_count = 0
42
+ self.active_info_pid = None
43
+
44
+ def compose(self):
45
+ self.info_static = Static("Processes: 0", id="programm_list_info")
46
+ yield self.info_static
47
+
48
+ header_str = f"[bold underline]{'PID':>6}[/bold underline] [bold underline]{'NAME':<25}[/bold underline] [bold underline]{'RAM(MB)':>10}[/bold underline] [bold underline]{'CPU(%)':>7}[/bold underline]"
49
+ self.header_static = Static(header_str, id="programm_list_header")
50
+ yield self.header_static
51
+
52
+ self.list_view = FastListView(classes="programm_list_view")
53
+ self.list_view.on_scroll_cb = self.check_expand
54
+ self.list_view.on_highlight_cb = self.update_details
55
+ yield self.list_view
56
+
57
+ def on_mount(self):
58
+ start_process_cache_thread(interval=1.5)
59
+ self.proc_info_box = ProcInfoWidget(id="proc_info_box")
60
+ self.screen.mount(self.proc_info_box)
61
+ self.update_list()
62
+
63
+ def update_info_box_position(self):
64
+ if hasattr(self, "proc_info_box") and self.proc_info_box:
65
+ box_width = 30 # matches StatusWidget width
66
+ self.proc_info_box.styles.offset = (
67
+ self.region.x + self.size.width - box_width,
68
+ self.region.y,
69
+ )
70
+
71
+ def on_resize(self):
72
+ self.update_info_box_position()
73
+
74
+ @on(ListView.Selected)
75
+ def on_list_view_selected(self, event: ListView.Selected) -> None:
76
+ idx = self.list_view.index
77
+ if idx is not None and 0 <= idx < len(self.current_sorted_data):
78
+ d = self.current_sorted_data[idx]
79
+ pid = d.get("pid")
80
+ if pid is not None:
81
+ if self.active_info_pid == pid:
82
+ self.active_info_pid = None
83
+ else:
84
+ self.active_info_pid = pid
85
+ self.update_details()
86
+
87
+ @on(ListView.Highlighted)
88
+ def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
89
+ if self.active_info_pid is not None:
90
+ idx = self.list_view.index
91
+ if idx is not None and 0 <= idx < len(self.current_sorted_data):
92
+ d = self.current_sorted_data[idx]
93
+ pid = d.get("pid")
94
+ if pid is not None:
95
+ self.active_info_pid = pid
96
+ self.update_details()
97
+
98
+ def update_details(self) -> None:
99
+ if not hasattr(self, "info_static") or not hasattr(self, "list_view"):
100
+ return
101
+
102
+ self.info_static.update(f"Processes: {self.total_count}")
103
+
104
+ if not self.display or self.active_info_pid is None:
105
+ if hasattr(self, "proc_info_box") and self.proc_info_box:
106
+ self.proc_info_box.display = False
107
+ return
108
+
109
+ self.update_info_box_position()
110
+
111
+ active_data = None
112
+ for d in self.current_sorted_data:
113
+ if d.get("pid") == self.active_info_pid:
114
+ active_data = d
115
+ break
116
+
117
+ if active_data and hasattr(self, "proc_info_box") and self.proc_info_box:
118
+ self.proc_info_box.update_process(active_data)
119
+ self.proc_info_box.display = True
120
+ else:
121
+ if hasattr(self, "proc_info_box") and self.proc_info_box:
122
+ self.proc_info_box.display = False
123
+
124
+ def check_expand(self):
125
+ if not hasattr(self, "list_view"):
126
+ return
127
+ idx = self.list_view.index or 0
128
+ scroll_y = int(self.list_view.scroll_y)
129
+ height = self.size.height or 20
130
+ needed = max(idx, scroll_y + height) + 30
131
+ if needed > self.rendered_limit:
132
+ self.rendered_limit = max(self.rendered_limit, needed + int(height * 4))
133
+ self.update_list(force=True)
134
+
135
+ @on(Click, "#programm_list_header")
136
+ def on_header_click(self, event: Click) -> None:
137
+ x = event.x
138
+ new_sort = self.sort_by
139
+ if x <= 7:
140
+ new_sort = "pid"
141
+ elif x <= 34:
142
+ new_sort = "name"
143
+ elif x <= 46:
144
+ new_sort = "ram_ussage"
145
+ else:
146
+ new_sort = "cpu_ussage"
147
+
148
+ if self.sort_by == new_sort:
149
+ self.sort_reverse = not self.sort_reverse
150
+ else:
151
+ self.sort_by = new_sort
152
+ self.sort_reverse = True if new_sort in ["ram_ussage", "cpu_ussage"] else False
153
+
154
+ self.update_list(force=True)
155
+
156
+ def update_list(self, force: bool = False):
157
+ if not hasattr(self, "info_static") or not hasattr(self, "list_view"):
158
+ return
159
+
160
+ version, count, items_data = get_latest_process_items_data()
161
+ self.total_count = count
162
+
163
+ if not force and version == self.last_cache_version:
164
+ self.update_details()
165
+ return
166
+
167
+ self.last_cache_version = version
168
+
169
+ # Sort items_data based on user selection
170
+ if self.sort_by in ["pid", "ram_ussage", "cpu_ussage"]:
171
+ items_data.sort(key=lambda d: d.get(self.sort_by, 0), reverse=self.sort_reverse)
172
+ else:
173
+ items_data.sort(key=lambda d: str(d.get(self.sort_by, "")).lower(), reverse=self.sort_reverse)
174
+
175
+ height = self.size.height or 20
176
+ max_visible = max(20, int(height * 2.5))
177
+ current_index = self.list_view.index or 0
178
+ scroll_y = int(self.list_view.scroll_y)
179
+
180
+ needed = max(current_index, scroll_y + height) + 30
181
+ self.rendered_limit = max(self.rendered_limit, max_visible, needed)
182
+
183
+ sliced_data = items_data[:self.rendered_limit]
184
+ self.current_sorted_data = sliced_data
185
+ existing_items = list(self.list_view.query(ListItem))
186
+ new_count = len(sliced_data)
187
+ existing_count = len(existing_items)
188
+
189
+ # In-place update
190
+ update_limit = min(existing_count, new_count)
191
+ for i in range(update_limit):
192
+ formatted_text = _format_process_item(sliced_data[i])
193
+ existing_items[i].query_one(Static).update(formatted_text)
194
+
195
+ if new_count > existing_count:
196
+ new_items = [ListItem(Static(_format_process_item(d))) for d in sliced_data[existing_count:]]
197
+ self.list_view.extend(new_items)
198
+ elif existing_count > new_count:
199
+ for item in existing_items[new_count:]:
200
+ item.remove()
201
+
202
+ self.update_details()
@@ -0,0 +1,36 @@
1
+ # pyrefly: ignore [missing-import]
2
+ from textual.widgets import Static
3
+ # pyrefly: ignore [missing-import]
4
+ from textual.containers import Container
5
+ from monitorify.ui.tui.widgets.brailleGraph import BrailleGraph, make_time_grid
6
+
7
+
8
+ class RamWidget(Container):
9
+ BORDER_TITLE = "MEM"
10
+
11
+ def compose(self):
12
+ self.ram_usage_static = Static("–%")
13
+ yield self.ram_usage_static
14
+
15
+ self.sparkline = BrailleGraph(
16
+ data_lines=1, color1="#c678dd", y_max=100.0,
17
+ show_timeline=True,
18
+ id="ram_sparkline"
19
+ )
20
+ yield self.sparkline
21
+
22
+ def update_ram(self, snapshot) -> None:
23
+ """Update label and append one live data point to the graph."""
24
+ usage = snapshot.ram
25
+ self.ram_usage_static.update(f"{usage:.1f}%")
26
+ self.sparkline.update_value(usage)
27
+
28
+ def update_text(self, snapshot) -> None:
29
+ """Update only the label — does not touch the graph."""
30
+ self.ram_usage_static.update(f"{snapshot.ram:.1f}%")
31
+
32
+ def load_history(self, snapshots: list, time_span: float = 0.0) -> None:
33
+ """Bulk-load RAM history, time-positioned to show real gaps."""
34
+ w = self.sparkline.size.width * 2 or 400
35
+ values = make_time_grid(snapshots, lambda s: s.ram, time_span, w)
36
+ self.sparkline.load_history(values, timeline_seconds=time_span)
@@ -0,0 +1,45 @@
1
+ from textual.containers import Container
2
+ from textual.widgets import Static
3
+ import socket
4
+
5
+ class StatusWidget(Container):
6
+ BORDER_TITLE = "Status"
7
+
8
+ def compose(self):
9
+ self.system_info = Static(f"System: {get_kernel_version()}")
10
+ yield self.system_info
11
+
12
+ self.hostname = Static(f"Hostname: {get_hostname()}")
13
+ yield self.hostname
14
+
15
+ self.uptime = Static(f"Uptime: {get_uptime()}")
16
+ yield self.uptime
17
+
18
+ def on_mount(self):
19
+ self.set_interval(1, self.update_uptime)
20
+
21
+ def update_uptime(self):
22
+ self.uptime.update(f"Uptime: {get_uptime()}")
23
+
24
+ def get_uptime():
25
+ with open("/proc/uptime", "r") as f:
26
+ uptime = float(f.read().split()[0])
27
+ hours = int(uptime // 3600)
28
+ minutes = int((uptime % 3600) // 60)
29
+ seconds = int(uptime % 60)
30
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
31
+
32
+ def get_hostname():
33
+ return socket.gethostname()
34
+
35
+ def get_OS_name():
36
+ with open("/etc/os-release", "r") as f:
37
+ for line in f:
38
+ if line.startswith("NAME="):
39
+ name = line.split("=")[1].strip().strip('"')
40
+ return name.replace("Linux", "").strip()
41
+ return "Unknown"
42
+
43
+ def get_kernel_version():
44
+ with open("/proc/version", "r") as f:
45
+ return f.read().split()[2]