Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 74 additions & 64 deletions resources/translations/cs.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/de.ts

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions resources/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<context>
<name>PreferenceWindow</name>
<message numerus="yes">
<location filename="../../src/app/pref-window.cpp" line="516"/>
<location filename="../../src/app/pref-window.cpp" line="517"/>
<location filename="../../src/app/pref-window.cpp" line="518"/>
<location filename="../../src/app/pref-window.cpp" line="519"/>
<location filename="../../src/app/pref-window.cpp" line="520"/>
<location filename="../../src/app/pref-window.cpp" line="521"/>
<source>%n min</source>
<translation>
<numerusform>%n min</numerusform>
Expand All @@ -23,7 +23,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../src/app/pref-window.cpp" line="515"/>
<location filename="../../src/app/pref-window.cpp" line="518"/>
<source>%n sec</source>
<translation>
<numerusform>%n sec</numerusform>
Expand Down
138 changes: 74 additions & 64 deletions resources/translations/es.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/fr.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/he.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/hu.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/it.ts

Large diffs are not rendered by default.

140 changes: 75 additions & 65 deletions resources/translations/nl.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/pl.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/ru.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/sv.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/uk.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/zh_CN.ts

Large diffs are not rendered by default.

138 changes: 74 additions & 64 deletions resources/translations/zh_TW.ts

Large diffs are not rendered by default.

