mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-28 10:02:06 +08:00
280 lines
8.8 KiB
Python
Executable File
280 lines
8.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Standalone replay media controls bar for StarPilot desktop replay.
|
|
Compact always-on-top window with probe-rod scrubber and transport buttons.
|
|
"""
|
|
import os
|
|
import json
|
|
import pyray as rl
|
|
|
|
# ── Config ──────────────────────────────────────
|
|
W, H = 460, 64
|
|
|
|
# Colours
|
|
BG = rl.Color(18, 18, 22, 245)
|
|
TRACK_COL = rl.Color(55, 55, 65, 255)
|
|
FILL_COL = rl.Color(0, 200, 130, 255)
|
|
KNOB_COL = rl.WHITE
|
|
BTN_HOV = rl.Color(255, 255, 255, 28)
|
|
TXT_DIM = rl.Color(175, 175, 185, 255)
|
|
TXT_HOT = rl.WHITE
|
|
SEP_COL = rl.Color(60, 60, 72, 255)
|
|
PIN_ON = rl.Color(0, 200, 130, 255)
|
|
PIN_OFF = rl.Color(90, 90, 100, 255)
|
|
|
|
|
|
def pid_alive(pid: int) -> bool:
|
|
try:
|
|
os.kill(pid, 0)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def format_time(seconds: float) -> str:
|
|
s = int(seconds)
|
|
return f"{s // 60:02d}:{s % 60:02d}"
|
|
|
|
|
|
def draw_btn(font, text: str, rect: rl.Rectangle, mouse: rl.Vector2, fsize: int = 12) -> bool:
|
|
hovered = rl.check_collision_point_rec(mouse, rect)
|
|
if hovered:
|
|
rl.draw_rectangle_rounded(rect, 0.35, 6, BTN_HOV)
|
|
tw = rl.measure_text_ex(font, text, fsize, 0)
|
|
tx = rect.x + (rect.width - tw.x) / 2
|
|
ty = rect.y + (rect.height - tw.y) / 2
|
|
rl.draw_text_ex(font, text, rl.Vector2(tx, ty), fsize, 0,
|
|
TXT_HOT if hovered else TXT_DIM)
|
|
return hovered and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
|
|
|
|
def main():
|
|
prefix = os.getenv("OPENPILOT_PREFIX", "default")
|
|
state_f = f"/tmp/replay_state_{prefix}.json"
|
|
cmd_f = f"/tmp/replay_cmd_{prefix}.json"
|
|
|
|
# PIDs to watch — exit when ALL are dead
|
|
watch_pids: list[int] = []
|
|
raw = os.getenv("REPLAY_WATCH_PIDS", "")
|
|
if raw.strip():
|
|
for tok in raw.split():
|
|
try:
|
|
watch_pids.append(int(tok))
|
|
except ValueError:
|
|
pass
|
|
|
|
# ── Init window ────────────────────────────────
|
|
rl.init_window(W, H, "Replay Controls")
|
|
rl.set_target_fps(60)
|
|
|
|
# Position: bottom-right corner, clear of taskbar
|
|
mon_w = rl.get_monitor_width(0) or 1920
|
|
mon_h = rl.get_monitor_height(0) or 1080
|
|
rl.set_window_position(mon_w - W - 20, mon_h - H - 60)
|
|
|
|
# Always on top by default
|
|
rl.set_window_state(rl.ConfigFlags.FLAG_WINDOW_TOPMOST)
|
|
|
|
# Load font (Inter-Medium.fnt — same as rest of desktop UI)
|
|
basedir = os.environ.get("BASEDIR", "") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
font_path = os.path.join(basedir, "selfdrive", "assets", "fonts", "Inter-Medium.fnt")
|
|
if os.path.exists(font_path):
|
|
font = rl.load_font(font_path)
|
|
rl.gen_texture_mipmaps(font.texture)
|
|
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_TRILINEAR)
|
|
else:
|
|
font = rl.get_font_default()
|
|
|
|
# State
|
|
min_sec = max_sec = cur_sec = 0.0
|
|
paused = False
|
|
speed = 1.0
|
|
dragging = False
|
|
pinned = True
|
|
|
|
# Cleanup leftover cmd file
|
|
try:
|
|
os.remove(cmd_f)
|
|
except Exception:
|
|
pass
|
|
|
|
SPEEDS = [0.5, 1.0, 1.5, 2.0, 3.0]
|
|
|
|
# ── Main loop ──────────────────────────────────
|
|
while not rl.window_should_close():
|
|
|
|
# Exit if all watched parent processes have died
|
|
if watch_pids and not any(pid_alive(p) for p in watch_pids):
|
|
break
|
|
|
|
# Read state
|
|
try:
|
|
with open(state_f) as f:
|
|
s = json.load(f)
|
|
min_sec = s.get("min_sec", 0.0)
|
|
max_sec = s.get("max_sec", 0.0)
|
|
if not dragging:
|
|
cur_sec = s.get("cur_sec", 0.0)
|
|
paused = s.get("paused", False)
|
|
speed = s.get("speed", 1.0)
|
|
except Exception:
|
|
pass
|
|
|
|
mouse = rl.get_mouse_position()
|
|
|
|
# ── Layout constants ───────────────────────
|
|
PAD = 8
|
|
SL_Y = 15 # scrubber centre y
|
|
SL_H = 4 # track height
|
|
BW = 46 # button width
|
|
BH = 22 # button height
|
|
BY = H - BH - 8 # button row y
|
|
|
|
TL_W = 36
|
|
SL_X = PAD + TL_W + 4
|
|
SL_W = W - SL_X - TL_W - PAD - 4
|
|
|
|
# ── Scrubber drag ──────────────────────────
|
|
has_data = max_sec > min_sec
|
|
if has_data:
|
|
drag_rect = rl.Rectangle(SL_X - 4, 0, SL_W + 8, SL_Y + 12)
|
|
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
if rl.check_collision_point_rec(mouse, drag_rect):
|
|
dragging = True
|
|
if dragging:
|
|
if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
p = max(0.0, min(1.0, (mouse.x - SL_X) / SL_W))
|
|
cur_sec = min_sec + p * (max_sec - min_sec)
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"seek": cur_sec}, f)
|
|
except Exception:
|
|
pass
|
|
else:
|
|
dragging = False
|
|
|
|
# ── Draw ───────────────────────────────────
|
|
rl.begin_drawing()
|
|
rl.clear_background(BG)
|
|
|
|
# Top separator line
|
|
rl.draw_rectangle(0, 0, W, 1, SEP_COL)
|
|
|
|
if has_data:
|
|
prog = max(0.0, min(1.0, (cur_sec - min_sec) / (max_sec - min_sec)))
|
|
|
|
# Track background
|
|
rl.draw_rectangle(SL_X, SL_Y - SL_H // 2, SL_W, SL_H, TRACK_COL)
|
|
# Fill
|
|
fill_w = int(SL_W * prog)
|
|
if fill_w > 0:
|
|
rl.draw_rectangle(SL_X, SL_Y - SL_H // 2, fill_w, SL_H, FILL_COL)
|
|
# Knob
|
|
kx = int(SL_X + SL_W * prog)
|
|
rl.draw_circle(kx, SL_Y, 6 if dragging else 4, KNOB_COL)
|
|
|
|
# Time labels
|
|
rl.draw_text_ex(font, format_time(cur_sec),
|
|
rl.Vector2(PAD, SL_Y - 6), 12, 0, TXT_DIM)
|
|
rl.draw_text_ex(font, format_time(max_sec),
|
|
rl.Vector2(W - PAD - TL_W + 2, SL_Y - 6), 12, 0, TXT_DIM)
|
|
|
|
# ── Button row ────────────────────────────
|
|
# Layout (left → right): |< -10s PLAY/PAUSE +10s >| speed pin
|
|
total_btns = 7
|
|
gap = 4
|
|
seg_bw = 28 # |< and >|
|
|
speed_bw = 36
|
|
pin_bw = 26
|
|
inner_w = W - 2 * PAD
|
|
play_bw = inner_w - 2 * seg_bw - 2 * BW - speed_bw - pin_bw - gap * (total_btns - 1)
|
|
play_bw = max(48, play_bw)
|
|
|
|
cx = PAD
|
|
|
|
def btn(label, bw):
|
|
nonlocal cx
|
|
r = rl.Rectangle(cx, BY, bw, BH)
|
|
cx += bw + gap
|
|
return draw_btn(font, label, r, mouse, 12)
|
|
|
|
if btn("|<", seg_bw):
|
|
seg = int(cur_sec / 60)
|
|
target = ((seg - 1) if cur_sec - seg * 60 < 2 and seg > 0 else seg) * 60.0
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"seek": max(min_sec, target)}, f)
|
|
except Exception:
|
|
pass
|
|
|
|
if btn("-10", BW):
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"seek": max(min_sec, cur_sec - 10.0)}, f)
|
|
except Exception:
|
|
pass
|
|
|
|
if btn("PAUSE" if not paused else "PLAY", play_bw):
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"play": paused}, f)
|
|
except Exception:
|
|
pass
|
|
|
|
if btn("+10", BW):
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"seek": min(max_sec, cur_sec + 10.0)}, f)
|
|
except Exception:
|
|
pass
|
|
|
|
if btn(">|", seg_bw):
|
|
seg = int(cur_sec / 60)
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"seek": min(max_sec, (seg + 1) * 60.0)}, f)
|
|
except Exception:
|
|
pass
|
|
|
|
if btn(f"{speed:.1f}x", speed_bw):
|
|
try:
|
|
idx = SPEEDS.index(speed)
|
|
except ValueError:
|
|
idx = 0
|
|
next_speed = SPEEDS[(idx + 1) % len(SPEEDS)]
|
|
try:
|
|
with open(cmd_f, "w") as f:
|
|
json.dump({"speed": next_speed}, f)
|
|
except Exception:
|
|
pass
|
|
|
|
# Pin (always-on-top) toggle
|
|
pin_rect = rl.Rectangle(cx, BY, pin_bw, BH)
|
|
hovered = rl.check_collision_point_rec(mouse, pin_rect)
|
|
if hovered:
|
|
rl.draw_rectangle_rounded(pin_rect, 0.35, 6, BTN_HOV)
|
|
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
pinned = not pinned
|
|
if pinned:
|
|
rl.set_window_state(rl.ConfigFlags.FLAG_WINDOW_TOPMOST)
|
|
else:
|
|
rl.clear_window_state(rl.ConfigFlags.FLAG_WINDOW_TOPMOST)
|
|
pin_col = PIN_ON if pinned else PIN_OFF
|
|
tw = rl.measure_text_ex(font, "Top", 11, 0)
|
|
rl.draw_text_ex(font, "Top", rl.Vector2(pin_rect.x + (pin_rect.width - tw.x) / 2,
|
|
pin_rect.y + (pin_rect.height - tw.y) / 2), 11, 0, pin_col)
|
|
|
|
else:
|
|
# Waiting state
|
|
msg = "Waiting for replay..."
|
|
tw = rl.measure_text_ex(font, msg, 12)
|
|
rl.draw_text_ex(font, msg, rl.Vector2((W - tw.x) / 2, H / 2 - 6), 12, 0, TXT_DIM)
|
|
|
|
rl.end_drawing()
|
|
|
|
rl.close_window()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|