8501fd074c
Go TUI sleep timer with modular architecture. - Config: YAML-based, profile inheritance, auto WM detection - Engine: timer, executor, stage pipeline (pre→timer→post) - Modules: workspace (niri/hyprland/kde), media, lock, kill, notify, sound, brightness, mute, custom, script - TUI: bubbletea app with profiles, module toggles, progress bar - CLI: run, list-profiles, export, import, history, stats - History: JSONL log with statistics - QR: config export as ASCII QR
163 lines
4.8 KiB
Python
Executable File
163 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import subprocess
|
|
import asyncio
|
|
from textual.app import App, ComposeResult
|
|
from textual.widgets import (
|
|
Static, Input, Checkbox, Button, Footer, Header, Select
|
|
)
|
|
from textual.containers import Vertical, Horizontal, Center
|
|
|
|
|
|
class TimerTUI(App):
|
|
CSS = """
|
|
Screen {
|
|
align: center middle;
|
|
background: #101010;
|
|
}
|
|
|
|
#main {
|
|
width: 50%;
|
|
border: solid #33ccff;
|
|
padding: 1 2;
|
|
background: #181818;
|
|
border-title-align: center;
|
|
border-title-color: cyan;
|
|
}
|
|
|
|
.label {
|
|
color: #33ccff;
|
|
text-align: center;
|
|
height: auto;
|
|
}
|
|
|
|
Input, Select {
|
|
width: 100%;
|
|
border: round #444;
|
|
padding: 0 1;
|
|
}
|
|
|
|
Checkbox {
|
|
width: 100%;
|
|
}
|
|
|
|
Button {
|
|
width: 48%;
|
|
margin: 0 1;
|
|
}
|
|
|
|
#status {
|
|
color: cyan;
|
|
text-align: center;
|
|
padding-top: 1;
|
|
}
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.timer_process = None
|
|
self.message_label = Static("", id="status")
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header()
|
|
with Center():
|
|
with Vertical(id="main"):
|
|
yield Static("⏱ TIMER CONFIGURATION ⏱", classes="label")
|
|
|
|
self.input_time = Input(value="", placeholder="Время (например 20m)")
|
|
self.cb_workspace = Checkbox("Переключить воркспейс", value=True)
|
|
self.input_workspace = Input(value="12", placeholder="Номер воркспейса")
|
|
|
|
self.cb_player = Checkbox("Управлять плеером", value=True)
|
|
self.select_player_action = Select(
|
|
options=[
|
|
("Ничего", "none"),
|
|
("stop", "stop"),
|
|
("pause", "pause"),
|
|
("next", "next"),
|
|
("previous", "previous"),
|
|
],
|
|
value="stop",
|
|
)
|
|
|
|
self.cb_lock = Checkbox("Заблокировать экран", value=True)
|
|
self.cb_kill = Checkbox("Убить приложения", value=False)
|
|
self.input_kill = Input(value="firefox, telegram-desktop", placeholder="через запятую")
|
|
|
|
yield self.input_time
|
|
yield self.cb_workspace
|
|
yield self.input_workspace
|
|
yield self.cb_player
|
|
yield self.select_player_action
|
|
yield self.cb_lock
|
|
yield self.cb_kill
|
|
yield self.input_kill
|
|
|
|
with Horizontal():
|
|
yield Button("▶ Start / Restart Timer", id="start", variant="success")
|
|
yield Button("✖ Quit", id="quit", variant="error")
|
|
|
|
yield self.message_label
|
|
|
|
yield Footer()
|
|
|
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "quit":
|
|
await self.stop_timer()
|
|
await self.action_quit()
|
|
elif event.button.id == "start":
|
|
await self.restart_timer()
|
|
|
|
async def stop_timer(self):
|
|
if self.timer_process and self.timer_process.poll() is None:
|
|
self.timer_process.terminate()
|
|
await asyncio.sleep(0.2)
|
|
if self.timer_process.poll() is None:
|
|
self.timer_process.kill()
|
|
|
|
async def restart_timer(self):
|
|
await self.stop_timer()
|
|
self.notify("⏳ Запуск таймера...")
|
|
|
|
time_str = self.input_time.value.strip()
|
|
commands = []
|
|
|
|
# Workspace switch
|
|
if self.cb_workspace.value:
|
|
workspace = self.input_workspace.value.strip()
|
|
commands.append(f"niri msg action focus-workspace {workspace}")
|
|
|
|
# Player action
|
|
if self.cb_player.value:
|
|
action = self.select_player_action.value
|
|
if action != "none":
|
|
commands.append(f"playerctl {action}")
|
|
|
|
# Lock screen
|
|
if self.cb_lock.value:
|
|
commands.append("qs -c noctalia-shell ipc call lockScreen lock")
|
|
|
|
# Kill apps
|
|
if self.cb_kill.value and self.input_kill.value.strip():
|
|
for proc in self.input_kill.value.split(","):
|
|
proc = proc.strip()
|
|
if proc:
|
|
commands.append(f"pkill {proc}")
|
|
|
|
cmd_after = " ; ".join(commands)
|
|
full_cmd = f"timer {time_str} && bash -c '{cmd_after}'"
|
|
|
|
self.timer_process = subprocess.Popen(
|
|
full_cmd,
|
|
shell=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
self.notify(f"✅ Таймер запущен на {time_str}")
|
|
|
|
def notify(self, msg: str):
|
|
self.message_label.update(msg)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
TimerTUI().run()
|