disallow to disable guard during focus
docker / image (push) Failing after 24s
lint / ruff (push) Failing after 28s
tests / backend (push) Successful in 37s
tests / desktop (push) Successful in 22s
tests / android-core (push) Successful in 49s
tests / web (push) Successful in 1m20s
android / apk (push) Successful in 8m49s
docker / image (push) Failing after 24s
lint / ruff (push) Failing after 28s
tests / backend (push) Successful in 37s
tests / desktop (push) Successful in 22s
tests / android-core (push) Successful in 49s
tests / web (push) Successful in 1m20s
android / apk (push) Successful in 8m49s
This commit is contained in:
21 files changed
+481
-43
No files matched your search
@@ -37,6 +37,14 @@ log = logging.getLogger(__name__)
|
||||
|
||||
KIND_LABELS = {"work": "Focus", "short_break": "Short break", "long_break": "Long break"}
|
||||
|
||||
#: The local switches that stop this machine enforcing. While the server reports
|
||||
#: the guard locked, these only go one way — on. Turning enforcement off is what a
|
||||
#: focus guard exists to make inconvenient, and this app is the thing enforcing.
|
||||
_ENFORCEMENT_KEYS = ("enforce_blocking", "block_hosts_file", "block_apps")
|
||||
|
||||
#: Said when the policy has not been polled yet, so there is no server sentence.
|
||||
_GUARD_LOCK_FALLBACK = "Finish or drop the interval to switch the focus guard off."
|
||||
|
||||
|
||||
def clock(seconds: int) -> str:
|
||||
seconds = max(0, int(seconds))
|
||||
@@ -252,6 +260,20 @@ class Controller(QObject):
|
||||
def blockerReason(self) -> str:
|
||||
return str(self._policy.get("reason", ""))
|
||||
|
||||
@pyqtProperty(bool, notify=blockerChanged)
|
||||
def blockerLocked(self) -> bool:
|
||||
"""The server will not let the guard be switched off right now.
|
||||
|
||||
Not the same as [blockerActive]: in scheduled mode outside every window
|
||||
nothing is being blocked and the guard is still locked on, because an
|
||||
interval is running or paused.
|
||||
"""
|
||||
return bool(self._policy.get("locked"))
|
||||
|
||||
@pyqtProperty(str, notify=blockerChanged)
|
||||
def blockerLockNote(self) -> str:
|
||||
return str(self._policy.get("lock_reason", ""))
|
||||
|
||||
@pyqtProperty("QVariantList", notify=blockerChanged)
|
||||
def blockedDomains(self) -> list[str]:
|
||||
"""Every hostname the policy wants blocked, server-expanded.
|
||||
@@ -447,6 +469,15 @@ class Controller(QObject):
|
||||
@pyqtSlot(str, "QVariant")
|
||||
def setConfig(self, key: str, value: Any) -> None:
|
||||
"""Change a desktop-local setting and apply it immediately."""
|
||||
if key in _ENFORCEMENT_KEYS and not value and self.blockerLocked:
|
||||
# This machine is the one that actually enforces, so switching
|
||||
# enforcement off here is the real escape hatch — the server's
|
||||
# `blocker.enabled` is untouchable from this app anyway. The greyed
|
||||
# switch in the dialog is not the enforcement: the policy poll can be
|
||||
# seconds stale, and the tray and D-Bus come through here too.
|
||||
self.toast.emit(self.blockerLockNote or _GUARD_LOCK_FALLBACK)
|
||||
self.configChanged.emit() # snap the switch back to the stored value
|
||||
return
|
||||
if key == "autostart":
|
||||
autostart.set_autostart(bool(value), minimized=self._config.start_minimized)
|
||||
elif key == "service_enabled":
|
||||
|
||||
@@ -81,6 +81,13 @@ Window {
|
||||
property string detail: ""
|
||||
property bool on: false
|
||||
property bool available: true
|
||||
/**
|
||||
* A one-way ratchet: while this is set the switch can be turned on but not
|
||||
* off, so it greys only when it is *already* on. `available` greys both
|
||||
* directions, which is the wrong shape for "you may not switch this off".
|
||||
*/
|
||||
property bool lockedOn: false
|
||||
readonly property bool settable: available && !(lockedOn && on)
|
||||
signal toggledTo(bool value)
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -91,7 +98,7 @@ Window {
|
||||
spacing: 1
|
||||
Label {
|
||||
text: toggleRow.label
|
||||
color: toggleRow.available ? Theme.ink : Theme.faint
|
||||
color: toggleRow.settable ? Theme.ink : Theme.faint
|
||||
font.pixelSize: 13
|
||||
font.family: Theme.sansFamily
|
||||
}
|
||||
@@ -107,7 +114,7 @@ Window {
|
||||
}
|
||||
Switch {
|
||||
checked: toggleRow.on
|
||||
enabled: toggleRow.available
|
||||
enabled: toggleRow.settable
|
||||
onToggled: toggleRow.toggledTo(checked)
|
||||
}
|
||||
}
|
||||
@@ -264,11 +271,16 @@ Window {
|
||||
visible: controller.blockerError !== ""
|
||||
text: controller.blockerError
|
||||
}
|
||||
Banner {
|
||||
visible: controller.blockerLocked
|
||||
text: controller.blockerLockNote
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: "Enforce the server's rules here"
|
||||
detail: "Off, this machine is a timer and nothing else."
|
||||
on: dialog.config.enforce_blocking
|
||||
lockedOn: controller.blockerLocked
|
||||
onToggledTo: (value) => controller.setConfig("enforce_blocking", value)
|
||||
}
|
||||
Toggle {
|
||||
@@ -276,6 +288,7 @@ Window {
|
||||
detail: "The managed region of /etc/hosts, plus a local DNS stub for the masks it cannot express."
|
||||
on: dialog.config.block_hosts_file
|
||||
available: dialog.config.enforce_blocking
|
||||
lockedOn: controller.blockerLocked
|
||||
onToggledTo: (value) => controller.setConfig("block_hosts_file", value)
|
||||
}
|
||||
Toggle {
|
||||
@@ -283,6 +296,7 @@ Window {
|
||||
detail: "/proc is scanned every few seconds and matches are asked to quit, then made to."
|
||||
on: dialog.config.block_apps
|
||||
available: dialog.config.enforce_blocking
|
||||
lockedOn: controller.blockerLocked
|
||||
onToggledTo: (value) => controller.setConfig("block_apps", value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user