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.
- netwatchpy-0.2.9/LICENSE +21 -0
- netwatchpy-0.2.9/PKG-INFO +130 -0
- netwatchpy-0.2.9/README.md +112 -0
- netwatchpy-0.2.9/pyproject.toml +33 -0
- netwatchpy-0.2.9/setup.cfg +4 -0
- netwatchpy-0.2.9/src/netwatch/__init__.py +0 -0
- netwatchpy-0.2.9/src/netwatch/monitor.py +108 -0
- netwatchpy-0.2.9/src/netwatch/tui.py +370 -0
- netwatchpy-0.2.9/src/netwatch/utils.py +31 -0
- netwatchpy-0.2.9/src/netwatchpy.egg-info/PKG-INFO +130 -0
- netwatchpy-0.2.9/src/netwatchpy.egg-info/SOURCES.txt +13 -0
- netwatchpy-0.2.9/src/netwatchpy.egg-info/dependency_links.txt +1 -0
- netwatchpy-0.2.9/src/netwatchpy.egg-info/entry_points.txt +2 -0
- netwatchpy-0.2.9/src/netwatchpy.egg-info/requires.txt +9 -0
- netwatchpy-0.2.9/src/netwatchpy.egg-info/top_level.txt +1 -0
netwatchpy-0.2.9/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/netwatchpy/)
|
|
22
|
+

|
|
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
|
+
[](https://pypi.org/project/netwatchpy/)
|
|
4
|
+

|
|
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"
|
|
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
|
+
[](https://pypi.org/project/netwatchpy/)
|
|
22
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
netwatch
|