nix-conf/user/bin/breaktime

264 lines
7.6 KiB
Plaintext
Raw Normal View History

2026-03-21 21:06:03 -04:00
# vi: filetype=python
import sys
import subprocess
from enum import Enum
from threading import Thread
from PyQt6.QtWidgets import (
QApplication,
QSystemTrayIcon,
QMenu,
QWidget,
QVBoxLayout,
QMessageBox,
)
from PyQt6.QtGui import QIcon, QPixmap, QColor, QAction, QPainter
2026-03-24 01:18:54 -04:00
from PyQt6.QtCore import QTimer, Qt, pyqtSignal, QEventLoop
2026-03-21 21:06:03 -04:00
class State(Enum):
IDLE = 0
PENDING = 1
BREAK = 2
S = 1000
M = 60 * S
DURATION = {
State.IDLE: 15*M,
State.PENDING: 1*M,
State.BREAK: 15*S,
}
TOOLTIP = {
State.IDLE: "Waiting for next break",
State.PENDING: "Click to take a break",
State.BREAK: "Taking a break",
}
COLOR = {
State.IDLE: "#608c58",
State.PENDING: "#d32a5d",
State.BREAK: "#7e95d3",
2026-03-24 01:18:54 -04:00
0: "rgba(40, 20, 40, 200)",
1: "rgba(40, 20, 40, 255)",
2026-03-21 21:06:03 -04:00
}
DONE = '/run/current-system/sw/share/sounds/freedesktop/stereo/complete.oga'
POP = '/run/current-system/sw/share/sounds/freedesktop/stereo/message.oga'
audio = True
def m_ceil(n):
return (n + M - 1) // M
def circle_icon(color, text=None):
pixmap = QPixmap(64, 64)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QColor(color))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(2, 2, 60, 60)
if text:
painter.setPen(QColor("white" if color != "white" else "black"))
font = painter.font()
font.setPixelSize(34)
font.setBold(True)
painter.setFont(font)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, text)
painter.end()
return QIcon(pixmap)
def play_sound(sound=DONE, n=1):
global audio
audio = True
def play():
for _ in range(n):
if audio:
subprocess.call(['paplay', sound])
Thread(target=play).start()
class BreakTimer(QSystemTrayIcon):
def __init__(self, parent=None):
super().__init__(parent)
self.state = State.IDLE
self.timer = QTimer(self)
self.timer.timeout.connect(self.on_timeout)
self.activated.connect(self.on_tray_click)
self.icon_color = ""
self.icon_text = ""
self.color = ""
self.text = ""
self.icon_timer = QTimer(self)
self.icon_timer.timeout.connect(self.update_ui)
self.icon_timer.start(S)
self.overlay = TransparentOverlay()
screen_geometry = app.primaryScreen().virtualGeometry()
self.overlay.setGeometry(screen_geometry)
menu = QMenu()
2026-03-24 01:18:54 -04:00
self.p_act = QAction("Pause Timer", menu)
self.p_act.triggered.connect(self.pause_timers)
menu.addAction(self.p_act)
act = QAction("Reset Timer", menu)
act.triggered.connect(self.start_idle)
menu.addAction(act)
act = QAction("Break Now", menu)
act.triggered.connect(self.start_break)
menu.addAction(act)
act = QAction("Go Inactive", menu)
act.triggered.connect(self.inactive_popup)
menu.addAction(act)
act = QAction("Quit", menu)
act.triggered.connect(sys.exit)
menu.addAction(act)
2026-03-21 21:06:03 -04:00
self.setContextMenu(menu)
self.start_idle()
def update_icon(self):
if self.color != self.icon_color or self.text != self.icon_text:
self.icon_color = self.color
self.icon_text = self.text
self.setIcon(circle_icon(self.color, self.text))
def update_ui(self):
if self.state is State.IDLE:
self.text = str(m_ceil(self.timer.remainingTime()) or self.text)
self.update_icon()
elif self.state is State.PENDING:
if self.color == "white":
self.color = COLOR[State.PENDING]
else:
self.color = "white"
self.update_icon()
def set_state(self, state, text):
global audio
audio = False
self.state = state
self.color = COLOR[self.state]
self.text = str(text)
self.update_icon()
self.setToolTip(TOOLTIP[self.state])
self.timer.start(DURATION[self.state])
def start_idle(self):
2026-03-24 01:18:54 -04:00
self.icon_timer.start()
self.p_act.setText("Pause Timer")
2026-03-21 21:06:03 -04:00
self.set_state(State.IDLE, DURATION[State.IDLE]//M)
def start_break(self):
self.set_state(State.BREAK, "...")
2026-03-24 01:18:54 -04:00
self.overlay.start()
2026-03-21 21:06:03 -04:00
def on_timeout(self):
if self.state is State.IDLE:
self.set_state(State.PENDING, 0)
play_sound()
elif self.state is State.PENDING:
n = int(self.text or 0) + 1
self.text = str(n)
self.update_icon()
if n < 5:
play_sound(n=n)
else:
self.inactive_popup()
elif self.state is State.BREAK:
play_sound(POP)
2026-03-24 01:18:54 -04:00
self.overlay.stop()
self.start_idle()
2026-03-21 21:06:03 -04:00
self.showMessage('Timer', 'Timer is complete')
def inactive_popup(self):
self.pause_timers()
popup = QMessageBox()
popup.setWindowTitle("Break Timer")
popup.setText("Timer paused due to inactivity")
resume = popup.addButton("Resume", QMessageBox.ButtonRole.AcceptRole)
resume.clicked.connect(self.start_idle)
quit = popup.addButton("Quit", QMessageBox.ButtonRole.RejectRole)
quit.clicked.connect(sys.exit)
popup.exec()
def on_tray_click(self, reason):
if reason == QSystemTrayIcon.ActivationReason.Trigger:
if self.state is State.PENDING:
self.start_break()
def pause_timers(self):
if self.timer.isActive():
self.remaining_time = self.timer.remainingTime()
self.color = "gray"
self.update_icon()
self.timer.stop()
self.icon_timer.stop()
2026-03-24 01:18:54 -04:00
self.p_act.setText("Resume Timer")
2026-03-21 21:06:03 -04:00
else:
self.color = COLOR[self.state]
self.update_icon()
self.timer.start(self.remaining_time)
self.icon_timer.start()
2026-03-24 01:18:54 -04:00
self.p_act.setText("Pause Timer")
2026-03-21 21:06:03 -04:00
class TransparentOverlay(QWidget):
2026-03-24 01:18:54 -04:00
unlock_sig = pyqtSignal()
2026-03-21 21:06:03 -04:00
def __init__(self):
super().__init__()
2026-03-24 01:18:54 -04:00
self.keys = []
2026-03-21 21:06:03 -04:00
self.setWindowFlags(Qt.WindowType.BypassWindowManagerHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
2026-03-24 01:18:54 -04:00
self.bg = QWidget()
self.bg.setStyleSheet(f"background-color: {COLOR[0]};")
layout.addWidget(self.bg)
2026-03-21 21:06:03 -04:00
self.setLayout(layout)
2026-03-24 01:18:54 -04:00
def start(self):
self.keys = []
self.locked = False
self.show()
self.grabKeyboard()
def stop(self):
if self.locked:
loop = QEventLoop()
self.unlock_sig.connect(loop.quit)
loop.exec()
self.hide()
2026-03-21 21:06:03 -04:00
def keyPressEvent(self, event):
2026-03-24 01:18:54 -04:00
k = event.key()
self.keys.append(k)
self.keys = self.keys[-10:]
if self.locked:
play_sound(POP)
if self.keys[-6:] == list(b'UNLOCK'):
self.locked = False
self.bg.setStyleSheet(f"background-color: {COLOR[0]};")
self.unlock_sig.emit()
elif k == Qt.Key.Key_Escape:
2026-03-21 21:06:03 -04:00
self.hide()
2026-03-24 01:18:54 -04:00
elif self.keys[-4:] == list(b'LOCK'):
self.locked = True
self.bg.setStyleSheet(f"background-color: {COLOR[1]};")
2026-03-21 21:06:03 -04:00
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
reminder = BreakTimer()
reminder.show()
sys.exit(app.exec())