Add per-core CPU visualizer and total-encode-time stat
* New collapsible pane sits between the main split and the footer and shows per-core CPU utilisation as btop-style vertical bars. Polls /proc/stat once a second, paints with the live Omarchy accent colour via Cairo. Collapsed/expanded state persists in ~/.config/nocoder/config.json as cpu_pane_expanded (default true) so the pane reopens however the user last left it. * Footer "complete" state now reports TOTAL TIME alongside Succeeded / Failed / Output size. Wall time captured via time.monotonic() when the encode worker starts and again when the state flips to complete; only successful (non-cancelled) batches get a time. Formatted via the existing data.format_duration helper so it matches the pre-encode estimate's display. The /proc/stat parser lives in its own pure-Python module (nocoder/cpu_sampler.py) so it's GTK-free and easy to test in isolation. SystemPane mirrors the Footer attachment pattern (Gtk.Box subclass + __gtype_name__ + add_css_class), attached from MainWindow exactly like the footer is. Timer auto-stops on widget detach via a get_root() check, so no explicit teardown wiring is needed. CSS gets a .system-pane block (top border + padding) and a .cpu-bar-area rule that sets foreground colour to @accent — the Cairo draw callback reads that via widget.get_color() so the bars track the live theme without re-plumbing. Verified: * CpuSampler reports 32 cores at idle baseline; under stress-ng --cpu 0 aggregate hit 99.9% with all bars saturated. * Theme accent cascades correctly; bars use the active Omarchy theme. * cpu_pane_expanded round-trips (manually setting false then relaunching reopens the pane collapsed). * TOTAL TIME shows 0:02 for a 2.28s encode of a 3-second HEVC test clip. * Installed copy at ~/.local/share/nocoder/ updated and verified via the walker launcher.
This commit is contained in:
parent
7d66dcac87
commit
e72a46cce5
7 changed files with 278 additions and 10 deletions
16
README.md
16
README.md
|
|
@ -9,7 +9,8 @@ A native GTK4 + libadwaita batch transcoder for Omarchy. Drop video files (or wh
|
||||||
- **Theme-aware** — palette tracks the active Omarchy theme on every launch (parses `colors.toml` / `ghostty.conf` / `alacritty.toml` / `kitty.conf` in priority order). 34 stock + custom themes verified.
|
- **Theme-aware** — palette tracks the active Omarchy theme on every launch (parses `colors.toml` / `ghostty.conf` / `alacritty.toml` / `kitty.conf` in priority order). 34 stock + custom themes verified.
|
||||||
- **Pro camera ready** — `.MXF` from Canon XF / Sony XDCAM / Panasonic AVC-Intra, with proxy-directory pruning so dropping a Sony XAVC card maps only the masters in `CLIP/` and not the low-res duplicates in `SUB/`.
|
- **Pro camera ready** — `.MXF` from Canon XF / Sony XDCAM / Panasonic AVC-Intra, with proxy-directory pruning so dropping a Sony XAVC card maps only the masters in `CLIP/` and not the low-res duplicates in `SUB/`.
|
||||||
- **Multi-track audio preserved** — Canon C300/C500 records 4 mono PCM streams; all four land in the output `.mov` as separate tracks. Optional 24-bit toggle for pro delivery.
|
- **Multi-track audio preserved** — Canon C300/C500 records 4 mono PCM streams; all four land in the output `.mov` as separate tracks. Optional 24-bit toggle for pro delivery.
|
||||||
- **Live encode-speed indicator** — footer shows real `1.5×` throughput from ffmpeg and refines the ETA from actual measured rate, not a fixed heuristic.
|
- **Live encode-speed indicator** — footer shows real `1.5×` throughput from ffmpeg and refines the ETA from actual measured rate, not a fixed heuristic. After the batch finishes, the footer also reports total wall time alongside succeeded / failed / output size.
|
||||||
|
- **Per-core CPU visualizer** — collapsible btop-style pane between the queue and the footer, polling `/proc/stat` once a second and painting one vertical bar per logical core in the live theme accent. Useful for seeing at a glance whether the chosen profile / hwaccel combo is saturating the box or leaving headroom. Expand/collapse state persists across launches.
|
||||||
- **Hyprland-aware install** — registers a `.desktop` entry with the walker, installs the icon at six hicolor sizes, appends a windowrule that floats and centres the app at 1280×880.
|
- **Hyprland-aware install** — registers a `.desktop` entry with the walker, installs the icon at six hicolor sizes, appends a windowrule that floats and centres the app at 1280×880.
|
||||||
|
|
||||||
## Supported source formats
|
## Supported source formats
|
||||||
|
|
@ -72,12 +73,13 @@ Removes the installed app tree, launcher, desktop entry, all six icon sizes, the
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"hwaccel": "cuda" | "qsv" | "vaapi" | "none",
|
"hwaccel": "cuda" | "qsv" | "vaapi" | "none",
|
||||||
"profile": "proxy" | "lt" | "standard" | "hq" | "4444" | "4444xq",
|
"profile": "proxy" | "lt" | "standard" | "hq" | "4444" | "4444xq",
|
||||||
"naming": "keep" | "suffix",
|
"naming": "keep" | "suffix",
|
||||||
"out_dir": "/absolute/path/to/output/folder",
|
"out_dir": "/absolute/path/to/output/folder",
|
||||||
"audio_bits": 16 | 24,
|
"audio_bits": 16 | 24,
|
||||||
"auto_reveal": true | false
|
"auto_reveal": true | false,
|
||||||
|
"cpu_pane_expanded": true | false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ Schema (all keys optional):
|
||||||
"profile": "hq",
|
"profile": "hq",
|
||||||
"naming": "suffix" | "keep",
|
"naming": "suffix" | "keep",
|
||||||
"audio_bits": 16 | 24,
|
"audio_bits": 16 | 24,
|
||||||
"auto_reveal": false
|
"auto_reveal": false,
|
||||||
|
"cpu_pane_expanded": true
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
|
||||||
70
nocoder/cpu_sampler.py
Normal file
70
nocoder/cpu_sampler.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""Per-core CPU utilisation sampling from /proc/stat.
|
||||||
|
|
||||||
|
Pure-Python, no GTK. Keeps a snapshot of the last (idle, total) jiffies per
|
||||||
|
core so each ``sample()`` returns the delta-derived utilisation since the
|
||||||
|
previous call. The first call returns zeros (no baseline yet).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
PROC_STAT = "/proc/stat"
|
||||||
|
|
||||||
|
|
||||||
|
class CpuSampler:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._prev: List[Optional[Tuple[int, int]]] = []
|
||||||
|
|
||||||
|
def sample(self) -> List[float]:
|
||||||
|
"""Return per-core utilisation 0–100.
|
||||||
|
|
||||||
|
First call returns a list of zeros sized to ``os.cpu_count()`` (or the
|
||||||
|
number of per-core lines in /proc/stat, whichever is found).
|
||||||
|
"""
|
||||||
|
snapshot = _read_cpu_jiffies()
|
||||||
|
if not self._prev:
|
||||||
|
self._prev = [None] * len(snapshot)
|
||||||
|
|
||||||
|
# /proc/stat may grow if a core comes online between calls. Pad prev.
|
||||||
|
if len(snapshot) > len(self._prev):
|
||||||
|
self._prev.extend([None] * (len(snapshot) - len(self._prev)))
|
||||||
|
|
||||||
|
out: List[float] = []
|
||||||
|
for i, cur in enumerate(snapshot):
|
||||||
|
prev = self._prev[i] if i < len(self._prev) else None
|
||||||
|
if prev is None:
|
||||||
|
out.append(0.0)
|
||||||
|
else:
|
||||||
|
d_idle = cur[0] - prev[0]
|
||||||
|
d_total = cur[1] - prev[1]
|
||||||
|
if d_total <= 0:
|
||||||
|
out.append(0.0)
|
||||||
|
else:
|
||||||
|
busy = d_total - d_idle
|
||||||
|
out.append(max(0.0, min(100.0, 100.0 * busy / d_total)))
|
||||||
|
self._prev[i] = cur
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _read_cpu_jiffies() -> List[Tuple[int, int]]:
|
||||||
|
"""Parse /proc/stat and return [(idle, total), ...] per core (cpu0..cpuN)."""
|
||||||
|
rows: List[Tuple[int, int]] = []
|
||||||
|
try:
|
||||||
|
with open(PROC_STAT, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if not line.startswith("cpu"):
|
||||||
|
break
|
||||||
|
parts = line.split()
|
||||||
|
name = parts[0]
|
||||||
|
if name == "cpu":
|
||||||
|
continue # aggregate row; skip
|
||||||
|
# Fields: user nice system idle iowait irq softirq steal guest guest_nice
|
||||||
|
# idle time = idle + iowait
|
||||||
|
fields = [int(x) for x in parts[1:]]
|
||||||
|
idle = fields[3] + (fields[4] if len(fields) > 4 else 0)
|
||||||
|
total = sum(fields)
|
||||||
|
rows.append((idle, total))
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
return rows
|
||||||
|
|
@ -43,6 +43,7 @@ class Footer(Gtk.Box):
|
||||||
self._overall = 0.0
|
self._overall = 0.0
|
||||||
self._current_idx = 0
|
self._current_idx = 0
|
||||||
self._speed: Optional[float] = None
|
self._speed: Optional[float] = None
|
||||||
|
self._elapsed: Optional[float] = None
|
||||||
|
|
||||||
self._build()
|
self._build()
|
||||||
|
|
||||||
|
|
@ -50,13 +51,15 @@ class Footer(Gtk.Box):
|
||||||
|
|
||||||
def update(self, state: str, files: list[FileEntry], profile_id: str,
|
def update(self, state: str, files: list[FileEntry], profile_id: str,
|
||||||
overall: float, current_idx: int,
|
overall: float, current_idx: int,
|
||||||
speed: Optional[float] = None) -> None:
|
speed: Optional[float] = None,
|
||||||
|
elapsed: Optional[float] = None) -> None:
|
||||||
self._state = state
|
self._state = state
|
||||||
self._files = files
|
self._files = files
|
||||||
self._profile_id = profile_id
|
self._profile_id = profile_id
|
||||||
self._overall = max(0.0, min(1.0, overall))
|
self._overall = max(0.0, min(1.0, overall))
|
||||||
self._current_idx = current_idx
|
self._current_idx = current_idx
|
||||||
self._speed = speed
|
self._speed = speed
|
||||||
|
self._elapsed = elapsed
|
||||||
self._render()
|
self._render()
|
||||||
|
|
||||||
# ---------- build ----------
|
# ---------- build ----------
|
||||||
|
|
@ -161,6 +164,9 @@ class Footer(Gtk.Box):
|
||||||
stats.append(_divider())
|
stats.append(_divider())
|
||||||
self._stat_out = _make_stat("Output size", "—", small=True)
|
self._stat_out = _make_stat("Output size", "—", small=True)
|
||||||
stats.append(self._stat_out.root)
|
stats.append(self._stat_out.root)
|
||||||
|
stats.append(_divider())
|
||||||
|
self._stat_total_time = _make_stat("Total time", "—", small=True, with_clock=True)
|
||||||
|
stats.append(self._stat_total_time.root)
|
||||||
box.append(stats)
|
box.append(stats)
|
||||||
|
|
||||||
actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
|
@ -266,6 +272,10 @@ class Footer(Gtk.Box):
|
||||||
else:
|
else:
|
||||||
self._stat_fail.value.remove_css_class("danger")
|
self._stat_fail.value.remove_css_class("danger")
|
||||||
self._stat_out.value.set_text(format_bytes(total_out) if total_out else "—")
|
self._stat_out.value.set_text(format_bytes(total_out) if total_out else "—")
|
||||||
|
if self._elapsed is not None and self._elapsed > 0:
|
||||||
|
self._stat_total_time.value.set_text(format_duration(self._elapsed))
|
||||||
|
else:
|
||||||
|
self._stat_total_time.value.set_text("—")
|
||||||
|
|
||||||
|
|
||||||
# ---------- helpers ----------
|
# ---------- helpers ----------
|
||||||
|
|
|
||||||
137
nocoder/system_pane.py
Normal file
137
nocoder/system_pane.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
"""SystemPane — collapsible per-core CPU visualizer (btop-style).
|
||||||
|
|
||||||
|
A thin row sandwiched between the main split and the footer. Header shows a
|
||||||
|
disclosure toggle plus a "CPU" label and an aggregate percentage; the body is
|
||||||
|
a ``Gtk.DrawingArea`` rendering one vertical bar per logical core using Cairo.
|
||||||
|
|
||||||
|
Polls /proc/stat once a second via ``GLib.timeout_add_seconds``. The timer
|
||||||
|
returns ``False`` (auto-stops) once the widget is detached from any root, so
|
||||||
|
no explicit teardown signal wiring is needed.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version("Gtk", "4.0")
|
||||||
|
gi.require_version("GObject", "2.0")
|
||||||
|
from gi.repository import GLib, GObject, Gtk
|
||||||
|
|
||||||
|
from .config import update_config
|
||||||
|
from .cpu_sampler import CpuSampler
|
||||||
|
|
||||||
|
|
||||||
|
_SAMPLE_INTERVAL_SECONDS = 1
|
||||||
|
_BAR_MIN_WIDTH = 4
|
||||||
|
_BAR_MAX_WIDTH = 14
|
||||||
|
_BAR_GAP = 2
|
||||||
|
_AREA_MIN_HEIGHT = 56
|
||||||
|
|
||||||
|
|
||||||
|
class SystemPane(Gtk.Box):
|
||||||
|
__gtype_name__ = "NoCoderSystemPane"
|
||||||
|
|
||||||
|
def __init__(self, *, initial_expanded: bool = True) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
self.add_css_class("system-pane")
|
||||||
|
self.set_hexpand(True)
|
||||||
|
|
||||||
|
self._sampler = CpuSampler()
|
||||||
|
self._last: list[float] = []
|
||||||
|
|
||||||
|
# ---- Header row: toggle ▾/▸ + "CPU" label + aggregate % ----
|
||||||
|
header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
header.add_css_class("system-pane-header")
|
||||||
|
|
||||||
|
self._toggle = Gtk.ToggleButton()
|
||||||
|
self._toggle.add_css_class("flat")
|
||||||
|
self._toggle.add_css_class("system-pane-toggle")
|
||||||
|
self._toggle.set_active(initial_expanded)
|
||||||
|
self._toggle_label = Gtk.Label()
|
||||||
|
self._toggle.set_child(self._toggle_label)
|
||||||
|
self._toggle.connect("toggled", self._on_toggled)
|
||||||
|
header.append(self._toggle)
|
||||||
|
|
||||||
|
title = Gtk.Label(label="CPU", xalign=0)
|
||||||
|
title.add_css_class("system-pane-title")
|
||||||
|
header.append(title)
|
||||||
|
|
||||||
|
self._agg_label = Gtk.Label(label="—", xalign=1.0)
|
||||||
|
self._agg_label.add_css_class("system-pane-agg")
|
||||||
|
self._agg_label.set_hexpand(True)
|
||||||
|
header.append(self._agg_label)
|
||||||
|
|
||||||
|
self.append(header)
|
||||||
|
|
||||||
|
# ---- Body: revealer wrapping the cairo bar canvas ----
|
||||||
|
self._revealer = Gtk.Revealer()
|
||||||
|
self._revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN)
|
||||||
|
self._revealer.set_transition_duration(150)
|
||||||
|
self._revealer.set_reveal_child(initial_expanded)
|
||||||
|
|
||||||
|
self._area = Gtk.DrawingArea()
|
||||||
|
self._area.add_css_class("cpu-bar-area")
|
||||||
|
self._area.set_content_height(_AREA_MIN_HEIGHT)
|
||||||
|
self._area.set_hexpand(True)
|
||||||
|
self._area.set_draw_func(self._draw, None)
|
||||||
|
self._revealer.set_child(self._area)
|
||||||
|
self.append(self._revealer)
|
||||||
|
|
||||||
|
self._refresh_toggle_label()
|
||||||
|
|
||||||
|
# Kick off the polling timer. Returns False to auto-cleanup when the
|
||||||
|
# pane is no longer rooted (e.g. window closed).
|
||||||
|
GLib.timeout_add_seconds(_SAMPLE_INTERVAL_SECONDS, self._tick)
|
||||||
|
# Render an initial frame immediately so bars aren't blank for a second.
|
||||||
|
self._tick()
|
||||||
|
|
||||||
|
# ---------- timer ----------
|
||||||
|
|
||||||
|
def _tick(self) -> bool:
|
||||||
|
if self.get_root() is None:
|
||||||
|
return False
|
||||||
|
self._last = self._sampler.sample()
|
||||||
|
if self._last:
|
||||||
|
avg = sum(self._last) / len(self._last)
|
||||||
|
self._agg_label.set_text(f"{avg:5.1f}% · {len(self._last)} threads")
|
||||||
|
self._area.queue_draw()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ---------- toggle ----------
|
||||||
|
|
||||||
|
def _on_toggled(self, _btn: Gtk.ToggleButton) -> None:
|
||||||
|
expanded = self._toggle.get_active()
|
||||||
|
self._revealer.set_reveal_child(expanded)
|
||||||
|
self._refresh_toggle_label()
|
||||||
|
update_config({"cpu_pane_expanded": expanded})
|
||||||
|
|
||||||
|
def _refresh_toggle_label(self) -> None:
|
||||||
|
self._toggle_label.set_text("▾" if self._toggle.get_active() else "▸")
|
||||||
|
|
||||||
|
# ---------- draw ----------
|
||||||
|
|
||||||
|
def _draw(self, area: Gtk.DrawingArea, cr, w: int, h: int, _user) -> None:
|
||||||
|
n = len(self._last)
|
||||||
|
if n <= 0 or w <= 0 or h <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# The widget's foreground colour (set via CSS to the theme accent)
|
||||||
|
# paints the active portion of each bar.
|
||||||
|
accent = area.get_color()
|
||||||
|
|
||||||
|
# Bar width adapts to fit N cores across the canvas. Cap so a 4-core
|
||||||
|
# box doesn't get giant bars and a 64-core box doesn't get hairlines.
|
||||||
|
bar_w = (w - _BAR_GAP * (n - 1)) / n
|
||||||
|
bar_w = max(_BAR_MIN_WIDTH, min(_BAR_MAX_WIDTH, bar_w))
|
||||||
|
total_w = bar_w * n + _BAR_GAP * (n - 1)
|
||||||
|
x0 = (w - total_w) / 2 # centre the row
|
||||||
|
|
||||||
|
for i, pct in enumerate(self._last):
|
||||||
|
x = x0 + i * (bar_w + _BAR_GAP)
|
||||||
|
# Muted track
|
||||||
|
cr.set_source_rgba(accent.red, accent.green, accent.blue, 0.14)
|
||||||
|
cr.rectangle(x, 0, bar_w, h)
|
||||||
|
cr.fill()
|
||||||
|
# Filled portion (bottom-up)
|
||||||
|
fill_h = max(1.0, h * (pct / 100.0))
|
||||||
|
cr.set_source_rgba(accent.red, accent.green, accent.blue, accent.alpha)
|
||||||
|
cr.rectangle(x, h - fill_h, bar_w, fill_h)
|
||||||
|
cr.fill()
|
||||||
|
|
@ -8,6 +8,7 @@ from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -39,8 +40,9 @@ from .encoder import (
|
||||||
)
|
)
|
||||||
from .footer import Footer
|
from .footer import Footer
|
||||||
from .queue_pane import FileEntry, QueuePane
|
from .queue_pane import FileEntry, QueuePane
|
||||||
from .config import update_config
|
from .config import load_config, update_config
|
||||||
from .settings_pane import Settings, SettingsPane, load_persisted_settings
|
from .settings_pane import Settings, SettingsPane, load_persisted_settings
|
||||||
|
from .system_pane import SystemPane
|
||||||
|
|
||||||
WINDOW_WIDTH = 1280
|
WINDOW_WIDTH = 1280
|
||||||
WINDOW_HEIGHT = 880
|
WINDOW_HEIGHT = 880
|
||||||
|
|
@ -73,6 +75,8 @@ class MainWindow(Adw.ApplicationWindow):
|
||||||
self._active_job: Optional[EncodeJob] = None
|
self._active_job: Optional[EncodeJob] = None
|
||||||
self._current_idx: int = 0
|
self._current_idx: int = 0
|
||||||
self._current_speed: Optional[float] = None
|
self._current_speed: Optional[float] = None
|
||||||
|
self._encode_start_mono: Optional[float] = None
|
||||||
|
self._encode_elapsed: Optional[float] = None
|
||||||
|
|
||||||
# Root layout
|
# Root layout
|
||||||
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
|
@ -99,6 +103,10 @@ class MainWindow(Adw.ApplicationWindow):
|
||||||
self._settings_pane.connect("choose-folder-requested", lambda *_: self._open_out_dir_dialog())
|
self._settings_pane.connect("choose-folder-requested", lambda *_: self._open_out_dir_dialog())
|
||||||
split.append(self._settings_pane)
|
split.append(self._settings_pane)
|
||||||
|
|
||||||
|
cpu_pane_expanded = bool(load_config().get("cpu_pane_expanded", True))
|
||||||
|
self._system_pane = SystemPane(initial_expanded=cpu_pane_expanded)
|
||||||
|
root.append(self._system_pane)
|
||||||
|
|
||||||
self._footer = Footer()
|
self._footer = Footer()
|
||||||
self._footer.connect("encode-requested", lambda *_: self._start_encode())
|
self._footer.connect("encode-requested", lambda *_: self._start_encode())
|
||||||
self._footer.connect("cancel-requested", lambda *_: self._cancel_encode())
|
self._footer.connect("cancel-requested", lambda *_: self._cancel_encode())
|
||||||
|
|
@ -253,6 +261,7 @@ class MainWindow(Adw.ApplicationWindow):
|
||||||
profile_id=self._settings.profile,
|
profile_id=self._settings.profile,
|
||||||
overall=self._overall_progress(),
|
overall=self._overall_progress(),
|
||||||
current_idx=self._current_idx,
|
current_idx=self._current_idx,
|
||||||
|
elapsed=self._encode_elapsed,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _overall_progress(self) -> float:
|
def _overall_progress(self) -> float:
|
||||||
|
|
@ -512,6 +521,8 @@ class MainWindow(Adw.ApplicationWindow):
|
||||||
self._state = "encoding"
|
self._state = "encoding"
|
||||||
self._current_idx = 0
|
self._current_idx = 0
|
||||||
self._cancel_event = threading.Event()
|
self._cancel_event = threading.Event()
|
||||||
|
self._encode_start_mono = time.monotonic()
|
||||||
|
self._encode_elapsed = None
|
||||||
self._refresh_all()
|
self._refresh_all()
|
||||||
|
|
||||||
self._encode_thread = threading.Thread(target=self._encode_worker, daemon=True)
|
self._encode_thread = threading.Thread(target=self._encode_worker, daemon=True)
|
||||||
|
|
@ -651,11 +662,14 @@ class MainWindow(Adw.ApplicationWindow):
|
||||||
self._state = "ready"
|
self._state = "ready"
|
||||||
else:
|
else:
|
||||||
self._state = "complete"
|
self._state = "complete"
|
||||||
|
if self._encode_start_mono is not None:
|
||||||
|
self._encode_elapsed = time.monotonic() - self._encode_start_mono
|
||||||
# If at least one file landed AND the user opted in, pop the
|
# If at least one file landed AND the user opted in, pop the
|
||||||
# output folder open. Skip on cancel (intent unclear) and on
|
# output folder open. Skip on cancel (intent unclear) and on
|
||||||
# full-batch failure (annoying to be shown an empty folder).
|
# full-batch failure (annoying to be shown an empty folder).
|
||||||
if self._settings.auto_reveal and any(f.status == "done" for f in self._files):
|
if self._settings.auto_reveal and any(f.status == "done" for f in self._files):
|
||||||
self._reveal_output_dir()
|
self._reveal_output_dir()
|
||||||
|
self._encode_start_mono = None
|
||||||
self._refresh_all()
|
self._refresh_all()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
34
style.css
34
style.css
|
|
@ -591,6 +591,40 @@ button.cmd-disclosure:hover { color: @text_main; background-color: alpha(@text_m
|
||||||
caret-color: @accent;
|
caret-color: @accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- System pane (CPU visualizer) ---------------- */
|
||||||
|
|
||||||
|
.system-pane {
|
||||||
|
border-top: 1px solid @border_muted;
|
||||||
|
background-color: @window_bg;
|
||||||
|
padding: 4px 18px 6px 18px;
|
||||||
|
}
|
||||||
|
.system-pane-header {
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
.system-pane-toggle {
|
||||||
|
min-width: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 0 4px;
|
||||||
|
color: @text_dim;
|
||||||
|
}
|
||||||
|
.system-pane-toggle:hover { color: @text_main; }
|
||||||
|
.system-pane-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: @text_dim;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.system-pane-agg {
|
||||||
|
font-size: 11px;
|
||||||
|
color: @text_muted;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
}
|
||||||
|
.cpu-bar-area {
|
||||||
|
color: @accent;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- Footer ---------------- */
|
/* ---------------- Footer ---------------- */
|
||||||
|
|
||||||
.footer-bar {
|
.footer-bar {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue