netshow 0.2.0__tar.gz → 0.2.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.
- {netshow-0.2.0 → netshow-0.2.2}/PKG-INFO +16 -7
- {netshow-0.2.0 → netshow-0.2.2}/README.md +15 -6
- netshow-0.2.2/how_to_per_proc_bw.md +384 -0
- {netshow-0.2.0 → netshow-0.2.2}/pyproject.toml +1 -1
- {netshow-0.2.0 → netshow-0.2.2}/src/netshow/app.py +15 -263
- netshow-0.2.2/src/netshow/detail_screen.py +223 -0
- {netshow-0.2.0 → netshow-0.2.2}/src/netshow/styles.py +5 -8
- netshow-0.2.2/src/netshow/types_and_constants.py +41 -0
- {netshow-0.2.0 → netshow-0.2.2}/uv.lock +1 -1
- {netshow-0.2.0 → netshow-0.2.2}/.claude/settings.local.json +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/.github/workflows/python-app.yml +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/.gitignore +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/LICENSE +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/diffout.txt +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/github_netshow.png +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/netshow-logo.png +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/release.py +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/src/netshow/__init__.py +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/src/netshow/cli.py +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/src/netshow/helpers.py +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/tests/__init__.py +0 -0
- {netshow-0.2.0 → netshow-0.2.2}/tests/test_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netshow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A real-time network connection monitor with friendly service names
|
|
5
5
|
Author: Taylor Wilsdon
|
|
6
6
|
License: MIT
|
|
@@ -45,19 +45,21 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
47
|
<div align="center" width="600px">
|
|
48
|
-
<video width="600px" src="https://github.com/user-attachments/assets/
|
|
48
|
+
<video width="600px" src="https://github.com/user-attachments/assets/e110f7db-f803-4aad-bef8-c3e928eed6e5"></video>
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
53
|
### A quick plug for AI-Enhanced Docs
|
|
54
|
+
<details>
|
|
55
|
+
<summary>But why?</summary>
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
>
|
|
57
|
-
> As a solo developer building open source tools that may only ever serve my own needs, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
|
57
|
+
**This README was written with AI assistance, and here's why that matters**
|
|
58
58
|
>
|
|
59
|
-
>
|
|
60
|
-
|
|
59
|
+
> As a solo dev building open source tools that many never see outside use, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
|
60
|
+
>
|
|
61
|
+
> In this case, Sonnet 4 took a pass & a human (me) verified them 7/10/25.
|
|
62
|
+
</details>
|
|
61
63
|
|
|
62
64
|
## ✨ **Features**
|
|
63
65
|
|
|
@@ -143,6 +145,7 @@ netshow [--interval 1.0] [--no-colors]
|
|
|
143
145
|
| **q** | Quit NetShow |
|
|
144
146
|
| **Ctrl+C** | Force quit (hard quit) |
|
|
145
147
|
| **Ctrl+R** | Force refresh |
|
|
148
|
+
| **e** | Toggle emoji display on/off |
|
|
146
149
|
| **f** | Toggle filter input |
|
|
147
150
|
| **/** | Quick search (focus filter) |
|
|
148
151
|
| **s** | Sort by connection status |
|
|
@@ -157,6 +160,12 @@ netshow [--interval 1.0] [--no-colors]
|
|
|
157
160
|
- Filters across process names, addresses, and connection status
|
|
158
161
|
- Live updates as you type (with debouncing)
|
|
159
162
|
|
|
163
|
+
**🎨 Emoji Toggle**
|
|
164
|
+
- Press `e` to toggle emoji display on/off for a cleaner interface
|
|
165
|
+
- When disabled, removes all emoji prefixes from UI elements
|
|
166
|
+
- Useful for terminals with limited emoji support or accessibility preferences
|
|
167
|
+
- Setting persists during the session
|
|
168
|
+
|
|
160
169
|
**📊 Bandwidth Monitoring**
|
|
161
170
|
- Real-time bandwidth display in the metrics bar
|
|
162
171
|
- Press `i` to cycle through network interfaces (`all`, `eth0`, `wlan0`, etc.)
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
<div align="center" width="600px">
|
|
16
|
-
<video width="600px" src="https://github.com/user-attachments/assets/
|
|
16
|
+
<video width="600px" src="https://github.com/user-attachments/assets/e110f7db-f803-4aad-bef8-c3e928eed6e5"></video>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
### A quick plug for AI-Enhanced Docs
|
|
22
|
+
<details>
|
|
23
|
+
<summary>But why?</summary>
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
>
|
|
25
|
-
> As a solo developer building open source tools that may only ever serve my own needs, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
|
25
|
+
**This README was written with AI assistance, and here's why that matters**
|
|
26
26
|
>
|
|
27
|
-
>
|
|
28
|
-
|
|
27
|
+
> As a solo dev building open source tools that many never see outside use, comprehensive documentation often wouldn't happen without AI help. Using agentic dev tools like **Roo** & **Claude Code** that understand the entire codebase, AI doesn't just regurgitate generic content - it extracts real implementation details and creates accurate, specific documentation.
|
|
28
|
+
>
|
|
29
|
+
> In this case, Sonnet 4 took a pass & a human (me) verified them 7/10/25.
|
|
30
|
+
</details>
|
|
29
31
|
|
|
30
32
|
## ✨ **Features**
|
|
31
33
|
|
|
@@ -111,6 +113,7 @@ netshow [--interval 1.0] [--no-colors]
|
|
|
111
113
|
| **q** | Quit NetShow |
|
|
112
114
|
| **Ctrl+C** | Force quit (hard quit) |
|
|
113
115
|
| **Ctrl+R** | Force refresh |
|
|
116
|
+
| **e** | Toggle emoji display on/off |
|
|
114
117
|
| **f** | Toggle filter input |
|
|
115
118
|
| **/** | Quick search (focus filter) |
|
|
116
119
|
| **s** | Sort by connection status |
|
|
@@ -125,6 +128,12 @@ netshow [--interval 1.0] [--no-colors]
|
|
|
125
128
|
- Filters across process names, addresses, and connection status
|
|
126
129
|
- Live updates as you type (with debouncing)
|
|
127
130
|
|
|
131
|
+
**🎨 Emoji Toggle**
|
|
132
|
+
- Press `e` to toggle emoji display on/off for a cleaner interface
|
|
133
|
+
- When disabled, removes all emoji prefixes from UI elements
|
|
134
|
+
- Useful for terminals with limited emoji support or accessibility preferences
|
|
135
|
+
- Setting persists during the session
|
|
136
|
+
|
|
128
137
|
**📊 Bandwidth Monitoring**
|
|
129
138
|
- Real-time bandwidth display in the metrics bar
|
|
130
139
|
- Press `i` to cycle through network interfaces (`all`, `eth0`, `wlan0`, etc.)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# The Definitive Guide to Per-Process Bandwidth Monitoring with Python
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Per-process bandwidth monitoring tracks network usage by individual processes, enabling you to identify which applications consume the most bandwidth. This guide covers cross-platform solutions that work on both macOS and Linux.
|
|
6
|
+
|
|
7
|
+
## Core Approaches
|
|
8
|
+
|
|
9
|
+
### 1. **System-Level Tools with Python Wrappers**
|
|
10
|
+
|
|
11
|
+
The most reliable approach leverages existing system monitoring tools through Python subprocess calls.
|
|
12
|
+
|
|
13
|
+
#### **Linux: nethogs**
|
|
14
|
+
```python
|
|
15
|
+
import subprocess
|
|
16
|
+
import re
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
class NethogsBandwidthMonitor:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.process = None
|
|
22
|
+
|
|
23
|
+
def start_monitoring(self, interface='eth0'):
|
|
24
|
+
"""Start nethogs in JSON mode for easy parsing"""
|
|
25
|
+
cmd = ['sudo', 'nethogs', '-t', '-d', '1', interface]
|
|
26
|
+
self.process = subprocess.Popen(
|
|
27
|
+
cmd,
|
|
28
|
+
stdout=subprocess.PIPE,
|
|
29
|
+
stderr=subprocess.PIPE,
|
|
30
|
+
universal_newlines=True
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def parse_output(self):
|
|
34
|
+
"""Parse nethogs output and return bandwidth data"""
|
|
35
|
+
bandwidth_data = {}
|
|
36
|
+
|
|
37
|
+
for line in self.process.stdout:
|
|
38
|
+
# Format: program/pid/uid sent_kb/s recv_kb/s
|
|
39
|
+
match = re.match(r'(\S+)/(\d+)/(\d+)\s+([\d.]+)\s+([\d.]+)', line)
|
|
40
|
+
if match:
|
|
41
|
+
program, pid, uid, sent, recv = match.groups()
|
|
42
|
+
bandwidth_data[pid] = {
|
|
43
|
+
'program': program,
|
|
44
|
+
'sent_kbps': float(sent),
|
|
45
|
+
'recv_kbps': float(recv),
|
|
46
|
+
'total_kbps': float(sent) + float(recv)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return bandwidth_data
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### **macOS: nettop**
|
|
53
|
+
```python
|
|
54
|
+
import subprocess
|
|
55
|
+
import re
|
|
56
|
+
|
|
57
|
+
class NettopBandwidthMonitor:
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.last_values = {}
|
|
60
|
+
|
|
61
|
+
def get_bandwidth(self):
|
|
62
|
+
"""Get current bandwidth usage per process"""
|
|
63
|
+
cmd = ['nettop', '-P', '-L', '1', '-J', 'bytes_in,bytes_out']
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
output = subprocess.check_output(cmd, text=True)
|
|
67
|
+
return self._parse_nettop_output(output)
|
|
68
|
+
except subprocess.CalledProcessError as e:
|
|
69
|
+
print(f"Error running nettop: {e}")
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
def _parse_nettop_output(self, output):
|
|
73
|
+
"""Parse nettop output"""
|
|
74
|
+
bandwidth_data = {}
|
|
75
|
+
|
|
76
|
+
for line in output.split('\n'):
|
|
77
|
+
if '.' in line and 'bytes' not in line.lower():
|
|
78
|
+
parts = line.split(',')
|
|
79
|
+
if len(parts) >= 3:
|
|
80
|
+
process_info = parts[0].strip()
|
|
81
|
+
bytes_in = int(parts[1].strip())
|
|
82
|
+
bytes_out = int(parts[2].strip())
|
|
83
|
+
|
|
84
|
+
# Extract PID from process info
|
|
85
|
+
pid_match = re.search(r'\.(\d+)', process_info)
|
|
86
|
+
if pid_match:
|
|
87
|
+
pid = pid_match.group(1)
|
|
88
|
+
bandwidth_data[pid] = {
|
|
89
|
+
'process': process_info.split('.')[0],
|
|
90
|
+
'bytes_in': bytes_in,
|
|
91
|
+
'bytes_out': bytes_out,
|
|
92
|
+
'total_bytes': bytes_in + bytes_out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return bandwidth_data
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. **psutil-based Solution**
|
|
99
|
+
|
|
100
|
+
A more portable but less accurate approach uses psutil to track network connections per process.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import psutil
|
|
104
|
+
import time
|
|
105
|
+
from collections import defaultdict
|
|
106
|
+
|
|
107
|
+
class PsutilBandwidthMonitor:
|
|
108
|
+
def __init__(self):
|
|
109
|
+
self.last_io_counters = {}
|
|
110
|
+
self.connection_map = defaultdict(set)
|
|
111
|
+
|
|
112
|
+
def update_connections(self):
|
|
113
|
+
"""Map network connections to processes"""
|
|
114
|
+
self.connection_map.clear()
|
|
115
|
+
|
|
116
|
+
for conn in psutil.net_connections(kind='inet'):
|
|
117
|
+
if conn.pid:
|
|
118
|
+
self.connection_map[conn.pid].add(
|
|
119
|
+
(conn.laddr, conn.raddr, conn.status)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def estimate_bandwidth(self, interval=1.0):
|
|
123
|
+
"""Estimate bandwidth by monitoring system-wide changes"""
|
|
124
|
+
# Get initial network stats
|
|
125
|
+
net_io_1 = psutil.net_io_counters()
|
|
126
|
+
proc_io_1 = {}
|
|
127
|
+
|
|
128
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
129
|
+
try:
|
|
130
|
+
proc_io_1[proc.pid] = proc.io_counters()
|
|
131
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
time.sleep(interval)
|
|
135
|
+
|
|
136
|
+
# Get final network stats
|
|
137
|
+
net_io_2 = psutil.net_io_counters()
|
|
138
|
+
bandwidth_data = {}
|
|
139
|
+
|
|
140
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
141
|
+
try:
|
|
142
|
+
if proc.pid in proc_io_1:
|
|
143
|
+
io_2 = proc.io_counters()
|
|
144
|
+
io_1 = proc_io_1[proc.pid]
|
|
145
|
+
|
|
146
|
+
# Calculate I/O delta (approximation)
|
|
147
|
+
read_delta = io_2.read_bytes - io_1.read_bytes
|
|
148
|
+
write_delta = io_2.write_bytes - io_1.write_bytes
|
|
149
|
+
|
|
150
|
+
# Only include if process has network connections
|
|
151
|
+
if proc.pid in self.connection_map:
|
|
152
|
+
bandwidth_data[proc.pid] = {
|
|
153
|
+
'name': proc.name(),
|
|
154
|
+
'connections': len(self.connection_map[proc.pid]),
|
|
155
|
+
'read_bytes_sec': read_delta / interval,
|
|
156
|
+
'write_bytes_sec': write_delta / interval
|
|
157
|
+
}
|
|
158
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
return bandwidth_data
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 3. **eBPF-based Solution (Linux only)**
|
|
165
|
+
|
|
166
|
+
For the most accurate Linux monitoring, use eBPF with Python bindings.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from bcc import BPF
|
|
170
|
+
import time
|
|
171
|
+
|
|
172
|
+
# eBPF program to track network bytes per PID
|
|
173
|
+
bpf_program = """
|
|
174
|
+
#include <uapi/linux/ptrace.h>
|
|
175
|
+
#include <net/sock.h>
|
|
176
|
+
#include <bcc/proto.h>
|
|
177
|
+
|
|
178
|
+
BPF_HASH(pid_bytes, u32, u64);
|
|
179
|
+
|
|
180
|
+
int trace_tcp_sendmsg(struct pt_regs *ctx, struct sock *sk,
|
|
181
|
+
struct msghdr *msg, size_t size) {
|
|
182
|
+
u32 pid = bpf_get_current_pid_tgid() >> 32;
|
|
183
|
+
u64 *bytes = pid_bytes.lookup(&pid);
|
|
184
|
+
if (bytes) {
|
|
185
|
+
*bytes += size;
|
|
186
|
+
} else {
|
|
187
|
+
pid_bytes.update(&pid, &size);
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
class EBPFBandwidthMonitor:
|
|
194
|
+
def __init__(self):
|
|
195
|
+
self.bpf = BPF(text=bpf_program)
|
|
196
|
+
self.bpf.attach_kprobe(event="tcp_sendmsg", fn_name="trace_tcp_sendmsg")
|
|
197
|
+
self.last_values = {}
|
|
198
|
+
|
|
199
|
+
def get_bandwidth_stats(self, interval=1.0):
|
|
200
|
+
"""Get bandwidth usage per PID"""
|
|
201
|
+
time.sleep(interval)
|
|
202
|
+
|
|
203
|
+
bandwidth_data = {}
|
|
204
|
+
pid_bytes = self.bpf["pid_bytes"]
|
|
205
|
+
|
|
206
|
+
for k, v in pid_bytes.items():
|
|
207
|
+
pid = k.value
|
|
208
|
+
bytes_sent = v.value
|
|
209
|
+
|
|
210
|
+
if pid in self.last_values:
|
|
211
|
+
bytes_per_sec = (bytes_sent - self.last_values[pid]) / interval
|
|
212
|
+
bandwidth_data[pid] = {
|
|
213
|
+
'bytes_per_sec': bytes_per_sec,
|
|
214
|
+
'mbps': (bytes_per_sec * 8) / 1_000_000
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
self.last_values[pid] = bytes_sent
|
|
218
|
+
|
|
219
|
+
return bandwidth_data
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Unified Cross-Platform Interface
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import platform
|
|
226
|
+
import os
|
|
227
|
+
|
|
228
|
+
class BandwidthMonitor:
|
|
229
|
+
def __init__(self):
|
|
230
|
+
self.system = platform.system()
|
|
231
|
+
self.monitor = None
|
|
232
|
+
self._initialize_monitor()
|
|
233
|
+
|
|
234
|
+
def _initialize_monitor(self):
|
|
235
|
+
"""Initialize the appropriate monitor for the current OS"""
|
|
236
|
+
if self.system == 'Linux':
|
|
237
|
+
# Check for available tools
|
|
238
|
+
if os.path.exists('/usr/sbin/nethogs'):
|
|
239
|
+
self.monitor = NethogsBandwidthMonitor()
|
|
240
|
+
else:
|
|
241
|
+
# Fallback to psutil
|
|
242
|
+
self.monitor = PsutilBandwidthMonitor()
|
|
243
|
+
elif self.system == 'Darwin': # macOS
|
|
244
|
+
self.monitor = NettopBandwidthMonitor()
|
|
245
|
+
else:
|
|
246
|
+
# Default fallback
|
|
247
|
+
self.monitor = PsutilBandwidthMonitor()
|
|
248
|
+
|
|
249
|
+
def get_process_bandwidth(self, interval=1.0):
|
|
250
|
+
"""Get bandwidth data for all processes"""
|
|
251
|
+
if hasattr(self.monitor, 'get_bandwidth'):
|
|
252
|
+
return self.monitor.get_bandwidth()
|
|
253
|
+
elif hasattr(self.monitor, 'estimate_bandwidth'):
|
|
254
|
+
return self.monitor.estimate_bandwidth(interval)
|
|
255
|
+
else:
|
|
256
|
+
raise NotImplementedError("No bandwidth monitoring available")
|
|
257
|
+
|
|
258
|
+
def format_output(self, bandwidth_data, top_n=10):
|
|
259
|
+
"""Format bandwidth data for display"""
|
|
260
|
+
# Sort by total bandwidth
|
|
261
|
+
sorted_procs = sorted(
|
|
262
|
+
bandwidth_data.items(),
|
|
263
|
+
key=lambda x: x[1].get('total_bytes', 0) or
|
|
264
|
+
x[1].get('total_kbps', 0) or 0,
|
|
265
|
+
reverse=True
|
|
266
|
+
)[:top_n]
|
|
267
|
+
|
|
268
|
+
print(f"{'PID':<8} {'Process':<25} {'Download':<15} {'Upload':<15}")
|
|
269
|
+
print("-" * 70)
|
|
270
|
+
|
|
271
|
+
for pid, data in sorted_procs:
|
|
272
|
+
process_name = data.get('name', data.get('program', 'Unknown'))[:24]
|
|
273
|
+
|
|
274
|
+
# Handle different data formats
|
|
275
|
+
if 'recv_kbps' in data:
|
|
276
|
+
download = f"{data['recv_kbps']:.2f} KB/s"
|
|
277
|
+
upload = f"{data['sent_kbps']:.2f} KB/s"
|
|
278
|
+
elif 'bytes_in' in data:
|
|
279
|
+
download = f"{data['bytes_in'] / 1024:.2f} KB"
|
|
280
|
+
upload = f"{data['bytes_out'] / 1024:.2f} KB"
|
|
281
|
+
else:
|
|
282
|
+
download = f"{data.get('read_bytes_sec', 0) / 1024:.2f} KB/s"
|
|
283
|
+
upload = f"{data.get('write_bytes_sec', 0) / 1024:.2f} KB/s"
|
|
284
|
+
|
|
285
|
+
print(f"{pid:<8} {process_name:<25} {download:<15} {upload:<15}")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Usage Example
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
def main():
|
|
292
|
+
monitor = BandwidthMonitor()
|
|
293
|
+
|
|
294
|
+
print("Starting bandwidth monitoring...")
|
|
295
|
+
print("Press Ctrl+C to stop\n")
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
while True:
|
|
299
|
+
bandwidth_data = monitor.get_process_bandwidth(interval=2.0)
|
|
300
|
+
|
|
301
|
+
# Clear screen (works on most terminals)
|
|
302
|
+
print("\033[2J\033[H")
|
|
303
|
+
|
|
304
|
+
# Display current bandwidth usage
|
|
305
|
+
monitor.format_output(bandwidth_data, top_n=15)
|
|
306
|
+
|
|
307
|
+
time.sleep(2)
|
|
308
|
+
|
|
309
|
+
except KeyboardInterrupt:
|
|
310
|
+
print("\nMonitoring stopped.")
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
main()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Installation Requirements
|
|
317
|
+
|
|
318
|
+
### **Linux**
|
|
319
|
+
```bash
|
|
320
|
+
# For nethogs approach
|
|
321
|
+
sudo apt-get install nethogs # Debian/Ubuntu
|
|
322
|
+
sudo yum install nethogs # RHEL/CentOS
|
|
323
|
+
|
|
324
|
+
# For eBPF approach
|
|
325
|
+
pip install bcc
|
|
326
|
+
|
|
327
|
+
# For psutil approach
|
|
328
|
+
pip install psutil
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### **macOS**
|
|
332
|
+
```bash
|
|
333
|
+
# nettop comes pre-installed
|
|
334
|
+
# For psutil approach
|
|
335
|
+
pip install psutil
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Important Considerations
|
|
339
|
+
|
|
340
|
+
1. **Permissions**: Most accurate monitoring requires root/sudo access
|
|
341
|
+
2. **Accuracy**: System tools (nethogs, nettop) are most accurate; psutil provides estimates
|
|
342
|
+
3. **Performance**: eBPF has minimal overhead; subprocess calls have higher overhead
|
|
343
|
+
4. **Portability**: psutil works everywhere but with reduced accuracy
|
|
344
|
+
|
|
345
|
+
## Advanced Features
|
|
346
|
+
|
|
347
|
+
### Real-time Graphing
|
|
348
|
+
```python
|
|
349
|
+
import matplotlib.pyplot as plt
|
|
350
|
+
from matplotlib.animation import FuncAnimation
|
|
351
|
+
from collections import deque
|
|
352
|
+
|
|
353
|
+
class BandwidthGraph:
|
|
354
|
+
def __init__(self, monitor, max_points=60):
|
|
355
|
+
self.monitor = monitor
|
|
356
|
+
self.max_points = max_points
|
|
357
|
+
self.data = defaultdict(lambda: deque(maxlen=max_points))
|
|
358
|
+
|
|
359
|
+
self.fig, self.ax = plt.subplots()
|
|
360
|
+
self.lines = {}
|
|
361
|
+
|
|
362
|
+
def update(self, frame):
|
|
363
|
+
bandwidth_data = self.monitor.get_process_bandwidth()
|
|
364
|
+
|
|
365
|
+
for pid, data in bandwidth_data.items():
|
|
366
|
+
total_kbps = data.get('total_kbps', 0)
|
|
367
|
+
self.data[pid].append(total_kbps)
|
|
368
|
+
|
|
369
|
+
if pid not in self.lines:
|
|
370
|
+
line, = self.ax.plot([], [], label=f"PID {pid}")
|
|
371
|
+
self.lines[pid] = line
|
|
372
|
+
|
|
373
|
+
self.lines[pid].set_data(range(len(self.data[pid])), self.data[pid])
|
|
374
|
+
|
|
375
|
+
self.ax.relim()
|
|
376
|
+
self.ax.autoscale_view()
|
|
377
|
+
self.ax.legend()
|
|
378
|
+
|
|
379
|
+
def start(self):
|
|
380
|
+
ani = FuncAnimation(self.fig, self.update, interval=1000)
|
|
381
|
+
plt.show()
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
This guide provides a comprehensive foundation for implementing per-process bandwidth monitoring across platforms, with multiple approaches ranging from simple to advanced, ensuring you can track network usage effectively regardless of your specific requirements or constraints.
|
|
@@ -1,277 +1,23 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
|
-
from typing import Any, Optional,
|
|
3
|
+
from typing import Any, Optional, Union, cast
|
|
4
4
|
|
|
5
5
|
import psutil
|
|
6
6
|
from textual.app import App, ComposeResult
|
|
7
7
|
from textual.binding import Binding
|
|
8
|
-
from textual.containers import
|
|
9
|
-
Container,
|
|
10
|
-
Horizontal,
|
|
11
|
-
ScrollableContainer,
|
|
12
|
-
Vertical,
|
|
13
|
-
)
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
14
9
|
from textual.reactive import reactive
|
|
15
|
-
from textual.screen import Screen
|
|
16
10
|
from textual.timer import Timer
|
|
17
|
-
from textual.widgets import
|
|
18
|
-
Button,
|
|
19
|
-
DataTable,
|
|
20
|
-
Footer,
|
|
21
|
-
Header,
|
|
22
|
-
Input,
|
|
23
|
-
Static,
|
|
24
|
-
)
|
|
11
|
+
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
25
12
|
|
|
13
|
+
from .detail_screen import ConnectionDetailScreen
|
|
26
14
|
from .helpers import get_lsof_conns, get_psutil_conns
|
|
27
15
|
from .styles import CSS
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
REFRESH_INTERVAL
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
("q", "quit", "Quit"),
|
|
34
|
-
("ctrl+r", "refresh", "Force Refresh"),
|
|
35
|
-
("f", "toggle_filter", "Filter"),
|
|
36
|
-
("s", "sort_by_status", "Sort by Status"),
|
|
37
|
-
("p", "sort_by_process", "Sort by Process"),
|
|
38
|
-
("i", "toggle_interface", "Interface"),
|
|
39
|
-
("e", "toggle_emojis", "Emojis"),
|
|
40
|
-
("ctrl+c", "quit", "Hard Quit"),
|
|
41
|
-
("/", "search", "Search"),
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class ConnectionData(TypedDict):
|
|
46
|
-
"""Type definition for connection data."""
|
|
47
|
-
|
|
48
|
-
pid: str
|
|
49
|
-
friendly: str
|
|
50
|
-
proc: str
|
|
51
|
-
laddr: str
|
|
52
|
-
raddr: str
|
|
53
|
-
status: str
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class NetworkStats(TypedDict):
|
|
57
|
-
"""Type definition for network statistics."""
|
|
58
|
-
|
|
59
|
-
total_connections: int
|
|
60
|
-
established: int
|
|
61
|
-
listening: int
|
|
62
|
-
time_wait: int
|
|
63
|
-
bytes_sent: int
|
|
64
|
-
bytes_recv: int
|
|
65
|
-
packets_sent: int
|
|
66
|
-
packets_recv: int
|
|
67
|
-
timestamp: float
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class ConnectionDetailScreen(Screen):
|
|
71
|
-
"""Screen for displaying detailed information about a selected connection."""
|
|
72
|
-
|
|
73
|
-
BINDINGS = [("escape", "app.pop_screen", "Back to connections")]
|
|
74
|
-
|
|
75
|
-
def __init__(self, connection_data: ConnectionData):
|
|
76
|
-
super().__init__()
|
|
77
|
-
self.connection_data = connection_data
|
|
78
|
-
self.process_info = self._get_process_info(connection_data["pid"])
|
|
79
|
-
|
|
80
|
-
def _get_status_icon(self, status: str) -> str:
|
|
81
|
-
"""Get an appropriate icon for connection status."""
|
|
82
|
-
# Check if parent app has emoji toggle disabled
|
|
83
|
-
if hasattr(self.app, "show_emojis") and not self.app.show_emojis:
|
|
84
|
-
return ""
|
|
85
|
-
status_icons = {
|
|
86
|
-
"ESTABLISHED": "✅",
|
|
87
|
-
"LISTEN": "👂",
|
|
88
|
-
"TIME_WAIT": "⏳",
|
|
89
|
-
"CLOSE_WAIT": "⏸️",
|
|
90
|
-
"SYN_SENT": "📤",
|
|
91
|
-
"SYN_RECV": "📥",
|
|
92
|
-
"FIN_WAIT1": "🔄",
|
|
93
|
-
"FIN_WAIT2": "🔁",
|
|
94
|
-
"CLOSING": "🔚",
|
|
95
|
-
"LAST_ACK": "🏁",
|
|
96
|
-
}
|
|
97
|
-
return status_icons.get(status, "❓")
|
|
98
|
-
|
|
99
|
-
def _get_process_info(self, pid_str: str) -> dict:
|
|
100
|
-
"""Get additional process information if PID is available."""
|
|
101
|
-
if pid_str == "-":
|
|
102
|
-
return {}
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
pid = int(pid_str)
|
|
106
|
-
proc = psutil.Process(pid)
|
|
107
|
-
return {
|
|
108
|
-
"name": proc.name(),
|
|
109
|
-
"exe": proc.exe(),
|
|
110
|
-
"cmd": " ".join(proc.cmdline()),
|
|
111
|
-
"create_time": proc.create_time(),
|
|
112
|
-
"status": proc.status(),
|
|
113
|
-
"username": proc.username(),
|
|
114
|
-
"cwd": proc.cwd(),
|
|
115
|
-
"num_threads": proc.num_threads(),
|
|
116
|
-
"cpu_percent": proc.cpu_percent(interval=0.1),
|
|
117
|
-
"memory_percent": proc.memory_percent(),
|
|
118
|
-
"open_files": proc.open_files(),
|
|
119
|
-
"connections": proc.connections(),
|
|
120
|
-
}
|
|
121
|
-
except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError):
|
|
122
|
-
return {}
|
|
123
|
-
|
|
124
|
-
def compose(self) -> ComposeResult:
|
|
125
|
-
"""Compose the detail screen layout."""
|
|
126
|
-
yield Header(show_clock=True)
|
|
127
|
-
|
|
128
|
-
with ScrollableContainer():
|
|
129
|
-
# Check emoji setting from parent app
|
|
130
|
-
show_emojis = getattr(self.app, "show_emojis", True)
|
|
131
|
-
title_prefix = "🔗 " if show_emojis else ""
|
|
132
|
-
yield Static(
|
|
133
|
-
f"{title_prefix}Connection Details: {self.connection_data['friendly']}",
|
|
134
|
-
id="detail_title",
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
with Horizontal(id="main_content"):
|
|
138
|
-
with Container(id="connection_details"):
|
|
139
|
-
conn_prefix = "🌐 " if show_emojis else ""
|
|
140
|
-
pid_prefix = "🆔 " if show_emojis else ""
|
|
141
|
-
proc_prefix = "⚙️ " if show_emojis else ""
|
|
142
|
-
friendly_prefix = "🏷️ " if show_emojis else ""
|
|
143
|
-
local_prefix = "🏠 " if show_emojis else ""
|
|
144
|
-
remote_prefix = "🌐 " if show_emojis else ""
|
|
145
|
-
|
|
146
|
-
yield Static(
|
|
147
|
-
f"{conn_prefix}Connection Info", classes="detail_title"
|
|
148
|
-
)
|
|
149
|
-
yield Static(
|
|
150
|
-
f"{pid_prefix}PID: {self.connection_data['pid']}",
|
|
151
|
-
classes="detail_item",
|
|
152
|
-
)
|
|
153
|
-
yield Static(
|
|
154
|
-
f"{proc_prefix}Process: {self.connection_data['proc']}",
|
|
155
|
-
classes="detail_item",
|
|
156
|
-
)
|
|
157
|
-
yield Static(
|
|
158
|
-
f"{friendly_prefix}Friendly Name: {self.connection_data['friendly']}",
|
|
159
|
-
classes="detail_item",
|
|
160
|
-
)
|
|
161
|
-
yield Static(
|
|
162
|
-
f"{local_prefix}Local Address: {self.connection_data['laddr']}",
|
|
163
|
-
classes="detail_item",
|
|
164
|
-
markup=False,
|
|
165
|
-
)
|
|
166
|
-
yield Static(
|
|
167
|
-
f"{remote_prefix}Remote Address: {self.connection_data['raddr']}",
|
|
168
|
-
classes="detail_item",
|
|
169
|
-
markup=False,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
status = self.connection_data["status"]
|
|
173
|
-
status_icon = self._get_status_icon(status)
|
|
174
|
-
yield Static(
|
|
175
|
-
f"{status_icon} Status: {status}",
|
|
176
|
-
classes=f"detail_item status-{status}",
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
# Show additional process info if available
|
|
180
|
-
if self.process_info:
|
|
181
|
-
with Container(id="process_info"):
|
|
182
|
-
process_title_prefix = "🔧 " if show_emojis else ""
|
|
183
|
-
exe_prefix = "📁 " if show_emojis else ""
|
|
184
|
-
cmd_prefix = "💻 " if show_emojis else ""
|
|
185
|
-
status_prefix = "📊 " if show_emojis else ""
|
|
186
|
-
user_prefix = "👤 " if show_emojis else ""
|
|
187
|
-
cwd_prefix = "📂 " if show_emojis else ""
|
|
188
|
-
threads_prefix = "🧵 " if show_emojis else ""
|
|
189
|
-
|
|
190
|
-
yield Static(
|
|
191
|
-
f"{process_title_prefix}Process Details",
|
|
192
|
-
classes="detail_title",
|
|
193
|
-
)
|
|
194
|
-
yield Static(
|
|
195
|
-
f"{exe_prefix}Executable: {self.process_info.get('exe', 'N/A')}",
|
|
196
|
-
classes="detail_item",
|
|
197
|
-
)
|
|
198
|
-
yield Static(
|
|
199
|
-
f"{cmd_prefix}Command Line: {self.process_info.get('cmd', 'N/A')}",
|
|
200
|
-
classes="detail_item",
|
|
201
|
-
)
|
|
202
|
-
yield Static(
|
|
203
|
-
f"{status_prefix}Status: {self.process_info.get('status', 'N/A')}",
|
|
204
|
-
classes="detail_item",
|
|
205
|
-
)
|
|
206
|
-
yield Static(
|
|
207
|
-
f"{user_prefix}User: {self.process_info.get('username', 'N/A')}",
|
|
208
|
-
classes="detail_item",
|
|
209
|
-
)
|
|
210
|
-
yield Static(
|
|
211
|
-
f"{cwd_prefix}Working Directory: {self.process_info.get('cwd', 'N/A')}",
|
|
212
|
-
classes="detail_item",
|
|
213
|
-
)
|
|
214
|
-
yield Static(
|
|
215
|
-
f"{threads_prefix}Threads: "
|
|
216
|
-
f"{self.process_info.get('num_threads', 'N/A')}",
|
|
217
|
-
classes="detail_item",
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
cpu_percent = self.process_info.get("cpu_percent", 0.0)
|
|
221
|
-
if show_emojis:
|
|
222
|
-
cpu_icon = (
|
|
223
|
-
"🔥"
|
|
224
|
-
if cpu_percent > 50
|
|
225
|
-
else "⚡" if cpu_percent > 10 else "💤"
|
|
226
|
-
)
|
|
227
|
-
else:
|
|
228
|
-
cpu_icon = ""
|
|
229
|
-
cpu_prefix = f"{cpu_icon} " if cpu_icon else ""
|
|
230
|
-
yield Static(
|
|
231
|
-
f"{cpu_prefix}CPU Usage: {cpu_percent:.1f}%",
|
|
232
|
-
classes="detail_item",
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
memory_percent = self.process_info.get("memory_percent", 0.0)
|
|
236
|
-
memory_display = (
|
|
237
|
-
f"{memory_percent:.2f}%"
|
|
238
|
-
if isinstance(memory_percent, (int, float))
|
|
239
|
-
else "N/A"
|
|
240
|
-
)
|
|
241
|
-
if show_emojis:
|
|
242
|
-
memory_icon = (
|
|
243
|
-
"🚨"
|
|
244
|
-
if memory_percent > 80
|
|
245
|
-
else "⚠️" if memory_percent > 50 else "💾"
|
|
246
|
-
)
|
|
247
|
-
else:
|
|
248
|
-
memory_icon = ""
|
|
249
|
-
memory_prefix = f"{memory_icon} " if memory_icon else ""
|
|
250
|
-
yield Static(
|
|
251
|
-
f"{memory_prefix}Memory Usage: {memory_display}",
|
|
252
|
-
classes="detail_item",
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
# Network connections from this process
|
|
256
|
-
connections = self.process_info.get("connections", [])
|
|
257
|
-
if connections:
|
|
258
|
-
conn_count = (
|
|
259
|
-
len(connections) if isinstance(connections, list) else 0
|
|
260
|
-
)
|
|
261
|
-
active_conn_prefix = "🌐 " if show_emojis else ""
|
|
262
|
-
yield Static(
|
|
263
|
-
f"{active_conn_prefix}Active Connections: {conn_count}",
|
|
264
|
-
classes="detail_item",
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
with Container(id="button_container"):
|
|
268
|
-
back_prefix = "🔙 " if show_emojis else ""
|
|
269
|
-
yield Button(f"{back_prefix}Back to Connections", id="back_button")
|
|
270
|
-
|
|
271
|
-
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
272
|
-
"""Handle button press events."""
|
|
273
|
-
if event.button.id == "back_button":
|
|
274
|
-
self.app.pop_screen()
|
|
16
|
+
from .types_and_constants import (
|
|
17
|
+
BASIC_KEYBINDINGS,
|
|
18
|
+
REFRESH_INTERVAL,
|
|
19
|
+
ConnectionData,
|
|
20
|
+
)
|
|
275
21
|
|
|
276
22
|
|
|
277
23
|
class NetshowApp(App):
|
|
@@ -743,6 +489,12 @@ class NetshowApp(App):
|
|
|
743
489
|
# Reset bandwidth stats when changing interface
|
|
744
490
|
self.last_network_stats = None
|
|
745
491
|
self.last_stats_time = None
|
|
492
|
+
self.refresh_connections()
|
|
493
|
+
|
|
494
|
+
# Update metrics display immediately
|
|
495
|
+
self._update_metrics_display(
|
|
496
|
+
self.total_connections, self.active_connections, self.listening_connections
|
|
497
|
+
)
|
|
746
498
|
|
|
747
499
|
def action_toggle_emojis(self) -> None:
|
|
748
500
|
"""Toggle emoji display on/off."""
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import Container, Horizontal, ScrollableContainer
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.widgets import Button, Header, Static
|
|
8
|
+
|
|
9
|
+
from .types_and_constants import ConnectionData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConnectionDetailScreen(Screen):
|
|
13
|
+
"""Screen for displaying detailed information about a selected connection."""
|
|
14
|
+
|
|
15
|
+
BINDINGS = [("escape", "app.pop_screen", "Back to connections")]
|
|
16
|
+
|
|
17
|
+
def __init__(self, connection_data: ConnectionData):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.connection_data = connection_data
|
|
20
|
+
self.proc: Optional[psutil.Process] = None
|
|
21
|
+
self.process_info = self._get_process_info(connection_data["pid"])
|
|
22
|
+
|
|
23
|
+
def _get_status_icon(self, status: str) -> str:
|
|
24
|
+
"""Get an appropriate icon for connection status."""
|
|
25
|
+
# Check if parent app has emoji toggle disabled
|
|
26
|
+
if hasattr(self.app, "show_emojis") and not self.app.show_emojis:
|
|
27
|
+
return ""
|
|
28
|
+
status_icons = {
|
|
29
|
+
"ESTABLISHED": "✅",
|
|
30
|
+
"LISTEN": "👂",
|
|
31
|
+
"TIME_WAIT": "⏳",
|
|
32
|
+
"CLOSE_WAIT": "⏸️",
|
|
33
|
+
"SYN_SENT": "📤",
|
|
34
|
+
"SYN_RECV": "📥",
|
|
35
|
+
"FIN_WAIT1": "🔄",
|
|
36
|
+
"FIN_WAIT2": "🔁",
|
|
37
|
+
"CLOSING": "🔚",
|
|
38
|
+
"LAST_ACK": "🏁",
|
|
39
|
+
}
|
|
40
|
+
return status_icons.get(status, "❓")
|
|
41
|
+
|
|
42
|
+
def _get_process_info(self, pid_str: str) -> dict:
|
|
43
|
+
"""Get additional process information if PID is available."""
|
|
44
|
+
if pid_str == "-":
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
pid = int(pid_str)
|
|
49
|
+
if self.proc is None:
|
|
50
|
+
self.proc = psutil.Process(pid)
|
|
51
|
+
self.proc.cpu_percent() # Initialize CPU percent baseline
|
|
52
|
+
|
|
53
|
+
# Type guard to ensure mypy understands proc is not None
|
|
54
|
+
assert self.proc is not None
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"name": self.proc.name(),
|
|
58
|
+
"exe": self.proc.exe(),
|
|
59
|
+
"cmd": " ".join(self.proc.cmdline()),
|
|
60
|
+
"create_time": self.proc.create_time(),
|
|
61
|
+
"status": self.proc.status(),
|
|
62
|
+
"username": self.proc.username(),
|
|
63
|
+
"cwd": self.proc.cwd(),
|
|
64
|
+
"num_threads": self.proc.num_threads(),
|
|
65
|
+
"cpu_percent": self.proc.cpu_percent(),
|
|
66
|
+
"memory_percent": self.proc.memory_percent(),
|
|
67
|
+
"open_files": self.proc.open_files(),
|
|
68
|
+
"connections": self.proc.connections(),
|
|
69
|
+
}
|
|
70
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError):
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
def compose(self) -> ComposeResult:
|
|
74
|
+
"""Compose the detail screen layout."""
|
|
75
|
+
yield Header(show_clock=True)
|
|
76
|
+
|
|
77
|
+
with ScrollableContainer():
|
|
78
|
+
# Check emoji setting from parent app
|
|
79
|
+
show_emojis = getattr(self.app, "show_emojis", True)
|
|
80
|
+
title_prefix = "🔗 " if show_emojis else ""
|
|
81
|
+
yield Static(
|
|
82
|
+
f"{title_prefix}Connection Details: {self.connection_data['friendly']}",
|
|
83
|
+
id="detail_title",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
with Horizontal(id="main_content"):
|
|
87
|
+
with Container(id="connection_details"):
|
|
88
|
+
conn_prefix = "🌐 " if show_emojis else ""
|
|
89
|
+
pid_prefix = "🆔 " if show_emojis else ""
|
|
90
|
+
proc_prefix = "⚙️ " if show_emojis else ""
|
|
91
|
+
friendly_prefix = "🏷️ " if show_emojis else ""
|
|
92
|
+
local_prefix = "🏠 " if show_emojis else ""
|
|
93
|
+
remote_prefix = "🌐 " if show_emojis else ""
|
|
94
|
+
|
|
95
|
+
yield Static(
|
|
96
|
+
f"{conn_prefix}Connection Info", classes="detail_title"
|
|
97
|
+
)
|
|
98
|
+
yield Static(
|
|
99
|
+
f"{pid_prefix}PID: {self.connection_data['pid']}",
|
|
100
|
+
classes="detail_item",
|
|
101
|
+
)
|
|
102
|
+
yield Static(
|
|
103
|
+
f"{proc_prefix}Process: {self.connection_data['proc']}",
|
|
104
|
+
classes="detail_item",
|
|
105
|
+
)
|
|
106
|
+
yield Static(
|
|
107
|
+
f"{friendly_prefix}Friendly Name: {self.connection_data['friendly']}",
|
|
108
|
+
classes="detail_item",
|
|
109
|
+
)
|
|
110
|
+
yield Static(
|
|
111
|
+
f"{local_prefix}Local Address: {self.connection_data['laddr']}",
|
|
112
|
+
classes="detail_item",
|
|
113
|
+
markup=False,
|
|
114
|
+
)
|
|
115
|
+
yield Static(
|
|
116
|
+
f"{remote_prefix}Remote Address: {self.connection_data['raddr']}",
|
|
117
|
+
classes="detail_item",
|
|
118
|
+
markup=False,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
status = self.connection_data["status"]
|
|
122
|
+
status_icon = self._get_status_icon(status)
|
|
123
|
+
yield Static(
|
|
124
|
+
f"{status_icon} Status: {status}",
|
|
125
|
+
classes=f"detail_item status-{status}",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Show additional process info if available
|
|
129
|
+
if self.process_info:
|
|
130
|
+
with Container(id="process_info"):
|
|
131
|
+
process_title_prefix = "🔧 " if show_emojis else ""
|
|
132
|
+
exe_prefix = "📁 " if show_emojis else ""
|
|
133
|
+
cmd_prefix = "💻 " if show_emojis else ""
|
|
134
|
+
status_prefix = "📊 " if show_emojis else ""
|
|
135
|
+
user_prefix = "👤 " if show_emojis else ""
|
|
136
|
+
cwd_prefix = "📂 " if show_emojis else ""
|
|
137
|
+
threads_prefix = "🧵 " if show_emojis else ""
|
|
138
|
+
|
|
139
|
+
yield Static(
|
|
140
|
+
f"{process_title_prefix}Process Details",
|
|
141
|
+
classes="detail_title",
|
|
142
|
+
)
|
|
143
|
+
yield Static(
|
|
144
|
+
f"{exe_prefix}Executable: {self.process_info.get('exe', 'N/A')}",
|
|
145
|
+
classes="detail_item",
|
|
146
|
+
)
|
|
147
|
+
yield Static(
|
|
148
|
+
f"{cmd_prefix}Command Line: {self.process_info.get('cmd', 'N/A')}",
|
|
149
|
+
classes="detail_item",
|
|
150
|
+
)
|
|
151
|
+
yield Static(
|
|
152
|
+
f"{status_prefix}Status: {self.process_info.get('status', 'N/A')}",
|
|
153
|
+
classes="detail_item",
|
|
154
|
+
)
|
|
155
|
+
yield Static(
|
|
156
|
+
f"{user_prefix}User: {self.process_info.get('username', 'N/A')}",
|
|
157
|
+
classes="detail_item",
|
|
158
|
+
)
|
|
159
|
+
yield Static(
|
|
160
|
+
f"{cwd_prefix}Working Directory: {self.process_info.get('cwd', 'N/A')}",
|
|
161
|
+
classes="detail_item",
|
|
162
|
+
)
|
|
163
|
+
yield Static(
|
|
164
|
+
f"{threads_prefix}Threads: "
|
|
165
|
+
f"{self.process_info.get('num_threads', 'N/A')}",
|
|
166
|
+
classes="detail_item",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
cpu_percent = self.process_info.get("cpu_percent", 0.0)
|
|
170
|
+
if show_emojis:
|
|
171
|
+
cpu_icon = (
|
|
172
|
+
"🔥"
|
|
173
|
+
if cpu_percent > 50
|
|
174
|
+
else "⚡" if cpu_percent > 10 else "💤"
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
cpu_icon = ""
|
|
178
|
+
cpu_prefix = f"{cpu_icon} " if cpu_icon else ""
|
|
179
|
+
yield Static(
|
|
180
|
+
f"{cpu_prefix}CPU Usage: {cpu_percent:.1f}%",
|
|
181
|
+
classes="detail_item",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
memory_percent = self.process_info.get("memory_percent", 0.0)
|
|
185
|
+
memory_display = (
|
|
186
|
+
f"{memory_percent:.2f}%"
|
|
187
|
+
if isinstance(memory_percent, (int, float))
|
|
188
|
+
else "N/A"
|
|
189
|
+
)
|
|
190
|
+
if show_emojis:
|
|
191
|
+
memory_icon = (
|
|
192
|
+
"🚨"
|
|
193
|
+
if memory_percent > 80
|
|
194
|
+
else "⚠️" if memory_percent > 50 else "💾"
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
memory_icon = ""
|
|
198
|
+
memory_prefix = f"{memory_icon} " if memory_icon else ""
|
|
199
|
+
yield Static(
|
|
200
|
+
f"{memory_prefix}Memory Usage: {memory_display}",
|
|
201
|
+
classes="detail_item",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Network connections from this process
|
|
205
|
+
connections = self.process_info.get("connections", [])
|
|
206
|
+
if connections:
|
|
207
|
+
conn_count = (
|
|
208
|
+
len(connections) if isinstance(connections, list) else 0
|
|
209
|
+
)
|
|
210
|
+
active_conn_prefix = "🌐 " if show_emojis else ""
|
|
211
|
+
yield Static(
|
|
212
|
+
f"{active_conn_prefix}Active Connections: {conn_count}",
|
|
213
|
+
classes="detail_item",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
with Container(id="button_container"):
|
|
217
|
+
back_prefix = "🔙 " if show_emojis else ""
|
|
218
|
+
yield Button(f"{back_prefix}Back to Connections", id="back_button")
|
|
219
|
+
|
|
220
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
221
|
+
"""Handle button press events."""
|
|
222
|
+
if event.button.id == "back_button":
|
|
223
|
+
self.app.pop_screen()
|
|
@@ -12,9 +12,9 @@ $fg_1: #cad8d9;
|
|
|
12
12
|
$red: #fa5750;
|
|
13
13
|
$green: #75b938;
|
|
14
14
|
$yellow: #dbb32d;
|
|
15
|
-
$blue: #
|
|
15
|
+
$blue: #2d5b69;
|
|
16
16
|
$magenta: #f275be;
|
|
17
|
-
$cyan: #
|
|
17
|
+
$cyan: #4191a5;
|
|
18
18
|
$orange: #ed8649;
|
|
19
19
|
$violet: #af88eb;
|
|
20
20
|
|
|
@@ -101,7 +101,7 @@ Header {
|
|
|
101
101
|
color: $fg_1;
|
|
102
102
|
border-bottom: solid $blue;
|
|
103
103
|
text-style: bold;
|
|
104
|
-
height:
|
|
104
|
+
height: 2;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
Footer {
|
|
@@ -141,7 +141,7 @@ Vertical {
|
|
|
141
141
|
|
|
142
142
|
/* === EDGE BORDER FIX === */
|
|
143
143
|
#connection_details {
|
|
144
|
-
border-right:
|
|
144
|
+
border-right: #fff;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
#process_info {
|
|
@@ -202,6 +202,7 @@ DataTable:focus .datatable--cursor {
|
|
|
202
202
|
margin: 0;
|
|
203
203
|
padding: 0;
|
|
204
204
|
border: none;
|
|
205
|
+
width: 100%;
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
#connection_details, #process_info {
|
|
@@ -241,7 +242,6 @@ DataTable:focus .datatable--cursor {
|
|
|
241
242
|
.detail_item:hover {
|
|
242
243
|
color: $fg_1;
|
|
243
244
|
background: $bg_2;
|
|
244
|
-
border-left: $br_blue;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/* === BUTTONS === */
|
|
@@ -305,9 +305,6 @@ ScrollableContainer {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
/* === ACCESSIBILITY === */
|
|
308
|
-
*:focus {
|
|
309
|
-
outline: thick $blue;
|
|
310
|
-
}
|
|
311
308
|
|
|
312
309
|
.epic-glow {
|
|
313
310
|
color: $br_blue;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
3
|
+
# Constants
|
|
4
|
+
REFRESH_INTERVAL = 3.0 # seconds
|
|
5
|
+
CONNECTION_COLUMNS = ["pid", "friendly", "proc", "laddr", "raddr", "status"]
|
|
6
|
+
BASIC_KEYBINDINGS = [
|
|
7
|
+
("q", "quit", "Quit"),
|
|
8
|
+
("ctrl+r", "refresh", "Force Refresh"),
|
|
9
|
+
("f", "toggle_filter", "Filter"),
|
|
10
|
+
("s", "sort_by_status", "Sort by Status"),
|
|
11
|
+
("p", "sort_by_process", "Sort by Process"),
|
|
12
|
+
("i", "toggle_interface", "Interface"),
|
|
13
|
+
("e", "toggle_emojis", "Emojis"),
|
|
14
|
+
("ctrl+c", "quit", "Hard Quit"),
|
|
15
|
+
("/", "search", "Search"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConnectionData(TypedDict):
|
|
20
|
+
"""Type definition for connection data."""
|
|
21
|
+
|
|
22
|
+
pid: str
|
|
23
|
+
friendly: str
|
|
24
|
+
proc: str
|
|
25
|
+
laddr: str
|
|
26
|
+
raddr: str
|
|
27
|
+
status: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NetworkStats(TypedDict):
|
|
31
|
+
"""Type definition for network statistics."""
|
|
32
|
+
|
|
33
|
+
total_connections: int
|
|
34
|
+
established: int
|
|
35
|
+
listening: int
|
|
36
|
+
time_wait: int
|
|
37
|
+
bytes_sent: int
|
|
38
|
+
bytes_recv: int
|
|
39
|
+
packets_sent: int
|
|
40
|
+
packets_recv: int
|
|
41
|
+
timestamp: float
|
|
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
|
|
File without changes
|
|
File without changes
|