Binary file modified screenshots/pref-pause.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/app/pref-window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,9 @@ PreferenceWindow::PreferenceWindow(SanePreferences* preferences, QWidget* parent
controllers->add(PrefGroup::Pause,
new PrefController<QSpinBox, int>(ui->pauseOnIdleBox,
preferences->pauseOnIdleFor, 60));
controllers->add(PrefGroup::Pause, new PrefController<QCheckBox, bool>(
ui->treatInhibitorAsActivityCheck,
preferences->treatInhibitorAsActivity));
connect(controllers->add(PrefGroup::Pause,
new PrefController<QSpinBox, int>(
ui->resetBreakBox, preferences->resetAfterPause, 60)),
Expand Down
10 changes: 10 additions & 0 deletions src/app/pref-window.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,16 @@ All color are in &lt;code&gt;#AARRGGBB&lt;/code&gt; format.</string>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="treatInhibitorAsActivityCheck">
<property name="toolTip">
<string>Video players, presentations, and some games prevent the screen from sleeping. When on, that counts as activity so the idle pause won't trigger while they run; when off, only your actual input counts. Effectiveness varies by platform: full on ext-idle-notify Wayland; best-effort on X11 where only XScreenSaverSuspend-based inhibition is detected.</string>
</property>
<property name="text">
<string>When an app keeps the screen awake, count it as activity (e.g. video playback)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="pauseOnBatteryCheck">
<property name="text">
Expand Down
16 changes: 16 additions & 0 deletions src/core/app-states.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "core/app-data.h"
#include "core/break-windows.h"
#include "core/flags.h"
#include "core/idle-time.h"

namespace {

Expand Down Expand Up @@ -146,11 +147,20 @@ void AppContext::checkBreakReadiness() {
if (secondsToNextBreak <= 0) transitionTo(std::make_unique<AppStateBreak>());
}

void AppContext::applyIdleMode() {
idleTimer->setIdleMode(preferences->treatInhibitorAsActivity->get()
? IdleMode::InhibitorAware
: IdleMode::InputOnly);
}

void AppStateNormal::enter(AppContext* app) {
app->openCurrentSpan("normal");
// Use low accuracy (5s) for idle detection in normal state as it can last a long time
app->idleTimer->setWatchAccuracy(5000);
app->idleTimer->setMinIdleTime(app->preferences->pauseOnIdleFor->get() * 1000);
// Restore the user's preferred idle mode (inhibitor-aware idle pause suppression
// belongs here, not during breaks which override it to InputOnly).
app->applyIdleMode();
}
void AppStateNormal::exit(AppContext* app) {
app->closeCurrentSpan();
Expand Down Expand Up @@ -309,6 +319,11 @@ void AppStateBreak::enter(AppContext* app) {
// transitions
app->idleTimer->setWatchAccuracy(500);
app->idleTimer->setMinIdleTime(2000);
// Breaks always use raw input idle (InputOnly), ignoring idle inhibitors. A
// background video player holding an inhibitor must not make the break think the
// user is active — we need to detect actual presence to drive phase transitions
// (prompt ↔ full-screen), force-break escalation, and post-break auto-close.
app->idleTimer->setIdleMode(IdleMode::InputOnly);
app->breakWindows->create(app->data->breakType(), app->preferences,
data->totalSeconds(),
app->data->schedule().isBreakExtendedByPostpone());
Expand Down Expand Up @@ -517,6 +532,7 @@ void AppStateMeeting::enter(AppContext* app) {
app->data->schedule().resetSecondsToNextBreak(app->data->currentBreakConfig());
app->idleTimer->setWatchAccuracy(5000);
app->idleTimer->setMinIdleTime(app->preferences->pauseOnIdleFor->get() * 1000);
app->applyIdleMode();
}

void AppStateMeeting::exit(AppContext* app) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/app-states.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ class AppContext : public QObject {
void transitionTo(std::unique_ptr<AppState> state);
void exitCurrentState();
void checkBreakReadiness();
// Apply the user's preferred idle mode (treatInhibitorAsActivity pref → IdleMode)
// to the idle timer. Called on start, on preference change, and by states that
// resume normal idle measurement after a break (which overrides it to InputOnly).
void applyIdleMode();

signals:
void appStateChanged();
Expand Down
5 changes: 5 additions & 0 deletions src/core/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ AbstractApp::AbstractApp(const AppDependencies& deps, QObject* parent)

connect(preferences->pauseOnBattery, &SettingWithSignal::changed, this,
&AbstractApp::onBatterySettingChange);
connect(preferences->treatInhibitorAsActivity, &SettingWithSignal::changed, this,
[this]() { applyIdleMode(); });
connect(preferences->smallEvery, &SettingWithSignal::changed, this, [this]() {
if (!this->data->focus().isActive()) {
this->data->schedule().resetSecondsToNextBreak(this->data->currentBreakConfig());
Expand All @@ -85,6 +87,9 @@ AbstractApp::AbstractApp(const AppDependencies& deps, QObject* parent)
void AbstractApp::start() {
db->logEvent("app::start");
transitionTo(std::make_unique<AppStateNormal>());
// Apply the idle mode before watching so the correct notification request /
// wrapper is active first.
applyIdleMode();
idleTimer->startWatching();
m_countDownTimer->start();
m_systemMonitor->start();
Expand Down
17 changes: 16 additions & 1 deletion src/core/idle-time.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2025 Sane Break developers
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once
Expand All @@ -22,6 +22,15 @@
#define COREIDLE_EXPORT
#endif

// How idle is measured. The enum describes the *mechanism*; the user-facing
// policy is a single bool preference mapped to this enum in app.cpp.
// InputOnly - input-only idle (Wayland v2; polling backends use the raw
// reader). Pause still applies during a video (old behavior).
// InhibitorAware - inhibitor-aware idle (Wayland v1; polling backends wrap the
// raw reader so idle is held at 0 while an inhibitor is
// active). Don't pause during video (default).
enum class IdleMode { InputOnly, InhibitorAware };

class COREIDLE_EXPORT SystemIdleTime : public QObject {
Q_OBJECT
public:
Expand All @@ -32,6 +41,11 @@ class COREIDLE_EXPORT SystemIdleTime : public QObject {
int minIdleTime() { return m_minIdleTime; }
virtual void setWatchAccuracy(int accuracy) = 0;
virtual void setMinIdleTime(int idleTime) = 0;
// Select how idle is measured. Default no-op just records the mode; backends
// that support inhibitor-aware idle (Wayland v1/v2 switch, or polling backends
// with an inhibitor monitor wired in) override this.
virtual void setIdleMode(IdleMode mode) { m_idleMode = mode; }
IdleMode idleMode() const { return m_idleMode; }
bool isIdle() { return m_isIdle; }
signals:
void idleStart();
Expand All @@ -41,4 +55,5 @@ class COREIDLE_EXPORT SystemIdleTime : public QObject {
bool m_isIdle = false;
int m_watchAccuracy = 500; // How often we watch idle time (ms)
int m_minIdleTime = 2000; // How long will we consider idle (ms)
IdleMode m_idleMode = IdleMode::InhibitorAware; // matches pref default (on)
};
2 changes: 2 additions & 0 deletions src/core/preferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ SanePreferences::SanePreferences(QSettings* settings, QObject* parent)
resetAfterPause = new Setting<int>(settings, "pause/reset-after", 120);
resetCycleAfterPause = new Setting<int>(settings, "pause/reset-cycle-after", 300);
pauseOnBattery = new Setting<bool>(settings, "pause/on-battery", false);
treatInhibitorAsActivity =
new Setting<bool>(settings, "pause/treat-inhibitor-as-activity", true);
programsToMonitor =
new Setting<QStringList>(settings, "pause/programs-to-monitor", QStringList());
pauseOnUnknownMonitor =
Expand Down
4 changes: 4 additions & 0 deletions src/core/preferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ class SanePreferences : public QObject {
Setting<int>* resetAfterPause;
Setting<int>* resetCycleAfterPause;
Setting<bool>* pauseOnBattery;
// When true (default), an app keeping the screen awake (video player,
// presentation) counts as activity and the idle pause won't trigger.
// Mapped to IdleMode in app.cpp.
Setting<bool>* treatInhibitorAsActivity;
Setting<QStringList>* programsToMonitor;
Setting<bool>* pauseOnUnknownMonitor;
Setting<QStringList>* knownMonitors;
Expand Down
24 changes: 19 additions & 5 deletions src/idle/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_library(
sane-idle STATIC factory.cpp factory.h idle-interface.h read-based-idle.cpp
read-based-idle.h
sane-idle STATIC
factory.cpp
factory.h
idle-interface.h
idle-reader.cpp
idle-reader.h
read-based-idle.cpp
read-based-idle.h
)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(Qt6 REQUIRED COMPONENTS DBus)
target_sources(sane-idle PRIVATE linux/factory.h linux/factory.cpp)
target_sources(
sane-idle
PRIVATE linux/factory.h linux/factory.cpp linux/inhibitor-proxy-idle.h
linux/inhibitor-proxy-idle.cpp linux/linux-idle-inhibitor.h
linux/linux-idle-inhibitor.cpp
)
target_link_libraries(sane-idle PRIVATE Qt6::DBus)
add_subdirectory(linux)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_sources(sane-idle PRIVATE windows-idle.h)
target_sources(
sane-idle PRIVATE windows-idle.h windows-inhibitor.h windows-inhibitor.cpp
)
target_link_libraries(sane-idle PRIVATE PowrProf)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_sources(sane-idle PRIVATE macos-idle.h)
target_sources(sane-idle PRIVATE macos-idle.h macos-inhibitor.h macos-inhibitor.cpp)
endif()
target_link_libraries(sane-idle PUBLIC Qt6::Widgets)
13 changes: 10 additions & 3 deletions src/idle/factory.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2025 Sane Break developers
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#include "factory.h"
Expand All @@ -16,18 +16,25 @@
#include "linux/factory.h"
#elif defined Q_OS_MACOS
#include "macos-idle.h"
#include "macos-inhibitor.h"
#include "read-based-idle.h"
#elif defined Q_OS_WIN
#include "read-based-idle.h"
#include "windows-idle.h"
#include "windows-inhibitor.h"
#endif

SystemIdleTime* createIdleTimer(QObject* parent) {
#ifdef Q_OS_LINUX
return createLinuxIdleTimer(parent);
#elif defined Q_OS_MACOS
return new ReadBasedIdleTime(parent, macosIdleTime);
auto* rbt = new ReadBasedIdleTime(parent, macosIdleTime);
auto* monitor = new MacosInhibitor(rbt);
rbt->setInhibitor([monitor]() { return monitor->isDisplayInhibited(); });
return rbt;
#elif defined Q_OS_WIN
return new ReadBasedIdleTime(parent, windowsIdleTime);
auto* rbt = new ReadBasedIdleTime(parent, windowsIdleTime);
rbt->setInhibitor(windowsDisplayInhibited);
return rbt;
#endif
}
8 changes: 6 additions & 2 deletions src/idle/idle-interface.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2025 Sane Break developers
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once
Expand All @@ -19,8 +19,12 @@ Q_DECLARE_INTERFACE(IdleTimeInterface, IdleTimeInterface_iid)
class IdleTimeReaderInterface : public QObject {
public:
virtual int read() = 0;
// Whether an idle/display inhibitor is currently active, as observed alongside
// the idle read. Default false; X11 overrides it (XScreenSaverQueryInfo.state ==
// ScreenSaverDisabled) since inhibition is free in the same call as idle.
virtual bool isInhibited() { return false; }
};

#define IdleTimeReaderInterface_iid \
"io.github.AllanChain.sane-break.IdleTimeReaderInterface/1.0"
"io.github.AllanChain.sane-break.IdleTimeReaderInterface/1.1"
Q_DECLARE_INTERFACE(IdleTimeReaderInterface, IdleTimeReaderInterface_iid)
34 changes: 34 additions & 0 deletions src/idle/idle-reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#include "idle-reader.h"

#include <functional>
#include <utility>

#include "core/idle-time.h"

IdleReader::IdleReader(std::function<int()> rawReader, std::function<bool()> inhibited)
: m_rawReader(std::move(rawReader)), m_inhibited(std::move(inhibited)) {}

int IdleReader::read(IdleMode mode) {
int raw = m_rawReader();
if (raw < 0) return raw; // propagate error sentinels
if (mode == IdleMode::InputOnly) return raw; // raw passthrough, inhibitor ignored
if (m_inhibited()) {
// Advance the baseline so that, on release, counting starts from 0 (the user is
// treated as having just become active). This prevents an instant idleStart the
// moment a long video ends.
m_baseline = raw;
return 0; // "reset the idle time to zero"
}
int reported = raw - m_baseline; // fresh count since release
if (reported < 0) {
// Raw dropped below the baseline (e.g. input during inhibition that the reader
// still observed). Reset and follow raw.
m_baseline = 0;
reported = raw;
}
return reported;
}
37 changes: 37 additions & 0 deletions src/idle/idle-reader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include <functional>

#include "core/idle-time.h"

// Composes a raw idle source with an inhibitor flag and yields the effective idle time
// for a given IdleMode. This is the polling-backend implementation of the mode
// mechanism; the Wayland equivalent is the v1/v2 protocol-object switch.
//
// read(InputOnly) - raw input idle, passthrough (inhibitor ignored).
// read(InhibitorAware) - idle held at 0 while inhibited; on release, counting
// starts from 0 via a captured baseline (matching Wayland
// v1 compositor semantics).
//
// Mode is passed to read() rather than stored, so it lives only on SystemIdleTime and
// there is no second copy to keep in sync. The baseline is inhibit-tracking state
// (not mode) and persists across reads here.
//
// Used by polling backends (GNOME, X11, macOS, Windows). Pure logic over injected
// callables, so it is unit-testable without a real platform.
class IdleReader {
public:
// rawReader - returns raw input idle (ms), or a negative sentinel on error.
// inhibited - reads a cached bool: is an idle/display inhibitor active right now?
IdleReader(std::function<int()> rawReader, std::function<bool()> inhibited);
int read(IdleMode mode);

private:
std::function<int()> m_rawReader;
std::function<bool()> m_inhibited;
int m_baseline = 0; // raw idle captured while inhibited
};
Loading
Loading