Skip to content

Commit 9d6673e

Browse files
feat: Added a loading screen to prevent info blackout at start, less schizo shit, better cpu delta calc (using EMA values)
1 parent 8845f66 commit 9d6673e

11 files changed

Lines changed: 240 additions & 62 deletions

File tree

WinHtop-Setup.exe

-30.7 KB
Binary file not shown.

dist/winhtop.exe

-44.2 KB
Binary file not shown.

modules/audio_vis.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,6 @@ def get_magnitudes(self):
333333
@property
334334
def is_running(self):
335335
"""Check if audio capture is active."""
336-
return self._running
336+
return self._running
337+
338+
# dont ask how many hours were spent on a FUCKING easter egg of all things

modules/hardware.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ def get_hardware_info():
132132
133133
$chosen
134134
'''
135+
# powershell command breakdown:
136+
# 1- get disk info for letter 'c' drives
137+
# 2- get partition info for disk
138+
# 3- get disk info for partition
139+
# 4- select first disk
140+
# 5- get friendly name (if exists)
141+
# 6- get model, get bus type, get size
142+
# 7- get chosen name
143+
# 8- if no chosen name, use disk info
144+
# 9- if chosen name is less than 8 chars, add bus type and size
145+
# 10- return chosen name
135146

136147
try:
137148
result = subprocess.run(

modules/input.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,5 @@ def handle_input():
308308
elif ch == '\x03':
309309
state.app_running = False
310310
elif ch.isprintable():
311-
state.input_buffer += ch
311+
if len(state.input_buffer) < 120:
312+
state.input_buffer += ch

modules/processes.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,27 +85,8 @@ def get_process_tree_info(targets):
8585
tree_lines = []
8686
parent_groups = {} # parent_pid -> list of child processes
8787

88-
# Recalculate full list for tree building if needed, or search current state.processes?
89-
# get_process_tree_info takes 'targets' which are psutil.Process objects usually...
90-
# Wait, 'targets' in ui.py or input.py might be passed as PID or dict?
91-
# In pending_confirmation, targets is list of dicts or objects.
92-
# We need to verify what 'targets' contains.
93-
# Since we replaced get_processes, state.processes now contains dicts, not psutil.Process objects.
94-
# Previous code: procs.append(pinfo) -> pinfo was dict (proc.info.copy()).
95-
# So targets is list of dicts.
96-
97-
# However, get_process_tree_info implementation in original used p.ppid() which implies p is psutil.Process?
98-
# Let's check the original code again.
99-
# "for p in targets: try: ppid = p.ppid() ..."
100-
# "procs.append(pinfo)" where pinfo is dict.
101-
# Ah, the `targets` passed to `get_process_tree_info` might come from `state.pending_confirmation`?
102-
# Let's assume we need to handle dicts now, or re-instantiate psutil objects.
103-
# The user asked for "Displaying the entire process tree...".
104-
105-
# If targets contains dicts from our new get_processes, they have 'pid' and 'ppid' (we added ppid in processsn return).
106-
# processsn.compute_cpu_deltas returns list of dicts WITH 'ppid'.
107-
# So we can use that.
108-
88+
# targets consists of process info dicts (including 'ppid') from the native snapshot
89+
# instead of psutil.Process objects to maintain performance and consistency.
10990
for p in targets:
11091
# p is dict
11192
try:
@@ -118,8 +99,6 @@ def get_process_tree_info(targets):
11899
except:
119100
pass
120101

121-
# We need to look up parent names.
122-
# We can use state.processes to find names of parents!
123102
# Map pid -> name
124103
proc_map = {proc['pid']: proc['name'] for proc in state.processes}
125104

modules/processsn.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
"""Process info/list module using native windows APIs"""
22
import ctypes
33
import ctypes.wintypes as wt
4+
import time
45

56
# Windows types for NtQuerySystemInformation
67
ntdll = ctypes.WinDLL("ntdll")
78

89
SystemProcessInformation = 5
910

11+
# Cache for EMA values
12+
_last_snapshot_time = None
13+
_ema_cache = {}
14+
1015
class UNICODE_STRING(ctypes.Structure):
1116
_fields_ = [
1217
("Length", wt.WORD),
@@ -21,7 +26,7 @@ class SYSTEM_THREAD_INFORMATION(ctypes.Structure):
2126
("CreateTime", wt.LARGE_INTEGER),
2227
("WaitTime", wt.ULONG),
2328
("StartAddress", wt.LPVOID),
24-
("ClientId", wt.LARGE_INTEGER * 1), # not used here
29+
("ClientId", wt.LARGE_INTEGER * 1), # not used here but we take anyway
2530
("Priority", wt.LONG),
2631
("BasePriority", wt.LONG),
2732
("ContextSwitches", wt.ULONG),
@@ -112,7 +117,7 @@ def get_native_process_snapshot():
112117
continue
113118

114119
if ret != 0:
115-
raise OSError("NtQuerySystemInformation failed")
120+
raise OSError("NtQuerySystemInformation failed, WinHTop is unable to retrieve process information")
116121

117122
break
118123

@@ -172,52 +177,61 @@ def compute_cpu_deltas(prev_cache, curr_snapshot, interval_seconds, cpu_count):
172177
list[dict]:
173178
proc_list with computed stats
174179
"""
180+
global _last_snapshot_time
181+
smoothing_factor = 0.2
175182
results = []
176183

177-
# Clamp interval to avoid division by zero or noisy spikes on very fast updates
178-
if interval_seconds < 0.05:
179-
interval_seconds = 0.05
184+
# use perf counter for more precision
185+
now = time.perf_counter()
186+
if _last_snapshot_time is None:
187+
interval = 0.1 # Default for first run
188+
else:
189+
interval = now - _last_snapshot_time
190+
_last_snapshot_time = now
180191

181192
# Track which pids we see in this snapshot to evict dead ones later
182193
seen_pids = set()
183194

184195
for proc in curr_snapshot:
185196
pid = proc["pid"]
186197
seen_pids.add(pid)
187-
188198
prev = prev_cache.get(pid)
189199

190200
total_time_now = proc["user_time_100ns"] + proc["kernel_time_100ns"]
191201

192202
if prev:
193203
total_time_prev = prev["user_time_100ns"] + prev["kernel_time_100ns"]
194204
dt_100ns = max(0, total_time_now - total_time_prev)
195-
196-
# convert 100ns units to seconds
197205
dt_seconds = dt_100ns / 10_000_000.0
198-
199-
cpu = (dt_seconds / interval_seconds) * 100 / max(1, cpu_count)
206+
207+
# Raw calculation
208+
raw_cpu = (dt_seconds / interval) * 100 / max(1, cpu_count)
209+
raw_cpu = max(0.0, min(raw_cpu, 100.0))
210+
211+
# Apply EMA Smoothing: (New * Alpha) + (Old * (1 - Alpha))
212+
prev_smooth = _ema_cache.get(pid, raw_cpu)
213+
smooth_cpu = (raw_cpu * smoothing_factor) + (prev_smooth * (1.0 - smoothing_factor))
214+
_ema_cache[pid] = smooth_cpu
200215
else:
201-
cpu = 0.0
202-
203-
cpu = max(0.0, min(cpu, 100.0))
216+
smooth_cpu = 0.0
217+
_ema_cache[pid] = 0.0
204218

205219
results.append({
206220
"pid": pid,
207221
"ppid": proc["ppid"],
208222
"name": proc["name"],
209223
"threads": proc["threads"],
210-
"cpu_percent": cpu,
224+
"cpu_percent": round(smooth_cpu, 2), # Rounding helps UI jitter
211225
"rss_bytes": proc["rss_bytes"],
212226
})
213-
214-
# Update cache in-place
227+
# Update in-place
215228
prev_cache[pid] = proc
216229

217-
# Evict dead pids
218-
# We do this by checking keys in prev_cache that are NOT in seen_pids
219-
dead_pids = [pid for pid in prev_cache if pid not in seen_pids]
220-
for pid in dead_pids:
221-
del prev_cache[pid]
230+
# Evict dead pids from cache
231+
# did you know: i hate the eviction notice from tf2
232+
dead_pids = [p for p in prev_cache if p not in seen_pids]
233+
for p in dead_pids:
234+
del prev_cache[p]
235+
if p in _ema_cache: del _ema_cache[p]
222236

223237
return results

modules/ui.py

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,134 @@
44
from .config import *
55
from .utils import get_terminal_size, draw_bar, format_bytes
66

7+
# Startup splash messages
8+
STARTUP_MESSAGES = [
9+
"Initializing terminal magic...",
10+
"Poking the system kernel...",
11+
"Interrogating your CPU...",
12+
"Snooping on your storage...",
13+
"Waking up the GPU...",
14+
"Collecting process breadcrumbs...",
15+
"Priming the performance counters...",
16+
"Polishing the UI...",
17+
"Reordering electrons...",
18+
"Compiling bitstreams...",
19+
"Calibrating flux capacitor...",
20+
"Reticulating splines...",
21+
]
22+
23+
def render_startup(step: int, total_steps: int, message: str = None):
24+
"""Render a startup splash screen with progress.
25+
26+
Args:
27+
step: Current step number (0-indexed)
28+
total_steps: Total number of steps
29+
message: Optional custom message, otherwise uses STARTUP_MESSAGES
30+
"""
31+
cols, rows, _ = get_terminal_size()
32+
33+
# Calculate progress
34+
progress = min(100, int((step / max(1, total_steps)) * 100))
35+
36+
# Get message
37+
if message is None:
38+
msg_idx = min(step, len(STARTUP_MESSAGES) - 1)
39+
message = STARTUP_MESSAGES[msg_idx]
40+
41+
# ASCII art logo
42+
# cant we use literals so this doesnt look like shit?
43+
logo = [
44+
" __ __ _____ ___ ___ ",
45+
" / / /\\ \\ \\/\\ /\\/__ \\/___\\/ _ \\",
46+
" \\ \\/ \\/ / /_/ / / /\\// // /_)/",
47+
" \\ /\\ / __ / / / / \\_// ___/ ",
48+
" \\/ \\/\\/ /_/ \\/ \\___/\\/ ",
49+
]
50+
51+
# We built this buffer on line by line (Heyo!)
52+
lines = []
53+
54+
# Vertical centering
55+
content_height = len(logo) + 6
56+
start_row = max(0, (rows - content_height) // 2)
57+
58+
# Fill top padding
59+
for _ in range(start_row):
60+
lines.append("\033[K") # Empty line
61+
62+
# Draw Logo
63+
for line in logo:
64+
padding = max(0, (cols - len(line)) // 2)
65+
lines.append(f"{' ' * padding}{C_CYAN}{C_BOLD}{line}{C_RESET}\033[K")
66+
67+
lines.append("\033[K")
68+
lines.append("\033[K")
69+
70+
# Draw Progress Bar
71+
bar_width = min(50, cols - 20)
72+
filled = int((progress / 100) * bar_width)
73+
empty = bar_width - filled
74+
75+
bar_str = f"{C_GREEN}{'█' * filled}{C_DIM}{'░' * empty}{C_RESET}"
76+
progress_text = f" [{bar_str}] {C_BOLD}{progress:3d}%{C_RESET}"
77+
78+
# Center progress bar
79+
# Text length approx bar_width + 12 chars invisible chars don't count for padding calculation but needed for total len
80+
# We visually center based on visible width ~ bar_width + 8
81+
pad_len = max(0, (cols - (bar_width + 8)) // 2)
82+
lines.append(f"{' ' * pad_len}{progress_text}\033[K")
83+
84+
lines.append("\033[K")
85+
86+
# Draw Message
87+
msg_len = len(message)
88+
msg_pad = max(0, (cols - msg_len) // 2)
89+
lines.append(f"{' ' * msg_pad}{C_YELLOW}{message}{C_RESET}\033[K")
90+
91+
# Fill rest of screen
92+
sys.stdout.write("\033[H") # Home
93+
94+
# Output limited to rows to avoid scroll
95+
# We need to construct string carefully
96+
final_output = "\n".join(lines)
97+
sys.stdout.write(final_output)
98+
99+
# Clear rest of screen below content
100+
sys.stdout.write("\033[J")
101+
sys.stdout.flush()
102+
7103
def render():
8104
"""Render the entire UI."""
9-
cols, rows = get_terminal_size()
105+
cols, rows, toosmall = get_terminal_size()
10106

11107
# Detect terminal resize
12108
if (cols, rows) != state.prev_term_size:
13109
sys.stdout.write("\033[2J")
14110
state.prev_term_size = (cols, rows)
15111

16-
# Validation for extremely small windows to prevent crash
17-
if cols < 79 or rows < 18:
18-
return
19-
20112
# We build the buffer line by line.
21113
lines = []
22114

23-
# --- Helper to add a line with safe ANSI handling ---
115+
# Detect if the terminal is too small and write to status message
116+
if toosmall:
117+
state.status_message = "Window too small!"
118+
119+
# Helper to add a line with safe ANSI handling
24120
def add_line(text, bg_color=None):
25121
if bg_color:
26122
lines.append(f"{bg_color}{text}{C_RESET}\033[K")
27123
else:
28124
lines.append(f"{text}{C_RESET}\033[K")
29125

30-
# --- Header: Hardware Info ---
126+
# Header: Hardware Info
31127
gpu_name_short = state.sys_stats['gpu_name'][:25] if state.sys_stats['gpu_available'] else "No GPU"
32128
cpu_name_short = state.sys_stats['cpu_name'][:35]
33129

34130
header_content = f" CPU: {cpu_name_short} | GPU: {gpu_name_short} "
35131
padding_len = max(0, cols - len(header_content)) # approx check
36132
lines.append(f"{C_BG_HEADER}{C_BOLD}{header_content}{' ' * padding_len}{C_RESET}")
37133

38-
# --- Command Bar ---
134+
# Command Bar
39135
speed_indicator = f"[{state.current_refresh_rate}]"
40136
cmd_display = f" > {state.input_buffer}"
41137
vis_len = len(cmd_display) + len(speed_indicator) + 1
@@ -45,7 +141,7 @@ def add_line(text, bg_color=None):
45141

46142
lines.append(f"{C_DIM}{'─' * cols}{C_RESET}\033[K")
47143

48-
# --- System Monitor ---
144+
# System Monitor
49145
# Reserve header(3), footer(header+status+sep=3), min_proc(1) -> 7 lines reserved.
50146
max_sys_mon_lines = max(1, rows - 7)
51147
sys_mon_lines = []

modules/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ def get_terminal_size():
66
"""Get terminal dimensions."""
77
try:
88
size = os.get_terminal_size()
9-
return size.columns, size.lines
9+
if size.columns < 79 or size.lines < 18:
10+
return size.columns, size.lines, True
11+
else:
12+
return size.columns, size.lines, False
1013
except:
11-
return 100, 30
14+
return 100, 30, False
1215

1316
def draw_bar(percent, width=20, color=C_GREEN, empty_char="░", fill_char="█"):
1417
"""Draw a colored progress bar."""

0 commit comments

Comments
 (0)