breaktime
This commit is contained in:
parent
7eb36b622b
commit
90a4a23bd4
|
|
@ -15,7 +15,7 @@ lib.mkIf config.u.has.graphical {
|
||||||
"copyq.service"
|
"copyq.service"
|
||||||
"fcitx5.service"
|
"fcitx5.service"
|
||||||
"fastcompmgr.service"
|
"fastcompmgr.service"
|
||||||
"safeeyes.service"
|
# "safeeyes.service"
|
||||||
"snixembed.service"
|
"snixembed.service"
|
||||||
];
|
];
|
||||||
Requires = [
|
Requires = [
|
||||||
|
|
|
||||||
227
user/bin/breaktime
Executable file
227
user/bin/breaktime
Executable file
|
|
@ -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())
|
||||||
|
|
@ -28,13 +28,21 @@ in {
|
||||||
} (builtins.readFile ./furigana))
|
} (builtins.readFile ./furigana))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
home.file = builtins.listToAttrs (
|
home.file =
|
||||||
|
builtins.listToAttrs (
|
||||||
map
|
map
|
||||||
(x: {
|
(x: {
|
||||||
name = ".local/bin/${x}";
|
name = ".local/bin/${x}";
|
||||||
value = {source = ln x;};
|
value = {source = ln x;};
|
||||||
})
|
})
|
||||||
(builtins.attrNames config.u.bin)
|
(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);
|
home.packages = pkgs.lib.lists.flatten (builtins.attrValues config.u.bin);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ dbus-update-activation-environment $_vars
|
||||||
|
|
||||||
xrdb -merge ~/.config/Xresources
|
xrdb -merge ~/.config/Xresources
|
||||||
xset r rate 200 30
|
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
|
xsetwacom set "Wacom One by Wacom S Pen stylus" MapToOutput DisplayPort-0
|
||||||
|
breaktime &
|
||||||
sct 4500
|
sct 4500
|
||||||
|
|
||||||
systemctl --user start autostart.target
|
systemctl --user start autostart.target
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue