feat: initial gosleep-timer implementation
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
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user