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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netshow
3
- Version: 0.2.0
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/97f5af92-ec0a-4243-9894-d0c9d9470e1a"></video>
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
- > **This README was crafted with AI assistance, and here's why that matters**
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
- > In this case, Sonnet 4 took a pass & a human (me) verified them 6/28/25.
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/97f5af92-ec0a-4243-9894-d0c9d9470e1a"></video>
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
- > **This README was crafted with AI assistance, and here's why that matters**
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
- > In this case, Sonnet 4 took a pass & a human (me) verified them 6/28/25.
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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "netshow"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "A real-time network connection monitor with friendly service names"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,277 +1,23 @@
1
1
  import os
2
2
  import time
3
- from typing import Any, Optional, TypedDict, Union, cast
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
- # Constants
30
- REFRESH_INTERVAL = 3.0 # seconds
31
- CONNECTION_COLUMNS = ["pid", "friendly", "proc", "laddr", "raddr", "status"]
32
- BASIC_KEYBINDINGS = [
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: #4695f7;
15
+ $blue: #2d5b69;
16
16
  $magenta: #f275be;
17
- $cyan: #41c7b9;
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: 3;
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: none;
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
@@ -214,7 +214,7 @@ wheels = [
214
214
 
215
215
  [[package]]
216
216
  name = "netshow"
217
- version = "0.1.2"
217
+ version = "0.2.0"
218
218
  source = { editable = "." }
219
219
  dependencies = [
220
220
  { name = "psutil" },
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