diff --git a/user/autostart.nix b/user/autostart.nix index 76a180e..9c7a102 100644 --- a/user/autostart.nix +++ b/user/autostart.nix @@ -15,7 +15,7 @@ lib.mkIf config.u.has.graphical { "copyq.service" "fcitx5.service" "fastcompmgr.service" - "safeeyes.service" + # "safeeyes.service" "snixembed.service" ]; Requires = [ diff --git a/user/bin/breaktime b/user/bin/breaktime new file mode 100755 index 0000000..1849748 --- /dev/null +++ b/user/bin/breaktime @@ -0,0 +1,227 @@ +# 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 +from PyQt6.QtCore import QTimer, Qt + + +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", + QWidget: "rgba(40, 20, 40, 200)", +} +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() + self.p_ac = QAction("Pause Timer", menu) + self.p_ac.triggered.connect(self.pause_timers) + menu.addAction(self.p_ac) + b_ac = QAction("Break Now", menu) + b_ac.triggered.connect(self.start_break) + menu.addAction(b_ac) + q_ac = QAction("Quit", menu) + q_ac.triggered.connect(sys.exit) + menu.addAction(q_ac) + 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): + self.set_state(State.IDLE, DURATION[State.IDLE]//M) + + def start_break(self): + self.set_state(State.BREAK, "...") + self.overlay.show() + self.overlay.grabKeyboard() + + 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: + self.overlay.hide() + self.start_idle() + play_sound(POP) + 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() + self.p_ac.setText("Resume Timer") + else: + self.color = COLOR[self.state] + self.update_icon() + self.timer.start(self.remaining_time) + self.icon_timer.start() + self.p_ac.setText("Pause Timer") + + +class TransparentOverlay(QWidget): + def __init__(self): + super().__init__() + self.setWindowFlags(Qt.WindowType.BypassWindowManagerHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + bg = QWidget() + bg.setStyleSheet(f"background-color: {COLOR[QWidget]};") + layout.addWidget(bg) + self.setLayout(layout) + + def keyPressEvent(self, event): + if event.key() == Qt.Key.Key_Escape: + self.hide() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) + reminder = BreakTimer() + reminder.show() + sys.exit(app.exec()) diff --git a/user/bin/default.nix b/user/bin/default.nix index 90196ed..e67a293 100644 --- a/user/bin/default.nix +++ b/user/bin/default.nix @@ -28,13 +28,21 @@ in { } (builtins.readFile ./furigana)) ]; }; - home.file = builtins.listToAttrs ( - map - (x: { - name = ".local/bin/${x}"; - value = {source = ln x;}; - }) - (builtins.attrNames config.u.bin) - ); + home.file = + builtins.listToAttrs ( + map + (x: { + name = ".local/bin/${x}"; + value = {source = ln x;}; + }) + (builtins.attrNames config.u.bin) + ) + // { + ".local/bin/breaktime".source = let + breaktime = pkgs.writers.writePython3Bin "breaktime" { + libraries = [pkgs.python3Packages.pyqt6]; + } (builtins.readFile ./breaktime); + in "${breaktime}/bin/breaktime"; + }; home.packages = pkgs.lib.lists.flatten (builtins.attrValues config.u.bin); } diff --git a/user/config/sx/sxrc b/user/config/sx/sxrc index 30f2932..573a23a 100755 --- a/user/config/sx/sxrc +++ b/user/config/sx/sxrc @@ -12,8 +12,8 @@ dbus-update-activation-environment $_vars xrdb -merge ~/.config/Xresources xset r rate 200 30 -xrandr --output DisplayPort-1 --mode 1920x1080 --rate 144 xsetwacom set "Wacom One by Wacom S Pen stylus" MapToOutput DisplayPort-0 +breaktime & sct 4500 systemctl --user start autostart.target