netwatchpy 0.2.9__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pranav Kishan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: netwatchpy
3
+ Version: 0.2.9
4
+ Summary: A TUI network usage monitor.
5
+ Author-email: Pranav Kishan <ty.pranavkishan7905@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Destroyer795/Netwatchpy-package
8
+ Project-URL: Issues, https://github.com/Destroyer795/Netwatchpy-package/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: textual
13
+ Requires-Dist: psutil
14
+ Requires-Dist: platformdirs
15
+ Requires-Dist: desktop-notifier; platform_system != "Linux"
16
+ Requires-Dist: desktop-notifier[dbus]; platform_system == "Linux"
17
+ Dynamic: license-file
18
+
19
+ # Netwatch TUI (netwatchpy)
20
+
21
+ [![PyPI Version](https://img.shields.io/pypi/v/netwatchpy.svg)](https://pypi.org/project/netwatchpy/)
22
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
23
+
24
+ A TUI (Text-based User Interface) for monitoring network usage in real-time, with support for data limits and desktop notifications.
25
+
26
+ ---
27
+ <p align="center">
28
+ <img width="800" height="500" alt="netwatch_demo" src="/assets/demo.png" />
29
+ </p>
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ * **Real-time Dashboard:** A clean, terminal-based dashboard showing total download, upload, and current speeds.
36
+ * **Persistent Quota:** Automatically saves your total usage, so you can track a monthly data cap across multiple sessions.
37
+
38
+ * **Data Cap Monitoring:** Set a data limit (e.g., `10GB`) and see your usage on a real-time progress bar.
39
+ * **Desktop Notifications:** Get native desktop notifications when you hit 80% and 100% of your data limit.
40
+ * **Live Activity Log:** An interactive, scrolling log of all network activity, updated every second.
41
+ * **Command Palette & Dark Mode:** A modern TUI experience with a built-in command palette (`Ctrl+P`) and dark mode support (`Ctrl+D`).
42
+ * **CSV Logging:** Optionally log all network traffic to a CSV file for later analysis.
43
+ * **Cross-Platform:** Runs on Windows, macOS, and Linux.
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ `netwatchpy` is available on PyPI. All you need is Python 3.8+ and `pip`.
50
+
51
+ ```bash
52
+ pip install netwatchpy
53
+ ```
54
+ ---
55
+
56
+ ## Usage
57
+
58
+ Once installed, the netwatch command will be available in your terminal.
59
+
60
+ **To start the monitor (monitors all interfaces):**
61
+
62
+ ```bash
63
+ netwatch
64
+ ```
65
+
66
+ **To see all available options and help:**
67
+
68
+ ```bash
69
+ netwatch --help
70
+ ```
71
+ ---
72
+ ## Command-Line Arguments
73
+
74
+ You can control the monitor with these arguments:
75
+
76
+ * **`-h`,` --help`**
77
+
78
+ * Shows the help message and all options.
79
+
80
+ * **`-i INTERFACE`,` --interface INTERFACE`**
81
+
82
+ * Monitors a single, specific network interface (e.g., -i "Wi-Fi"). Defaults to "all".
83
+
84
+ * **`-l LIMIT`,` --limit LIMIT`**
85
+
86
+ * Sets a data cap and shows a progress bar (e.g., -l "10GB" or -l "500MB").
87
+
88
+ * **`--log LOG`**
89
+
90
+ * Saves all network activity to a specified CSV file (e.g., --log "usage.csv").
91
+
92
+ ---
93
+
94
+ ### Examples
95
+
96
+ **Set a 10GB data limit:**
97
+
98
+ ```bash
99
+ netwatch -l "10GB"
100
+ ```
101
+
102
+ **Monitor a specific interface ("Wi-Fi") and log to a file:**
103
+
104
+ ```bash
105
+ netwatch -i "Wi-Fi" --log "my_usage.csv"
106
+ ```
107
+ ---
108
+ ### In-App Keybindings
109
+
110
+ * **`Ctrl+p`**: Open the Command Palette
111
+
112
+ * **`Ctrl+d`**: Toggle Dark / Light Mode
113
+
114
+ * **`Ctrl+s`**: Toggle Dark / Light Mode
115
+
116
+ * **`r`**: Reset all counters back to zero
117
+
118
+ * **`Ctrl+q`**: Quit the application
119
+
120
+ ---
121
+
122
+ Bug Reports & Feature Requests
123
+ ---------------------------------
124
+
125
+ Found a bug or have a great idea? Please [open an issue]([https://github.com/destroyer795/Netwatchpy-package/issues](https://github.com/destroyer795/Netwatchpy-package/issues)) on the GitHub repository.
126
+
127
+ License
128
+ ----------
129
+
130
+ This project is licensed under the MIT License.
@@ -0,0 +1,112 @@
1
+ # Netwatch TUI (netwatchpy)
2
+
3
+ [![PyPI Version](https://img.shields.io/pypi/v/netwatchpy.svg)](https://pypi.org/project/netwatchpy/)
4
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
5
+
6
+ A TUI (Text-based User Interface) for monitoring network usage in real-time, with support for data limits and desktop notifications.
7
+
8
+ ---
9
+ <p align="center">
10
+ <img width="800" height="500" alt="netwatch_demo" src="/assets/demo.png" />
11
+ </p>
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ * **Real-time Dashboard:** A clean, terminal-based dashboard showing total download, upload, and current speeds.
18
+ * **Persistent Quota:** Automatically saves your total usage, so you can track a monthly data cap across multiple sessions.
19
+
20
+ * **Data Cap Monitoring:** Set a data limit (e.g., `10GB`) and see your usage on a real-time progress bar.
21
+ * **Desktop Notifications:** Get native desktop notifications when you hit 80% and 100% of your data limit.
22
+ * **Live Activity Log:** An interactive, scrolling log of all network activity, updated every second.
23
+ * **Command Palette & Dark Mode:** A modern TUI experience with a built-in command palette (`Ctrl+P`) and dark mode support (`Ctrl+D`).
24
+ * **CSV Logging:** Optionally log all network traffic to a CSV file for later analysis.
25
+ * **Cross-Platform:** Runs on Windows, macOS, and Linux.
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ `netwatchpy` is available on PyPI. All you need is Python 3.8+ and `pip`.
32
+
33
+ ```bash
34
+ pip install netwatchpy
35
+ ```
36
+ ---
37
+
38
+ ## Usage
39
+
40
+ Once installed, the netwatch command will be available in your terminal.
41
+
42
+ **To start the monitor (monitors all interfaces):**
43
+
44
+ ```bash
45
+ netwatch
46
+ ```
47
+
48
+ **To see all available options and help:**
49
+
50
+ ```bash
51
+ netwatch --help
52
+ ```
53
+ ---
54
+ ## Command-Line Arguments
55
+
56
+ You can control the monitor with these arguments:
57
+
58
+ * **`-h`,` --help`**
59
+
60
+ * Shows the help message and all options.
61
+
62
+ * **`-i INTERFACE`,` --interface INTERFACE`**
63
+
64
+ * Monitors a single, specific network interface (e.g., -i "Wi-Fi"). Defaults to "all".
65
+
66
+ * **`-l LIMIT`,` --limit LIMIT`**
67
+
68
+ * Sets a data cap and shows a progress bar (e.g., -l "10GB" or -l "500MB").
69
+
70
+ * **`--log LOG`**
71
+
72
+ * Saves all network activity to a specified CSV file (e.g., --log "usage.csv").
73
+
74
+ ---
75
+
76
+ ### Examples
77
+
78
+ **Set a 10GB data limit:**
79
+
80
+ ```bash
81
+ netwatch -l "10GB"
82
+ ```
83
+
84
+ **Monitor a specific interface ("Wi-Fi") and log to a file:**
85
+
86
+ ```bash
87
+ netwatch -i "Wi-Fi" --log "my_usage.csv"
88
+ ```
89
+ ---
90
+ ### In-App Keybindings
91
+
92
+ * **`Ctrl+p`**: Open the Command Palette
93
+
94
+ * **`Ctrl+d`**: Toggle Dark / Light Mode
95
+
96
+ * **`Ctrl+s`**: Toggle Dark / Light Mode
97
+
98
+ * **`r`**: Reset all counters back to zero
99
+
100
+ * **`Ctrl+q`**: Quit the application
101
+
102
+ ---
103
+
104
+ Bug Reports & Feature Requests
105
+ ---------------------------------
106
+
107
+ Found a bug or have a great idea? Please [open an issue]([https://github.com/destroyer795/Netwatchpy-package/issues](https://github.com/destroyer795/Netwatchpy-package/issues)) on the GitHub repository.
108
+
109
+ License
110
+ ----------
111
+
112
+ This project is licensed under the MIT License.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "netwatchpy"
7
+ version = "0.2.9"
8
+ authors = [
9
+ { name = "Pranav Kishan", email = "ty.pranavkishan7905@gmail.com" }
10
+ ]
11
+ description = "A TUI network usage monitor."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ license = { text = "MIT" }
15
+
16
+ dependencies = [
17
+ "textual",
18
+ "psutil",
19
+ "platformdirs",
20
+ "desktop-notifier; platform_system != 'Linux'",
21
+ "desktop-notifier[dbus]; platform_system == 'Linux'"
22
+ ]
23
+
24
+ [project.scripts]
25
+ netwatch = "netwatch.tui:main"
26
+
27
+ [tool.setuptools]
28
+ package-dir = {"" = "src"}
29
+ packages = ["netwatch"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/Destroyer795/Netwatchpy-package"
33
+ Issues = "https://github.com/Destroyer795/Netwatchpy-package/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,108 @@
1
+ import psutil
2
+ import threading
3
+ from datetime import datetime
4
+ import csv
5
+ from .utils import get_size
6
+
7
+
8
+ class NetworkMonitorThread(threading.Thread):
9
+ def __init__(self, callback, interface="all", log_file=None,
10
+ initial_upload=0, initial_download=0, interval=1.0):
11
+ super().__init__()
12
+ self.daemon = True
13
+ self.callback = callback
14
+ self.interface = interface
15
+ self.log_file = log_file
16
+ self.interval = interval
17
+ self.stop_event = threading.Event()
18
+
19
+ self.total_up = int(initial_upload)
20
+ self.total_down = int(initial_download)
21
+ if self.log_file:
22
+ try:
23
+ file_exists = False
24
+ try:
25
+ with open(self.log_file, "r", encoding="utf-8"):
26
+ file_exists = True
27
+ except FileNotFoundError:
28
+ file_exists = False
29
+ with open(self.log_file, "a", newline="", encoding="utf-8") as f:
30
+ writer = csv.writer(f)
31
+ if not file_exists:
32
+ writer.writerow(["Timestamp", "Upload Speed (B/s)", "Download Speed (B/s)", "Total Upload", "Total Download", "Total Usage"])
33
+ except Exception:
34
+ self.log_file = None
35
+
36
+ def stop(self):
37
+ self.stop_event.set()
38
+
39
+ def run(self):
40
+ try:
41
+ last = psutil.net_io_counters(pernic=True)
42
+ except Exception:
43
+ self.callback({"error": "Failed to get initial network stats."})
44
+ return
45
+
46
+ if not last:
47
+ self.callback({"error": "No network interfaces found."})
48
+ return
49
+ if self.interface != 'all' and self.interface not in last:
50
+ self.callback({"error": f"Interface '{self.interface}' not found."})
51
+ return
52
+
53
+ while not self.stop_event.is_set():
54
+ try:
55
+ self.stop_event.wait(self.interval)
56
+ if self.stop_event.is_set():
57
+ break
58
+
59
+ now = psutil.net_io_counters(pernic=True)
60
+ if not now:
61
+ continue
62
+
63
+ up = 0
64
+ down = 0
65
+
66
+ if self.interface == "all":
67
+ for iface in now:
68
+ if iface in last:
69
+ up += now[iface].bytes_sent - last[iface].bytes_sent
70
+ down += now[iface].bytes_recv - last[iface].bytes_recv
71
+ else:
72
+ if self.interface in now and self.interface in last:
73
+ up = now[self.interface].bytes_sent - last[self.interface].bytes_sent
74
+ down = now[self.interface].bytes_recv - last[self.interface].bytes_recv
75
+
76
+ last = now
77
+
78
+ up = max(0, int(up))
79
+ down = max(0, int(down))
80
+
81
+ self.total_up += up
82
+ self.total_down += down
83
+
84
+ packet = {
85
+ "upload_speed": up,
86
+ "download_speed": down,
87
+ "total_upload": self.total_up,
88
+ "total_download": self.total_down,
89
+ "total_usage": self.total_up + self.total_down,
90
+ "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
91
+ }
92
+
93
+ if self.log_file:
94
+ try:
95
+ with open(self.log_file, "a", newline="", encoding="utf-8") as f:
96
+ writer = csv.writer(f)
97
+ writer.writerow([packet["timestamp"], up, down, get_size(self.total_up), get_size(self.total_down), get_size(packet["total_usage"])])
98
+ except Exception:
99
+ self.log_file = None
100
+
101
+ if self.stop_event.is_set():
102
+ break
103
+ self.callback(packet)
104
+
105
+ except Exception as e:
106
+ if not self.stop_event.is_set():
107
+ self.callback({"error": f"Error in monitor loop: {e}"})
108
+ self.stop_event.wait(3.0)
@@ -0,0 +1,370 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import sys
5
+ from textwrap import dedent
6
+ from platformdirs import user_config_dir
7
+
8
+ from textual.app import App, ComposeResult
9
+ from textual.containers import VerticalScroll, Horizontal, Container
10
+ from textual.widgets import Header, Footer, DataTable, ProgressBar, Static
11
+ from textual.reactive import var
12
+ from desktop_notifier import DesktopNotifier
13
+
14
+ from .utils import get_size, parse_limit
15
+ from .monitor import NetworkMonitorThread
16
+
17
+
18
+
19
+ class NetMonitorTUI(App):
20
+ TITLE = "Network Usage Monitor"
21
+ SUB_TITLE = "Press Ctrl+P for commands, Ctrl+Q to quit"
22
+
23
+ BINDINGS = [
24
+ ("ctrl+p", "command_palette", "Command Palette"),
25
+ ("ctrl+d", "toggle_dark", "Toggle Dark Mode"),
26
+ ("r", "reset_counters", "Reset Counters"),
27
+ ("ctrl+s", "save_quota", "Save Quota"),
28
+ ("ctrl+q", "quit", "Quit"),
29
+ ]
30
+
31
+ CSS = dedent("""
32
+ Screen { background: #f8f8f8; color: black; }
33
+ .-dark-mode Screen { background: #101010; color: #f0f0f0; }
34
+
35
+ #main_container { layout: vertical; }
36
+ #summary_cards { layout: horizontal; height: auto; padding: 1 0; }
37
+
38
+ .summary_card {
39
+ width: 1fr;
40
+ min-height: 5;
41
+ border: solid black;
42
+ padding: 1;
43
+ margin: 0 1;
44
+ background: #e8e8e8;
45
+ }
46
+ .-dark-mode .summary_card {
47
+ border: solid #888;
48
+ background: #222;
49
+ color: #e0e0e0;
50
+ }
51
+
52
+ #limit_container { height: auto; padding: 0 1 1 1; }
53
+
54
+ #stats_table {
55
+ height: 1fr;
56
+ margin: 0 1;
57
+ border: solid black;
58
+ background: white; /* Light mode background */
59
+ color: black; /* Light mode text */
60
+ }
61
+ .-dark-mode #stats_table {
62
+ border: solid #666;
63
+ background: #222; /* Dark mode background */
64
+ color: #e0e0e0; /* Dark mode text */
65
+ }
66
+
67
+ ProgressBar {
68
+ background: #e8e8e8; /* Light mode track */
69
+ }
70
+ .-dark-mode ProgressBar {
71
+ background: #222; /* Dark mode track */
72
+ }
73
+
74
+ ProgressBar > .progress-bar--bar { background: #007acc; }
75
+ .-dark-mode ProgressBar > .progress-bar--bar { background: #55aaff; }
76
+
77
+ #error_box {
78
+ height: auto;
79
+ padding: 1 2;
80
+ color: red;
81
+ display: none;
82
+ }
83
+ """)
84
+
85
+ # Reactive vars (auto update UI)
86
+ total_upload = var(0)
87
+ total_download = var(0)
88
+ total_usage = var(0)
89
+ upload_speed = var(0)
90
+ download_speed = var(0)
91
+ dark = var(False)
92
+
93
+ def __init__(self, interface="all", limit_str=None, log_file=None):
94
+ super().__init__()
95
+ self.interface = interface
96
+ self.limit_bytes = parse_limit(limit_str)
97
+ self.limit_str = limit_str or "No Limit"
98
+ self.log_file = log_file
99
+
100
+ self.notifier = DesktopNotifier(app_name="Netwatch")
101
+ self.monitor_thread = None
102
+ self._save_lock = None # added in on_mount
103
+
104
+ self.alert_80_sent = False
105
+ self.alert_100_sent = False
106
+
107
+ # Persistent storage location
108
+ self.config_dir = user_config_dir("netwatchpy")
109
+ self.quota_file = os.path.join(self.config_dir, "quota.json")
110
+
111
+ def _load_persistent_quota(self):
112
+ try:
113
+ with open(self.quota_file, "r", encoding="utf-8") as f:
114
+ data = json.load(f)
115
+ return (
116
+ int(data.get("total_upload", 0)),
117
+ int(data.get("total_download", 0)),
118
+ )
119
+ except Exception:
120
+ return 0, 0
121
+
122
+ def _save_persistent_quota(self):
123
+ """Safe atomic save of totals."""
124
+ try:
125
+ os.makedirs(self.config_dir, exist_ok=True)
126
+
127
+ payload = {
128
+ "total_upload": int(self.total_upload),
129
+ "total_download": int(self.total_download),
130
+ }
131
+
132
+ tmp = self.quota_file + ".tmp"
133
+ with open(tmp, "w", encoding="utf-8") as f:
134
+ json.dump(payload, f, indent=2)
135
+
136
+ os.replace(tmp, self.quota_file)
137
+ except Exception as e:
138
+ print(f"[netwatch save error] {e}", file=sys.stderr)
139
+
140
+ def compose(self) -> ComposeResult:
141
+ yield Header()
142
+
143
+ with VerticalScroll(id="main_container"):
144
+ with Horizontal(id="summary_cards"):
145
+ yield Static("Total Download\n[b]0.00 B[/b]", id="total-dl-card", classes="summary_card")
146
+ yield Static("Total Upload\n[b]0.00 B[/b]", id="total-ul-card", classes="summary_card")
147
+ yield Static("Total Usage\n[b]0.00 B[/b]", id="total-usage-card", classes="summary_card")
148
+
149
+ with Container(id="limit_container"):
150
+ if self.limit_bytes:
151
+ yield Static(f"Usage Limit: {get_size(self.limit_bytes)}")
152
+ yield ProgressBar(id="limit_bar", total=self.limit_bytes, show_eta=False)
153
+ else:
154
+ yield Static("Usage Limit: Not Set")
155
+
156
+ yield Static(id="error_box")
157
+ yield DataTable(id="stats_table")
158
+
159
+ yield Footer()
160
+
161
+ def on_mount(self):
162
+ """Executed when TUI is ready."""
163
+ from threading import Lock
164
+ self._save_lock = Lock()
165
+
166
+ # Build table
167
+ table = self.query_one(DataTable)
168
+ table.add_column("Time")
169
+ table.add_column("Up Speed")
170
+ table.add_column("Down Speed")
171
+ table.add_column("Total Up")
172
+ table.add_column("Total Down")
173
+ table.add_column("Total Usage")
174
+
175
+ # Load saved totals before thread start
176
+ up, down = self._load_persistent_quota()
177
+ self.total_upload = up
178
+ self.total_download = down
179
+ self.total_usage = up + down
180
+
181
+ if self.limit_bytes:
182
+ try:
183
+ bar = self.query_one(ProgressBar)
184
+ bar.progress = self.total_usage
185
+ except Exception:
186
+ pass
187
+
188
+ # Start autosave (every 10 seconds)
189
+ self.set_interval(10, self._autosave_job)
190
+
191
+ self.monitor_thread = NetworkMonitorThread(
192
+ self.on_data_update,
193
+ interface=self.interface,
194
+ log_file=self.log_file,
195
+ initial_upload=up,
196
+ initial_download=down,
197
+ )
198
+ self.monitor_thread.start()
199
+
200
+ def on_exit(self):
201
+ """
202
+ Stop the thread, then do a final save.
203
+ """
204
+ if self.monitor_thread:
205
+ self.monitor_thread.stop()
206
+ self.monitor_thread.join(timeout=1.5)
207
+
208
+ print("\n[netwatch] Quitting... saving final quota.")
209
+ if self._save_lock.acquire(timeout=2.0):
210
+ try:
211
+ self._save_persistent_quota()
212
+ print("[netwatch] Final save complete.")
213
+ except Exception as e:
214
+ print(f"[netwatch] Final save failed: {e}", file=sys.stderr)
215
+ finally:
216
+ self._save_lock.release()
217
+ else:
218
+ print("[netwatch] Could not acquire lock, final save skipped.", file=sys.stderr)
219
+
220
+ def _autosave_job(self):
221
+ """Called every 10 seconds."""
222
+ if self._save_lock.acquire(timeout=0.1):
223
+ try:
224
+ self._save_persistent_quota()
225
+ finally:
226
+ self._save_lock.release()
227
+
228
+ def on_data_update(self, data: dict):
229
+ self.call_from_thread(self._process_data_packet, data)
230
+
231
+ def _process_data_packet(self, data: dict):
232
+ if "error" in data:
233
+ box = self.query_one("#error_box")
234
+ box.update("ERROR: " + data["error"])
235
+ box.styles.display = "block"
236
+ return
237
+
238
+ if "timestamp" not in data:
239
+ return
240
+
241
+ self.upload_speed = data["upload_speed"]
242
+ self.download_speed = data["download_speed"]
243
+ self.total_upload = data["total_upload"]
244
+ self.total_download = data["total_download"]
245
+ self.total_usage = data["total_usage"]
246
+
247
+ table = self.query_one(DataTable)
248
+ table.add_row(
249
+ data["timestamp"].split(" ")[1],
250
+ f"{get_size(self.upload_speed)}/s",
251
+ f"{get_size(self.download_speed)}/s",
252
+ get_size(self.total_upload),
253
+ get_size(self.total_download),
254
+ get_size(self.total_usage),
255
+ )
256
+ table.scroll_end(animate=False)
257
+
258
+ if table.row_count > 50:
259
+ first_key = next(iter(table.rows.keys()))
260
+ table.remove_row(first_key)
261
+
262
+ def watch_total_download(self, new):
263
+ self.query_one("#total-dl-card").update(
264
+ f"Total Download\n[b]{get_size(new)}[/b]"
265
+ )
266
+
267
+ def watch_total_upload(self, new):
268
+ self.query_one("#total-ul-card").update(
269
+ f"Total Upload\n[b]{get_size(new)}[/b]"
270
+ )
271
+
272
+ async def watch_total_usage(self, new_total_usage: int):
273
+ self.query_one("#total-usage-card").update(
274
+ f"Total Usage\n[b]{get_size(new_total_usage)}[/b]"
275
+ )
276
+
277
+ if self.limit_bytes:
278
+ bar = self.query_one(ProgressBar)
279
+ bar.progress = new_total_usage
280
+
281
+ # 80% warning
282
+ if new_total_usage >= 0.8 * self.limit_bytes and not self.alert_80_sent:
283
+ self.alert_80_sent = True
284
+ bar.styles.color = "yellow"
285
+ self.sub_title = "⚠️ 80% of limit reached!"
286
+ try:
287
+ await self.notifier.send(
288
+ title="Netwatch: 80% Usage Warning",
289
+ message=f"You have used {get_size(new_total_usage)} of your {get_size(self.limit_bytes)} limit."
290
+ )
291
+ except Exception as e:
292
+ print(f"[Notification Error] {e}", file=sys.stderr) # Log to stderr
293
+
294
+ # 100% alert
295
+ if new_total_usage >= self.limit_bytes and not self.alert_100_sent:
296
+ self.alert_100_sent = True
297
+ bar.styles.color = "red"
298
+ self.sub_title = "🚨 Data limit exceeded!"
299
+ try:
300
+ await self.notifier.send(
301
+ title="Netwatch: Data Limit Exceeded!",
302
+ message=f"You have exceeded your {get_size(self.limit_bytes)} data limit."
303
+ )
304
+ except Exception as e:
305
+ print(f"[Notification Error] {e}", file=sys.stderr) # Log to stderr
306
+
307
+ def action_toggle_dark(self):
308
+ self.dark = not self.dark
309
+ self.set_class(self.dark, "-dark-mode")
310
+ self.sub_title = "🌙 Dark Mode" if self.dark else "☀️ Light Mode"
311
+
312
+ def action_reset_counters(self):
313
+ """Resets all counters to 0 and saves this reset."""
314
+ if self.monitor_thread:
315
+ self.monitor_thread.stop()
316
+ self.monitor_thread.join(timeout=1)
317
+
318
+ self.total_upload = 0
319
+ self.total_download = 0
320
+ self.total_usage = 0
321
+ self.sub_title = "Quota Reset to 0"
322
+
323
+ self.alert_80_sent = False
324
+ self.alert_100_sent = False
325
+
326
+ # get lock to save the reset
327
+ if self._save_lock.acquire(timeout=1.0):
328
+ try:
329
+ self._save_persistent_quota()
330
+ finally:
331
+ self._save_lock.release()
332
+
333
+ # Restart fresh thread
334
+ self.monitor_thread = NetworkMonitorThread(
335
+ self.on_data_update,
336
+ interface=self.interface,
337
+ log_file=self.log_file,
338
+ initial_upload=0,
339
+ initial_download=0,
340
+ )
341
+ self.monitor_thread.start()
342
+
343
+ def action_save_quota(self) -> None:
344
+ """Manually save the current quota totals."""
345
+ if self._save_lock.acquire(timeout=0.1):
346
+ try:
347
+ self._save_persistent_quota()
348
+ self.sub_title = "Quota Saved!"
349
+ finally:
350
+ self._save_lock.release()
351
+ else:
352
+ self.sub_title = "Saving... please wait."
353
+
354
+ def main():
355
+ parser = argparse.ArgumentParser(description="Network Usage Monitor TUI")
356
+ parser.add_argument("-i", "--interface", default="all")
357
+ parser.add_argument("-l", "--limit")
358
+ parser.add_argument("--log")
359
+ args = parser.parse_args()
360
+
361
+ app = NetMonitorTUI(
362
+ interface=args.interface,
363
+ limit_str=args.limit,
364
+ log_file=args.log,
365
+ )
366
+ app.run()
367
+
368
+
369
+ if __name__ == "__main__":
370
+ main()
@@ -0,0 +1,31 @@
1
+ def get_size(byte_val):
2
+ """Convert bytes into human readable string."""
3
+ if byte_val is None:
4
+ return "0 B"
5
+ power = 1024
6
+ n = 0
7
+ units = ["", "K", "M", "G", "T"]
8
+ while byte_val >= power and n < len(units) - 1:
9
+ byte_val /= power
10
+ n += 1
11
+ return f"{byte_val:.2f} {units[n]}B"
12
+
13
+
14
+ def parse_limit(size_str):
15
+ """Parse a string like '10GB' or '500MB' into bytes."""
16
+ if not size_str:
17
+ return None
18
+ s = size_str.upper().strip()
19
+
20
+ if s.endswith("GB"):
21
+ return int(float(s[:-2]) * 1024**3)
22
+ if s.endswith("MB"):
23
+ return int(float(s[:-2]) * 1024**2)
24
+ if s.endswith("KB"):
25
+ return int(float(s[:-2]) * 1024)
26
+
27
+ # fallback: maybe a plain number
28
+ try:
29
+ return int(float(s))
30
+ except ValueError:
31
+ return None
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: netwatchpy
3
+ Version: 0.2.9
4
+ Summary: A TUI network usage monitor.
5
+ Author-email: Pranav Kishan <ty.pranavkishan7905@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Destroyer795/Netwatchpy-package
8
+ Project-URL: Issues, https://github.com/Destroyer795/Netwatchpy-package/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: textual
13
+ Requires-Dist: psutil
14
+ Requires-Dist: platformdirs
15
+ Requires-Dist: desktop-notifier; platform_system != "Linux"
16
+ Requires-Dist: desktop-notifier[dbus]; platform_system == "Linux"
17
+ Dynamic: license-file
18
+
19
+ # Netwatch TUI (netwatchpy)
20
+
21
+ [![PyPI Version](https://img.shields.io/pypi/v/netwatchpy.svg)](https://pypi.org/project/netwatchpy/)
22
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
23
+
24
+ A TUI (Text-based User Interface) for monitoring network usage in real-time, with support for data limits and desktop notifications.
25
+
26
+ ---
27
+ <p align="center">
28
+ <img width="800" height="500" alt="netwatch_demo" src="/assets/demo.png" />
29
+ </p>
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ * **Real-time Dashboard:** A clean, terminal-based dashboard showing total download, upload, and current speeds.
36
+ * **Persistent Quota:** Automatically saves your total usage, so you can track a monthly data cap across multiple sessions.
37
+
38
+ * **Data Cap Monitoring:** Set a data limit (e.g., `10GB`) and see your usage on a real-time progress bar.
39
+ * **Desktop Notifications:** Get native desktop notifications when you hit 80% and 100% of your data limit.
40
+ * **Live Activity Log:** An interactive, scrolling log of all network activity, updated every second.
41
+ * **Command Palette & Dark Mode:** A modern TUI experience with a built-in command palette (`Ctrl+P`) and dark mode support (`Ctrl+D`).
42
+ * **CSV Logging:** Optionally log all network traffic to a CSV file for later analysis.
43
+ * **Cross-Platform:** Runs on Windows, macOS, and Linux.
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ `netwatchpy` is available on PyPI. All you need is Python 3.8+ and `pip`.
50
+
51
+ ```bash
52
+ pip install netwatchpy
53
+ ```
54
+ ---
55
+
56
+ ## Usage
57
+
58
+ Once installed, the netwatch command will be available in your terminal.
59
+
60
+ **To start the monitor (monitors all interfaces):**
61
+
62
+ ```bash
63
+ netwatch
64
+ ```
65
+
66
+ **To see all available options and help:**
67
+
68
+ ```bash
69
+ netwatch --help
70
+ ```
71
+ ---
72
+ ## Command-Line Arguments
73
+
74
+ You can control the monitor with these arguments:
75
+
76
+ * **`-h`,` --help`**
77
+
78
+ * Shows the help message and all options.
79
+
80
+ * **`-i INTERFACE`,` --interface INTERFACE`**
81
+
82
+ * Monitors a single, specific network interface (e.g., -i "Wi-Fi"). Defaults to "all".
83
+
84
+ * **`-l LIMIT`,` --limit LIMIT`**
85
+
86
+ * Sets a data cap and shows a progress bar (e.g., -l "10GB" or -l "500MB").
87
+
88
+ * **`--log LOG`**
89
+
90
+ * Saves all network activity to a specified CSV file (e.g., --log "usage.csv").
91
+
92
+ ---
93
+
94
+ ### Examples
95
+
96
+ **Set a 10GB data limit:**
97
+
98
+ ```bash
99
+ netwatch -l "10GB"
100
+ ```
101
+
102
+ **Monitor a specific interface ("Wi-Fi") and log to a file:**
103
+
104
+ ```bash
105
+ netwatch -i "Wi-Fi" --log "my_usage.csv"
106
+ ```
107
+ ---
108
+ ### In-App Keybindings
109
+
110
+ * **`Ctrl+p`**: Open the Command Palette
111
+
112
+ * **`Ctrl+d`**: Toggle Dark / Light Mode
113
+
114
+ * **`Ctrl+s`**: Toggle Dark / Light Mode
115
+
116
+ * **`r`**: Reset all counters back to zero
117
+
118
+ * **`Ctrl+q`**: Quit the application
119
+
120
+ ---
121
+
122
+ Bug Reports & Feature Requests
123
+ ---------------------------------
124
+
125
+ Found a bug or have a great idea? Please [open an issue]([https://github.com/destroyer795/Netwatchpy-package/issues](https://github.com/destroyer795/Netwatchpy-package/issues)) on the GitHub repository.
126
+
127
+ License
128
+ ----------
129
+
130
+ This project is licensed under the MIT License.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/netwatch/__init__.py
5
+ src/netwatch/monitor.py
6
+ src/netwatch/tui.py
7
+ src/netwatch/utils.py
8
+ src/netwatchpy.egg-info/PKG-INFO
9
+ src/netwatchpy.egg-info/SOURCES.txt
10
+ src/netwatchpy.egg-info/dependency_links.txt
11
+ src/netwatchpy.egg-info/entry_points.txt
12
+ src/netwatchpy.egg-info/requires.txt
13
+ src/netwatchpy.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ netwatch = netwatch.tui:main
@@ -0,0 +1,9 @@
1
+ textual
2
+ psutil
3
+ platformdirs
4
+
5
+ [:platform_system != "Linux"]
6
+ desktop-notifier
7
+
8
+ [:platform_system == "Linux"]
9
+ desktop-notifier[dbus]
@@ -0,0 +1 @@
1
+ netwatch