devpulse-tui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devpulse/__init__.py +0 -0
- devpulse/activity.py +50 -0
- devpulse/database.py +75 -0
- devpulse/devinfo.py +240 -0
- devpulse/doctor.py +71 -0
- devpulse/live.py +579 -0
- devpulse/main.py +32 -0
- devpulse/stats.py +84 -0
- devpulse_tui-0.1.0.dist-info/METADATA +305 -0
- devpulse_tui-0.1.0.dist-info/RECORD +13 -0
- devpulse_tui-0.1.0.dist-info/WHEEL +5 -0
- devpulse_tui-0.1.0.dist-info/entry_points.txt +2 -0
- devpulse_tui-0.1.0.dist-info/top_level.txt +1 -0
devpulse/__init__.py
ADDED
|
File without changes
|
devpulse/activity.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
CODING_KEYWORDS = [
|
|
6
|
+
"code",
|
|
7
|
+
"nvim",
|
|
8
|
+
"vim",
|
|
9
|
+
"pycharm",
|
|
10
|
+
"idea",
|
|
11
|
+
"terminal",
|
|
12
|
+
"konsole",
|
|
13
|
+
"kitty",
|
|
14
|
+
"github",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
session_start = time.time()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_active_window():
|
|
22
|
+
try:
|
|
23
|
+
window_name = subprocess.check_output(
|
|
24
|
+
["xdotool", "getactivewindow", "getwindowname"],
|
|
25
|
+
text=True
|
|
26
|
+
).strip()
|
|
27
|
+
|
|
28
|
+
return window_name
|
|
29
|
+
|
|
30
|
+
except Exception:
|
|
31
|
+
return "Unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_coding(window_name):
|
|
35
|
+
window_name = window_name.lower()
|
|
36
|
+
|
|
37
|
+
return any(
|
|
38
|
+
keyword in window_name
|
|
39
|
+
for keyword in CODING_KEYWORDS
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_session_duration():
|
|
44
|
+
seconds = int(time.time() - session_start)
|
|
45
|
+
|
|
46
|
+
hours = seconds // 3600
|
|
47
|
+
minutes = (seconds % 3600) // 60
|
|
48
|
+
secs = seconds % 60
|
|
49
|
+
|
|
50
|
+
return f"{hours:02}:{minutes:02}:{secs:02}"
|
devpulse/database.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from sqlalchemy import (
|
|
3
|
+
create_engine,
|
|
4
|
+
Column,
|
|
5
|
+
Integer,
|
|
6
|
+
Float,
|
|
7
|
+
String,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from sqlalchemy.orm import (
|
|
11
|
+
declarative_base,
|
|
12
|
+
sessionmaker,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Base = declarative_base()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SystemStat(Base):
|
|
19
|
+
__tablename__ = "system_stats"
|
|
20
|
+
|
|
21
|
+
id = Column(Integer, primary_key=True)
|
|
22
|
+
timestamp = Column(
|
|
23
|
+
String,
|
|
24
|
+
default=lambda: datetime.now().isoformat()
|
|
25
|
+
)
|
|
26
|
+
cpu = Column(Float)
|
|
27
|
+
ram = Column(Float)
|
|
28
|
+
disk = Column(Float)
|
|
29
|
+
|
|
30
|
+
gpu = Column(String)
|
|
31
|
+
|
|
32
|
+
upload = Column(Float)
|
|
33
|
+
download = Column(Float)
|
|
34
|
+
|
|
35
|
+
repo = Column(String)
|
|
36
|
+
branch = Column(String)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
engine = create_engine(
|
|
40
|
+
"sqlite:///devpulse.db"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
SessionLocal = sessionmaker(bind=engine)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def init_db():
|
|
47
|
+
Base.metadata.create_all(engine)
|
|
48
|
+
|
|
49
|
+
def log_system_stats(
|
|
50
|
+
cpu,
|
|
51
|
+
ram,
|
|
52
|
+
disk,
|
|
53
|
+
gpu,
|
|
54
|
+
upload,
|
|
55
|
+
download,
|
|
56
|
+
repo,
|
|
57
|
+
branch,
|
|
58
|
+
):
|
|
59
|
+
session = SessionLocal()
|
|
60
|
+
|
|
61
|
+
stat = SystemStat(
|
|
62
|
+
cpu=cpu,
|
|
63
|
+
ram=ram,
|
|
64
|
+
disk=disk,
|
|
65
|
+
gpu=gpu,
|
|
66
|
+
upload=upload,
|
|
67
|
+
download=download,
|
|
68
|
+
repo=repo,
|
|
69
|
+
branch=branch,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
session.add(stat)
|
|
73
|
+
session.commit()
|
|
74
|
+
|
|
75
|
+
session.close()
|
devpulse/devinfo.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from git import Repo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_git_repo():
|
|
9
|
+
current = Path.cwd()
|
|
10
|
+
|
|
11
|
+
for parent in [current] + list(current.parents):
|
|
12
|
+
if (parent / ".git").exists():
|
|
13
|
+
return parent
|
|
14
|
+
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_git_info():
|
|
19
|
+
try:
|
|
20
|
+
repo = subprocess.check_output(
|
|
21
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
22
|
+
stderr=subprocess.DEVNULL
|
|
23
|
+
).decode().strip().split("/")[-1]
|
|
24
|
+
|
|
25
|
+
branch = subprocess.check_output(
|
|
26
|
+
["git", "branch", "--show-current"],
|
|
27
|
+
stderr=subprocess.DEVNULL
|
|
28
|
+
).decode().strip()
|
|
29
|
+
|
|
30
|
+
status_output = subprocess.check_output(
|
|
31
|
+
["git", "status", "--short"],
|
|
32
|
+
stderr=subprocess.DEVNULL
|
|
33
|
+
).decode().splitlines()
|
|
34
|
+
|
|
35
|
+
commit_msg = subprocess.check_output(
|
|
36
|
+
["git", "log", "-1", "--pretty=%s"],
|
|
37
|
+
stderr=subprocess.DEVNULL
|
|
38
|
+
).decode().strip()
|
|
39
|
+
|
|
40
|
+
author = subprocess.check_output(
|
|
41
|
+
["git", "log", "-1", "--pretty=%an"],
|
|
42
|
+
stderr=subprocess.DEVNULL
|
|
43
|
+
).decode().strip()
|
|
44
|
+
|
|
45
|
+
commit_age = subprocess.check_output(
|
|
46
|
+
["git", "log", "-1", "--pretty=%cr"],
|
|
47
|
+
stderr=subprocess.DEVNULL
|
|
48
|
+
).decode().strip()
|
|
49
|
+
|
|
50
|
+
insertions = 0
|
|
51
|
+
deletions = 0
|
|
52
|
+
untracked = 0
|
|
53
|
+
|
|
54
|
+
for line in status_output:
|
|
55
|
+
if line.startswith("??"):
|
|
56
|
+
untracked += 1
|
|
57
|
+
|
|
58
|
+
diff_stats = subprocess.check_output(
|
|
59
|
+
["git", "diff", "--shortstat"],
|
|
60
|
+
stderr=subprocess.DEVNULL
|
|
61
|
+
).decode().strip()
|
|
62
|
+
|
|
63
|
+
if "insertion" in diff_stats:
|
|
64
|
+
insertions = diff_stats.split("insertion")[0].split()[-1]
|
|
65
|
+
|
|
66
|
+
if "deletion" in diff_stats:
|
|
67
|
+
deletions = diff_stats.split("deletion")[0].split()[-1]
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"repo": repo,
|
|
71
|
+
"branch": branch,
|
|
72
|
+
"status": "DIRTY" if status_output else "CLEAN",
|
|
73
|
+
"untracked": str(untracked),
|
|
74
|
+
"insertions": f"+{insertions}",
|
|
75
|
+
"deletions": f"-{deletions}",
|
|
76
|
+
"author": author,
|
|
77
|
+
"commit_age": commit_age,
|
|
78
|
+
"last_commit": commit_msg,
|
|
79
|
+
"files_changed": str(len(status_output))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
except Exception:
|
|
83
|
+
return {
|
|
84
|
+
"repo": "Unknown",
|
|
85
|
+
"branch": "Unknown",
|
|
86
|
+
"status": "Unknown",
|
|
87
|
+
"untracked": "0",
|
|
88
|
+
"insertions": "0",
|
|
89
|
+
"deletions": "0",
|
|
90
|
+
"author": "Unknown",
|
|
91
|
+
"commit_age": "Unknown",
|
|
92
|
+
"last_commit": "Unknown",
|
|
93
|
+
"files_changed": "0"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def get_shell():
|
|
97
|
+
return os.environ.get("SHELL", "Unknown")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_terminal():
|
|
101
|
+
return os.environ.get("TERM", "Unknown")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_current_directory():
|
|
105
|
+
return str(Path.cwd())
|
|
106
|
+
|
|
107
|
+
def get_last_commit():
|
|
108
|
+
repo_path = find_git_repo()
|
|
109
|
+
|
|
110
|
+
if not repo_path:
|
|
111
|
+
return "No Repo"
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
repo = Repo(repo_path)
|
|
115
|
+
|
|
116
|
+
commit = repo.head.commit.message.strip()
|
|
117
|
+
|
|
118
|
+
return commit[:40]
|
|
119
|
+
|
|
120
|
+
except Exception:
|
|
121
|
+
return "Unknown"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_changed_files_count():
|
|
125
|
+
repo_path = find_git_repo()
|
|
126
|
+
|
|
127
|
+
if not repo_path:
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
repo = Repo(repo_path)
|
|
132
|
+
|
|
133
|
+
changed_files = repo.index.diff(None)
|
|
134
|
+
|
|
135
|
+
return len(changed_files)
|
|
136
|
+
|
|
137
|
+
except Exception:
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_repo_status():
|
|
142
|
+
repo_path = find_git_repo()
|
|
143
|
+
|
|
144
|
+
if not repo_path:
|
|
145
|
+
return "NO REPO"
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
repo = Repo(repo_path)
|
|
149
|
+
|
|
150
|
+
return "DIRTY" if repo.is_dirty() else "CLEAN"
|
|
151
|
+
|
|
152
|
+
except Exception:
|
|
153
|
+
return "UNKNOWN"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_untracked_files_count():
|
|
157
|
+
repo_path = find_git_repo()
|
|
158
|
+
|
|
159
|
+
if not repo_path:
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
repo = Repo(repo_path)
|
|
164
|
+
|
|
165
|
+
return len(repo.untracked_files)
|
|
166
|
+
|
|
167
|
+
except Exception:
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_insertions_deletions():
|
|
172
|
+
repo_path = find_git_repo()
|
|
173
|
+
|
|
174
|
+
if not repo_path:
|
|
175
|
+
return 0, 0
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
repo = Repo(repo_path)
|
|
179
|
+
|
|
180
|
+
diff = repo.git.diff("--shortstat")
|
|
181
|
+
|
|
182
|
+
insertions = 0
|
|
183
|
+
deletions = 0
|
|
184
|
+
|
|
185
|
+
import re
|
|
186
|
+
|
|
187
|
+
insert_match = re.search(r'(\d+) insertion', diff)
|
|
188
|
+
delete_match = re.search(r'(\d+) deletion', diff)
|
|
189
|
+
|
|
190
|
+
if insert_match:
|
|
191
|
+
insertions = int(insert_match.group(1))
|
|
192
|
+
|
|
193
|
+
if delete_match:
|
|
194
|
+
deletions = int(delete_match.group(1))
|
|
195
|
+
|
|
196
|
+
return insertions, deletions
|
|
197
|
+
|
|
198
|
+
except Exception:
|
|
199
|
+
return 0, 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_last_commit_author():
|
|
203
|
+
repo_path = find_git_repo()
|
|
204
|
+
|
|
205
|
+
if not repo_path:
|
|
206
|
+
return "Unknown"
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
repo = Repo(repo_path)
|
|
210
|
+
|
|
211
|
+
return repo.head.commit.author.name
|
|
212
|
+
|
|
213
|
+
except Exception:
|
|
214
|
+
return "Unknown"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_last_commit_age():
|
|
218
|
+
repo_path = find_git_repo()
|
|
219
|
+
|
|
220
|
+
if not repo_path:
|
|
221
|
+
return "Unknown"
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
repo = Repo(repo_path)
|
|
225
|
+
|
|
226
|
+
commit_time = datetime.fromtimestamp(
|
|
227
|
+
repo.head.commit.committed_date
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
delta = datetime.now() - commit_time
|
|
231
|
+
|
|
232
|
+
hours = delta.seconds // 3600
|
|
233
|
+
|
|
234
|
+
if delta.days > 0:
|
|
235
|
+
return f"{delta.days}d ago"
|
|
236
|
+
|
|
237
|
+
return f"{hours}h ago"
|
|
238
|
+
|
|
239
|
+
except Exception:
|
|
240
|
+
return "Unknown"
|
devpulse/doctor.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
import importlib.util
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_package(package_name):
|
|
13
|
+
return importlib.util.find_spec(package_name) is not None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_doctor():
|
|
17
|
+
table = Table(title="DevPulse Doctor")
|
|
18
|
+
|
|
19
|
+
table.add_column("Check", style="cyan")
|
|
20
|
+
table.add_column("Status", style="green")
|
|
21
|
+
|
|
22
|
+
# Database check
|
|
23
|
+
db_exists = os.path.exists("devpulse.db")
|
|
24
|
+
|
|
25
|
+
table.add_row(
|
|
26
|
+
"SQLite Database",
|
|
27
|
+
"OK" if db_exists else "MISSING"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# SQLite readable
|
|
31
|
+
try:
|
|
32
|
+
conn = sqlite3.connect("devpulse.db")
|
|
33
|
+
conn.execute("SELECT 1")
|
|
34
|
+
conn.close()
|
|
35
|
+
|
|
36
|
+
table.add_row(
|
|
37
|
+
"Database Access",
|
|
38
|
+
"OK"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
except Exception:
|
|
42
|
+
table.add_row(
|
|
43
|
+
"Database Access",
|
|
44
|
+
"FAILED"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Dependencies
|
|
48
|
+
dependencies = [
|
|
49
|
+
"psutil",
|
|
50
|
+
"rich",
|
|
51
|
+
"sqlalchemy",
|
|
52
|
+
"git",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for dep in dependencies:
|
|
56
|
+
installed = check_package(dep)
|
|
57
|
+
|
|
58
|
+
table.add_row(
|
|
59
|
+
f"Dependency: {dep}",
|
|
60
|
+
"OK" if installed else "MISSING"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# NVIDIA support
|
|
64
|
+
nvidia_available = check_package("py3nvml")
|
|
65
|
+
|
|
66
|
+
table.add_row(
|
|
67
|
+
"NVIDIA Support",
|
|
68
|
+
"ENABLED" if nvidia_available else "DISABLED"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
console.print(table)
|