From f964aa1583157999e8f5d427b9e16b866e203de5 Mon Sep 17 00:00:00 2001 From: Archisman Panigrahi Date: Mon, 5 May 2025 10:00:50 -0400 Subject: [PATCH 001/134] make dependencies readable in debian/control --- debian/control | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index da665f0b..f1d02bee 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,18 @@ Homepage: https://github.com/slgobinath/SafeEyes/ Package: safeeyes Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, python3 (>= 3.10.0), python3-xlib, gir1.2-notify-0.7, python3-babel, x11-utils, xprintidle, alsa-utils, python3-psutil, python3-croniter, python3-packaging, gir1.2-gtk-4.0 +Depends: ${misc:Depends}, ${python3:Depends}, + python3 (>= 3.10.0), + python3-xlib, + python3-babel, + x11-utils, + xprintidle, + alsa-utils, + python3-psutil, + python3-croniter, + python3-packaging, + gir1.2-notify-0.7, + gir1.2-gtk-4.0 Description: Prevent eye strain with Safe Eyes – an essential screen break reminder. Safe Eyes is a simple tool to remind you to take periodic breaks for your eyes. This is essential for anyone spending more time on the computer to avoid eye strain and other physical problems. . From ff7cb0b8828007d39ba0e7cc0467a9fb3c081727 Mon Sep 17 00:00:00 2001 From: Archisman Panigrahi Date: Fri, 9 May 2025 17:14:34 -0400 Subject: [PATCH 002/134] Automatically create a .deb file in GitHub release (#717) * Build deb package on release --- .github/workflows/build-deb.yml | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/build-deb.yml diff --git a/.github/workflows/build-deb.yml b/.github/workflows/build-deb.yml new file mode 100644 index 00000000..615b1216 --- /dev/null +++ b/.github/workflows/build-deb.yml @@ -0,0 +1,40 @@ +name: Build Debian Package + +on: + push: + branches: [ release ] + workflow_dispatch: + +jobs: + build-deb: + name: Build Debian Package + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential python3-stdeb fakeroot dpkg-dev debhelper dh-python python3 python3-packaging python3-setuptools + + - name: Build .deb package + run: | + DPKG_DEB_COMPRESSOR_TYPE=xz dpkg-buildpackage -us -uc -nc + mv ../*.deb . + + - name: Upload .deb to GitHub Release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v2 + with: + files: "*.deb" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Save .deb as workflow artifact + if: github.event_name != 'release' + uses: actions/upload-artifact@v4 + with: + name: deb-package + path: "*.deb" \ No newline at end of file From dd97369101b21b68e5a9d8317aa316fd1d82177c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BA=8B=E9=BA=93=20BigELK176?= Date: Sat, 10 May 2025 08:51:13 +0200 Subject: [PATCH 003/134] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (135 of 135 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/zh_Hant/ --- .../locale/zh_TW/LC_MESSAGES/safeeyes.po | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po index 11d8252c..3cf77ded 100644 --- a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po @@ -6,20 +6,20 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-01-30 14:01+0000\n" -"Last-Translator: 麋悟BigELK176 \n" -"Language-Team: Chinese (Traditional) \n" +"PO-Revision-Date: 2025-05-11 07:01+0000\n" +"Last-Translator: 麋麓 BigELK176 \n" +"Language-Team: Chinese (Traditional Han script) \n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.4-dev\n" +"X-Generator: Weblate 5.12-dev\n" # Short break msgid "Gently close your eyes" -msgstr "" +msgstr "輕柔閉起您的眼睛" # Short break msgid "Roll your eyes a few times to each side" @@ -108,11 +108,11 @@ msgstr "授權" # About dialog msgid "List of Contributors" -msgstr "" +msgstr "貢獻者名單" # About dialog msgid "Help us translate this app" -msgstr "" +msgstr "協助我們翻譯應用程式" # Break screen msgid "Skip" @@ -144,7 +144,7 @@ msgstr "推遲休息時長(分鐘)" # Settings dialog msgid "Show breaks in random order" -msgstr "顯示休息隨機順序" +msgstr "以隨機順序顯示休息" # Settings dialog msgid "Strict break (No way to skip breaks)" @@ -490,15 +490,15 @@ msgstr "立即休息" #: plugins/trayicon msgid "Any break" -msgstr "" +msgstr "任何休息" #: plugins/trayicon msgid "Short break" -msgstr "" +msgstr "短暫休息" #: plugins/trayicon msgid "Long break" -msgstr "" +msgstr "長時休息" #: plugins/trayicon msgid "Until restart" @@ -522,57 +522,57 @@ msgstr "暫停多媒體" # plugin/limitconsecutiveskipping msgid "Limit Consecutive Skipping" -msgstr "" +msgstr "限制連續跳過" # plugin/limitconsecutiveskipping msgid "How many skips or postpones are allowed in a row" -msgstr "" +msgstr "連續允許多少個跳過或延遲" # plugin/limitconsecutiveskipping msgid "Limit how many breaks can be skipped or postponed in a row" -msgstr "" +msgstr "限制可以連續跳過或延遲多少次休息" # plugin/limitconsecutiveskipping #, python-format msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" -msgstr "" +msgstr "跳過或延遲 %(num)d/%(allowed)d 個休息" # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop msgid "RSI Prevention" -msgstr "" +msgstr "RSI 預防" msgid "" "Please install service providing tray icons for your desktop environment." -msgstr "" +msgstr "請安裝為桌面環境提供系統匣圖示的服務。" #, python-format msgid "Next long break at %s" -msgstr "" +msgstr "下次長時休息於 %s" #, python-format msgid "Next breaks at %(short)s/%(long)s" -msgstr "" +msgstr "下次休息於 %(short)s/%(long)s" #, python-format msgid "The required plugin '%s' is missing dependencies!" -msgstr "" +msgstr "所要求的插件 '%s' 是缺少的依賴項目!" msgid "" "Please install the dependencies or disable the plugin. To hide this message, " "you can also deactivate the plugin in the settings." -msgstr "" +msgstr "請安裝依賴項目或停用插件。要隱藏此訊息,也可以在設置中停用插件。" msgid "Click here for more information" -msgstr "" +msgstr "在此點按查看更多資訊" msgid "Disable plugin temporarily" -msgstr "" +msgstr "暫時停用外掛插件" msgid "Disable permanently" -msgstr "" +msgstr "永久停用" msgid "License:" -msgstr "" +msgstr "授權:" # Short break #~ msgid "Tightly close your eyes" From 459ae9fbc0b9481d014723361eda8930d8a09701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 12 May 2025 10:50:04 +0200 Subject: [PATCH 004/134] Translated using Weblate (Estonian) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/et/ --- safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po index 19d63f41..5e84f729 100644 --- a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-11-12 23:00+0000\n" +"PO-Revision-Date: 2025-05-12 09:03+0000\n" "Last-Translator: Priit Jõerüüt \n" "Language-Team: Estonian \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.9-dev\n" +"X-Generator: Weblate 5.12-dev\n" # Short break msgid "Gently close your eyes" @@ -590,6 +590,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Vana ja eiratav laaditabel leidub siin: „%(old)s“. Kui tahad välimust oma " +"käe järgi sättida, siis tee laaditabel pigem siia: „%(new)s“." # Short break #~ msgid "Tightly close your eyes" From ca052da400a02931935eb11d2e796565ec0efd4c Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 11 May 2025 11:01:20 +0200 Subject: [PATCH 005/134] Translated using Weblate (Russian) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/ru/ --- safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po index bc10fb31..8a23df76 100644 --- a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-09-06 22:09+0000\n" -"Last-Translator: AircGroup \n" +"PO-Revision-Date: 2025-05-12 09:03+0000\n" +"Last-Translator: Sergey Ponomarev \n" "Language-Team: Russian \n" "Language: ru\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Weblate 5.8-dev\n" +"X-Generator: Weblate 5.12-dev\n" # Short break msgid "Gently close your eyes" @@ -597,6 +597,9 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Найдена старая таблица стилей на '%(old)s' и будет игнорироваться. Для " +"пользовательских стилей вместо этого создайте новую таблицу стилей в " +"'%(new)s'." # Short break #~ msgid "Tightly close your eyes" From b193551c7655f54c17c86da243c4d295ef29b5c4 Mon Sep 17 00:00:00 2001 From: Christian Rodriguez Benthake Date: Thu, 15 May 2025 03:37:19 +0200 Subject: [PATCH 006/134] Translated using Weblate (German) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/de/ --- safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po index 42e21832..cfc3abe4 100644 --- a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-09-21 15:40+0000\n" -"Last-Translator: Erik Michelson \n" +"PO-Revision-Date: 2025-05-16 02:01+0000\n" +"Last-Translator: Christian Rodriguez Benthake \n" "Language-Team: German \n" "Language: de\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.8-dev\n" +"X-Generator: Weblate 5.12-dev\n" # Short break msgid "Gently close your eyes" @@ -603,6 +603,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Altes Stylesheet unter '%(old)s' gefunden, wird ignoriert. Erstelle " +"stattdessen für eigene Styles ein neues Stylesheet in '%(new)s'." # Short break #~ msgid "Tightly close your eyes" From 65ee9659a9212b81c9fda7057a579b096cfc69ab Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 21 May 2025 18:46:44 +0200 Subject: [PATCH 007/134] utility: fix module detection --- safeeyes/utility.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/safeeyes/utility.py b/safeeyes/utility.py index b6f0fe0b..7ff5ec93 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -362,12 +362,9 @@ def command_exist(command): def module_exist(module): - """Check wther the given Python module exists or not.""" - try: - importlib.util.find_spec(module) - return True - except ImportError: - return False + """Check whether the given Python module exists or not.""" + module_spec = importlib.util.find_spec(module) + return module_spec is not None def merge_configs(new_config, old_config): From 2ccad0d5a13c1998d7b65a28f0c1637da057f052 Mon Sep 17 00:00:00 2001 From: abdelbasset jabrane Date: Wed, 21 May 2025 10:06:52 +0200 Subject: [PATCH 008/134] Translated using Weblate (Arabic) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/ar/ --- safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po index 8714e59c..6fb9f859 100644 --- a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po @@ -6,8 +6,9 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-01-29 10:02+0000\n" -"Last-Translator: knowcart \n" +"PO-Revision-Date: 2025-05-22 09:20+0000\n" +"Last-Translator: abdelbasset jabrane " +"\n" "Language-Team: Arabic \n" "Language: ar\n" @@ -16,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" -"X-Generator: Weblate 5.10-dev\n" +"X-Generator: Weblate 5.12-dev\n" # Short break msgid "Gently close your eyes" @@ -250,7 +251,7 @@ msgstr "وقت الانتظار" # Settings dialog msgid "Override" -msgstr "تجاوز" +msgstr "استبدال" # Settings dialog msgid "Time (in seconds)" @@ -599,6 +600,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"تم العثور على ورقة الأنماط القديمة في '%(old)s'، تم تجاهلها. بالنسبة للأنماط " +"المخصصة، قم بإنشاء ورقة أنماط جديدة في '%(new)s' بدلاً من ذلك." # Short break #~ msgid "Tightly close your eyes" From 619d11dd22a54b3f7dc1aeb611ab045e05745046 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Fri, 23 May 2025 14:51:56 +0200 Subject: [PATCH 009/134] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/zh_Hant/ --- safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po index 8560effc..6bf58fca 100644 --- a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-05-11 07:01+0000\n" -"Last-Translator: 麋麓 BigELK176 \n" +"PO-Revision-Date: 2025-05-24 13:01+0000\n" +"Last-Translator: Peter Dave Hello \n" "Language-Team: Chinese (Traditional Han script) \n" "Language: zh_TW\n" @@ -578,7 +578,8 @@ msgstr "授權:" msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." -msgstr "" +msgstr "在 '%(old)s' 發現舊的樣式表,已忽略。若需自訂樣式,請在 '%(new)s' " +"中建立新的樣式表。" # Short break #~ msgid "Tightly close your eyes" From 38c91ef3d5a83584be57c1aa2cfee459d464f24e Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Mon, 26 May 2025 11:04:28 +0200 Subject: [PATCH 010/134] Translated using Weblate (Dutch) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/nl/ --- .../config/locale/nl/LC_MESSAGES/safeeyes.po | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po index 35ee536c..ea7dbd82 100644 --- a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2021-02-10 15:50+0000\n" -"Last-Translator: Heimen Stoffels \n" +"PO-Revision-Date: 2025-05-27 10:01+0000\n" +"Last-Translator: Heimen Stoffels \n" "Language-Team: Dutch \n" "Language: nl\n" @@ -15,15 +15,15 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.5-dev\n" +"X-Generator: Weblate 5.12-dev\n" # Short break msgid "Gently close your eyes" -msgstr "" +msgstr "Sluit langzaam je ogen" # Short break msgid "Roll your eyes a few times to each side" -msgstr "Rol je oogballen een paar keer heen en weer" +msgstr "Rol je ogen een paar keer heen en weer" # Short break msgid "Rotate your eyes in clockwise direction" @@ -113,11 +113,11 @@ msgstr "Licentie" # About dialog msgid "List of Contributors" -msgstr "" +msgstr "Lijst met bijdragers" # About dialog msgid "Help us translate this app" -msgstr "" +msgstr "Helpen met vertalen" # Break screen msgid "Skip" @@ -503,15 +503,15 @@ msgstr "Neem nu een pauze" #: plugins/trayicon msgid "Any break" -msgstr "" +msgstr "Iedere pauze" #: plugins/trayicon msgid "Short break" -msgstr "" +msgstr "Korte pauze" #: plugins/trayicon msgid "Long break" -msgstr "" +msgstr "Lange pauze" #: plugins/trayicon msgid "Until restart" @@ -535,63 +535,69 @@ msgstr "Media pauzeren" # plugin/limitconsecutiveskipping msgid "Limit Consecutive Skipping" -msgstr "" +msgstr "Aantal keer overslaan beperken" # plugin/limitconsecutiveskipping msgid "How many skips or postpones are allowed in a row" -msgstr "" +msgstr "Geef aan hoe vaak pauzen mogen worden overgeslagen of uitgesteld" # plugin/limitconsecutiveskipping msgid "Limit how many breaks can be skipped or postponed in a row" msgstr "" +"Geef aan hoeveel pauzes achter elkaar mogen worden uitgesteld of overgeslagen" # plugin/limitconsecutiveskipping #, python-format msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" -msgstr "" +msgstr "%(num)d/%(allowed)d pauzes achter elkaar uitgesteld of overgeslagen" # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop msgid "RSI Prevention" -msgstr "" +msgstr "RSI voorkomen" msgid "" "Please install service providing tray icons for your desktop environment." msgstr "" +"Installeer de werkomgevingsdienst voor het tonen van systeemvakpictogrammen." #, python-format msgid "Next long break at %s" -msgstr "" +msgstr "Volgende lange pauze: %s" #, python-format msgid "Next breaks at %(short)s/%(long)s" -msgstr "" +msgstr "Volgende pauzes: %(short)s/%(long)s" #, python-format msgid "The required plugin '%s' is missing dependencies!" -msgstr "" +msgstr "De afhankelijkheden van de vereist plug-in ‘%s’ ontbreken!" msgid "" "Please install the dependencies or disable the plugin. To hide this message, " "you can also deactivate the plugin in the settings." msgstr "" +"Installeer de afhankelijkheden van deze plug-in. Dit bericht kan worden " +"verborgen door de plug-in uit te schakelen." msgid "Click here for more information" -msgstr "" +msgstr "Klik voor meer informatie" msgid "Disable plugin temporarily" -msgstr "" +msgstr "Plug-in tijdelijk uitschakelen" msgid "Disable permanently" -msgstr "" +msgstr "Permanent uitschakelen" msgid "License:" -msgstr "" +msgstr "Licentie:" #, python-format msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Er is een oud stijlblad aangetroffen: ‘%(old)s’. Dit stijlblad wordt " +"genegeerd. Nieuwe stijlen kunnen worden aangemaakt in ‘%(new)s’." # Short break #~ msgid "Tightly close your eyes" From d4e9968663aabbc11b431da79d33ef97c5498479 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 30 May 2025 20:35:18 +0200 Subject: [PATCH 011/134] fix ci: call apt-get update before install --- .github/workflows/mypy.yml | 4 +++- .github/workflows/release.yml | 1 + .github/workflows/translations.yml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 8627a712..63b1d51a 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -30,7 +30,9 @@ jobs: uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: '3.13' - - run: sudo apt-get install -y libwayland-dev libcairo2-dev libgirepository-2.0-dev + - run: | + sudo apt-get update + sudo apt-get install -y libwayland-dev libcairo2-dev libgirepository-2.0-dev - run: uv pip install -r pyproject.toml - run: uv pip install --group types - run: mypy safeeyes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5621be4a..2dc44332 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,7 @@ jobs: run: | python -m pip install --upgrade pip pip install build wheel + sudo apt-get update sudo apt-get install -y gettext - name: Get Current Version diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index ac2bf6c8..025c5a6e 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -25,6 +25,7 @@ jobs: run: | python3 -m pip install --upgrade pip setuptools wheel python3 -m pip install polib + sudo apt-get update sudo apt-get install -y gettext - name: Check translations From b1cd67fc419f24784bff6eab445d75cc135f2f4f Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Tue, 17 Jun 2025 11:44:48 +0200 Subject: [PATCH 012/134] Translated using Weblate (Czech) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/cs/ --- safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po index b8d8b899..91df2e9b 100644 --- a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-08-27 19:09+0000\n" -"Last-Translator: vikdevelop \n" +"PO-Revision-Date: 2025-06-18 10:01+0000\n" +"Last-Translator: vikdevelop \n" "Language-Team: Czech \n" "Language: cs\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" -"X-Generator: Weblate 5.7.1-dev\n" +"X-Generator: Weblate 5.12.1\n" # Short break msgid "Gently close your eyes" @@ -596,6 +596,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Starý soubor stylů nalezen na adrese '%(old)s', ignorování. Pro vlastní " +"styly vytvořte místo toho nový soubor stylů v '%(new)s'." # Short break #~ msgid "Tightly close your eyes" From a5c33ed1a083dce90e3fe1e76e2b7d0b7e87c6da Mon Sep 17 00:00:00 2001 From: lalala Date: Thu, 19 Jun 2025 08:56:46 +0200 Subject: [PATCH 013/134] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/zh_Hans/ --- .../locale/zh_CN/LC_MESSAGES/safeeyes.po | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po index 92ac9dc7..f2040b74 100644 --- a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po @@ -6,20 +6,20 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-06-27 00:57+0000\n" -"Last-Translator: aerowolf \n" -"Language-Team: Chinese (Simplified) \n" +"PO-Revision-Date: 2025-06-20 06:03+0000\n" +"Last-Translator: lalala \n" +"Language-Team: Chinese (Simplified Han script) \n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.6-rc\n" +"X-Generator: Weblate 5.12.1\n" # Short break msgid "Gently close your eyes" -msgstr "" +msgstr "轻轻闭上眼睛" # Short break msgid "Roll your eyes a few times to each side" @@ -111,11 +111,11 @@ msgstr "许可" # About dialog msgid "List of Contributors" -msgstr "" +msgstr "贡献者列表" # About dialog msgid "Help us translate this app" -msgstr "" +msgstr "帮助我们翻译这个软件" # Break screen msgid "Skip" @@ -493,15 +493,15 @@ msgstr "立即休息" #: plugins/trayicon msgid "Any break" -msgstr "" +msgstr "下一个休息" #: plugins/trayicon msgid "Short break" -msgstr "" +msgstr "短休息" #: plugins/trayicon msgid "Long break" -msgstr "" +msgstr "长休息" #: plugins/trayicon msgid "Until restart" @@ -525,63 +525,64 @@ msgstr "暂停媒体" # plugin/limitconsecutiveskipping msgid "Limit Consecutive Skipping" -msgstr "" +msgstr "连续跳过限制" # plugin/limitconsecutiveskipping msgid "How many skips or postpones are allowed in a row" -msgstr "" +msgstr "允许连续跳过或推迟的次数" # plugin/limitconsecutiveskipping msgid "Limit how many breaks can be skipped or postponed in a row" -msgstr "" +msgstr "限制连续跳过或推迟的次数" # plugin/limitconsecutiveskipping #, python-format msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" -msgstr "" +msgstr "连续跳过或推迟休息 %(num)d/%(allowed)d 次" # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop msgid "RSI Prevention" -msgstr "" +msgstr "RSI 预防" msgid "" "Please install service providing tray icons for your desktop environment." -msgstr "" +msgstr "请为您的桌面环境安装提供托盘图标的服务。" #, python-format msgid "Next long break at %s" -msgstr "" +msgstr "下次长休息在 %s" #, python-format msgid "Next breaks at %(short)s/%(long)s" -msgstr "" +msgstr "下次休息在 %(short)s/%(long)s" #, python-format msgid "The required plugin '%s' is missing dependencies!" -msgstr "" +msgstr "所需插件 '%s' 缺少依赖项!" msgid "" "Please install the dependencies or disable the plugin. To hide this message, " "you can also deactivate the plugin in the settings." -msgstr "" +msgstr "请安装依赖项或禁用该插件。要隐藏此消息,您还可以在设置中停用该插件。" msgid "Click here for more information" -msgstr "" +msgstr "点击此处了解更多信息" msgid "Disable plugin temporarily" -msgstr "" +msgstr "临时禁用插件" msgid "Disable permanently" -msgstr "" +msgstr "永久禁用" msgid "License:" -msgstr "" +msgstr "许可:" #, python-format msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." -msgstr "" +msgstr "在 '%(old)s' 发现旧版样式表,已忽略。若需自定义样式,请在 '%(new)s' " +"创建新版样式表。" # Short break #~ msgid "Tightly close your eyes" From 9fa7ab43923bf89cee74eb731d5d8bb4556875c3 Mon Sep 17 00:00:00 2001 From: Moo Date: Thu, 19 Jun 2025 20:00:26 +0200 Subject: [PATCH 014/134] Translated using Weblate (Lithuanian) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/lt/ --- safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po index a4f7c76d..d7978a81 100644 --- a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po @@ -6,17 +6,17 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-10-14 06:16+0000\n" -"Last-Translator: openSUSE Lietuviškai \n" +"PO-Revision-Date: 2025-06-20 06:03+0000\n" +"Last-Translator: Moo \n" "Language-Team: Lithuanian \n" "Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"(n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Weblate 5.8-dev\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (" +"n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 5.12.1\n" # Short break msgid "Gently close your eyes" @@ -597,6 +597,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Rastas senas stilių aprašas ties „%(old)s“, jo nepaisoma. Norėdami naudoti " +"tinkintus stilius, vietoj jo, sukurkite naują stilių aprašą ties „%(new)s“." # Short break #~ msgid "Tightly close your eyes" From 89b0ff896b697d326822256f51fe8f106b80f70b Mon Sep 17 00:00:00 2001 From: lalala Date: Thu, 19 Jun 2025 08:40:49 +0200 Subject: [PATCH 015/134] Translated using Weblate (Danish) Currently translated at 85.2% (116 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/da/ --- safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po index c175165f..a2178548 100644 --- a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2021-03-16 20:02+0000\n" -"Last-Translator: jan madsen \n" +"PO-Revision-Date: 2025-06-20 06:03+0000\n" +"Last-Translator: lalala \n" "Language-Team: Danish \n" "Language: da\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.5.2-dev\n" +"X-Generator: Weblate 5.12.1\n" # Short break msgid "Gently close your eyes" @@ -532,7 +532,7 @@ msgstr "" # plugin/limitconsecutiveskipping msgid "How many skips or postpones are allowed in a row" -msgstr "" +msgstr "Hvor mange spring eller udsættelser er tilladt i træk" # plugin/limitconsecutiveskipping msgid "Limit how many breaks can be skipped or postponed in a row" From 7d18316de86635096898e0da84ff3dae1dc6c784 Mon Sep 17 00:00:00 2001 From: Victor Viriato Date: Fri, 20 Jun 2025 17:35:54 +0200 Subject: [PATCH 016/134] Translated using Weblate (Portuguese) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/pt/ --- .../config/locale/pt/LC_MESSAGES/safeeyes.po | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po index 4fe70528..e45372ce 100644 --- a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2021-04-18 10:26+0000\n" -"Last-Translator: José Vieira \n" +"PO-Revision-Date: 2025-06-21 16:03+0000\n" +"Last-Translator: Victor Viriato \n" "Language-Team: Portuguese \n" "Language: pt\n" @@ -15,11 +15,11 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.6-dev\n" +"X-Generator: Weblate 5.13-dev\n" # Short break msgid "Gently close your eyes" -msgstr "" +msgstr "Feche seus olhos suavemente" # Short break msgid "Roll your eyes a few times to each side" @@ -113,11 +113,11 @@ msgstr "Licença" # About dialog msgid "List of Contributors" -msgstr "" +msgstr "Lista de Contribuidores" # About dialog msgid "Help us translate this app" -msgstr "" +msgstr "Ajude-nos a traduzir esse app" # Break screen msgid "Skip" @@ -501,15 +501,15 @@ msgstr "Fazer uma pausa agora" #: plugins/trayicon msgid "Any break" -msgstr "" +msgstr "Qualquer pausa" #: plugins/trayicon msgid "Short break" -msgstr "" +msgstr "Pausa curta" #: plugins/trayicon msgid "Long break" -msgstr "" +msgstr "Pausa longa" #: plugins/trayicon msgid "Until restart" @@ -533,63 +533,69 @@ msgstr "Suspender a reprodução" # plugin/limitconsecutiveskipping msgid "Limit Consecutive Skipping" -msgstr "" +msgstr "Limitar Pulos Consecutivos" # plugin/limitconsecutiveskipping msgid "How many skips or postpones are allowed in a row" -msgstr "" +msgstr "Quantos pulos ou adiantamentos são permitidos em sequência" # plugin/limitconsecutiveskipping msgid "Limit how many breaks can be skipped or postponed in a row" -msgstr "" +msgstr "Limitar quantas pausas podem ser puladas ou adiadas em sequência" # plugin/limitconsecutiveskipping #, python-format msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" -msgstr "" +msgstr "%(num)d%(allowed)d pausas seguidas ignoradas ou adiadas" # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop msgid "RSI Prevention" -msgstr "" +msgstr "Prevenção de LER" msgid "" "Please install service providing tray icons for your desktop environment." msgstr "" +"Por favor, instale um serviço que forneça ícones na bandeja do sistema para " +"o seu ambiente de desktop." #, python-format msgid "Next long break at %s" -msgstr "" +msgstr "Próxima pausa longa as %s" #, python-format msgid "Next breaks at %(short)s/%(long)s" -msgstr "" +msgstr "Próximas pausas as %(short)s%(long)s" #, python-format msgid "The required plugin '%s' is missing dependencies!" -msgstr "" +msgstr "O plugin necessário '%s' está faltando dependências!" msgid "" "Please install the dependencies or disable the plugin. To hide this message, " "you can also deactivate the plugin in the settings." msgstr "" +"Por favor, instale as dependências ou desative o plugin. Para ocultar essa " +"mensagem, você também pode desativar o plugin nas configurações." msgid "Click here for more information" -msgstr "" +msgstr "Clique aqui para mais informações" msgid "Disable plugin temporarily" -msgstr "" +msgstr "Desativar plugin temporariamente" msgid "Disable permanently" -msgstr "" +msgstr "Desativar permanentemente" msgid "License:" -msgstr "" +msgstr "Licença:" #, python-format msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Folha de estilos antiga encontrada em %(old)s, ignorando. Para estilos " +"personalizados, crie uma nova folha de estilos em%(new)s." # Short break #~ msgid "Tightly close your eyes" From a3a244d04a4877b180e2434abb3e0dbfc7a501e0 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 23 Jun 2025 20:55:41 +0200 Subject: [PATCH 017/134] README: clarify instructions for installing from source --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 480a62c6..b66fd36d 100644 --- a/README.md +++ b/README.md @@ -118,16 +118,19 @@ flatpak install flathub io.github.slgobinath.SafeEyes Ensure to meet the following dependencies: - gir1.2-notify-0.7 +- gir1.2-gtk-4.0 - python3-babel - python3-croniter +- python3-gi - python3-psutil - python3-packaging - python3-xlib -- xprintidle (optional) -- wlrctl (wayland optional) +- python3-pywayland (optional for KDE/other wayland) +- xprintidle (optional for X11) +- wlrctl (optional for wayland/wlroots) - Python 3.10+ -**To install Safe Eyes:** +**To install Safe Eyes from PyPI:** ```bash sudo pip3 install safeeyes From 91f47ef0a456d6312590fda3148e01dc3e8f4e7e Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 26 Jul 2024 23:54:12 +0200 Subject: [PATCH 018/134] typing: import gettext instead of global function --- ruff.toml | 3 -- safeeyes/__main__.py | 23 ++------- safeeyes/model.py | 1 + .../donotdisturb/dependency_checker.py | 1 + .../plugins/healthstats/dependency_checker.py | 1 + safeeyes/plugins/healthstats/plugin.py | 1 + .../limitconsecutiveskipping/plugin.py | 1 + safeeyes/plugins/notification/plugin.py | 1 + .../plugins/smartpause/dependency_checker.py | 1 + .../plugins/trayicon/dependency_checker.py | 1 + safeeyes/plugins/trayicon/plugin.py | 1 + safeeyes/safeeyes.py | 1 + safeeyes/translations.py | 51 +++++++++++++++++++ safeeyes/ui/about_dialog.py | 1 + safeeyes/ui/break_screen.py | 1 + safeeyes/ui/required_plugin_dialog.py | 1 + safeeyes/ui/settings_dialog.py | 1 + safeeyes/utility.py | 3 +- 18 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 safeeyes/translations.py diff --git a/ruff.toml b/ruff.toml index 4724771d..3a8fcc03 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,8 +1,5 @@ target-version = "py310" -# gettext -builtins = ["_"] - [lint] select = ["E", "W", "F", "D2", "D3", "D4"] ignore = [ diff --git a/safeeyes/__main__.py b/safeeyes/__main__.py index e4e0d9f1..e8de3724 100755 --- a/safeeyes/__main__.py +++ b/safeeyes/__main__.py @@ -21,21 +21,18 @@ """ import argparse -import gettext -import locale import logging import signal import sys import psutil -from safeeyes import utility +from safeeyes import utility, translations +from safeeyes.translations import translate as _ from safeeyes.model import Config from safeeyes.safeeyes import SafeEyes from safeeyes.safeeyes import SAFE_EYES_VERSION from safeeyes.rpc import RPCClient -gettext.install("safeeyes", utility.LOCALE_PATH) - def __running(): """Check if SafeEyes is already running.""" @@ -68,21 +65,7 @@ def __running(): def main(): """Start the Safe Eyes.""" - system_locale = gettext.translation( - "safeeyes", - localedir=utility.LOCALE_PATH, - languages=[utility.system_locale(), "en_US"], - fallback=True, - ) - system_locale.install() - try: - # locale.bindtextdomain is required for Glade files - locale.bindtextdomain("safeeyes", utility.LOCALE_PATH) - except AttributeError: - logging.warning( - "installed python's gettext module does not support locale.bindtextdomain." - " locale.bindtextdomain is required for Glade files" - ) + system_locale = translations.setup() parser = argparse.ArgumentParser(prog="safeeyes") group = parser.add_mutually_exclusive_group() diff --git a/safeeyes/model.py b/safeeyes/model.py index d397ffb1..b19080f1 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -34,6 +34,7 @@ from gi.repository import Gtk from safeeyes import utility +from safeeyes.translations import translate as _ class Break: diff --git a/safeeyes/plugins/donotdisturb/dependency_checker.py b/safeeyes/plugins/donotdisturb/dependency_checker.py index 600cb2bf..96a00de4 100644 --- a/safeeyes/plugins/donotdisturb/dependency_checker.py +++ b/safeeyes/plugins/donotdisturb/dependency_checker.py @@ -17,6 +17,7 @@ # along with this program. If not, see . from safeeyes import utility +from safeeyes.translations import translate as _ def validate(plugin_config, plugin_settings): diff --git a/safeeyes/plugins/healthstats/dependency_checker.py b/safeeyes/plugins/healthstats/dependency_checker.py index daa3c942..475b535a 100644 --- a/safeeyes/plugins/healthstats/dependency_checker.py +++ b/safeeyes/plugins/healthstats/dependency_checker.py @@ -17,6 +17,7 @@ # along with this program. If not, see . from safeeyes import utility +from safeeyes.translations import translate as _ def validate(plugin_config, plugin_settings): diff --git a/safeeyes/plugins/healthstats/plugin.py b/safeeyes/plugins/healthstats/plugin.py index a1dc1f74..d5c0251c 100644 --- a/safeeyes/plugins/healthstats/plugin.py +++ b/safeeyes/plugins/healthstats/plugin.py @@ -21,6 +21,7 @@ import croniter import datetime import logging +from safeeyes.translations import translate as _ context = None session = None diff --git a/safeeyes/plugins/limitconsecutiveskipping/plugin.py b/safeeyes/plugins/limitconsecutiveskipping/plugin.py index 864b609e..16101bf9 100644 --- a/safeeyes/plugins/limitconsecutiveskipping/plugin.py +++ b/safeeyes/plugins/limitconsecutiveskipping/plugin.py @@ -19,6 +19,7 @@ """Limit how many breaks can be skipped or postponed in a row.""" import logging +from safeeyes.translations import translate as _ context = None no_of_skipped_breaks = 0 diff --git a/safeeyes/plugins/notification/plugin.py b/safeeyes/plugins/notification/plugin.py index 5dd4d51d..5c3a3d0b 100644 --- a/safeeyes/plugins/notification/plugin.py +++ b/safeeyes/plugins/notification/plugin.py @@ -20,6 +20,7 @@ import gi from safeeyes.model import BreakType +from safeeyes.translations import translate as _ gi.require_version("Notify", "0.7") from gi.repository import Notify diff --git a/safeeyes/plugins/smartpause/dependency_checker.py b/safeeyes/plugins/smartpause/dependency_checker.py index 6d9b7b29..c3df55d4 100644 --- a/safeeyes/plugins/smartpause/dependency_checker.py +++ b/safeeyes/plugins/smartpause/dependency_checker.py @@ -17,6 +17,7 @@ # along with this program. If not, see . from safeeyes import utility +from safeeyes.translations import translate as _ def validate(plugin_config, plugin_settings): diff --git a/safeeyes/plugins/trayicon/dependency_checker.py b/safeeyes/plugins/trayicon/dependency_checker.py index 2ea5d217..5febe594 100644 --- a/safeeyes/plugins/trayicon/dependency_checker.py +++ b/safeeyes/plugins/trayicon/dependency_checker.py @@ -18,6 +18,7 @@ from safeeyes import utility from safeeyes.model import PluginDependency +from safeeyes.translations import translate as _ import gi diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index 21ced707..fbbeb1bc 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -24,6 +24,7 @@ from gi.repository import Gio, GLib import logging from safeeyes import utility +from safeeyes.translations import translate as _ import threading import time import typing diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index d7f743ef..4ea5a93e 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -32,6 +32,7 @@ from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog from safeeyes.model import State, RequiredPluginException from safeeyes.rpc import RPCServer +from safeeyes.translations import translate as _ from safeeyes.plugin_manager import PluginManager from safeeyes.core import SafeEyesCore from safeeyes.ui.settings_dialog import SettingsDialog diff --git a/safeeyes/translations.py b/safeeyes/translations.py new file mode 100644 index 00000000..f9ee344d --- /dev/null +++ b/safeeyes/translations.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2024 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Translation setup and helpers.""" + +import locale +import logging +import gettext +from safeeyes import utility + +_translations = gettext.NullTranslations() + + +def setup(): + global _translations + _translations = gettext.translation( + "safeeyes", + localedir=utility.LOCALE_PATH, + languages=[utility.system_locale(), "en_US"], + fallback=True, + ) + try: + # locale.bindtextdomain is required for Glade files + locale.bindtextdomain("safeeyes", utility.LOCALE_PATH) + except AttributeError: + logging.warning( + "installed python's gettext module does not support locale.bindtextdomain." + " locale.bindtextdomain is required for Glade files" + ) + + return _translations + + +def translate(message: str) -> str: + """Translate the message using the current translator.""" + return _translations.gettext(message) diff --git a/safeeyes/ui/about_dialog.py b/safeeyes/ui/about_dialog.py index 62f3ed07..5914d44a 100644 --- a/safeeyes/ui/about_dialog.py +++ b/safeeyes/ui/about_dialog.py @@ -21,6 +21,7 @@ import os from safeeyes import utility +from safeeyes.translations import translate as _ ABOUT_DIALOG_GLADE = os.path.join(utility.BIN_DIRECTORY, "glade/about_dialog.glade") diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 3ff43469..e74594a7 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -23,6 +23,7 @@ import gi from safeeyes import utility +from safeeyes.translations import translate as _ import Xlib from Xlib.display import Display from Xlib import X diff --git a/safeeyes/ui/required_plugin_dialog.py b/safeeyes/ui/required_plugin_dialog.py index 0c29decd..ebfbd5d7 100644 --- a/safeeyes/ui/required_plugin_dialog.py +++ b/safeeyes/ui/required_plugin_dialog.py @@ -24,6 +24,7 @@ from safeeyes import utility from safeeyes.model import PluginDependency +from safeeyes.translations import translate as _ REQUIRED_PLUGIN_DIALOG_GLADE = os.path.join( utility.BIN_DIRECTORY, "glade/required_plugin_dialog.glade" diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 03adf447..1189d109 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -22,6 +22,7 @@ import gi from safeeyes import utility from safeeyes.model import Config, PluginDependency +from safeeyes.translations import translate as _ gi.require_version("Gtk", "4.0") from gi.repository import Gtk, Gio diff --git a/safeeyes/utility.py b/safeeyes/utility.py index 7ff5ec93..1ec4b601 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -46,6 +46,7 @@ from gi.repository import GLib from gi.repository import GdkPixbuf from packaging.version import parse +from safeeyes.translations import translate as _ BIN_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) HOME_DIRECTORY = os.environ.get("HOME") or os.path.expanduser("~") @@ -533,7 +534,7 @@ def initialize_platform(): logging.error("Failed to create desktop entry at %s" % desktop_entry) # Add links for all icons - for path, _, filenames in os.walk(SYSTEM_ICONS): + for path, _dirnames, filenames in os.walk(SYSTEM_ICONS): for filename in filenames: system_icon = os.path.join(path, filename) local_icon = os.path.join( From 8f656ac6e15d43673d84443026a8a4871e9dfedb Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 24 Apr 2025 13:32:06 +0200 Subject: [PATCH 019/134] style: use default theme for reset button for dark mode --- safeeyes/config/style/safeeyes_style.css | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/safeeyes/config/style/safeeyes_style.css b/safeeyes/config/style/safeeyes_style.css index 6df25a45..f2b62555 100644 --- a/safeeyes/config/style/safeeyes_style.css +++ b/safeeyes/config/style/safeeyes_style.css @@ -109,19 +109,3 @@ opacity: 0.9; border-color: transparent; } - -.btn_menu { - border-width: 0px; - border-radius: 0px; - border-image: None; - background: white; - border-color: transparent; -} - -.btn_menu:hover { - border-width: 0px; - border-radius: 0px; - border-image: None; - background: whitesmoke; - border-color: transparent; -} From 688fa5b07155650cb809fbc43fd76ed0933ce63f Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 25 Jun 2025 19:50:25 +0200 Subject: [PATCH 020/134] trayicon: do not block main thread with animation The plugin hooks (on_pre_break, on_start_break) are always called on the main thread (and most plugins rely on that, so this is pretty much guaranteed at this point). Before this commit, start_animation was blocking (`time.sleep()`) for a second in total, blocking the main thread. Only the second call to start_animation() happened in a separate thread. This can be avoided by using GLib timeouts instead of blocking. We also save the extra thread (and any synchronization it'd need), and a bunch of execute_main_thread calls. --- safeeyes/plugins/trayicon/plugin.py | 60 +++++++++++++++-------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index fbbeb1bc..af9dfa0f 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -26,7 +26,6 @@ from safeeyes import utility from safeeyes.translations import translate as _ import threading -import time import typing """ @@ -429,6 +428,9 @@ def set_xayatanalabel(self, label): class TrayIcon: """Create and show the tray icon along with the tray menu.""" + _animation_timeout_id: typing.Optional[int] = None + _animation_icon_enabled: bool = False + def __init__(self, context, plugin_config): self.context = context self.on_show_settings = context["api"]["show_settings"] @@ -446,7 +448,6 @@ def __init__(self, context, plugin_config): self.idle_condition = threading.Condition() self.lock = threading.Lock() self.allow_disabling = plugin_config["allow_disabling"] - self.animate = False self.menu_locked = False session_bus = Gio.bus_get_sync(Gio.BusType.SESSION) @@ -785,35 +786,37 @@ def __schedule_resume(self, time_minutes): if not self.active: utility.execute_main_thread(self.on_enable_clicked) - def start_animation(self): - if not self.active or not self.animate: - return - utility.execute_main_thread( - lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled") - ) - time.sleep(0.5) - utility.execute_main_thread( - lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled") - ) - if self.animate and self.active: - time.sleep(0.5) - if self.animate and self.active: - utility.start_thread(self.start_animation) + def start_animation(self) -> None: + if self._animation_timeout_id is not None: + self.stop_animation() + + self._animation_icon_enabled = False + + self._animation_timeout_id = GLib.timeout_add(500, self._do_animate) + + def _do_animate(self) -> bool: + if not self.active: + self._animation_timeout_id = None + return GLib.SOURCE_REMOVE + + if self._animation_icon_enabled: + self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled") + else: + self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled") + + self._animation_icon_enabled = not self._animation_icon_enabled + + return GLib.SOURCE_CONTINUE + + def stop_animation(self) -> None: + if self._animation_timeout_id is not None: + GLib.source_remove(self._animation_timeout_id) + self._animation_timeout_id = None - def stop_animation(self): - self.animate = False if self.active: - utility.execute_main_thread( - lambda: self.sni_service.set_icon( - "io.github.slgobinath.SafeEyes-enabled" - ) - ) + self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled") else: - utility.execute_main_thread( - lambda: self.sni_service.set_icon( - "io.github.slgobinath.SafeEyes-disabled" - ) - ) + self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled") def init(ctx, safeeyes_cfg, plugin_config): @@ -839,7 +842,6 @@ def on_pre_break(break_obj): """Disable the menu if strict_break is enabled.""" if safeeyes_config.get("strict_break"): tray_icon.lock_menu() - tray_icon.animate = True tray_icon.start_animation() From f23b26dd0702831d516e905369f6b48479730615 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 5 Jul 2024 12:38:48 +0200 Subject: [PATCH 021/134] break screen shortcuts: implement on wayland --- safeeyes/ui/break_screen.py | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index e74594a7..5ba5fc2b 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -52,13 +52,15 @@ def __init__(self, application, context, on_skipped, on_postponed): self.enable_postpone = False self.enable_shortcut = False self.is_pretified = False - self.keycode_shortcut_postpone = 65 - self.keycode_shortcut_skip = 9 + self.keycode_shortcut_postpone = 65 # Space + self.keycode_shortcut_skip = 9 # Escape self.on_postponed = on_postponed self.on_skipped = on_skipped self.shortcut_disable_time = 2 self.strict_break = False self.windows = [] + self.show_skip_button = False + self.show_postpone_button = False if not self.context["is_wayland"]: self.x11_display = Display() @@ -146,7 +148,12 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): logging.info("Show break screens in %d display(s)", len(monitors)) skip_button_disabled = self.context.get("skip_button_disabled", False) + self.show_skip_button = not self.strict_break and not skip_button_disabled + postpone_button_disabled = self.context.get("postpone_button_disabled", False) + self.show_postpone_button = ( + self.enable_postpone and not postpone_button_disabled + ) i = 0 @@ -157,6 +164,15 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): window = builder.get_object("window_main") window.set_application(self.application) window.connect("close-request", self.on_window_delete) + + if self.context["is_wayland"]: + # Note: in theory, this could also be used on X11 + # however, that already has its own implementation below + controller = Gtk.EventControllerKey() + controller.connect("key_pressed", self.on_key_pressed_wayland) + controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + window.add_controller(controller) + window.set_title("SafeEyes-" + str(i)) lbl_message = builder.get_object("lbl_message") lbl_count = builder.get_object("lbl_count") @@ -182,7 +198,7 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): toolbar_button.show() # Add the buttons - if self.enable_postpone and not postpone_button_disabled: + if self.show_postpone_button: # Add postpone button btn_postpone = Gtk.Button.new_with_label(_("Postpone")) btn_postpone.get_style_context().add_class("btn_postpone") @@ -190,7 +206,7 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): btn_postpone.set_visible(True) box_buttons.append(btn_postpone) - if not self.strict_break and not skip_button_disabled: + if self.show_skip_button: # Add the skip button btn_skip = Gtk.Button.new_with_label(_("Skip")) btn_skip.get_style_context().add_class("btn_skip") @@ -214,6 +230,11 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): window.fullscreen_on_monitor(monitor) window.present() + # this ensures that none of the buttons is in focus immediately + # otherwise, pressing space presses that button instead of triggering the + # shortcut + window.set_focus(None) + if not self.context["is_wayland"]: self.__window_set_keep_above_x11(window) @@ -298,6 +319,17 @@ def __lock_keyboard_x11(self): # Reduce the CPU usage by sleeping for a second time.sleep(1) + def on_key_pressed_wayland(self, event_controller_key, keyval, keycode, state): + if self.enable_shortcut: + if keyval == Gdk.KEY_space and self.show_postpone_button: + self.postpone_break() + return True + elif keyval == Gdk.KEY_Escape and self.show_skip_button: + self.skip_break() + return True + + return False + def __release_keyboard_x11(self): """Release the locked keyboard.""" logging.info("Unlock the keyboard") From fd43b15dc79bd91ef9eb42b65ab58087e2aec065 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 9 May 2025 19:39:48 +0200 Subject: [PATCH 022/134] x11: disable shortcuts when buttons are disabled previously, it was possible to bypass the limitconsecutiveskipping plugin by pressing space or escape. --- safeeyes/ui/break_screen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 5ba5fc2b..6392b61f 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -305,13 +305,13 @@ def __lock_keyboard_x11(self): if self.enable_shortcut and event.type == X.KeyPress: if ( event.detail == self.keycode_shortcut_skip - and not self.strict_break + and self.show_skip_button ): self.skip_break() break elif ( - self.enable_postpone - and event.detail == self.keycode_shortcut_postpone + event.detail == self.keycode_shortcut_postpone + and self.show_postpone_button ): self.postpone_break() break From 001e83a675fa5e2665cfdbd6ebfa797b61a94d24 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 9 May 2025 19:46:57 +0200 Subject: [PATCH 023/134] wayland: warn when custom shortcuts are set --- safeeyes/config/locale/safeeyes.pot | 3 +++ safeeyes/ui/break_screen.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/safeeyes/config/locale/safeeyes.pot b/safeeyes/config/locale/safeeyes.pot index 0747768a..f7e00a69 100644 --- a/safeeyes/config/locale/safeeyes.pot +++ b/safeeyes/config/locale/safeeyes.pot @@ -561,3 +561,6 @@ msgstr "" #, python-format msgid "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new stylesheet in '%(new)s' instead." msgstr "" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 6392b61f..aeacff15 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -71,6 +71,17 @@ def initialize(self, config): self.enable_postpone = config.get("allow_postpone", False) self.keycode_shortcut_postpone = config.get("shortcut_postpone", 65) self.keycode_shortcut_skip = config.get("shortcut_skip", 9) + + if self.context["is_wayland"] and ( + self.keycode_shortcut_postpone != 65 or self.keycode_shortcut_skip != 9 + ): + logging.warning( + _( + "Customizing the postpone and skip shortcuts does not work on " + "Wayland." + ) + ) + self.shortcut_disable_time = config.get("shortcut_disable_time", 2) self.strict_break = config.get("strict_break", False) From 92c038085801bb61729c65a9e4a9cd3f16cec9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=A4=E0=AE=AE=E0=AE=BF=E0=AE=B4=E0=AF=8D=E0=AE=A8?= =?UTF-8?q?=E0=AF=87=E0=AE=B0=E0=AE=AE=E0=AF=8D?= Date: Thu, 3 Jul 2025 16:48:44 +0200 Subject: [PATCH 024/134] Translated using Weblate (Tamil) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/ta/ --- safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po index 08ac95f8..9789b236 100644 --- a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2017-09-17 07:59-0400\n" -"PO-Revision-Date: 2025-04-21 15:01+0000\n" +"PO-Revision-Date: 2025-07-04 15:01+0000\n" "Last-Translator: தமிழ்நேரம் \n" "Language-Team: Tamil \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.11.1-dev\n" +"X-Generator: Weblate 5.13-dev\n" "Generated-By: pygettext.py 1.5\n" # Short break @@ -592,6 +592,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"புறக்கணித்து, '%(old)s' இல் காணப்படும் பழைய பாணிதாள். தனிப்பயன் பாணிகளுக்கு, அதற்குப் " +"பதிலாக '%(new)s' இல் புதிய பாணிதாளை உருவாக்கவும்." # Short break #~ msgid "Tightly close your eyes" From 45a1138ddf8da8ee798526b0032154533a436861 Mon Sep 17 00:00:00 2001 From: cas9 Date: Sat, 12 Jul 2025 17:46:34 +0200 Subject: [PATCH 025/134] Translated using Weblate (Italian) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/it/ --- safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po index fbd5fc27..b64f2a49 100644 --- a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-08-27 19:09+0000\n" -"Last-Translator: albanobattistella \n" +"PO-Revision-Date: 2025-07-13 16:01+0000\n" +"Last-Translator: cas9 \n" "Language-Team: Italian \n" "Language: it\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.7.1-dev\n" +"X-Generator: Weblate 5.13-dev\n" # Short break msgid "Gently close your eyes" @@ -598,6 +598,8 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Vecchio foglio di stile trovato in '%(old)s', ignorando. Per stili " +"personalizzati, crea un nuovo foglio di stile in %(new)s invece." # Short break #~ msgid "Tightly close your eyes" From 8b9434f41263758813399bf828a39c203b99bbaa Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 21 May 2025 17:59:07 +0200 Subject: [PATCH 026/134] utility: add name to worker threads --- safeeyes/utility.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/safeeyes/utility.py b/safeeyes/utility.py index 1ec4b601..5444ae9e 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -98,7 +98,10 @@ def get_resource_path(resource_name): def start_thread(target_function, **args): """Execute the function in a separate thread.""" thread = threading.Thread( - target=target_function, name="WorkThread", daemon=False, kwargs=args + target=target_function, + name=f"WorkThread {target_function.__qualname__}", + daemon=False, + kwargs=args, ) thread.start() From 6a0bfa837c2698ea46ee444786d5692dd6968689 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 22 May 2025 15:45:10 +0200 Subject: [PATCH 027/134] remove dead code --- safeeyes/plugins/smartpause/plugin.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index fe79664c..05062bf4 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -39,7 +39,6 @@ smart_pause_activated = False idle_start_time = None next_break_time = None -next_break_duration = 0 short_break_interval = 0 waiting_time = 2 is_wayland_and_gnome = False @@ -195,7 +194,6 @@ def init(ctx, safeeyes_config, plugin_config): global postpone global idle_time global short_break_interval - global long_break_duration global waiting_time global postpone_if_active global is_wayland_and_gnome @@ -211,7 +209,6 @@ def init(ctx, safeeyes_config, plugin_config): short_break_interval = ( safeeyes_config.get("short_break_interval") * 60 ) # Convert to seconds - long_break_duration = safeeyes_config.get("long_break_duration") waiting_time = min(2, idle_time) # If idle time is 1 sec, wait only 1 sec is_wayland_and_gnome = context["desktop"] == "gnome" and context["is_wayland"] use_swayidle = context["desktop"] == "sway" @@ -301,9 +298,7 @@ def on_stop(): def update_next_break(break_obj, dateTime): """Update the next break time.""" global next_break_time - global next_break_duration next_break_time = dateTime - next_break_duration = break_obj.duration def on_start_break(break_obj): From 985aadee5b1e42ef5911f9df445080b70ec9ca41 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 21 May 2025 18:14:16 +0200 Subject: [PATCH 028/134] smartpause: ext_idle_notify: make more robust, log if unsupported This fixes two issues: - There was something going wrong with cleaning up the wayland connection/objects, and sometimes the ext_idle_notify object would be either gone or outright segfault. The resource management should be more obvious and easier to follow now. - When running on a compositor without support for ext-idle-notify-v1, the idle_time would just silently be 0. Now we at least log a message, with the potential for displaying a warning in the settings. --- .../plugins/smartpause/ext_idle_notify.py | 148 +++++++++++++----- safeeyes/plugins/smartpause/plugin.py | 15 +- 2 files changed, 117 insertions(+), 46 deletions(-) diff --git a/safeeyes/plugins/smartpause/ext_idle_notify.py b/safeeyes/plugins/smartpause/ext_idle_notify.py index 1c4f02ff..2a352247 100644 --- a/safeeyes/plugins/smartpause/ext_idle_notify.py +++ b/safeeyes/plugins/smartpause/ext_idle_notify.py @@ -22,6 +22,7 @@ import datetime import os import select +import typing from pywayland.client import Display from pywayland.protocol.wayland.wl_seat import WlSeat @@ -29,18 +30,31 @@ class ExtIdleNotify: - _idle_notifier = None - _seat = None - _notification = None - _notifier_set = False - _running = True + _ext_idle_notify_internal: typing.Optional["ExtIdleNotifyInternal"] = None _thread = None - _r_channel = None - _w_channel = None - _idle_since = None + _r_channel_started: int + _w_channel_started: int + + _r_channel_stop: int + _w_channel_stop: int def __init__(self): + self._r_channel_started, self._w_channel_started = os.pipe() + self._r_channel_stop, self._w_channel_stop = os.pipe() + + def run(self): + self._thread = threading.Thread( + target=self._run, name="ExtIdleNotify", daemon=False + ) + self._thread.start() + + result = os.read(self._r_channel_started, 1) + + if result == b"0": + raise Exception("ext-idle-notify-v1 not supported") + + def _run(self): # Note that this creates a new connection to the wayland compositor. # This is not an issue per se, but does mean that the compositor sees this as # a new, separate client, that just happens to run in the same process as @@ -54,53 +68,112 @@ def __init__(self): # https://lists.freedesktop.org/archives/wayland-devel/2019-March/040344.html # The best thing would be, of course, for gtk to gain native support for # ext-idle-notify-v1. - self._display = Display() - self._display.connect() - self._r_channel, self._w_channel = os.pipe() + with Display() as display: + self._ext_idle_notify_internal = ExtIdleNotifyInternal( + display, self._r_channel_stop, self._w_channel_started + ) + self._ext_idle_notify_internal.run() + self._ext_idle_notify_internal = None def stop(self): - self._running = False # write anything, just to wake up the channel - os.write(self._w_channel, b"!") - self._notification.destroy() - self._notification = None - self._seat = None - self._thread.join() - os.close(self._r_channel) - os.close(self._w_channel) + if self._thread is not None: + os.write(self._w_channel_stop, b"!") + self._thread.join() + self._thread = None + os.close(self._r_channel_stop) + os.close(self._w_channel_stop) - def run(self): - self._thread = threading.Thread( - target=self._run, name="ExtIdleNotify", daemon=False - ) - self._thread.start() + os.close(self._r_channel_started) + os.close(self._w_channel_started) - def _run(self): + def get_idle_time_seconds(self): + if self._ext_idle_notify_internal is None: + return 0 + + return self._ext_idle_notify_internal.get_idle_time_seconds() + + +class ExtIdleNotifyInternal: + """This runs in the thread, and is only alive while the display exists. + + Split out into a separate object to simplify lifetime handling. + """ + + _idle_notifier: typing.Optional[ExtIdleNotifierV1] = None + _display: Display + _r_channel_stop: int + _w_channel_started: int + _seat: typing.Optional[WlSeat] = None + + _idle_since = None + + def __init__( + self, display: Display, r_channel_stop: int, w_channel_started: int + ) -> None: + self._display = display + self._r_channel_stop = r_channel_stop + self._w_channel_started = w_channel_started + + def run(self) -> None: reg = self._display.get_registry() reg.dispatcher["global"] = self._global_handler + self._display.roundtrip() + + while self._seat is None: + self._display.dispatch(block=True) + + if self._idle_notifier is None: + self._seat = None + + self._display.roundtrip() + + # communicate to the outer thread that the compositor does not + # implement the ext-idle-notify-v1 protocol + os.write(self._w_channel_started, b"0") + + return + + os.write(self._w_channel_started, b"1") + + timeout_sec = 1 + # note that the typing doesn't work correctly here - it always says that + # get_idle_notification is not defined + notification = self._idle_notifier.get_idle_notification( # type: ignore[attr-defined] + timeout_sec * 1000, self._seat + ) + notification.dispatcher["idled"] = self._idle_notifier_handler + notification.dispatcher["resumed"] = self._idle_notifier_resume_handler + display_fd = self._display.get_fd() - while self._running: + while True: self._display.flush() # this blocks until either there are new events in self._display # (retrieved using dispatch()) - # or until there are events in self._r_channel - which means that stop() - # was called + # or until there are events in self._r_channel_stop - which means that + # stop() was called # unfortunately, this seems like the best way to make sure that dispatch # doesn't block potentially forever (up to multiple seconds in my usage) - read, _w, _x = select.select((display_fd, self._r_channel), (), ()) + read, _w, _x = select.select((display_fd, self._r_channel_stop), (), ()) - if self._r_channel in read: + if self._r_channel_stop in read: # the channel was written to, which means stop() was called - # at this point, self._running should be false as well break if display_fd in read: self._display.dispatch(block=True) - self._display.disconnect() + self._display.roundtrip() + + notification.destroy() + + self._display.roundtrip() + + self._seat = None + self._idle_notifier = None def _global_handler(self, reg, id_num, iface_name, version): if iface_name == "wl_seat": @@ -108,17 +181,6 @@ def _global_handler(self, reg, id_num, iface_name, version): if iface_name == "ext_idle_notifier_v1": self._idle_notifier = reg.bind(id_num, ExtIdleNotifierV1, version) - if self._idle_notifier and self._seat and not self._notifier_set: - self._notifier_set = True - timeout_sec = 1 - self._notification = self._idle_notifier.get_idle_notification( - timeout_sec * 1000, self._seat - ) - self._notification.dispatcher["idled"] = self._idle_notifier_handler - self._notification.dispatcher["resumed"] = ( - self._idle_notifier_resume_handler - ) - def _idle_notifier_handler(self, notification): self._idle_since = datetime.datetime.now() diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 05062bf4..d4107d19 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -52,6 +52,7 @@ ext_idle_notify_lock = threading.Lock() ext_idle_notification_obj = None +ext_idle_notify_unsupported: bool = False # swayidle @@ -101,12 +102,17 @@ def __swayidle_idle_time(): # ext idle def __start_ext_idle_monitor(): - global ext_idle_notification_obj + global ext_idle_notification_obj, ext_idle_notify_unsupported from .ext_idle_notify import ExtIdleNotify ext_idle_notification_obj = ExtIdleNotify() - ext_idle_notification_obj.run() + try: + ext_idle_notification_obj.run() + except BaseException: + logging.warning("Unable to get idle time, ext-idle-notify-v1 not supported.") + ext_idle_notify_unsupported = True + ext_idle_notification_obj = None def __stop_ext_idle_monitor(): @@ -119,8 +125,11 @@ def __stop_ext_idle_monitor(): def __ext_idle_idle_time(): - global ext_idle_notification_obj + global ext_idle_notification_obj, ext_idle_notify_unsupported with ext_idle_notify_lock: + if ext_idle_notify_unsupported: + return 0 + if ext_idle_notification_obj is None: __start_ext_idle_monitor() else: From c0cb1b2addafec9f6dae47f1b35182b1720e9ca7 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 29 May 2025 12:00:14 +0200 Subject: [PATCH 029/134] smartpause: refactor, separate out idle time implementations into classes This commit really does two things: - Separate the different implementations into classes. Add a common interface, and implement it with a separate class for each implementation (x11, gnome, swayidle, ext-idle-notify). - Refactor for performance/battery life. Previously, the plugin would poll every 2 seconds to check the idle time. This is necessary on X11, as there is no better API. However, for the other platforms, it is possible to simple listen for events, saving a lot of unnecessary wakeups (especially while idle). The common interface was strongly influenced by this. This does however mean that the gnome and swayidle implementations were heavily refactored as well, to make use of the event model. It's pretty hard to split those two things up. It would be possible in theory, but it'd be a lot of work, and the intermediate state would either look nothing like the start or end, or just be broken. --- .../plugins/smartpause/dependency_checker.py | 4 +- .../plugins/smartpause/ext_idle_notify.py | 203 +++++++-- safeeyes/plugins/smartpause/gnome_dbus.py | 126 ++++++ safeeyes/plugins/smartpause/interface.py | 98 ++++ safeeyes/plugins/smartpause/plugin.py | 420 ++++++++---------- safeeyes/plugins/smartpause/swayidle.py | 117 +++++ safeeyes/plugins/smartpause/x11.py | 114 +++++ 7 files changed, 816 insertions(+), 266 deletions(-) create mode 100644 safeeyes/plugins/smartpause/gnome_dbus.py create mode 100644 safeeyes/plugins/smartpause/interface.py create mode 100644 safeeyes/plugins/smartpause/swayidle.py create mode 100644 safeeyes/plugins/smartpause/x11.py diff --git a/safeeyes/plugins/smartpause/dependency_checker.py b/safeeyes/plugins/smartpause/dependency_checker.py index c3df55d4..91e13574 100644 --- a/safeeyes/plugins/smartpause/dependency_checker.py +++ b/safeeyes/plugins/smartpause/dependency_checker.py @@ -22,9 +22,7 @@ def validate(plugin_config, plugin_settings): command = None - if utility.DESKTOP_ENVIRONMENT == "gnome" and utility.IS_WAYLAND: - command = "dbus-send" - elif utility.DESKTOP_ENVIRONMENT == "sway": + if utility.DESKTOP_ENVIRONMENT == "sway": command = "swayidle" elif utility.IS_WAYLAND: if not utility.module_exist("pywayland"): diff --git a/safeeyes/plugins/smartpause/ext_idle_notify.py b/safeeyes/plugins/smartpause/ext_idle_notify.py index 2a352247..a975d5a0 100644 --- a/safeeyes/plugins/smartpause/ext_idle_notify.py +++ b/safeeyes/plugins/smartpause/ext_idle_notify.py @@ -18,20 +18,34 @@ # This file is heavily inspired by https://github.com/juienpro/easyland/blob/efc26a0b22d7bdbb0f8436183428f7036da4662a/src/easyland/idle.py +from dataclasses import dataclass +import logging import threading -import datetime import os import select import typing from pywayland.client import Display from pywayland.protocol.wayland.wl_seat import WlSeat -from pywayland.protocol.ext_idle_notify_v1 import ExtIdleNotifierV1 +from pywayland.protocol.ext_idle_notify_v1 import ( + ExtIdleNotifierV1, + ExtIdleNotificationV1, +) +from .interface import IdleMonitorInterface +from safeeyes import utility -class ExtIdleNotify: + +@dataclass +class IdleConfig: + on_idle: typing.Callable[[], None] + on_resumed: typing.Callable[[], None] + idle_time: float + + +class IdleMonitorExtIdleNotify(IdleMonitorInterface): _ext_idle_notify_internal: typing.Optional["ExtIdleNotifyInternal"] = None - _thread = None + _thread: typing.Optional[threading.Thread] = None _r_channel_started: int _w_channel_started: int @@ -39,11 +53,19 @@ class ExtIdleNotify: _r_channel_stop: int _w_channel_stop: int - def __init__(self): + _r_channel_listen: int + _w_channel_listen: int + + _idle_config: typing.Optional[IdleConfig] = None + + def init(self) -> None: + # we spawn one wayland client once + # when the monitor is not running, it should be quite idle self._r_channel_started, self._w_channel_started = os.pipe() self._r_channel_stop, self._w_channel_stop = os.pipe() + self._r_channel_listen, self._w_channel_listen = os.pipe() + os.set_blocking(self._r_channel_listen, False) - def run(self): self._thread = threading.Thread( target=self._run, name="ExtIdleNotify", daemon=False ) @@ -52,9 +74,44 @@ def run(self): result = os.read(self._r_channel_started, 1) if result == b"0": + self._thread.join() + self._thread = None raise Exception("ext-idle-notify-v1 not supported") - def _run(self): + def start_monitor( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + self._idle_config = IdleConfig( + on_idle=on_idle, + on_resumed=on_resumed, + idle_time=idle_time, + ) + + # 1 means start listening, or that the configuration changed + os.write(self._w_channel_listen, b"1") + + def configuration_changed( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + self._idle_config = IdleConfig( + on_idle=on_idle, + on_resumed=on_resumed, + idle_time=idle_time, + ) + + # 1 means start listening, or that the configuration changed + os.write(self._w_channel_listen, b"1") + + def is_monitor_running(self) -> bool: + return self._idle_config is not None + + def _run(self) -> None: # Note that this creates a new connection to the wayland compositor. # This is not an issue per se, but does mean that the compositor sees this as # a new, separate client, that just happens to run in the same process as @@ -70,12 +127,40 @@ def _run(self): # ext-idle-notify-v1. with Display() as display: self._ext_idle_notify_internal = ExtIdleNotifyInternal( - display, self._r_channel_stop, self._w_channel_started + display, + self._r_channel_stop, + self._w_channel_started, + self._r_channel_listen, + self._on_idle, + self._on_resumed, + self._get_idle_time, ) self._ext_idle_notify_internal.run() self._ext_idle_notify_internal = None - def stop(self): + def _on_idle(self) -> None: + if self._idle_config is not None: + self._idle_config.on_idle() + + def _on_resumed(self) -> None: + if self._idle_config is not None: + self._idle_config.on_resumed() + + def _get_idle_time(self) -> typing.Optional[float]: + if self._idle_config is not None: + return self._idle_config.idle_time + else: + return None + + def stop_monitor(self) -> None: + # 0 means to stop listening + # It's not an issue to write to the channel if we're not listening anymore + # already + os.write(self._w_channel_listen, b"0") + + self._idle_config = None + + def stop(self) -> None: # write anything, just to wake up the channel if self._thread is not None: os.write(self._w_channel_stop, b"!") @@ -87,11 +172,8 @@ def stop(self): os.close(self._r_channel_started) os.close(self._w_channel_started) - def get_idle_time_seconds(self): - if self._ext_idle_notify_internal is None: - return 0 - - return self._ext_idle_notify_internal.get_idle_time_seconds() + os.close(self._r_channel_listen) + os.close(self._w_channel_listen) class ExtIdleNotifyInternal: @@ -101,21 +183,41 @@ class ExtIdleNotifyInternal: """ _idle_notifier: typing.Optional[ExtIdleNotifierV1] = None + _notification: typing.Optional[ExtIdleNotificationV1] = None _display: Display _r_channel_stop: int _w_channel_started: int + _r_channel_listen: int _seat: typing.Optional[WlSeat] = None - _idle_since = None + _on_idle: typing.Callable[[], None] + _on_resumed: typing.Callable[[], None] + _get_idle_time: typing.Callable[[], typing.Optional[float]] def __init__( - self, display: Display, r_channel_stop: int, w_channel_started: int + self, + display: Display, + r_channel_stop: int, + w_channel_started: int, + r_channel_listen: int, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + get_idle_time: typing.Callable[[], typing.Optional[float]], ) -> None: self._display = display self._r_channel_stop = r_channel_stop self._w_channel_started = w_channel_started + self._r_channel_listen = r_channel_listen + self._on_idle = on_idle + self._on_resumed = on_resumed + self._get_idle_time = get_idle_time def run(self) -> None: + """Run the wayland client. + + This will block until it's stopped by the channel. + When this stops, self should no longer be used. + """ reg = self._display.get_registry() reg.dispatcher["global"] = self._global_handler @@ -137,15 +239,6 @@ def run(self) -> None: os.write(self._w_channel_started, b"1") - timeout_sec = 1 - # note that the typing doesn't work correctly here - it always says that - # get_idle_notification is not defined - notification = self._idle_notifier.get_idle_notification( # type: ignore[attr-defined] - timeout_sec * 1000, self._seat - ) - notification.dispatcher["idled"] = self._idle_notifier_handler - notification.dispatcher["resumed"] = self._idle_notifier_resume_handler - display_fd = self._display.get_fd() while True: @@ -157,7 +250,20 @@ def run(self) -> None: # stop() was called # unfortunately, this seems like the best way to make sure that dispatch # doesn't block potentially forever (up to multiple seconds in my usage) - read, _w, _x = select.select((display_fd, self._r_channel_stop), (), ()) + read, _w, _x = select.select( + (display_fd, self._r_channel_stop, self._r_channel_listen), (), () + ) + + if self._r_channel_listen in read: + # r_channel_listen is nonblocking + # if there is nothing to read here, result should just be b"" + result = os.read(self._r_channel_listen, 1) + if result == b"1": + self._listen() + elif result == b"0": + if self._notification is not None: + self._notification.destroy() # type: ignore[attr-defined] + self._notification = None if self._r_channel_stop in read: # the channel was written to, which means stop() was called @@ -168,28 +274,47 @@ def run(self) -> None: self._display.roundtrip() - notification.destroy() + if self._notification is not None: + self._notification.destroy() # type: ignore[attr-defined] + self._notification = None self._display.roundtrip() self._seat = None self._idle_notifier = None - def _global_handler(self, reg, id_num, iface_name, version): + def _listen(self): + """Create a new idle notification listener. + + If one already exists, throw it away and recreate it with the new + idle time. + """ + # note that the typing doesn't work correctly here - it always says that + # get_idle_notification is not defined + # so just don't check this method + if self._notification is not None: + self._notification.destroy() + self._notification = None + + timeout_sec = self._get_idle_time() + if timeout_sec is None: + logging.debug( + "this should not happen. _listen() was called but idle time was not set" + ) + self._notification = self._idle_notifier.get_idle_notification( + int(timeout_sec * 1000), self._seat + ) + self._notification.dispatcher["idled"] = self._idle_notifier_handler + self._notification.dispatcher["resumed"] = self._idle_notifier_resume_handler + + def _global_handler(self, reg, id_num, iface_name, version) -> None: if iface_name == "wl_seat": self._seat = reg.bind(id_num, WlSeat, version) if iface_name == "ext_idle_notifier_v1": self._idle_notifier = reg.bind(id_num, ExtIdleNotifierV1, version) - def _idle_notifier_handler(self, notification): - self._idle_since = datetime.datetime.now() - - def _idle_notifier_resume_handler(self, notification): - self._idle_since = None - - def get_idle_time_seconds(self): - if self._idle_since is None: - return 0 + def _idle_notifier_handler(self, notification) -> None: + utility.execute_main_thread(self._on_idle) - result = datetime.datetime.now() - self._idle_since - return result.total_seconds() + def _idle_notifier_resume_handler(self, notification) -> None: + utility.execute_main_thread(self._on_resumed) diff --git a/safeeyes/plugins/smartpause/gnome_dbus.py b/safeeyes/plugins/smartpause/gnome_dbus.py new file mode 100644 index 00000000..229a0da4 --- /dev/null +++ b/safeeyes/plugins/smartpause/gnome_dbus.py @@ -0,0 +1,126 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import typing + +import gi + +gi.require_version("Gio", "2.0") +from gi.repository import Gio, GLib + + +from .interface import IdleMonitorInterface + + +class IdleMonitorGnomeDBus(IdleMonitorInterface): + """IdleMonitorInterface implementation for GNOME.""" + + dbus_proxy: typing.Optional[Gio.DBusProxy] = None + idle_watch_id: typing.Optional[int] = None + active_watch_id: typing.Optional[int] = None + + was_idle: bool = False + + _on_idle: typing.Optional[typing.Callable[[], None]] = None + _on_resumed: typing.Optional[typing.Callable[[], None]] = None + + def init(self) -> None: + if self.dbus_proxy is None: + self.dbus_proxy = Gio.DBusProxy.new_for_bus_sync( + bus_type=Gio.BusType.SESSION, + flags=Gio.DBusProxyFlags.NONE, + info=None, + name="org.gnome.Mutter.IdleMonitor", + object_path="/org/gnome/Mutter/IdleMonitor/Core", + interface_name="org.gnome.Mutter.IdleMonitor", + cancellable=None, + ) + + self.dbus_proxy.connect("g-signal", self._handle_proxy_signal) + + def start_monitor( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + """Start watching for idling. + + This is run on the main thread, and should not block. + """ + if self.is_monitor_running(): + self.stop() + + self._on_idle = on_idle + self._on_resumed = on_resumed + # NOTE: this is currently somewhat buggy, actually + # This does not start counting the idle time when the watch is added + # if the user was idle for more than `idle_time` s, this will fire immediately + # This is not a big issue, but does mean that it might pause safeeyes right + # after a break finishes + self.idle_watch_id = self.dbus_proxy.AddIdleWatch( # type: ignore[union-attr] + "(t)", idle_time * 1000 + ) + + def _handle_proxy_signal( + self, + dbus_proxy: Gio.DBusProxy, + sender_name: typing.Optional[str], + signal_name: str, + parameters: GLib.Variant, + ) -> None: + if signal_name == "WatchFired": + watch_id: int + (watch_id,) = parameters # type: ignore[misc] + + if watch_id == self.idle_watch_id: + if self.active_watch_id is not None: + dbus_proxy.RemoveWatch("(u)", self.active_watch_id) # type: ignore[attr-defined] + self.active_watch_id = dbus_proxy.AddUserActiveWatch("()") # type: ignore[attr-defined] + if not self.was_idle: + self.was_idle = True + if self._on_idle: + self._on_idle() + + if self.active_watch_id is not None and watch_id == self.active_watch_id: + self.active_watch_id = None + if self.was_idle: + self.was_idle = False + if self._on_resumed: + self._on_resumed() + + def is_monitor_running(self) -> bool: + return self.idle_watch_id is not None + + def stop_monitor(self) -> None: + """Stop watching for idling. + + This is run on the main thread. It may block a short time for cleanup. + """ + if self.is_monitor_running() and self.dbus_proxy is not None: + self.dbus_proxy.RemoveWatch("(u)", self.idle_watch_id) # type: ignore[attr-defined] + self.idle_watch_id = None + + if self.active_watch_id is not None: + self.dbus_proxy.RemoveWatch("(u)", self.active_watch_id) # type: ignore[attr-defined] + self.active_watch_id = None + + self.was_idle = False + + def stop(self) -> None: + self.dbus_proxy = None diff --git a/safeeyes/plugins/smartpause/interface.py b/safeeyes/plugins/smartpause/interface.py new file mode 100644 index 00000000..ccfe5a4e --- /dev/null +++ b/safeeyes/plugins/smartpause/interface.py @@ -0,0 +1,98 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from abc import ABC, abstractmethod +from typing import Callable + + +class IdleMonitorInterface(ABC): + """Platform-specific interface to notify when the user is idle. + + The on_idle and on_resumed hooks must be used to notify safeeyes when the user + has gone idle. The on_idle hook must be fired first, afterwards they must be called + in alternation. + They must be fired from the main thread. + """ + + @abstractmethod + def init(self) -> None: + """Initialize the monitor. + + This is called once initially. + This is run on the main thread, and should only block for a short time for + startup. + """ + pass + + @abstractmethod + def start_monitor( + self, + on_idle: Callable[[], None], + on_resumed: Callable[[], None], + idle_time: float, + ) -> None: + """Start watching for idling. + + This will be called multiple times, whenever the monitor should start watching + or after configuration changes. + This is run on the main thread, and should only block for a short time for + startup. + """ + pass + + @abstractmethod + def is_monitor_running(self) -> bool: + """Check if the monitor is running. + + This is run on the main thread, and should not block. + """ + pass + + @abstractmethod + def stop_monitor(self) -> None: + """Stop watching for idling. + + This will be called multiple times, whenever the monitor should stop watching + or when configuration changes. + This is run on the main thread. It may block a short time for cleanup. + """ + pass + + def configuration_changed( + self, + on_idle: Callable[[], None], + on_resumed: Callable[[], None], + idle_time: float, + ) -> None: + """Restart the idle watcher. + + This method will be called when configuration changes. It may be overridden + by implementations for optimization. + This is run on the main thread. It may block a short time for cleanup/startup. + """ + self.stop_monitor() + self.start_monitor(on_idle, on_resumed, idle_time) + + @abstractmethod + def stop(self) -> None: + """Deinitialize the monitor. + + This is called once before the monitor is destroyed. + This is run on the main thread. It may block a short time for cleanup. + """ + pass diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index d4107d19..55c308fd 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -18,184 +18,102 @@ import datetime import logging -import subprocess -import threading -import re +import typing -from safeeyes import utility from safeeyes.model import State +from .interface import IdleMonitorInterface +from .gnome_dbus import IdleMonitorGnomeDBus +from .swayidle import IdleMonitorSwayidle +from .x11 import IdleMonitorX11 + """ Safe Eyes smart pause plugin """ context = None -idle_condition = threading.Condition() -lock = threading.Lock() -active = False -idle_time = 0 +idle_time: float = 0 enable_safeeyes = None disable_safeeyes = None +postpone: typing.Optional[typing.Callable[[int], None]] = None smart_pause_activated = False -idle_start_time = None -next_break_time = None -short_break_interval = 0 -waiting_time = 2 -is_wayland_and_gnome = False +idle_start_time: typing.Optional[datetime.datetime] = None +next_break_time: typing.Optional[datetime.datetime] = None +short_break_interval: int = 0 +postpone_if_active: bool = False +is_wayland_and_gnome = False use_swayidle = False use_ext_idle_notify = False -swayidle_process = None -swayidle_lock = threading.Lock() -swayidle_idle = 0 -swayidle_active = 0 - -ext_idle_notify_lock = threading.Lock() -ext_idle_notification_obj = None -ext_idle_notify_unsupported: bool = False - - -# swayidle -def __swayidle_running(): - return swayidle_process is not None and swayidle_process.poll() is None - - -def __start_swayidle_monitor(): - global swayidle_process - global swayidle_start - global swayidle_idle - global swayidle_active - logging.debug("Starting swayidle subprocess") - swayidle_process = subprocess.Popen( - ["swayidle", "timeout", "1", "date +S%s", "resume", "date +R%s"], - stdout=subprocess.PIPE, - bufsize=1, - universal_newlines=True, - encoding="utf-8", - ) - for line in swayidle_process.stdout: - with swayidle_lock: - typ = line[0] - timestamp = int(line[1:]) - if typ == "S": - swayidle_idle = timestamp - elif typ == "R": - swayidle_active = timestamp - - -def __stop_swayidle_monitor(): - if __swayidle_running(): - logging.debug("Stopping swayidle subprocess") - swayidle_process.terminate() +idle_monitor: typing.Optional[IdleMonitorInterface] = None +idle_monitor_unsupported: bool = False -def __swayidle_idle_time(): - with swayidle_lock: - if not __swayidle_running(): - utility.start_thread(__start_swayidle_monitor) - # Idle more recently than active, meaning idle time isn't stale. - if swayidle_idle > swayidle_active: - idle_time = int(datetime.datetime.now().timestamp()) - swayidle_idle - return idle_time - return 0 +idle_monitor_is_pre_break: bool = False +pre_break_idle_start_time: typing.Optional[datetime.datetime] = None +# this is hardcoded currently +pre_break_postpone_idle_time: int = 2 -# ext idle -def __start_ext_idle_monitor(): - global ext_idle_notification_obj, ext_idle_notify_unsupported - from .ext_idle_notify import ExtIdleNotify - - ext_idle_notification_obj = ExtIdleNotify() - try: - ext_idle_notification_obj.run() - except BaseException: - logging.warning("Unable to get idle time, ext-idle-notify-v1 not supported.") - ext_idle_notify_unsupported = True - ext_idle_notification_obj = None - - -def __stop_ext_idle_monitor(): - global ext_idle_notification_obj +def _on_idle() -> None: + global smart_pause_activated + global idle_start_time - with ext_idle_notify_lock: - if ext_idle_notification_obj is not None: - ext_idle_notification_obj.stop() - ext_idle_notification_obj = None + if context["state"] == State.WAITING: # type: ignore[index] + smart_pause_activated = True + idle_start_time = datetime.datetime.now() - datetime.timedelta( + seconds=idle_time + ) + logging.info("Pause Safe Eyes due to system idle") + disable_safeeyes(None, True) # type: ignore[misc] -def __ext_idle_idle_time(): - global ext_idle_notification_obj, ext_idle_notify_unsupported - with ext_idle_notify_lock: - if ext_idle_notify_unsupported: - return 0 +def _on_resumed() -> None: + global smart_pause_activated + global idle_start_time - if ext_idle_notification_obj is None: - __start_ext_idle_monitor() + if ( + context["state"] == State.RESTING # type: ignore[index] + and idle_start_time is not None + ): + logging.info("Resume Safe Eyes due to user activity") + smart_pause_activated = False + idle_period = datetime.datetime.now() - idle_start_time + idle_seconds = idle_period.total_seconds() + context["idle_period"] = idle_seconds # type: ignore[index] + if idle_seconds < short_break_interval: + # Credit back the idle time + if next_break_time is not None: + # This method runs in a thread since the start. + # It may run before next_break is initialized in the + # update_next_break method + next_break = next_break_time + idle_period + enable_safeeyes(next_break.timestamp()) # type: ignore[misc] + else: + enable_safeeyes() # type: ignore[misc] else: - return ext_idle_notification_obj.get_idle_time_seconds() - return 0 - - -# gnome -def __gnome_wayland_idle_time(): - """Determine system idle time in seconds, specifically for gnome with - wayland. - - If there's a failure, return 0. - https://unix.stackexchange.com/a/492328/222290 - """ - try: - output = subprocess.check_output( - [ - "dbus-send", - "--print-reply", - "--dest=org.gnome.Mutter.IdleMonitor", - "/org/gnome/Mutter/IdleMonitor/Core", - "org.gnome.Mutter.IdleMonitor.GetIdletime", - ] - ) - return int(re.search(rb"\d+$", output).group(0)) / 1000 - except BaseException as e: - logging.warning("Failed to get system idle time for gnome/wayland.") - logging.warning(str(e)) - return 0 + # User is idle for more than the time between two breaks + enable_safeeyes() # type: ignore[misc] -def __system_idle_time(): - """Get system idle time in minutes. - - Return the idle time if xprintidle is available, otherwise return 0. - """ - try: - if is_wayland_and_gnome: - return __gnome_wayland_idle_time() - elif use_swayidle: - return __swayidle_idle_time() - elif use_ext_idle_notify: - return __ext_idle_idle_time() - # Convert to seconds - return int(subprocess.check_output(["xprintidle"]).decode("utf-8")) / 1000 - except BaseException: - return 0 +def _on_idle_pre_break() -> None: + global pre_break_idle_start_time + logging.debug("idled before break") + pre_break_idle_start_time = datetime.datetime.now() - datetime.timedelta( + seconds=pre_break_postpone_idle_time + ) -def __is_active(): - """Thread safe function to see if this plugin is active or not.""" - is_active = False - with lock: - is_active = active - return is_active +def _on_resumed_pre_break() -> None: + global pre_break_idle_start_time -def __set_active(is_active): - """Thread safe function to change the state of the plugin.""" - global active - with lock: - active = is_active + logging.debug("resumed before break") + pre_break_idle_start_time = None -def init(ctx, safeeyes_config, plugin_config): +def init(ctx, safeeyes_config, plugin_config) -> None: """Initialize the plugin.""" global context global enable_safeeyes @@ -203,7 +121,6 @@ def init(ctx, safeeyes_config, plugin_config): global postpone global idle_time global short_break_interval - global waiting_time global postpone_if_active global is_wayland_and_gnome global use_swayidle @@ -218,108 +135,163 @@ def init(ctx, safeeyes_config, plugin_config): short_break_interval = ( safeeyes_config.get("short_break_interval") * 60 ) # Convert to seconds - waiting_time = min(2, idle_time) # If idle time is 1 sec, wait only 1 sec is_wayland_and_gnome = context["desktop"] == "gnome" and context["is_wayland"] use_swayidle = context["desktop"] == "sway" use_ext_idle_notify = ( context["is_wayland"] and not use_swayidle and not is_wayland_and_gnome ) + if idle_monitor is not None and idle_monitor.is_monitor_running(): + idle_monitor.configuration_changed(_on_idle, _on_resumed, idle_time) -def __start_idle_monitor(): - """Continuously check the system idle time and pause/resume Safe Eyes based - on it. - """ - global smart_pause_activated - global idle_start_time - while __is_active(): - # Wait for waiting_time seconds - idle_condition.acquire() - idle_condition.wait(waiting_time) - idle_condition.release() - - if __is_active(): - # Get the system idle time - system_idle_time = __system_idle_time() - if system_idle_time >= idle_time and context["state"] == State.WAITING: - smart_pause_activated = True - idle_start_time = datetime.datetime.now() - datetime.timedelta( - seconds=system_idle_time - ) - logging.info("Pause Safe Eyes due to system idle") - disable_safeeyes(None, True) - elif ( - system_idle_time < idle_time - and context["state"] == State.RESTING - and idle_start_time is not None - ): - logging.info("Resume Safe Eyes due to user activity") - smart_pause_activated = False - idle_period = datetime.datetime.now() - idle_start_time - idle_seconds = idle_period.total_seconds() - context["idle_period"] = idle_seconds - if idle_seconds < short_break_interval: - # Credit back the idle time - if next_break_time is not None: - # This method runs in a thread since the start. - # It may run before next_break is initialized in the - # update_next_break method - next_break = next_break_time + idle_period - enable_safeeyes(next_break.timestamp()) - else: - enable_safeeyes() - else: - # User is idle for more than the time between two breaks - enable_safeeyes() - - -def on_start(): - """Start a thread to continuously call xprintidle.""" - global active - if not __is_active(): - # If SmartPause is already started, do not start it again - logging.debug("Start Smart Pause plugin") - __set_active(True) - utility.start_thread(__start_idle_monitor) - - -def on_stop(): - """Stop the thread from continuously calling xprintidle.""" - global active +def on_start() -> None: + """Start the platform idle monitor.""" + global idle_time + global idle_monitor + global idle_monitor_unsupported + + if idle_monitor_unsupported: + # Don't try and start again if we failed in the past + return + + if idle_monitor is None: + if is_wayland_and_gnome: + idle_monitor = IdleMonitorGnomeDBus() + elif use_swayidle: + idle_monitor = IdleMonitorSwayidle() + elif use_ext_idle_notify: + from .ext_idle_notify import IdleMonitorExtIdleNotify + + idle_monitor = IdleMonitorExtIdleNotify() + else: + idle_monitor = IdleMonitorX11() + + try: + idle_monitor.init() + except BaseException as e: + logging.warning("Unable to get idle time, idle monitor not supported.") + logging.warning(str(e)) + idle_monitor.stop() + idle_monitor = None + idle_monitor_unsupported = True + + if idle_monitor is not None: + if not idle_monitor.is_monitor_running(): + logging.debug("Start Smart Pause plugin") + try: + idle_monitor.start_monitor(_on_idle, _on_resumed, idle_time) + except BaseException as e: + logging.warning("Unable to get idle time, idle monitor not supported.") + logging.warning(str(e)) + idle_monitor.stop_monitor() + idle_monitor.stop() + idle_monitor = None + idle_monitor_unsupported = True + + +def on_stop() -> None: + """Stop the platform idle monitor.""" + global idle_monitor global smart_pause_activated + if smart_pause_activated: # Safe Eyes is stopped due to system idle smart_pause_activated = False return logging.debug("Stop Smart Pause plugin") - if use_swayidle: - __stop_swayidle_monitor() - __set_active(False) - idle_condition.acquire() - idle_condition.notify_all() - idle_condition.release() - if use_ext_idle_notify: - __stop_ext_idle_monitor() + if idle_monitor is not None: + if idle_monitor.is_monitor_running(): + idle_monitor.stop_monitor() -def update_next_break(break_obj, dateTime): +def update_next_break(break_obj, dateTime) -> None: """Update the next break time.""" global next_break_time next_break_time = dateTime -def on_start_break(break_obj): +def on_pre_break(break_obj) -> None: + """Executes at the start of the prepare time for a break.""" + global postpone_if_active + global idle_monitor_is_pre_break + global idle_monitor + + if idle_monitor is not None: + if postpone_if_active: + logging.debug("Enabling pre-break idle monitor") + idle_monitor.configuration_changed( + _on_idle_pre_break, + _on_resumed_pre_break, + pre_break_postpone_idle_time, + ) + idle_monitor_is_pre_break = True + else: + # Stop during the pre break + logging.debug("Stop idle monitor during break") + idle_monitor.stop_monitor() + + +def on_start_break(break_obj) -> None: """Lifecycle method executes just before the break.""" + global postpone_if_active + global idle_monitor_is_pre_break + global pre_break_idle_start_time + if postpone_if_active: - # Postpone this break if the user is active - system_idle_time = __system_idle_time() - if system_idle_time < 2: - postpone(2) # Postpone for 2 seconds + if idle_monitor_is_pre_break: + # Postpone this break if the user is active + system_idle_time = 0.0 + if pre_break_idle_start_time is not None: + idle_period = datetime.datetime.now() - pre_break_idle_start_time + system_idle_time = idle_period.total_seconds() + + if system_idle_time < pre_break_postpone_idle_time: + logging.debug("User is not idle, postponing") + postpone(pre_break_postpone_idle_time) # type: ignore[misc] + return + + logging.debug(f"User was idle for {system_idle_time}, time for the break") + + if idle_monitor is not None: + # Stop during the break + # The normal monitor should no longer be running here - try stopping anyways + if idle_monitor.is_monitor_running(): + logging.debug("Start break, disable the pre-break idle monitor") + idle_monitor.stop_monitor() + # We stopped, the pre_break monitor is no longer running + idle_monitor_is_pre_break = False + pre_break_idle_start_time = None + + +def on_stop_break() -> None: + """Lifecycle method executes after the break.""" + global idle_monitor_is_pre_break + global postpone_if_active + + if idle_monitor is not None: + if not idle_monitor.is_monitor_running(): + logging.debug("Break is done, reenable idle monitor") + idle_monitor.start_monitor(_on_idle, _on_resumed, idle_time) -def disable(): +def disable() -> None: """SmartPause plugin was active earlier but now user has disabled it.""" + global idle_monitor + # Remove the idle_period - context.pop("idle_period", None) + context.pop("idle_period", None) # type: ignore[union-attr] + + if idle_monitor is not None: + idle_monitor.stop() + idle_monitor = None + + +def on_exit() -> None: + """SafeEyes is exiting.""" + global idle_monitor + + if idle_monitor is not None: + idle_monitor.stop() + idle_monitor = None diff --git a/safeeyes/plugins/smartpause/swayidle.py b/safeeyes/plugins/smartpause/swayidle.py new file mode 100644 index 00000000..49434c97 --- /dev/null +++ b/safeeyes/plugins/smartpause/swayidle.py @@ -0,0 +1,117 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import math +import subprocess +import threading +import typing + +from safeeyes import utility + +from .interface import IdleMonitorInterface + + +class IdleMonitorSwayidle(IdleMonitorInterface): + """IdleMonitorInterface implementation for swayidle.""" + + swayidle_process: typing.Optional[subprocess.Popen] = None + swayidle_lock = threading.Lock() + swayidle_idle = 0 + swayidle_active = 0 + + def init(self) -> None: + pass + + def start_monitor( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + """Start watching for idling. + + This is run on the main thread, and should not block. + """ + if not self.is_monitor_running(): + utility.start_thread( + self._start_swayidle_monitor, + on_idle=on_idle, + on_resumed=on_resumed, + idle_time=idle_time, + ) + + def is_monitor_running(self) -> bool: + return ( + self.swayidle_process is not None and self.swayidle_process.poll() is None + ) + + def _start_swayidle_monitor( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + was_idle = False + + logging.debug("Starting swayidle subprocess") + + timeout = str(math.ceil(idle_time)) + + self.swayidle_process = subprocess.Popen( + [ + "swayidle", + "timeout", + timeout, + "date +S%s", + "resume", + "date +R%s", + ], + stdout=subprocess.PIPE, + bufsize=1, + universal_newlines=True, + encoding="utf-8", + ) + for line in self.swayidle_process.stdout: # type: ignore[union-attr] + with self.swayidle_lock: + typ = line[0] + timestamp = int(line[1:]) + if typ == "S": + self.swayidle_idle = timestamp + if not was_idle: + was_idle = True + utility.execute_main_thread(on_idle) + elif typ == "R": + self.swayidle_active = timestamp + if was_idle: + was_idle = False + utility.execute_main_thread(on_resumed) + + def stop_monitor(self) -> None: + """Stop watching for idling. + + This is run on the main thread. It may block a short time for cleanup. + """ + if self.is_monitor_running() and self.swayidle_process is not None: + logging.debug("Stopping swayidle subprocess") + self.swayidle_process.terminate() + self.swayidle_process.wait() + self.swayidle_process = None + + def stop(self) -> None: + pass diff --git a/safeeyes/plugins/smartpause/x11.py b/safeeyes/plugins/smartpause/x11.py new file mode 100644 index 00000000..2504dd4b --- /dev/null +++ b/safeeyes/plugins/smartpause/x11.py @@ -0,0 +1,114 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2017 Gobinath + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +import threading +import typing + +from safeeyes import utility + +from .interface import IdleMonitorInterface + + +class IdleMonitorX11(IdleMonitorInterface): + """IdleMonitorInterface implementation for X11. + + Note that this is quite inefficient. It polls every 2 seconds whether the user is + idle or not, keeping the CPU active a lot. + """ + + active: bool = False + lock = threading.Lock() + idle_condition = threading.Condition() + + def _is_active(self) -> bool: + """Thread safe function to see if this plugin is active or not.""" + is_active = False + with self.lock: + is_active = self.active + return is_active + + def _set_active(self, is_active: bool) -> None: + """Thread safe function to change the state of the plugin.""" + with self.lock: + self.active = is_active + + def init(self) -> None: + pass + + def start_monitor( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + """Start a thread to continuously call xprintidle.""" + if not self._is_active(): + # If SmartPause is already started, do not start it again + self._set_active(True) + utility.start_thread(self._start_idle_monitor) + utility.start_thread( + self._start_idle_monitor, + on_idle=on_idle, + on_resumed=on_resumed, + idle_time=idle_time, + ) + + def is_monitor_running(self) -> bool: + return self._is_active() + + def _start_idle_monitor( + self, + on_idle: typing.Callable[[], None], + on_resumed: typing.Callable[[], None], + idle_time: float, + ) -> None: + """Continuously check the system idle time and pause/resume Safe Eyes based + on it. + """ + waiting_time = min(idle_time, 2) + was_idle = False + + while self._is_active(): + # Wait for waiting_time seconds + self.idle_condition.acquire() + self.idle_condition.wait(waiting_time) + self.idle_condition.release() + + if self._is_active(): + # Get the system idle time + system_idle_time = ( + # Convert to seconds + int(subprocess.check_output(["xprintidle"]).decode("utf-8")) / 1000 + ) + if system_idle_time >= idle_time and not was_idle: + was_idle = True + utility.execute_main_thread(on_idle) + elif system_idle_time < idle_time and was_idle: + was_idle = False + utility.execute_main_thread(on_resumed) + + def stop_monitor(self) -> None: + """Stop the thread from continuously calling xprintidle.""" + self._set_active(False) + self.idle_condition.acquire() + self.idle_condition.notify_all() + self.idle_condition.release() + + def stop(self) -> None: + pass From 213fa4a9935243c8cd8f2f5a3c6c6a8963bf4114 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 22 Aug 2024 21:39:21 +0200 Subject: [PATCH 030/134] pytest: test model.Break and model.BreakQueue --- MANIFEST.in | 2 + pyproject.toml | 15 +- safeeyes/tests/__init__.py | 0 safeeyes/tests/test_model.py | 320 +++++++++++++++++++++++++++++++++++ 4 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 safeeyes/tests/__init__.py create mode 100644 safeeyes/tests/test_model.py diff --git a/MANIFEST.in b/MANIFEST.in index b0a6c493..b0ff7140 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,6 @@ include README.md graft safeeyes +prune safeeyes/tests + global-exclude *.py[cod] diff --git a/pyproject.toml b/pyproject.toml index 7765900b..f829466d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,12 +46,14 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include=["safeeyes*"] +exclude=["safeeyes.tests*"] [dependency-groups] dev = [ {include-group = "lint"}, {include-group = "scripts"}, - {include-group = "types"} + {include-group = "tests"}, + {include-group = "types"}, ] lint = [ "ruff==0.11.2" @@ -64,7 +66,11 @@ types = [ "PyGObject-stubs==2.13.0", "types-croniter==5.0.1.20250322", "types-psutil==7.0.0.20250218", - "types-python-xlib==0.33.0.20240407" + "types-python-xlib==0.33.0.20240407", + {include-group = "tests"}, +] +tests = [ + "pytest==8.3.5", ] [tool.mypy] @@ -78,3 +84,8 @@ enable_error_code = [ "ignore-without-code", "possibly-undefined" ] + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] diff --git a/safeeyes/tests/__init__.py b/safeeyes/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py new file mode 100644 index 00000000..d769a017 --- /dev/null +++ b/safeeyes/tests/test_model.py @@ -0,0 +1,320 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import random +from safeeyes import model + + +class TestBreak: + def test_break_short(self): + b = model.Break(model.BreakType.SHORT_BREAK, "test break", 15, 15, None, None) + + assert b.is_short_break() + assert not b.is_long_break() + + def test_break_long(self): + b = model.Break(model.BreakType.LONG_BREAK, "long break", 75, 60, None, None) + + assert not b.is_short_break() + assert b.is_long_break() + + +class TestBreakQueue: + def test_create_empty(self): + config = { + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + } + + context = {} + + bq = model.BreakQueue(config, context) + + assert bq.is_empty() + assert bq.is_empty(model.BreakType.LONG_BREAK) + assert bq.is_empty(model.BreakType.SHORT_BREAK) + assert bq.next() is None + assert bq.get_break() is None + + def get_bq_only_short(self, monkeypatch, random_seed=None): + if random_seed is not None: + random.seed(random_seed) + + monkeypatch.setattr( + model, "_", lambda message: "translated!: " + message, raising=False + ) + + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + ], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": random_seed is not None, + } + + context = { + "session": {}, + } + + return model.BreakQueue(config, context) + + def get_bq_only_long(self, monkeypatch, random_seed=None): + if random_seed is not None: + random.seed(random_seed) + + monkeypatch.setattr( + model, "_", lambda message: "translated!: " + message, raising=False + ) + + config = { + "short_breaks": [], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": random_seed is not None, + } + + context = { + "session": {}, + } + + return model.BreakQueue(config, context) + + def get_bq_full(self, monkeypatch, random_seed=None): + if random_seed is not None: + random.seed(random_seed) + + monkeypatch.setattr( + model, "_", lambda message: "translated!: " + message, raising=False + ) + + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": random_seed is not None, + } + + context = { + "session": {}, + } + + return model.BreakQueue(config, context) + + def test_create_only_short(self, monkeypatch): + bq = self.get_bq_only_short(monkeypatch) + + assert not bq.is_empty() + assert not bq.is_empty(model.BreakType.SHORT_BREAK) + assert bq.is_empty(model.BreakType.LONG_BREAK) + + def test_only_short_repeat_get_break_no_change(self, monkeypatch): + bq = self.get_bq_only_short(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: break 1" + + next = bq.get_break() + assert next.name == "translated!: break 1" + + assert not bq.is_long_break() + + def test_only_short_next_break(self, monkeypatch): + bq = self.get_bq_only_short(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: break 1" + + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + + def test_only_short_next_break_random(self, monkeypatch): + random_seed = 5 + bq = self.get_bq_only_short(monkeypatch, random_seed) + + next = bq.get_break() + assert next.name == "translated!: break 3" + + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 1" + + def test_create_only_long(self, monkeypatch): + bq = self.get_bq_only_long(monkeypatch) + + assert not bq.is_empty() + assert not bq.is_empty(model.BreakType.LONG_BREAK) + assert bq.is_empty(model.BreakType.SHORT_BREAK) + + def test_only_long_repeat_get_break_no_change(self, monkeypatch): + bq = self.get_bq_only_long(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: long break 1" + + next = bq.get_break() + assert next.name == "translated!: long break 1" + + assert bq.is_long_break() + + def test_only_long_next_break(self, monkeypatch): + bq = self.get_bq_only_long(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: long break 1" + + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: long break 3" + assert bq.next().name == "translated!: long break 1" + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: long break 3" + assert bq.next().name == "translated!: long break 1" + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: long break 3" + + def test_only_long_next_break_random(self, monkeypatch): + random_seed = 5 + bq = self.get_bq_only_long(monkeypatch, random_seed) + + next = bq.get_break() + assert next.name == "translated!: long break 3" + + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: long break 1" + assert bq.next().name == "translated!: long break 3" + assert bq.next().name == "translated!: long break 1" + + def test_create_full(self, monkeypatch): + bq = self.get_bq_full(monkeypatch) + + assert not bq.is_empty() + assert not bq.is_empty(model.BreakType.LONG_BREAK) + assert not bq.is_empty(model.BreakType.SHORT_BREAK) + + def test_full_repeat_get_break_no_change(self, monkeypatch): + bq = self.get_bq_full(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: break 1" + + next = bq.get_break() + assert next.name == "translated!: break 1" + + assert not bq.is_long_break() + + def test_full_next_break(self, monkeypatch): + bq = self.get_bq_full(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: break 1" + assert not bq.is_long_break() + + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 1" + assert bq.is_long_break() + assert bq.next().name == "translated!: break 1" + assert not bq.is_long_break() + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 3" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 1" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 3" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 1" + + def test_full_next_break_random(self, monkeypatch): + random_seed = 5 + bq = self.get_bq_full(monkeypatch, random_seed) + + next = bq.get_break() + assert next.name == "translated!: break 1" + + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: long break 3" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: long break 1" From a610d247f074b22fd2b69455b7377a40225eede6 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 30 Jun 2025 14:59:02 +0200 Subject: [PATCH 031/134] add github workflow for pytest --- .github/workflows/pytest.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..537b9599 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,36 @@ +# Workflow to run pytest +name: pytest + +on: [push, pull_request] + +permissions: + contents: read + +env: + UV_SYSTEM_PYTHON: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + +jobs: + pytest: + name: pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5.4.0 + with: + enable-cache: true + cache-dependency-glob: '' + cache-suffix: '3.13' + - name: Setup Python + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + with: + python-version: '3.13' + - name: Install OS dependencies + run: | + sudo apt-get update -yy + sudo apt-get install -yy --no-install-recommends libcairo2-dev libgirepository-2.0-dev gir1.2-gtk-4.0 + - run: uv pip install -r pyproject.toml + - run: uv pip install --group tests + - run: pytest From 5cc20c24a20cdd67a1051ad43302f1faf97dd5da Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 30 Jun 2025 15:13:16 +0200 Subject: [PATCH 032/134] test_model: add types --- safeeyes/tests/test_model.py | 60 +++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index d769a017..6bb91925 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -16,18 +16,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import pytest import random +import typing from safeeyes import model class TestBreak: - def test_break_short(self): + def test_break_short(self) -> None: b = model.Break(model.BreakType.SHORT_BREAK, "test break", 15, 15, None, None) assert b.is_short_break() assert not b.is_long_break() - def test_break_long(self): + def test_break_long(self) -> None: b = model.Break(model.BreakType.LONG_BREAK, "long break", 75, 60, None, None) assert not b.is_short_break() @@ -35,7 +37,7 @@ def test_break_long(self): class TestBreakQueue: - def test_create_empty(self): + def test_create_empty(self) -> None: config = { "short_breaks": [], "long_breaks": [], @@ -46,7 +48,7 @@ def test_create_empty(self): "random_order": False, } - context = {} + context: dict[str, typing.Any] = {} bq = model.BreakQueue(config, context) @@ -56,7 +58,9 @@ def test_create_empty(self): assert bq.next() is None assert bq.get_break() is None - def get_bq_only_short(self, monkeypatch, random_seed=None): + def get_bq_only_short( + self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None + ) -> model.BreakQueue: if random_seed is not None: random.seed(random_seed) @@ -78,13 +82,15 @@ def get_bq_only_short(self, monkeypatch, random_seed=None): "random_order": random_seed is not None, } - context = { + context: dict[str, typing.Any] = { "session": {}, } return model.BreakQueue(config, context) - def get_bq_only_long(self, monkeypatch, random_seed=None): + def get_bq_only_long( + self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None + ) -> model.BreakQueue: if random_seed is not None: random.seed(random_seed) @@ -106,13 +112,15 @@ def get_bq_only_long(self, monkeypatch, random_seed=None): "random_order": random_seed is not None, } - context = { + context: dict[str, typing.Any] = { "session": {}, } return model.BreakQueue(config, context) - def get_bq_full(self, monkeypatch, random_seed=None): + def get_bq_full( + self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None + ) -> model.BreakQueue: if random_seed is not None: random.seed(random_seed) @@ -139,20 +147,22 @@ def get_bq_full(self, monkeypatch, random_seed=None): "random_order": random_seed is not None, } - context = { + context: dict[str, typing.Any] = { "session": {}, } return model.BreakQueue(config, context) - def test_create_only_short(self, monkeypatch): + def test_create_only_short(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_short(monkeypatch) assert not bq.is_empty() assert not bq.is_empty(model.BreakType.SHORT_BREAK) assert bq.is_empty(model.BreakType.LONG_BREAK) - def test_only_short_repeat_get_break_no_change(self, monkeypatch): + def test_only_short_repeat_get_break_no_change( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: bq = self.get_bq_only_short(monkeypatch) next = bq.get_break() @@ -163,7 +173,7 @@ def test_only_short_repeat_get_break_no_change(self, monkeypatch): assert not bq.is_long_break() - def test_only_short_next_break(self, monkeypatch): + def test_only_short_next_break(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_short(monkeypatch) next = bq.get_break() @@ -178,7 +188,9 @@ def test_only_short_next_break(self, monkeypatch): assert bq.next().name == "translated!: break 2" assert bq.next().name == "translated!: break 3" - def test_only_short_next_break_random(self, monkeypatch): + def test_only_short_next_break_random( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: random_seed = 5 bq = self.get_bq_only_short(monkeypatch, random_seed) @@ -190,14 +202,16 @@ def test_only_short_next_break_random(self, monkeypatch): assert bq.next().name == "translated!: break 3" assert bq.next().name == "translated!: break 1" - def test_create_only_long(self, monkeypatch): + def test_create_only_long(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_long(monkeypatch) assert not bq.is_empty() assert not bq.is_empty(model.BreakType.LONG_BREAK) assert bq.is_empty(model.BreakType.SHORT_BREAK) - def test_only_long_repeat_get_break_no_change(self, monkeypatch): + def test_only_long_repeat_get_break_no_change( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: bq = self.get_bq_only_long(monkeypatch) next = bq.get_break() @@ -208,7 +222,7 @@ def test_only_long_repeat_get_break_no_change(self, monkeypatch): assert bq.is_long_break() - def test_only_long_next_break(self, monkeypatch): + def test_only_long_next_break(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_long(monkeypatch) next = bq.get_break() @@ -223,7 +237,7 @@ def test_only_long_next_break(self, monkeypatch): assert bq.next().name == "translated!: long break 2" assert bq.next().name == "translated!: long break 3" - def test_only_long_next_break_random(self, monkeypatch): + def test_only_long_next_break_random(self, monkeypatch: pytest.MonkeyPatch) -> None: random_seed = 5 bq = self.get_bq_only_long(monkeypatch, random_seed) @@ -235,14 +249,16 @@ def test_only_long_next_break_random(self, monkeypatch): assert bq.next().name == "translated!: long break 3" assert bq.next().name == "translated!: long break 1" - def test_create_full(self, monkeypatch): + def test_create_full(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_full(monkeypatch) assert not bq.is_empty() assert not bq.is_empty(model.BreakType.LONG_BREAK) assert not bq.is_empty(model.BreakType.SHORT_BREAK) - def test_full_repeat_get_break_no_change(self, monkeypatch): + def test_full_repeat_get_break_no_change( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: bq = self.get_bq_full(monkeypatch) next = bq.get_break() @@ -253,7 +269,7 @@ def test_full_repeat_get_break_no_change(self, monkeypatch): assert not bq.is_long_break() - def test_full_next_break(self, monkeypatch): + def test_full_next_break(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_full(monkeypatch) next = bq.get_break() @@ -297,7 +313,7 @@ def test_full_next_break(self, monkeypatch): assert bq.next().name == "translated!: break 4" assert bq.next().name == "translated!: long break 1" - def test_full_next_break_random(self, monkeypatch): + def test_full_next_break_random(self, monkeypatch: pytest.MonkeyPatch) -> None: random_seed = 5 bq = self.get_bq_full(monkeypatch, random_seed) From 158a59a0e718351601808952aed1717fa2641564 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 30 Jun 2025 15:24:19 +0200 Subject: [PATCH 033/134] add instructions for testing to readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b66fd36d..fdf49e6f 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,12 @@ To ensure that the coding and formatting guidelines are followed, install [ruff] To ensure that any types are correct, install [mypy](https://github.com/python/mypy) and run `mypy safeeyes`. -The last three checks are also run in CI, so a PR must pass all the tests for it to be mmerged. +To ensure that the tests still pass, install [pytest](https://docs.pytest.org/en/stable/) and run `pytest`. + +The last four checks are also run in CI, so a PR must pass all the tests for it to be mmerged. + +It is also possible to use dependency groups to install the needed dependencies. When using a new enough version of pip, run `pip install --group types` to install all dependencies to run the type check. +The available dependency groups can be found in the `pyproject.toml` file. ## How to Release? From 180fc433e2633630bd3556e36011515ef28c0a7a Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 30 Jun 2025 15:44:24 +0200 Subject: [PATCH 034/134] apply review suggestions --- safeeyes/tests/test_model.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index 6bb91925..7ff82818 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -24,13 +24,27 @@ class TestBreak: def test_break_short(self) -> None: - b = model.Break(model.BreakType.SHORT_BREAK, "test break", 15, 15, None, None) + b = model.Break( + break_type=model.BreakType.SHORT_BREAK, + name="test break", + time=15, + duration=15, + image=None, + plugins=None, + ) assert b.is_short_break() assert not b.is_long_break() def test_break_long(self) -> None: - b = model.Break(model.BreakType.LONG_BREAK, "long break", 75, 60, None, None) + b = model.Break( + break_type=model.BreakType.LONG_BREAK, + name="long break", + time=75, + duration=60, + image=None, + plugins=None, + ) assert not b.is_short_break() assert b.is_long_break() From 0b7e9887429c5fced789487f6dad61b9b9534397 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 23 Jul 2025 21:14:09 +0200 Subject: [PATCH 035/134] Config: split loading out of constructor --- safeeyes/__main__.py | 2 +- safeeyes/model.py | 99 ++++++++++++++++++----------- safeeyes/tests/test_model.py | 118 +++++++++++++++++++---------------- 3 files changed, 127 insertions(+), 92 deletions(-) diff --git a/safeeyes/__main__.py b/safeeyes/__main__.py index e8de3724..fe5c9e28 100755 --- a/safeeyes/__main__.py +++ b/safeeyes/__main__.py @@ -112,7 +112,7 @@ def main(): # Initialize the logging utility.initialize_logging(args.debug) utility.initialize_platform() - config = Config() + config = Config.load() utility.cleanup_old_user_stylesheet() if __running(): diff --git a/safeeyes/model.py b/safeeyes/model.py index b19080f1..fd5ccfff 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -20,11 +20,13 @@ plugins. """ +import copy import logging import random from enum import Enum from dataclasses import dataclass from typing import Optional, Union +import typing from packaging.version import parse @@ -297,45 +299,63 @@ def fire(self, *args, **keywargs): class Config: """The configuration of Safe Eyes.""" - def __init__(self, init=True): + __user_config: dict[str, typing.Any] + __system_config: dict[str, typing.Any] + + @classmethod + def load(cls) -> "Config": # Read the config files - self.__user_config = utility.load_json(utility.CONFIG_FILE_PATH) - self.__system_config = utility.load_json(utility.SYSTEM_CONFIG_FILE_PATH) + user_config = utility.load_json(utility.CONFIG_FILE_PATH) + system_config = utility.load_json(utility.SYSTEM_CONFIG_FILE_PATH) # If there any breaking changes in long_breaks, short_breaks or any other keys, - # use the __force_upgrade list - self.__force_upgrade = [] - # self.__force_upgrade = ['long_breaks', 'short_breaks'] - - if init: - # if create_startup_entry finds a broken autostart symlink, it will repair - # it - utility.create_startup_entry(force=False) - if self.__user_config is None: - utility.initialize_safeeyes() - self.__user_config = self.__system_config - self.save() + # use the force_upgrade_keys list + force_upgrade_keys: list[str] = [] + # force_upgrade_keys = ['long_breaks', 'short_breaks'] + + # if create_startup_entry finds a broken autostart symlink, it will repair + # it + utility.create_startup_entry(force=False) + if user_config is None: + utility.initialize_safeeyes() + user_config = copy.deepcopy(system_config) + cfg = cls(user_config, system_config) + cfg.save() + return cfg + else: + system_config_version = system_config["meta"]["config_version"] + meta_obj = user_config.get("meta", None) + if meta_obj is None: + # Corrupted user config + user_config = copy.deepcopy(system_config) else: - system_config_version = self.__system_config["meta"]["config_version"] - meta_obj = self.__user_config.get("meta", None) - if meta_obj is None: - # Corrupted user config - self.__user_config = self.__system_config - else: - user_config_version = str(meta_obj.get("config_version", "0.0.0")) - if parse(user_config_version) != parse(system_config_version): - # Update the user config - self.__merge_dictionary( - self.__user_config, self.__system_config - ) - self.__user_config = self.__system_config - - utility.merge_plugins(self.__user_config) - self.save() - - def __merge_dictionary(self, old_dict, new_dict): + user_config_version = str(meta_obj.get("config_version", "0.0.0")) + if parse(user_config_version) != parse(system_config_version): + # Update the user config + new_user_config = copy.deepcopy(system_config) + cls.__merge_dictionary( + user_config, new_user_config, force_upgrade_keys + ) + user_config = new_user_config + + utility.merge_plugins(user_config) + + cfg = cls(user_config, system_config) + cfg.save() + return cfg + + def __init__( + self, + user_config: dict[str, typing.Any], + system_config: dict[str, typing.Any], + ): + self.__user_config = user_config + self.__system_config = system_config + + @classmethod + def __merge_dictionary(cls, old_dict, new_dict, force_upgrade_keys: list[str]): """Merge the dictionaries.""" for key in new_dict: - if key == "meta" or key in self.__force_upgrade: + if key == "meta" or key in force_upgrade_keys: continue if key in old_dict: new_value = new_dict[key] @@ -343,15 +363,18 @@ def __merge_dictionary(self, old_dict, new_dict): if type(new_value) is type(old_value): # Both properties have same type if isinstance(new_value, dict): - self.__merge_dictionary(old_value, new_value) + cls.__merge_dictionary(old_value, new_value, force_upgrade_keys) else: new_dict[key] = old_value - def clone(self): - config = Config(init=False) + def clone(self) -> "Config": + config = Config( + user_config=copy.deepcopy(self.__user_config), + system_config=self.__system_config, + ) return config - def save(self): + def save(self) -> None: """Save the configuration to file.""" utility.write_json(utility.CONFIG_FILE_PATH, self.__user_config) diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index 7ff82818..089321ef 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -52,15 +52,18 @@ def test_break_long(self) -> None: class TestBreakQueue: def test_create_empty(self) -> None: - config = { - "short_breaks": [], - "long_breaks": [], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - } + config = model.Config( + user_config={ + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + }, + system_config={}, + ) context: dict[str, typing.Any] = {} @@ -82,19 +85,22 @@ def get_bq_only_short( model, "_", lambda message: "translated!: " + message, raising=False ) - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - ], - "long_breaks": [], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": random_seed is not None, - } + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + ], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": random_seed is not None, + }, + system_config={}, + ) context: dict[str, typing.Any] = { "session": {}, @@ -112,19 +118,22 @@ def get_bq_only_long( model, "_", lambda message: "translated!: " + message, raising=False ) - config = { - "short_breaks": [], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": random_seed is not None, - } + config = model.Config( + user_config={ + "short_breaks": [], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": random_seed is not None, + }, + system_config={}, + ) context: dict[str, typing.Any] = { "session": {}, @@ -142,24 +151,27 @@ def get_bq_full( model, "_", lambda message: "translated!: " + message, raising=False ) - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - {"name": "break 4"}, - ], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": random_seed is not None, - } + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": random_seed is not None, + }, + system_config={}, + ) context: dict[str, typing.Any] = { "session": {}, From c6732e10fd0e8f06270e80dd5712bd3dc205764e Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 8 Jun 2025 13:57:29 +0200 Subject: [PATCH 036/134] remove dead code --- safeeyes/model.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/safeeyes/model.py b/safeeyes/model.py index fd5ccfff..772cd7f0 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -96,15 +96,6 @@ def __init__(self, config, context): self.__build_longs() self.__build_shorts() - # Interface guarantees that short_interval >= 1 - # And that long_interval is a multiple of short_interval - short_interval = config.get("short_break_interval") - long_interval = config.get("long_break_interval") - self.__cycle_len = int(long_interval / short_interval) - # To count every long break as a cycle in .next() if there are no short breaks - if self.__short_queue is None: - self.__cycle_len = 1 - # Restore the last break from session if not self.is_empty(): last_break = context["session"].get("break") From 7da4ecb75598b70d85e234a85785d1b814985766 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 4 Jun 2025 16:25:55 +0200 Subject: [PATCH 037/134] typing: add types to core and breakqueue --- safeeyes/core.py | 82 +++++++++++++----------- safeeyes/model.py | 120 ++++++++++++++++++++++++----------- safeeyes/tests/test_model.py | 11 ++-- 3 files changed, 136 insertions(+), 77 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 44a68efb..01fc5218 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -22,27 +22,30 @@ import logging import threading import time +import typing from safeeyes import utility from safeeyes.model import BreakType from safeeyes.model import BreakQueue from safeeyes.model import EventHook from safeeyes.model import State +from safeeyes.model import Config class SafeEyesCore: """Core of Safe Eyes runs the scheduler and notifies the breaks.""" - def __init__(self, context): + break_queue: BreakQueue + scheduled_next_break_time: typing.Optional[datetime.datetime] = None + scheduled_next_break_timestamp: int = -1 + running: bool = False + paused_time: float = -1 + postpone_duration: int = 0 + default_postpone_duration: int = 0 + pre_break_warning_time: int = 0 + + def __init__(self, context) -> None: """Create an instance of SafeEyesCore and initialize the variables.""" - self.break_queue = None - self.postpone_duration = 0 - self.default_postpone_duration = 0 - self.pre_break_warning_time = 0 - self.running = False - self.scheduled_next_break_timestamp = -1 - self.scheduled_next_break_time = None - self.paused_time = -1 # This event is fired before for a break self.on_pre_break = EventHook() # This event is fired just before the start of a break @@ -64,7 +67,7 @@ def __init__(self, context): self.context["postpone_button_disabled"] = False self.context["state"] = State.WAITING - def initialize(self, config): + def initialize(self, config: Config): """Initialize the internal properties from configuration.""" logging.info("Initialize the core") self.pre_break_warning_time = config.get("pre_break_warning_time") @@ -74,9 +77,10 @@ def initialize(self, config): ) # Convert to seconds self.postpone_duration = self.default_postpone_duration - def start(self, next_break_time=-1, reset_breaks=False): + def start(self, next_break_time=-1, reset_breaks=False) -> None: """Start Safe Eyes is it is not running already.""" if self.break_queue.is_empty(): + logging.info("No breaks defined, not starting the core") return with self.lock: if not self.running: @@ -89,7 +93,7 @@ def start(self, next_break_time=-1, reset_breaks=False): self.scheduled_next_break_timestamp = int(next_break_time) utility.start_thread(self.__scheduler_job) - def stop(self, is_resting=False): + def stop(self, is_resting=False) -> None: """Stop Safe Eyes if it is running.""" with self.lock: if not self.running: @@ -105,11 +109,11 @@ def stop(self, is_resting=False): self.waiting_condition.notify_all() self.waiting_condition.release() - def skip(self): + def skip(self) -> None: """User skipped the break using Skip button.""" self.context["skipped"] = True - def postpone(self, duration=-1): + def postpone(self, duration=-1) -> None: """User postponed the break using Postpone button.""" if duration > 0: self.postpone_duration = duration @@ -118,17 +122,19 @@ def postpone(self, duration=-1): logging.debug("Postpone the break for %d seconds", self.postpone_duration) self.context["postponed"] = True - def get_break_time(self, break_type=None): + def get_break_time( + self, break_type: typing.Optional[BreakType] = None + ) -> typing.Optional[datetime.datetime]: """Returns the next break time.""" - break_obj = self.break_queue.get_break(break_type) - if not break_obj: - return False + break_obj = self.break_queue.get_break_with_type(break_type) + if not break_obj or self.scheduled_next_break_time is None: + return None time = self.scheduled_next_break_time + datetime.timedelta( minutes=break_obj.time - self.break_queue.get_break().time ) return time - def take_break(self, break_type=None): + def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Calling this method stops the scheduler and show the next break screen. """ @@ -138,14 +144,14 @@ def take_break(self, break_type=None): return utility.start_thread(self.__take_break, break_type=break_type) - def has_breaks(self, break_type=None): + def has_breaks(self, break_type: typing.Optional[BreakType] = None) -> bool: """Check whether Safe Eyes has breaks or not. Use the break_type to check for either short or long break. """ return not self.break_queue.is_empty(break_type) - def __take_break(self, break_type=None): + def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Show the next break screen.""" logging.info("Take a break due to external request") @@ -167,7 +173,7 @@ def __take_break(self, break_type=None): self.break_queue.next(break_type) utility.execute_main_thread(self.__fire_start_break) - def __scheduler_job(self): + def __scheduler_job(self) -> None: """Scheduler task to execute during every interval.""" if not self.running: return @@ -179,10 +185,8 @@ def __scheduler_job(self): # Safe Eyes was resting paused_duration = int(current_timestamp - self.paused_time) self.paused_time = -1 - if ( - paused_duration - > self.break_queue.get_break(BreakType.LONG_BREAK).duration - ): + next_long = self.break_queue.get_break_with_type(BreakType.LONG_BREAK) + if next_long is not None and paused_duration > next_long.duration: logging.info( "Skip next long break due to the pause %ds longer than break" " duration", @@ -221,14 +225,16 @@ def __scheduler_job(self): logging.info("Pre-break waiting is over") if not self.running: - return + # This can be reached if another thread changed running while __wait_for was + # blocking + return # type: ignore[unreachable] utility.execute_main_thread(self.__fire_pre_break) - def __fire_on_update_next_break(self, next_break_time): + def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> None: """Pass the next break information to the registered listeners.""" self.on_update_next_break.fire(self.break_queue.get_break(), next_break_time) - def __fire_pre_break(self): + def __fire_pre_break(self) -> None: """Show the notification and start the break after the notification.""" self.context["state"] = State.PRE_BREAK if not self.on_pre_break.fire(self.break_queue.get_break()): @@ -237,7 +243,7 @@ def __fire_pre_break(self): return utility.start_thread(self.__wait_until_prepare) - def __wait_until_prepare(self): + def __wait_until_prepare(self) -> None: logging.info( "Wait for %d seconds before the break", self.pre_break_warning_time ) @@ -247,11 +253,11 @@ def __wait_until_prepare(self): return utility.execute_main_thread(self.__fire_start_break) - def __postpone_break(self): + def __postpone_break(self) -> None: self.__wait_for(self.postpone_duration) utility.execute_main_thread(self.__fire_start_break) - def __fire_start_break(self): + def __fire_start_break(self) -> None: break_obj = self.break_queue.get_break() # Show the break screen if not self.on_start_break.fire(break_obj): @@ -261,6 +267,10 @@ def __fire_start_break(self): if self.context["postponed"]: # Plugins want to postpone this break self.context["postponed"] = False + + if self.scheduled_next_break_time is None: + raise Exception("this should never happen") + # Update the next break time self.scheduled_next_break_time = ( self.scheduled_next_break_time @@ -273,7 +283,7 @@ def __fire_start_break(self): self.start_break.fire(break_obj) utility.start_thread(self.__start_break) - def __start_break(self): + def __start_break(self) -> None: """Start the break screen.""" self.context["state"] = State.BREAK break_obj = self.break_queue.get_break() @@ -292,7 +302,7 @@ def __start_break(self): countdown -= 1 utility.execute_main_thread(self.__fire_stop_break) - def __fire_stop_break(self): + def __fire_stop_break(self) -> None: # Loop terminated because of timeout (not skipped) -> Close the break alert if not self.context["skipped"] and not self.context["postponed"]: logging.info("Break is terminated automatically") @@ -304,13 +314,13 @@ def __fire_stop_break(self): self.context["postpone_button_disabled"] = False self.__start_next_break() - def __wait_for(self, duration): + def __wait_for(self, duration: int) -> None: """Wait until someone wake up or the timeout happens.""" self.waiting_condition.acquire() self.waiting_condition.wait(duration) self.waiting_condition.release() - def __start_next_break(self): + def __start_next_break(self) -> None: if not self.context["postponed"]: self.break_queue.next() diff --git a/safeeyes/model.py b/safeeyes/model.py index 772cd7f0..94281b35 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -39,35 +39,56 @@ from safeeyes.translations import translate as _ +class BreakType(Enum): + """Type of Safe Eyes breaks.""" + + SHORT_BREAK = 1 + LONG_BREAK = 2 + + class Break: """An entity class which represents a break.""" - def __init__(self, break_type, name, time, duration, image, plugins): + type: BreakType + name: str + time: int + duration: int + image: typing.Optional[str] # path + plugins: dict + + def __init__( + self, + break_type: BreakType, + name: str, + time: int, + duration: int, + image: typing.Optional[str], + plugins: dict, + ): self.type = break_type self.name = name self.duration = duration self.image = image self.plugins = plugins self.time = time - self.next = None - def __str__(self): + def __str__(self) -> str: return 'Break: {{name: "{}", type: {}, duration: {}}}\n'.format( self.name, self.type, self.duration ) - def __repr__(self): + def __repr__(self) -> str: return str(self) - def is_long_break(self): + def is_long_break(self) -> bool: """Check whether this break is a long break.""" return self.type == BreakType.LONG_BREAK - def is_short_break(self): + def is_short_break(self) -> bool: """Check whether this break is a short break.""" return self.type == BreakType.SHORT_BREAK - def plugin_enabled(self, plugin_id, is_plugin_enabled): + def plugin_enabled(self, plugin_id: str, is_plugin_enabled: bool) -> bool: """Check whether this break supports the given plugin.""" if self.plugins: return plugin_id in self.plugins @@ -75,19 +96,18 @@ def plugin_enabled(self, plugin_id, is_plugin_enabled): return is_plugin_enabled -class BreakType(Enum): - """Type of Safe Eyes breaks.""" - - SHORT_BREAK = 1 - LONG_BREAK = 2 - - class BreakQueue: - def __init__(self, config, context): + __current_break: typing.Optional[Break] = None + __current_long: int = 0 + __current_short: int = 0 + __short_break_time: int + __long_break_time: int + __is_random_order: bool + __long_queue: typing.Optional[list[Break]] + __short_queue: typing.Optional[list[Break]] + + def __init__(self, config: "Config", context) -> None: self.context = context - self.__current_break = None - self.__current_long = 0 - self.__current_short = 0 self.__short_break_time = config.get("short_break_interval") self.__long_break_time = config.get("long_break_interval") self.__is_random_order = config.get("random_order") @@ -106,7 +126,15 @@ def __init__(self, config, context): while brk != current_break and brk.name != last_break: brk = self.next() - def get_break(self, break_type=None): + def get_break(self) -> Break: + if self.__current_break is None: + self.__current_break = self.next() + + return self.__current_break + + def get_break_with_type( + self, break_type: typing.Optional[BreakType] = None + ) -> typing.Optional[Break]: if self.__current_break is None: self.__current_break = self.next() @@ -122,32 +150,38 @@ def get_break(self, break_type=None): return None return self.__short_queue[self.__current_short] - def is_long_break(self): + def is_long_break(self) -> bool: return ( self.__current_break is not None and self.__current_break.type == BreakType.LONG_BREAK ) - def next(self, break_type=None): + def next(self, break_type: typing.Optional[BreakType] = None) -> Break: break_obj = None shorts = self.__short_queue longs = self.__long_queue # Reset break that has just ended if self.is_long_break(): - self.__current_break.time = self.__long_break_time + # __current_break is checked by is_long_break + self.__current_break.time = self.__long_break_time # type: ignore[union-attr] if self.__current_long == 0 and self.__is_random_order: # Shuffle queue self.__build_longs() elif self.__current_break: # Reduce the break time from the next long break (default) if longs: + if shorts is None: + raise Exception( + "this may not happen, either short or long breaks must be" + " defined" + ) longs[self.__current_long].time -= shorts[self.__current_short].time if self.__current_short == 0 and self.__is_random_order: self.__build_shorts() if self.is_empty(): - return None + raise Exception("this should never be called when the queue is empty") if shorts is None: break_obj = self.__next_long() @@ -166,14 +200,16 @@ def next(self, break_type=None): return break_obj - def reset(self): - for break_object in self.__short_queue: - break_object.time = self.__short_break_time + def reset(self) -> None: + if self.__short_queue: + for break_object in self.__short_queue: + break_object.time = self.__short_break_time - for break_object in self.__long_queue: - break_object.time = self.__long_break_time + if self.__long_queue: + for break_object in self.__long_queue: + break_object.time = self.__long_break_time - def is_empty(self, break_type=None): + def is_empty(self, break_type=None) -> bool: """Check if the given break type is empty or not. If the break_type is None, check for both short and long breaks. @@ -185,8 +221,12 @@ def is_empty(self, break_type=None): else: return self.__short_queue is None and self.__long_queue is None - def __next_short(self): + def __next_short(self) -> Break: shorts = self.__short_queue + + if shorts is None: + raise Exception("this may only be called when there are short breaks") + break_obj = shorts[self.__current_short] self.context["break_type"] = "short" @@ -195,8 +235,12 @@ def __next_short(self): return break_obj - def __next_long(self): + def __next_long(self) -> Break: longs = self.__long_queue + + if longs is None: + raise Exception("this may only be called when there are long breaks") + break_obj = longs[self.__current_long] self.context["break_type"] = "long" @@ -205,7 +249,9 @@ def __next_long(self): return break_obj - def __build_queue(self, break_type, break_configs, break_time, break_duration): + def __build_queue( + self, break_type, break_configs, break_time, break_duration + ) -> typing.Optional[list[Break]]: """Build a queue of breaks.""" size = len(break_configs) @@ -218,8 +264,8 @@ def __build_queue(self, break_type, break_configs, break_time, break_duration): else: breaks_order = break_configs - queue = [None] * size - for i, break_config in enumerate(breaks_order): + queue: list[Break] = [] + for break_config in breaks_order: name = _(break_config["name"]) duration = break_config.get("duration", break_duration) image = break_config.get("image") @@ -232,11 +278,11 @@ def __build_queue(self, break_type, break_configs, break_time, break_duration): continue break_obj = Break(break_type, name, interval, duration, image, plugins) - queue[i] = break_obj + queue.append(break_obj) return queue - def __build_shorts(self): + def __build_shorts(self) -> None: self.__short_queue = self.__build_queue( BreakType.SHORT_BREAK, self.__config.get("short_breaks"), @@ -244,7 +290,7 @@ def __build_shorts(self): self.__config.get("short_break_duration"), ) - def __build_longs(self): + def __build_longs(self) -> None: self.__long_queue = self.__build_queue( BreakType.LONG_BREAK, self.__config.get("long_breaks"), diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index 089321ef..1dc47b19 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -30,7 +30,7 @@ def test_break_short(self) -> None: time=15, duration=15, image=None, - plugins=None, + plugins={}, ) assert b.is_short_break() @@ -43,7 +43,7 @@ def test_break_long(self) -> None: time=75, duration=60, image=None, - plugins=None, + plugins={}, ) assert not b.is_short_break() @@ -72,8 +72,11 @@ def test_create_empty(self) -> None: assert bq.is_empty() assert bq.is_empty(model.BreakType.LONG_BREAK) assert bq.is_empty(model.BreakType.SHORT_BREAK) - assert bq.next() is None - assert bq.get_break() is None + + with pytest.raises(Exception, match="this should never be called"): + bq.next() + with pytest.raises(Exception, match="this should never be called"): + bq.get_break() def get_bq_only_short( self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None From 455f92e621bfff6be819a35cd93956d2d6ec0a00 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 8 Jun 2025 14:01:08 +0200 Subject: [PATCH 038/134] core: make break_queue protected --- safeeyes/core.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 01fc5218..784d1f17 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -35,7 +35,6 @@ class SafeEyesCore: """Core of Safe Eyes runs the scheduler and notifies the breaks.""" - break_queue: BreakQueue scheduled_next_break_time: typing.Optional[datetime.datetime] = None scheduled_next_break_timestamp: int = -1 running: bool = False @@ -44,6 +43,8 @@ class SafeEyesCore: default_postpone_duration: int = 0 pre_break_warning_time: int = 0 + _break_queue: BreakQueue + def __init__(self, context) -> None: """Create an instance of SafeEyesCore and initialize the variables.""" # This event is fired before for a break @@ -71,7 +72,7 @@ def initialize(self, config: Config): """Initialize the internal properties from configuration.""" logging.info("Initialize the core") self.pre_break_warning_time = config.get("pre_break_warning_time") - self.break_queue = BreakQueue(config, self.context) + self._break_queue = BreakQueue(config, self.context) self.default_postpone_duration = ( config.get("postpone_duration") * 60 ) # Convert to seconds @@ -79,7 +80,7 @@ def initialize(self, config: Config): def start(self, next_break_time=-1, reset_breaks=False) -> None: """Start Safe Eyes is it is not running already.""" - if self.break_queue.is_empty(): + if self._break_queue.is_empty(): logging.info("No breaks defined, not starting the core") return with self.lock: @@ -87,7 +88,7 @@ def start(self, next_break_time=-1, reset_breaks=False) -> None: logging.info("Start Safe Eyes core") if reset_breaks: logging.info("Reset breaks to start from the beginning") - self.break_queue.reset() + self._break_queue.reset() self.running = True self.scheduled_next_break_timestamp = int(next_break_time) @@ -126,11 +127,11 @@ def get_break_time( self, break_type: typing.Optional[BreakType] = None ) -> typing.Optional[datetime.datetime]: """Returns the next break time.""" - break_obj = self.break_queue.get_break_with_type(break_type) + break_obj = self._break_queue.get_break_with_type(break_type) if not break_obj or self.scheduled_next_break_time is None: return None time = self.scheduled_next_break_time + datetime.timedelta( - minutes=break_obj.time - self.break_queue.get_break().time + minutes=break_obj.time - self._break_queue.get_break().time ) return time @@ -138,7 +139,7 @@ def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Calling this method stops the scheduler and show the next break screen. """ - if self.break_queue.is_empty(): + if self._break_queue.is_empty(): return if not self.context["state"] == State.WAITING: return @@ -149,7 +150,7 @@ def has_breaks(self, break_type: typing.Optional[BreakType] = None) -> bool: Use the break_type to check for either short or long break. """ - return not self.break_queue.is_empty(break_type) + return not self._break_queue.is_empty(break_type) def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Show the next break screen.""" @@ -169,8 +170,8 @@ def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: time.sleep(1) # Wait for 1 sec to ensure the scheduler is dead self.running = True - if break_type is not None and self.break_queue.get_break().type != break_type: - self.break_queue.next(break_type) + if break_type is not None and self._break_queue.get_break().type != break_type: + self._break_queue.next(break_type) utility.execute_main_thread(self.__fire_start_break) def __scheduler_job(self) -> None: @@ -185,7 +186,7 @@ def __scheduler_job(self) -> None: # Safe Eyes was resting paused_duration = int(current_timestamp - self.paused_time) self.paused_time = -1 - next_long = self.break_queue.get_break_with_type(BreakType.LONG_BREAK) + next_long = self._break_queue.get_break_with_type(BreakType.LONG_BREAK) if next_long is not None and paused_duration > next_long.duration: logging.info( "Skip next long break due to the pause %ds longer than break" @@ -193,7 +194,7 @@ def __scheduler_job(self) -> None: paused_duration, ) # Skip the next long break - self.break_queue.reset() + self._break_queue.reset() if self.context["postponed"]: # Previous break was postponed @@ -208,7 +209,7 @@ def __scheduler_job(self) -> None: self.scheduled_next_break_timestamp = -1 else: # Use next break, convert to seconds - time_to_wait = self.break_queue.get_break().time * 60 + time_to_wait = self._break_queue.get_break().time * 60 self.scheduled_next_break_time = current_time + datetime.timedelta( seconds=time_to_wait @@ -232,12 +233,12 @@ def __scheduler_job(self) -> None: def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> None: """Pass the next break information to the registered listeners.""" - self.on_update_next_break.fire(self.break_queue.get_break(), next_break_time) + self.on_update_next_break.fire(self._break_queue.get_break(), next_break_time) def __fire_pre_break(self) -> None: """Show the notification and start the break after the notification.""" self.context["state"] = State.PRE_BREAK - if not self.on_pre_break.fire(self.break_queue.get_break()): + if not self.on_pre_break.fire(self._break_queue.get_break()): # Plugins wanted to ignore this break self.__start_next_break() return @@ -258,7 +259,7 @@ def __postpone_break(self) -> None: utility.execute_main_thread(self.__fire_start_break) def __fire_start_break(self) -> None: - break_obj = self.break_queue.get_break() + break_obj = self._break_queue.get_break() # Show the break screen if not self.on_start_break.fire(break_obj): # Plugins want to ignore this break @@ -286,7 +287,7 @@ def __fire_start_break(self) -> None: def __start_break(self) -> None: """Start the break screen.""" self.context["state"] = State.BREAK - break_obj = self.break_queue.get_break() + break_obj = self._break_queue.get_break() countdown = break_obj.duration total_break_time = countdown @@ -322,7 +323,7 @@ def __wait_for(self, duration: int) -> None: def __start_next_break(self) -> None: if not self.context["postponed"]: - self.break_queue.next() + self._break_queue.next() if self.running: # Schedule the break again From 521906218b0883951ec5929b5a2856dd3a62d865 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 24 Jul 2025 11:48:02 +0200 Subject: [PATCH 039/134] tests: make random break tests independent of actual order --- safeeyes/tests/test_model.py | 135 +++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index 1dc47b19..3b4cdae0 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -223,13 +223,33 @@ def test_only_short_next_break_random( random_seed = 5 bq = self.get_bq_only_short(monkeypatch, random_seed) - next = bq.get_break() - assert next.name == "translated!: break 3" - - assert bq.next().name == "translated!: break 2" - assert bq.next().name == "translated!: break 1" - assert bq.next().name == "translated!: break 3" - assert bq.next().name == "translated!: break 1" + breaks = [] + breaks.append(bq.get_break().name) + breaks.append(bq.next().name) + breaks.append(bq.next().name) + + assert sorted(breaks) == [ + "translated!: break 1", + "translated!: break 2", + "translated!: break 3", + ] + + prev_breaks = breaks + + for i in range(3): + breaks = [] + breaks.append(bq.next().name) + breaks.append(bq.next().name) + breaks.append(bq.next().name) + + assert sorted(breaks) == [ + "translated!: break 1", + "translated!: break 2", + "translated!: break 3", + ] + + assert prev_breaks != breaks + prev_breaks = breaks def test_create_only_long(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_long(monkeypatch) @@ -270,13 +290,33 @@ def test_only_long_next_break_random(self, monkeypatch: pytest.MonkeyPatch) -> N random_seed = 5 bq = self.get_bq_only_long(monkeypatch, random_seed) - next = bq.get_break() - assert next.name == "translated!: long break 3" + breaks = [] + breaks.append(bq.get_break().name) + breaks.append(bq.next().name) + breaks.append(bq.next().name) - assert bq.next().name == "translated!: long break 2" - assert bq.next().name == "translated!: long break 1" - assert bq.next().name == "translated!: long break 3" - assert bq.next().name == "translated!: long break 1" + assert sorted(breaks) == [ + "translated!: long break 1", + "translated!: long break 2", + "translated!: long break 3", + ] + + prev_breaks = breaks + + for i in range(3): + breaks = [] + breaks.append(bq.next().name) + breaks.append(bq.next().name) + breaks.append(bq.next().name) + + assert sorted(breaks) == [ + "translated!: long break 1", + "translated!: long break 2", + "translated!: long break 3", + ] + + assert prev_breaks != breaks + prev_breaks = breaks def test_create_full(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_full(monkeypatch) @@ -346,20 +386,55 @@ def test_full_next_break_random(self, monkeypatch: pytest.MonkeyPatch) -> None: random_seed = 5 bq = self.get_bq_full(monkeypatch, random_seed) - next = bq.get_break() - assert next.name == "translated!: break 1" - - assert bq.next().name == "translated!: break 2" - assert bq.next().name == "translated!: break 4" - assert bq.next().name == "translated!: break 3" - assert bq.next().name == "translated!: long break 3" - assert bq.next().name == "translated!: break 2" - assert bq.next().name == "translated!: break 1" - assert bq.next().name == "translated!: break 4" - assert bq.next().name == "translated!: break 3" - assert bq.next().name == "translated!: long break 2" - assert bq.next().name == "translated!: break 2" - assert bq.next().name == "translated!: break 4" - assert bq.next().name == "translated!: break 1" - assert bq.next().name == "translated!: break 3" - assert bq.next().name == "translated!: long break 1" + first = True + + prev_breaks: list[list[str]] = [] + prev_long_breaks: list[list[str]] = [] + + for i in range(5): + long_breaks = [] + for i in range(3): + breaks = [] + if first: + first = False + breaks.append(bq.get_break().name) + else: + breaks.append(bq.next().name) + breaks.append(bq.next().name) + breaks.append(bq.next().name) + breaks.append(bq.next().name) + long_breaks.append(bq.next().name) + + # assert that we used all breaks in this iteration + assert sorted(breaks) == [ + "translated!: break 1", + "translated!: break 2", + "translated!: break 3", + "translated!: break 4", + ] + + # assert that not all the iterations are exactly the same order + # (this may happen randomly sometimes of course - at least one + # should be different) + assert self.at_least_one_different(prev_breaks, breaks) + prev_breaks.append(breaks) + + assert sorted(long_breaks) == [ + "translated!: long break 1", + "translated!: long break 2", + "translated!: long break 3", + ] + assert self.at_least_one_different(prev_long_breaks, long_breaks) + prev_long_breaks.append(long_breaks) + + def at_least_one_different( + self, previous: list[list[str]], current: list[str] + ) -> bool: + if len(previous) == 0: + return True + + for prev in previous: + if prev != current: + return True + + return False From 7ad97f021e645d6cc28a1b0ad7d79cdea5ae2965 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 8 Jun 2025 14:46:31 +0200 Subject: [PATCH 040/134] typing: only instantiate BreakQueue when there are breaks --- safeeyes/core.py | 39 +++++++++-- safeeyes/model.py | 121 ++++++++++++++++++++++------------- safeeyes/tests/test_model.py | 32 ++++----- 3 files changed, 129 insertions(+), 63 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 784d1f17..386ea1ad 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -43,7 +43,7 @@ class SafeEyesCore: default_postpone_duration: int = 0 pre_break_warning_time: int = 0 - _break_queue: BreakQueue + _break_queue: typing.Optional[BreakQueue] = None def __init__(self, context) -> None: """Create an instance of SafeEyesCore and initialize the variables.""" @@ -72,7 +72,7 @@ def initialize(self, config: Config): """Initialize the internal properties from configuration.""" logging.info("Initialize the core") self.pre_break_warning_time = config.get("pre_break_warning_time") - self._break_queue = BreakQueue(config, self.context) + self._break_queue = BreakQueue.create(config, self.context) self.default_postpone_duration = ( config.get("postpone_duration") * 60 ) # Convert to seconds @@ -80,7 +80,7 @@ def initialize(self, config: Config): def start(self, next_break_time=-1, reset_breaks=False) -> None: """Start Safe Eyes is it is not running already.""" - if self._break_queue.is_empty(): + if self._break_queue is None: logging.info("No breaks defined, not starting the core") return with self.lock: @@ -127,6 +127,8 @@ def get_break_time( self, break_type: typing.Optional[BreakType] = None ) -> typing.Optional[datetime.datetime]: """Returns the next break time.""" + if self._break_queue is None: + return None break_obj = self._break_queue.get_break_with_type(break_type) if not break_obj or self.scheduled_next_break_time is None: return None @@ -139,7 +141,7 @@ def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Calling this method stops the scheduler and show the next break screen. """ - if self._break_queue.is_empty(): + if self._break_queue is None: return if not self.context["state"] == State.WAITING: return @@ -150,12 +152,22 @@ def has_breaks(self, break_type: typing.Optional[BreakType] = None) -> bool: Use the break_type to check for either short or long break. """ + if self._break_queue is None: + return False + + if break_type is None: + return True + return not self._break_queue.is_empty(break_type) def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Show the next break screen.""" logging.info("Take a break due to external request") + if self._break_queue is None: + # This will only be called by self.take_break, which checks this + return + with self.lock: if not self.running: return @@ -179,6 +191,10 @@ def __scheduler_job(self) -> None: if not self.running: return + if self._break_queue is None: + # This will only be called by methods which check this + return + current_time = datetime.datetime.now() current_timestamp = current_time.timestamp() @@ -233,10 +249,16 @@ def __scheduler_job(self) -> None: def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> None: """Pass the next break information to the registered listeners.""" + if self._break_queue is None: + # This will only be called by methods which check this + return self.on_update_next_break.fire(self._break_queue.get_break(), next_break_time) def __fire_pre_break(self) -> None: """Show the notification and start the break after the notification.""" + if self._break_queue is None: + # This will only be called by methods which check this + return self.context["state"] = State.PRE_BREAK if not self.on_pre_break.fire(self._break_queue.get_break()): # Plugins wanted to ignore this break @@ -259,6 +281,9 @@ def __postpone_break(self) -> None: utility.execute_main_thread(self.__fire_start_break) def __fire_start_break(self) -> None: + if self._break_queue is None: + # This will only be called by methods which check this + return break_obj = self._break_queue.get_break() # Show the break screen if not self.on_start_break.fire(break_obj): @@ -286,6 +311,9 @@ def __fire_start_break(self) -> None: def __start_break(self) -> None: """Start the break screen.""" + if self._break_queue is None: + # This will only be called by methods which check this + return self.context["state"] = State.BREAK break_obj = self._break_queue.get_break() countdown = break_obj.duration @@ -322,6 +350,9 @@ def __wait_for(self, duration: int) -> None: self.waiting_condition.release() def __start_next_break(self) -> None: + if self._break_queue is None: + # This will only be called by methods which check this + return if not self.context["postponed"]: self._break_queue.next() diff --git a/safeeyes/model.py b/safeeyes/model.py index 94281b35..8e383179 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -106,25 +106,70 @@ class BreakQueue: __long_queue: typing.Optional[list[Break]] __short_queue: typing.Optional[list[Break]] - def __init__(self, config: "Config", context) -> None: - self.context = context - self.__short_break_time = config.get("short_break_interval") - self.__long_break_time = config.get("long_break_interval") - self.__is_random_order = config.get("random_order") - self.__config = config + @classmethod + def create(cls, config: "Config", context) -> typing.Optional["BreakQueue"]: + short_break_time = config.get("short_break_interval") + long_break_time = config.get("long_break_interval") + is_random_order = config.get("random_order") + + short_queue = cls.__build_queue( + BreakType.SHORT_BREAK, + config.get("short_breaks"), + short_break_time, + config.get("short_break_duration"), + is_random_order, + ) + + long_queue = cls.__build_queue( + BreakType.LONG_BREAK, + config.get("long_breaks"), + long_break_time, + config.get("long_break_duration"), + is_random_order, + ) + + if short_queue is None and long_queue is None: + return None - self.__build_longs() - self.__build_shorts() + return cls( + context, + short_break_time, + long_break_time, + is_random_order, + short_queue, + long_queue, + ) + + def __init__( + self, + context, + short_break_time: int, + long_break_time: int, + is_random_order: bool, + short_queue: typing.Optional[list[Break]], + long_queue: typing.Optional[list[Break]], + ) -> None: + """Constructor for BreakQueue. Do not call this directly. + + Instead, use BreakQueue.create() instead. + short_queue and long_queue must not both be None, and must not be an empty + list. + """ + self.context = context + self.__short_break_time = short_break_time + self.__long_break_time = long_break_time + self.__is_random_order = is_random_order + self.__short_queue = short_queue + self.__long_queue = long_queue # Restore the last break from session - if not self.is_empty(): - last_break = context["session"].get("break") - if last_break is not None: - current_break = self.get_break() - if last_break != current_break.name: + last_break = context["session"].get("break") + if last_break is not None: + current_break = self.get_break() + if last_break != current_break.name: + brk = self.next() + while brk != current_break and brk.name != last_break: brk = self.next() - while brk != current_break and brk.name != last_break: - brk = self.next() def get_break(self) -> Break: if self.__current_break is None: @@ -167,7 +212,8 @@ def next(self, break_type: typing.Optional[BreakType] = None) -> Break: self.__current_break.time = self.__long_break_time # type: ignore[union-attr] if self.__current_long == 0 and self.__is_random_order: # Shuffle queue - self.__build_longs() + if self.__long_queue is not None: + random.shuffle(self.__long_queue) elif self.__current_break: # Reduce the break time from the next long break (default) if longs: @@ -178,10 +224,8 @@ def next(self, break_type: typing.Optional[BreakType] = None) -> Break: ) longs[self.__current_long].time -= shorts[self.__current_short].time if self.__current_short == 0 and self.__is_random_order: - self.__build_shorts() - - if self.is_empty(): - raise Exception("this should never be called when the queue is empty") + if self.__short_queue is not None: + random.shuffle(self.__short_queue) if shorts is None: break_obj = self.__next_long() @@ -209,17 +253,14 @@ def reset(self) -> None: for break_object in self.__long_queue: break_object.time = self.__long_break_time - def is_empty(self, break_type=None) -> bool: - """Check if the given break type is empty or not. - - If the break_type is None, check for both short and long breaks. - """ + def is_empty(self, break_type: BreakType) -> bool: + """Check if the given break type is empty or not.""" if break_type == BreakType.SHORT_BREAK: return self.__short_queue is None elif break_type == BreakType.LONG_BREAK: return self.__long_queue is None else: - return self.__short_queue is None and self.__long_queue is None + typing.assert_never(break_type) def __next_short(self) -> Break: shorts = self.__short_queue @@ -249,8 +290,13 @@ def __next_long(self) -> Break: return break_obj + @staticmethod def __build_queue( - self, break_type, break_configs, break_time, break_duration + break_type: BreakType, + break_configs: list[dict], + break_time: int, + break_duration: int, + is_random_order: bool, ) -> typing.Optional[list[Break]]: """Build a queue of breaks.""" size = len(break_configs) @@ -259,7 +305,7 @@ def __build_queue( # No breaks return None - if self.__is_random_order: + if is_random_order: breaks_order = random.sample(break_configs, size) else: breaks_order = break_configs @@ -280,23 +326,10 @@ def __build_queue( break_obj = Break(break_type, name, interval, duration, image, plugins) queue.append(break_obj) - return queue - - def __build_shorts(self) -> None: - self.__short_queue = self.__build_queue( - BreakType.SHORT_BREAK, - self.__config.get("short_breaks"), - self.__short_break_time, - self.__config.get("short_break_duration"), - ) + if len(queue) == 0: + return None - def __build_longs(self) -> None: - self.__long_queue = self.__build_queue( - BreakType.LONG_BREAK, - self.__config.get("long_breaks"), - self.__long_break_time, - self.__config.get("long_break_duration"), - ) + return queue class State(Enum): diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index 3b4cdae0..52a7b9e6 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -67,16 +67,9 @@ def test_create_empty(self) -> None: context: dict[str, typing.Any] = {} - bq = model.BreakQueue(config, context) + bq = model.BreakQueue.create(config, context) - assert bq.is_empty() - assert bq.is_empty(model.BreakType.LONG_BREAK) - assert bq.is_empty(model.BreakType.SHORT_BREAK) - - with pytest.raises(Exception, match="this should never be called"): - bq.next() - with pytest.raises(Exception, match="this should never be called"): - bq.get_break() + assert bq is None def get_bq_only_short( self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None @@ -109,7 +102,11 @@ def get_bq_only_short( "session": {}, } - return model.BreakQueue(config, context) + bq = model.BreakQueue.create(config, context) + + assert bq is not None + + return bq def get_bq_only_long( self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None @@ -142,7 +139,11 @@ def get_bq_only_long( "session": {}, } - return model.BreakQueue(config, context) + bq = model.BreakQueue.create(config, context) + + assert bq is not None + + return bq def get_bq_full( self, monkeypatch: pytest.MonkeyPatch, random_seed: typing.Optional[int] = None @@ -180,12 +181,15 @@ def get_bq_full( "session": {}, } - return model.BreakQueue(config, context) + bq = model.BreakQueue.create(config, context) + + assert bq is not None + + return bq def test_create_only_short(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_short(monkeypatch) - assert not bq.is_empty() assert not bq.is_empty(model.BreakType.SHORT_BREAK) assert bq.is_empty(model.BreakType.LONG_BREAK) @@ -254,7 +258,6 @@ def test_only_short_next_break_random( def test_create_only_long(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_only_long(monkeypatch) - assert not bq.is_empty() assert not bq.is_empty(model.BreakType.LONG_BREAK) assert bq.is_empty(model.BreakType.SHORT_BREAK) @@ -321,7 +324,6 @@ def test_only_long_next_break_random(self, monkeypatch: pytest.MonkeyPatch) -> N def test_create_full(self, monkeypatch: pytest.MonkeyPatch) -> None: bq = self.get_bq_full(monkeypatch) - assert not bq.is_empty() assert not bq.is_empty(model.BreakType.LONG_BREAK) assert not bq.is_empty(model.BreakType.SHORT_BREAK) From ce238c4b7d1f3e827309d5d3949b5ecc9dfae3e9 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 8 Jun 2025 15:08:39 +0200 Subject: [PATCH 041/134] typing: BreakQueue: __current_break is always set --- safeeyes/model.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/safeeyes/model.py b/safeeyes/model.py index 8e383179..9d02e70a 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -97,7 +97,7 @@ def plugin_enabled(self, plugin_id: str, is_plugin_enabled: bool) -> bool: class BreakQueue: - __current_break: typing.Optional[Break] = None + __current_break: Break __current_long: int = 0 __current_short: int = 0 __short_break_time: int @@ -162,6 +162,9 @@ def __init__( self.__short_queue = short_queue self.__long_queue = long_queue + # load first break + self.__set_next_break() + # Restore the last break from session last_break = context["session"].get("break") if last_break is not None: @@ -172,17 +175,11 @@ def __init__( brk = self.next() def get_break(self) -> Break: - if self.__current_break is None: - self.__current_break = self.next() - return self.__current_break def get_break_with_type( self, break_type: typing.Optional[BreakType] = None ) -> typing.Optional[Break]: - if self.__current_break is None: - self.__current_break = self.next() - if break_type is None or self.__current_break.type == break_type: return self.__current_break @@ -196,25 +193,27 @@ def get_break_with_type( return self.__short_queue[self.__current_short] def is_long_break(self) -> bool: - return ( - self.__current_break is not None - and self.__current_break.type == BreakType.LONG_BREAK - ) + return self.__current_break.type == BreakType.LONG_BREAK def next(self, break_type: typing.Optional[BreakType] = None) -> Break: - break_obj = None + """Advance to the next break, and return that break. + + If break_type is given, advance to the next break with that type. + If the last break in the queue is reached, this resets the internal index to + the first break again, and shuffle if needed. + """ shorts = self.__short_queue longs = self.__long_queue + previous_break = self.__current_break # Reset break that has just ended - if self.is_long_break(): - # __current_break is checked by is_long_break - self.__current_break.time = self.__long_break_time # type: ignore[union-attr] + if previous_break.is_long_break(): + previous_break.time = self.__long_break_time if self.__current_long == 0 and self.__is_random_order: # Shuffle queue if self.__long_queue is not None: random.shuffle(self.__long_queue) - elif self.__current_break: + else: # Reduce the break time from the next long break (default) if longs: if shorts is None: @@ -227,6 +226,14 @@ def next(self, break_type: typing.Optional[BreakType] = None) -> Break: if self.__short_queue is not None: random.shuffle(self.__short_queue) + self.__set_next_break(break_type) + + return self.__current_break + + def __set_next_break(self, break_type: typing.Optional[BreakType] = None) -> None: + shorts = self.__short_queue + longs = self.__long_queue + if shorts is None: break_obj = self.__next_long() elif longs is None: @@ -242,8 +249,6 @@ def next(self, break_type: typing.Optional[BreakType] = None) -> Break: self.__current_break = break_obj self.context["session"]["break"] = self.__current_break.name - return break_obj - def reset(self) -> None: if self.__short_queue: for break_object in self.__short_queue: From 92ec7aa18f5add4a6b73ae6315806e7202a0d327 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sat, 2 Aug 2025 20:17:35 +0200 Subject: [PATCH 042/134] fix missing dependency on pytest job --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 537b9599..21d947f4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -30,7 +30,7 @@ jobs: - name: Install OS dependencies run: | sudo apt-get update -yy - sudo apt-get install -yy --no-install-recommends libcairo2-dev libgirepository-2.0-dev gir1.2-gtk-4.0 + sudo apt-get install -yy --no-install-recommends libwayland-dev libcairo2-dev libgirepository-2.0-dev gir1.2-gtk-4.0 - run: uv pip install -r pyproject.toml - run: uv pip install --group tests - run: pytest From c2629ddd428836b65163c73b900da7d5f6c7a90d Mon Sep 17 00:00:00 2001 From: deltragon Date: Sat, 2 Aug 2025 20:50:15 +0200 Subject: [PATCH 043/134] update po files --- safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po | 9 +++++++++ .../config/locale/en_US/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po | 13 +++++++++++-- safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po | 9 +++++++++ .../config/locale/pt_BR/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/safeeyes.pot | 6 ++++++ safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po | 9 +++++++++ .../config/locale/uz_Latn/LC_MESSAGES/safeeyes.po | 9 +++++++++ safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po | 9 +++++++++ .../config/locale/zh_CN/LC_MESSAGES/safeeyes.po | 14 ++++++++++++-- .../config/locale/zh_TW/LC_MESSAGES/safeeyes.po | 14 ++++++++++++-- 43 files changed, 392 insertions(+), 6 deletions(-) diff --git a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po index 6fb9f859..ed5c9c71 100644 --- a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po @@ -603,6 +603,15 @@ msgstr "" "تم العثور على ورقة الأنماط القديمة في '%(old)s'، تم تجاهلها. بالنسبة للأنماط " "المخصصة، قم بإنشاء ورقة أنماط جديدة في '%(new)s' بدلاً من ذلك." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "أغلق عينيك بشدّة" diff --git a/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po index 59668f80..04d9fa81 100644 --- a/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po @@ -583,6 +583,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Затворете плътно очи" diff --git a/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po index 42ad89c0..170f0f33 100644 --- a/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po @@ -582,3 +582,12 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" diff --git a/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po index 4d65e38f..de791808 100644 --- a/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po @@ -602,6 +602,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Tanqueu fortament els ulls" diff --git a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po index 91df2e9b..2baf8068 100644 --- a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po @@ -599,6 +599,15 @@ msgstr "" "Starý soubor stylů nalezen na adrese '%(old)s', ignorování. Pro vlastní " "styly vytvořte místo toho nový soubor stylů v '%(new)s'." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Zavřete oči" diff --git a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po index a2178548..3d25ce39 100644 --- a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po @@ -586,6 +586,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Luk øjnene tæt" diff --git a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po index cfc3abe4..18469d80 100644 --- a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po @@ -606,6 +606,15 @@ msgstr "" "Altes Stylesheet unter '%(old)s' gefunden, wird ignoriert. Erstelle " "stattdessen für eigene Styles ein neues Stylesheet in '%(new)s'." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Augen fest schließen" diff --git a/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po index 9317118b..2e7550f3 100644 --- a/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po @@ -588,6 +588,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Tightly close your eyes" diff --git a/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po index bb50c699..d3c55acb 100644 --- a/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po @@ -588,6 +588,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Streĉe malfermu viajn okulojn" diff --git a/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po index 21bb9444..4de46a5e 100644 --- a/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po @@ -601,6 +601,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Cierre fuertemente los ojos" diff --git a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po index 5e84f729..c0c9c510 100644 --- a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po @@ -593,6 +593,15 @@ msgstr "" "Vana ja eiratav laaditabel leidub siin: „%(old)s“. Kui tahad välimust oma " "käe järgi sättida, siis tee laaditabel pigem siia: „%(new)s“." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Sulge silmad" diff --git a/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po index e5330989..7408e20e 100644 --- a/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po @@ -593,6 +593,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Itxi begiak indarrez" diff --git a/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po index b171ef78..76a4f4b5 100644 --- a/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po @@ -589,6 +589,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "محکم چشمانتان را ببندید" diff --git a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po index 5156a3ad..37223754 100644 --- a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po @@ -604,6 +604,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Fermez bien vos yeux" diff --git a/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po index cf04daaa..32910ebb 100644 --- a/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po @@ -587,6 +587,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "לעצום עיניים היטב" diff --git a/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po index 913a738f..9091fcb9 100644 --- a/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po @@ -583,6 +583,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "कसकर अपनी आँखें बंद करें" diff --git a/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po index 82ae9b67..54387c23 100644 --- a/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po @@ -585,6 +585,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Szorosan csukd be a szemed" diff --git a/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po index dad8450a..1efe9a0d 100644 --- a/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po @@ -588,6 +588,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Tutup rapat matamu" diff --git a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po index b64f2a49..81f1be7a 100644 --- a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po @@ -601,6 +601,15 @@ msgstr "" "Vecchio foglio di stile trovato in '%(old)s', ignorando. Per stili " "personalizzati, crea un nuovo foglio di stile in %(new)s invece." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Strizza gli occhi" diff --git a/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po index 4cf68d65..0aa04739 100644 --- a/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po @@ -582,6 +582,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "ಕಣ್ಣನ್ನು ಗಟ್ಟಿಯಾಗಿ ಮುಚ್ಚಿರಿ" diff --git a/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po index 6f0b10c4..52d27051 100644 --- a/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po @@ -582,6 +582,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "눈을 꼭 감으세요" diff --git a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po index d7978a81..331653af 100644 --- a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (" -"n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"(n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Weblate 5.12.1\n" # Short break @@ -600,6 +600,15 @@ msgstr "" "Rastas senas stilių aprašas ties „%(old)s“, jo nepaisoma. Norėdami naudoti " "tinkintus stilius, vietoj jo, sukurkite naują stilių aprašą ties „%(new)s“." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Stipriai užsimerkite" diff --git a/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po index 9e64f275..6ab1d280 100644 --- a/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po @@ -502,6 +502,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + #~ msgid "Tightly close your eyes" #~ msgstr "Cieši aizver acis" diff --git a/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po index c65a8dad..24368df1 100644 --- a/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po @@ -584,3 +584,12 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" diff --git a/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po index d6fad706..d8df6f69 100644 --- a/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po @@ -584,6 +584,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "डोळे घट्ट बंद करा" diff --git a/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po index e6155c3f..2a19f4f7 100644 --- a/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po @@ -590,6 +590,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Lukk øyene dine godt" diff --git a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po index ea7dbd82..8d988eea 100644 --- a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po @@ -599,6 +599,15 @@ msgstr "" "Er is een oud stijlblad aangetroffen: ‘%(old)s’. Dit stijlblad wordt " "genegeerd. Nieuwe stijlen kunnen worden aangemaakt in ‘%(new)s’." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Sluit je ogen goed" diff --git a/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po index 2c7739e1..5e992c92 100644 --- a/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po @@ -593,6 +593,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Dokładnie zamknij oczy" diff --git a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po index e45372ce..cf0340b6 100644 --- a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po @@ -597,6 +597,15 @@ msgstr "" "Folha de estilos antiga encontrada em %(old)s, ignorando. Para estilos " "personalizados, crie uma nova folha de estilos em%(new)s." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Fechar os olhos com força" diff --git a/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po index 55fe9680..2e8bdf32 100644 --- a/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po @@ -595,6 +595,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Feche firmemente seus olhos" diff --git a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po index 8a23df76..676c9b68 100644 --- a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po @@ -601,6 +601,15 @@ msgstr "" "пользовательских стилей вместо этого создайте новую таблицу стилей в " "'%(new)s'." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Плотно закройте глаза" diff --git a/safeeyes/config/locale/safeeyes.pot b/safeeyes/config/locale/safeeyes.pot index f7e00a69..a560a340 100644 --- a/safeeyes/config/locale/safeeyes.pot +++ b/safeeyes/config/locale/safeeyes.pot @@ -564,3 +564,9 @@ msgstr "" msgid "Customizing the postpone and skip shortcuts does not work on Wayland." msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" diff --git a/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po index d18cd143..d733438d 100644 --- a/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po @@ -595,6 +595,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Pevne zatvor oči" diff --git a/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po index de5ad6e7..252a81f0 100644 --- a/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po @@ -598,6 +598,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Чврсто затворите очи" diff --git a/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po index af0ca1db..32b94929 100644 --- a/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po @@ -587,6 +587,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Stäng ögonen tätt" diff --git a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po index 9789b236..2cf73009 100644 --- a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po @@ -595,6 +595,15 @@ msgstr "" "புறக்கணித்து, '%(old)s' இல் காணப்படும் பழைய பாணிதாள். தனிப்பயன் பாணிகளுக்கு, அதற்குப் " "பதிலாக '%(new)s' இல் புதிய பாணிதாளை உருவாக்கவும்." +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "உங்கள் கண்களை இறுக்கமாக மூடுங்கள்" diff --git a/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po index 5e182bd3..6c5cb1ec 100644 --- a/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po @@ -594,6 +594,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Gözlerinizi sıkıca kapatın" diff --git a/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po index 4259e966..303092d2 100644 --- a/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po @@ -578,3 +578,12 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" diff --git a/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po index 7e944387..e990483b 100644 --- a/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po @@ -594,6 +594,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Щільно заплющіть очі" diff --git a/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po index 80788bdf..8d218e30 100644 --- a/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po @@ -578,3 +578,12 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" diff --git a/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po index 5730325f..bd4f7865 100644 --- a/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po @@ -588,6 +588,15 @@ msgid "" "stylesheet in '%(new)s' instead." msgstr "" +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Nhắm chặt mắt lại" diff --git a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po index f2040b74..d2326fcf 100644 --- a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po @@ -581,8 +581,18 @@ msgstr "许可:" msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." -msgstr "在 '%(old)s' 发现旧版样式表,已忽略。若需自定义样式,请在 '%(new)s' " -"创建新版样式表。" +msgstr "" +"在 '%(old)s' 发现旧版样式表,已忽略。若需自定义样式,请在 '%(new)s' 创建新版" +"样式表。" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" # Short break #~ msgid "Tightly close your eyes" diff --git a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po index 6bf58fca..1f4693bc 100644 --- a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po @@ -578,8 +578,18 @@ msgstr "授權:" msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." -msgstr "在 '%(old)s' 發現舊的樣式表,已忽略。若需自訂樣式,請在 '%(new)s' " -"中建立新的樣式表。" +msgstr "" +"在 '%(old)s' 發現舊的樣式表,已忽略。若需自訂樣式,請在 '%(new)s' 中建立新的" +"樣式表。" + +msgid "Customizing the postpone and skip shortcuts does not work on Wayland." +msgstr "" + +msgid "Safe Eyes - Error" +msgstr "" + +msgid "A required plugin is missing dependencies!" +msgstr "" # Short break #~ msgid "Tightly close your eyes" From 5c4bfc6a576756e7ca0fdc7422785b80c5dce874 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 11 May 2025 12:46:25 +0200 Subject: [PATCH 044/134] port cli handling and uniqueness to gtk --- safeeyes/__main__.py | 94 +---------------------- safeeyes/safeeyes.py | 160 ++++++++++++++++++++++++++++++++++----- safeeyes/translations.py | 7 +- 3 files changed, 148 insertions(+), 113 deletions(-) diff --git a/safeeyes/__main__.py b/safeeyes/__main__.py index fe5c9e28..15af6067 100755 --- a/safeeyes/__main__.py +++ b/safeeyes/__main__.py @@ -20,18 +20,13 @@ your eyes from eye strain. """ -import argparse -import logging import signal import sys import psutil -from safeeyes import utility, translations -from safeeyes.translations import translate as _ +from safeeyes import translations from safeeyes.model import Config from safeeyes.safeeyes import SafeEyes -from safeeyes.safeeyes import SAFE_EYES_VERSION -from safeeyes.rpc import RPCClient def __running(): @@ -67,93 +62,10 @@ def main(): """Start the Safe Eyes.""" system_locale = translations.setup() - parser = argparse.ArgumentParser(prog="safeeyes") - group = parser.add_mutually_exclusive_group() - group.add_argument( - "-a", "--about", help=_("show the about dialog"), action="store_true" - ) - group.add_argument( - "-d", - "--disable", - help=_("disable the currently running safeeyes instance"), - action="store_true", - ) - group.add_argument( - "-e", - "--enable", - help=_("enable the currently running safeeyes instance"), - action="store_true", - ) - group.add_argument( - "-q", - "--quit", - help=_("quit the running safeeyes instance and exit"), - action="store_true", - ) - group.add_argument( - "-s", "--settings", help=_("show the settings dialog"), action="store_true" - ) - group.add_argument( - "-t", "--take-break", help=_("Take a break now").lower(), action="store_true" - ) - parser.add_argument( - "--debug", help=_("start safeeyes in debug mode"), action="store_true" - ) - parser.add_argument( - "--status", - help=_("print the status of running safeeyes instance and exit"), - action="store_true", - ) - parser.add_argument( - "--version", action="version", version="%(prog)s " + SAFE_EYES_VERSION - ) - args = parser.parse_args() - - # Initialize the logging - utility.initialize_logging(args.debug) - utility.initialize_platform() config = Config.load() - utility.cleanup_old_user_stylesheet() - if __running(): - logging.info("Safe Eyes is already running") - if not config.get("use_rpc_server", True): - # RPC sever is disabled - print( - _( - "Safe Eyes is running without an RPC server. Turn it on to use" - " command-line arguments." - ) - ) - sys.exit(0) - return - rpc_client = RPCClient(config.get("rpc_port")) - if args.about: - rpc_client.show_about() - elif args.disable: - rpc_client.disable_safeeyes() - elif args.enable: - rpc_client.enable_safeeyes() - elif args.settings: - rpc_client.show_settings() - elif args.take_break: - rpc_client.take_break() - elif args.status: - print(rpc_client.status()) - elif args.quit: - rpc_client.quit() - else: - # Default behavior is opening settings - rpc_client.show_settings() - sys.exit(0) - else: - if args.status: - print(_("Safe Eyes is not running")) - sys.exit(0) - elif not args.quit: - logging.info("Starting Safe Eyes") - safe_eyes = SafeEyes(system_locale, config, args) - safe_eyes.start() + safe_eyes = SafeEyes(system_locale, config) + safe_eyes.run(sys.argv) if __name__ == "__main__": diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index 4ea5a93e..8f3ce655 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -22,6 +22,7 @@ import atexit import logging +import typing from threading import Timer from importlib import metadata @@ -49,27 +50,157 @@ class SafeEyes(Gtk.Application): required_plugin_dialog_active = False retry_errored_plugins_count = 0 - def __init__(self, system_locale, config, cli_args): + def __init__(self, system_locale, config) -> None: super().__init__( application_id="io.github.slgobinath.SafeEyes", - # This is necessary for compatibility with Ubuntu 22.04. - flags=Gio.ApplicationFlags.FLAGS_NONE, + flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, ) + self.active = False self.break_screen = None self.safe_eyes_core = None self.config = config - self.context = {} + self.context: typing.Any = {} self.plugins_manager = None self.settings_dialog_active = False self.rpc_server = None self._status = "" - self.cli_args = cli_args self.system_locale = system_locale - def start(self): - """Start Safe Eyes.""" - self.run() + self.__register_cli_arguments() + self.__register_actions() + + def __register_cli_arguments(self): + flags = [ + # startup window + ("about", "a", _("show the about dialog")), + ("settings", "s", _("start safeeyes in debug mode")), + ("take-break", "t", _("Take a break now").lower()), + # activate action + ("disable", "d", _("disable the currently running safeeyes instance")), + ("enable", "e", _("enable the currently running safeeyes instance")), + ("quit", "q", _("quit the running safeeyes instance and exit")), + # toggle + ("debug", None, _("start safeeyes in debug mode")), + # TODO: translate + ("version", None, "show program's version number and exit"), + ] + + for flag, short, desc in flags: + # all flags are booleans + self.add_main_option( + flag, + ord(short) if short else 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + desc, + None, + ) + + def __register_actions(self) -> None: + actions = [ + ("show_about", self.show_about), + ("show_settings", self.show_settings), + ("take_break", self.take_break), + ("enable_safeeyes", self.enable_safeeyes), + ("disable_safeeyes", self.disable_safeeyes), + ("quit", self.quit), + ] + + # this is needed because of late bindings... + def create_cb_discard_args(callback): + return lambda parameter, user_data: callback() + + for name, callback in actions: + action = Gio.SimpleAction.new(name, None) + action.connect("activate", create_cb_discard_args(callback)) + self.add_action(action) + + def do_handle_local_options(self, options): + Gtk.Application.do_handle_local_options(self, options) + + # do not call options.end() here - this will clear the dict, + # and make it empty/broken inside do_command_line + + debug = False + if options.contains("debug"): + debug = True + + # Initialize the logging + utility.initialize_logging(debug) + utility.initialize_platform() + utility.cleanup_old_user_stylesheet() + + if options.contains("version"): + print(f"safeeyes {SAFE_EYES_VERSION}") + return 0 # exit + + # needed for calling is_remote + self.register(None) + + is_remote = self.get_is_remote() + + if is_remote: + logging.info("Remote instance") + + if options.contains("quit"): + self.activate_action("quit", None) + return 0 + + if options.contains("enable"): + self.activate_action("enable_safeeyes", None) + return 0 + + if options.contains("disable"): + self.activate_action("disable_safeeyes", None) + return 0 + + if options.contains("about"): + self.activate_action("show_about", None) + return 0 + + if options.contains("settings"): + self.activate_action("show_settings", None) + return 0 + + if options.contains("take-break"): + self.activate_action("take_break", None) + return 0 + + logging.info("Safe Eyes is already running") + return 0 # TODO: return error code here? + + else: + logging.info("Primary instance") + + if ( + options.contains("enable") + or options.contains("disable") + or options.contains("quit") + ): + print(_("Safe Eyes is not running")) + self.activate_action("quit", None) + return 1 + + return -1 # continue default handling + + def do_command_line(self, command_line): + Gtk.Application.do_command_line(self, command_line) + + cli = command_line.get_options_dict().end().unpack() + + logging.info("Handle primary command line") + + self.activate() + + if cli.get("about"): + self.show_about() + elif cli.get("settings"): + self.show_settings() + elif cli.get("take-break"): + self.take_break() + + return 0 def do_startup(self): Gtk.Application.do_startup(self) @@ -155,17 +286,6 @@ def do_activate(self): if self.plugins_manager.needs_retry(): GLib.timeout_add_seconds(1, self._retry_errored_plugins) - if self.cli_args.about: - self.show_about() - elif self.cli_args.disable: - self.disable_safeeyes() - elif self.cli_args.enable: - self.enable_safeeyes() - elif self.cli_args.settings: - self.show_settings() - elif self.cli_args.take_break: - self.take_break() - def _initialize_styles(self): utility.load_css_file( utility.SYSTEM_STYLE_SHEET_PATH, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION @@ -260,6 +380,8 @@ def quit(self): self.__stop_rpc_server() self.persist_session() + self.release() + super().quit() def handle_suspend_callback(self, sleeping): diff --git a/safeeyes/translations.py b/safeeyes/translations.py index f9ee344d..be71d466 100644 --- a/safeeyes/translations.py +++ b/safeeyes/translations.py @@ -19,8 +19,8 @@ """Translation setup and helpers.""" import locale -import logging import gettext +import sys from safeeyes import utility _translations = gettext.NullTranslations() @@ -38,9 +38,10 @@ def setup(): # locale.bindtextdomain is required for Glade files locale.bindtextdomain("safeeyes", utility.LOCALE_PATH) except AttributeError: - logging.warning( + print( "installed python's gettext module does not support locale.bindtextdomain." - " locale.bindtextdomain is required for Glade files" + " locale.bindtextdomain is required for Glade files", + file=sys.stderr, ) return _translations From 05bd45b132f7a644321cdd2f95ba5a41e30447fb Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 11 May 2025 12:48:59 +0200 Subject: [PATCH 045/134] avoid circular dependency between translations and utility --- safeeyes/utility.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/safeeyes/utility.py b/safeeyes/utility.py index 5444ae9e..28875471 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -46,7 +46,6 @@ from gi.repository import GLib from gi.repository import GdkPixbuf from packaging.version import parse -from safeeyes.translations import translate as _ BIN_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) HOME_DIRECTORY = os.environ.get("HOME") or os.path.expanduser("~") @@ -181,6 +180,8 @@ def delete(file_path): def check_plugin_dependencies(plugin_id, plugin_config, plugin_settings, plugin_path): """Check the plugin dependencies.""" + from safeeyes.translations import translate as _ + # Check the desktop environment if plugin_config["dependencies"]["desktop_environments"]: # Plugin has restrictions on desktop environments @@ -449,6 +450,8 @@ def cleanup_old_user_stylesheet(): logging.info("Deleting old stylesheet containing default content") delete(OLD_STYLE_SHEET_PATH) else: + from safeeyes.translations import translate as _ + # Stylesheet was likely customized, don't delete but warn logging.warning( _( @@ -711,6 +714,8 @@ def open_session(): def create_gtk_builder(glade_file): """Create a Gtk builder and load the glade file.""" + from safeeyes.translations import translate as _ + builder = Gtk.Builder() builder.set_translation_domain("safeeyes") builder.add_from_file(glade_file) From 01ebfa96d1d5f68968f69499f3cc9715f25c4689 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 11 May 2025 12:48:34 +0200 Subject: [PATCH 046/134] cli: --status: invoke on and print from primary instance --- safeeyes/safeeyes.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index 8f3ce655..af5da697 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -80,6 +80,12 @@ def __register_cli_arguments(self): ("disable", "d", _("disable the currently running safeeyes instance")), ("enable", "e", _("enable the currently running safeeyes instance")), ("quit", "q", _("quit the running safeeyes instance and exit")), + # special handling + ( + "status", + None, + _("print the status of running safeeyes instance and exit"), + ), # toggle ("debug", None, _("start safeeyes in debug mode")), # TODO: translate @@ -143,6 +149,12 @@ def do_handle_local_options(self, options): if is_remote: logging.info("Remote instance") + if options.contains("status"): + # fall through the default handling + # this will call do_command_line on the primary instance + # where we will handle this + return -1 + if options.contains("quit"): self.activate_action("quit", None) return 0 @@ -176,6 +188,7 @@ def do_handle_local_options(self, options): if ( options.contains("enable") or options.contains("disable") + or options.contains("status") or options.contains("quit") ): print(_("Safe Eyes is not running")) @@ -189,6 +202,13 @@ def do_command_line(self, command_line): cli = command_line.get_options_dict().end().unpack() + if cli.get("status"): + # this is only invoked remotely + # this code runs in the primary instance, but will print to the output + # of the remote instance + command_line.print_literal(self.status()) + return 0 + logging.info("Handle primary command line") self.activate() From 963877269e82a7419f083cf5e2e8ae63bdb546a5 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 11 May 2025 12:48:59 +0200 Subject: [PATCH 047/134] remove dead rpc + psutil uniqueness code --- README.md | 1 - debian/control | 1 - pyproject.toml | 2 - safeeyes/__main__.py | 30 -------------- safeeyes/rpc.py | 99 -------------------------------------------- safeeyes/safeeyes.py | 23 ---------- 6 files changed, 156 deletions(-) delete mode 100644 safeeyes/rpc.py diff --git a/README.md b/README.md index fdf49e6f..f4f08f3f 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,6 @@ Ensure to meet the following dependencies: - python3-babel - python3-croniter - python3-gi -- python3-psutil - python3-packaging - python3-xlib - python3-pywayland (optional for KDE/other wayland) diff --git a/debian/control b/debian/control index f1d02bee..e81d1d82 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,6 @@ Depends: ${misc:Depends}, ${python3:Depends}, x11-utils, xprintidle, alsa-utils, - python3-psutil, python3-croniter, python3-packaging, gir1.2-notify-0.7, diff --git a/pyproject.toml b/pyproject.toml index f829466d..1b2c0505 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "pywayland", "PyGObject", "babel", - "psutil", "packaging", "python-xlib", ] @@ -65,7 +64,6 @@ types = [ "mypy==1.15.0", "PyGObject-stubs==2.13.0", "types-croniter==5.0.1.20250322", - "types-psutil==7.0.0.20250218", "types-python-xlib==0.33.0.20240407", {include-group = "tests"}, ] diff --git a/safeeyes/__main__.py b/safeeyes/__main__.py index 15af6067..53e0a08e 100755 --- a/safeeyes/__main__.py +++ b/safeeyes/__main__.py @@ -23,41 +23,11 @@ import signal import sys -import psutil from safeeyes import translations from safeeyes.model import Config from safeeyes.safeeyes import SafeEyes -def __running(): - """Check if SafeEyes is already running.""" - process_count = 0 - current_user = psutil.Process().username() - for proc in psutil.process_iter(): - if not proc.cmdline: - continue - try: - # Check if safeeyes is in process arguments - if callable(proc.cmdline): - # Latest psutil has cmdline function - cmd_line = proc.cmdline() - else: - # In older versions cmdline was a list object - cmd_line = proc.cmdline - if ("python3" in cmd_line[0] or "python" in cmd_line[0]) and ( - "safeeyes" in cmd_line[1] or "safeeyes" in cmd_line - ): - if proc.username() == current_user: - process_count += 1 - if process_count > 1: - return True - - # Ignore if process does not exist or does not have command line args - except (IndexError, psutil.NoSuchProcess): - pass - return False - - def main(): """Start the Safe Eyes.""" system_locale = translations.setup() diff --git a/safeeyes/rpc.py b/safeeyes/rpc.py deleted file mode 100644 index e1398b5e..00000000 --- a/safeeyes/rpc.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -# Safe Eyes is a utility to remind you to take break frequently -# to protect your eyes from eye strain. - -# Copyright (C) 2017 Gobinath - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -"""RPC server and client implementation.""" - -import logging -from threading import Thread -from xmlrpc.server import SimpleXMLRPCServer -from xmlrpc.client import ServerProxy - - -class RPCServer: - """An asynchronous RPC server.""" - - def __init__(self, port, context): - self.__running = False - logging.info("Setting up an RPC server on port %d", port) - self.__server = SimpleXMLRPCServer( - ("localhost", port), logRequests=False, allow_none=True - ) - self.__server.register_function( - context["api"]["show_settings"], "show_settings" - ) - self.__server.register_function(context["api"]["show_about"], "show_about") - self.__server.register_function( - context["api"]["enable_safeeyes"], "enable_safeeyes" - ) - self.__server.register_function( - context["api"]["disable_safeeyes"], "disable_safeeyes" - ) - self.__server.register_function(context["api"]["take_break"], "take_break") - self.__server.register_function(context["api"]["status"], "status") - self.__server.register_function(context["api"]["quit"], "quit") - - def start(self): - """Start the RPC server.""" - if not self.__running: - self.__running = True - logging.info("Start the RPC server") - server_thread = Thread(target=self.__server.serve_forever) - server_thread.start() - - def stop(self): - """Stop the server.""" - if self.__running: - logging.info("Stop the RPC server") - self.__running = False - self.__server.shutdown() - - -class RPCClient: - """An RPC client to communicate with the RPC server.""" - - def __init__(self, port): - self.port = port - self.proxy = ServerProxy("http://localhost:%d/" % self.port, allow_none=True) - - def show_settings(self): - """Show the settings dialog.""" - self.proxy.show_settings() - - def show_about(self): - """Show the about dialog.""" - self.proxy.show_about() - - def enable_safeeyes(self): - """Enable Safe Eyes.""" - self.proxy.enable_safeeyes() - - def disable_safeeyes(self): - """Disable Safe Eyes.""" - self.proxy.disable_safeeyes(None) - - def take_break(self): - """Take a break now.""" - self.proxy.take_break() - - def status(self): - """Return the status of Safe Eyes.""" - return self.proxy.status() - - def quit(self): - """Quit Safe Eyes.""" - self.proxy.quit() diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index af5da697..56c1e48a 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -32,7 +32,6 @@ from safeeyes.ui.break_screen import BreakScreen from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog from safeeyes.model import State, RequiredPluginException -from safeeyes.rpc import RPCServer from safeeyes.translations import translate as _ from safeeyes.plugin_manager import PluginManager from safeeyes.core import SafeEyesCore @@ -63,7 +62,6 @@ def __init__(self, system_locale, config) -> None: self.context: typing.Any = {} self.plugins_manager = None self.settings_dialog_active = False - self.rpc_server = None self._status = "" self.system_locale = system_locale @@ -286,9 +284,6 @@ def do_startup(self): atexit.register(self.persist_session) - if self.config.get("use_rpc_server", True): - self.__start_rpc_server() - if ( not self.plugins_manager.needs_retry() and not self.required_plugin_dialog_active @@ -397,7 +392,6 @@ def quit(self): self.plugins_manager.stop() self.safe_eyes_core.stop() self.plugins_manager.exit() - self.__stop_rpc_server() self.persist_session() self.release() @@ -483,13 +477,6 @@ def save_settings(self, config): def restart(self, config, set_active=False): logging.info("Initialize SafeEyesCore with modified settings") - if self.rpc_server is None and config.get("use_rpc_server"): - # RPC server wasn't running but now enabled - self.__start_rpc_server() - elif self.rpc_server is not None and not config.get("use_rpc_server"): - # RPC server was running but now disabled - self.__stop_rpc_server() - # Restart the core and initialize the components self.config = config self.safe_eyes_core.initialize(config) @@ -576,13 +563,3 @@ def persist_session(self): utility.write_json(utility.SESSION_FILE_PATH, self.context["session"]) else: utility.delete(utility.SESSION_FILE_PATH) - - def __start_rpc_server(self): - if self.rpc_server is None: - self.rpc_server = RPCServer(self.config.get("rpc_port"), self.context) - self.rpc_server.start() - - def __stop_rpc_server(self): - if self.rpc_server is not None: - self.rpc_server.stop() - self.rpc_server = None From 89c7520c13622ea297f0641656cbdbee45f3c0c2 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sat, 2 Aug 2025 20:32:07 +0200 Subject: [PATCH 048/134] remove settings for rpc server (updates config version to 6.0.4) --- safeeyes/config/safeeyes.json | 4 +-- safeeyes/config/style/safeeyes_style.css | 4 --- safeeyes/glade/settings_dialog.glade | 34 ------------------------ safeeyes/ui/settings_dialog.py | 29 -------------------- 4 files changed, 1 insertion(+), 70 deletions(-) diff --git a/safeeyes/config/safeeyes.json b/safeeyes/config/safeeyes.json index 0230046f..06c05cba 100644 --- a/safeeyes/config/safeeyes.json +++ b/safeeyes/config/safeeyes.json @@ -1,6 +1,6 @@ { "meta": { - "config_version": "6.0.3" + "config_version": "6.0.4" }, "random_order": true, "allow_postpone": false, @@ -11,8 +11,6 @@ "short_break_duration": 15, "persist_state": false, "postpone_duration": 5, - "use_rpc_server": true, - "rpc_port": 7200, "shortcut_disable_time": 2, "shortcut_skip": 9, "shortcut_postpone": 65, diff --git a/safeeyes/config/style/safeeyes_style.css b/safeeyes/config/style/safeeyes_style.css index f2b62555..07698be6 100644 --- a/safeeyes/config/style/safeeyes_style.css +++ b/safeeyes/config/style/safeeyes_style.css @@ -99,10 +99,6 @@ border-radius: 5px; } -.warn_bar_rpc_server { - opacity: 0.9; - border-radius: 5px; -} .toolbar { background: black; diff --git a/safeeyes/glade/settings_dialog.glade b/safeeyes/glade/settings_dialog.glade index 43ab9a55..93a9735a 100644 --- a/safeeyes/glade/settings_dialog.glade +++ b/safeeyes/glade/settings_dialog.glade @@ -488,40 +488,6 @@ - - - 0 - warning - 1 - 1 - 1 - - - - - - 1 - 5 - 1 - - - 1 - start - Use RPC server to receive runtime commands - - - - - 1 - 1 - end - center - - - - diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 1189d109..76daf7e6 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -64,7 +64,6 @@ def __init__(self, application, config, on_save_settings): self.last_short_break_interval = config.get("short_break_interval") self.initializing = True self.infobar_long_break_shown = False - self.warn_bar_rpc_server_shown = False builder = utility.create_gtk_builder(SETTINGS_DIALOG_GLADE) @@ -88,11 +87,8 @@ def __init__(self, application, config, on_save_settings): self.switch_random_order = builder.get_object("switch_random_order") self.switch_postpone = builder.get_object("switch_postpone") self.switch_persist = builder.get_object("switch_persist") - self.switch_rpc_server = builder.get_object("switch_rpc_server") self.info_bar_long_break = builder.get_object("info_bar_long_break") - self.warn_bar_rpc_server = builder.get_object("warn_bar_rpc_server") self.info_bar_long_break.hide() - self.warn_bar_rpc_server.hide() self.window.connect("close-request", self.on_window_delete) builder.get_object("reset_menu").connect("clicked", self.on_reset_menu_clicked) @@ -104,8 +100,6 @@ def __init__(self, application, config, on_save_settings): self.spin_long_break_interval.connect( "value-changed", self.on_spin_long_break_interval_change ) - self.warn_bar_rpc_server.connect("close", self.on_warn_bar_rpc_server_close) - self.warn_bar_rpc_server.connect("response", self.on_warn_bar_rpc_server_close) builder.get_object("btn_add_break").connect("clicked", self.add_break) # Set the current values of input fields @@ -116,11 +110,6 @@ def __init__(self, application, config, on_save_settings): self.on_switch_postpone_activate( self.switch_postpone, self.switch_postpone.get_active() ) - # Add event listener to RPC server switch - self.switch_rpc_server.connect("state-set", self.on_switch_rpc_server_activate) - self.on_switch_rpc_server_activate( - self.switch_rpc_server, self.switch_rpc_server.get_active() - ) self.initializing = False @@ -148,7 +137,6 @@ def __initialize(self, config): self.switch_random_order.set_active(config.get("random_order")) self.switch_postpone.set_active(config.get("allow_postpone")) self.switch_persist.set_active(config.get("persist_state")) - self.switch_rpc_server.set_active(config.get("use_rpc_server")) self.infobar_long_break_shown = False def __create_break_item(self, break_config, is_short): @@ -357,22 +345,6 @@ def on_info_bar_long_break_close(self, infobar, *user_data): """Event handler for info bar close action.""" self.info_bar_long_break.hide() - def on_switch_rpc_server_activate(self, switch, enabled): - """Event handler to the state change of the rpc server switch. - - Show or hide the self.warn_bar_rpc_server based on the state of - the rpc server. - """ - if not self.initializing and not enabled and not self.warn_bar_rpc_server_shown: - self.warn_bar_rpc_server_shown = True - self.warn_bar_rpc_server.show() - if enabled: - self.warn_bar_rpc_server.hide() - - def on_warn_bar_rpc_server_close(self, warnbar, *user_data): - """Event handler for warning bar close action.""" - self.warn_bar_rpc_server.hide() - def add_break(self, button) -> None: """Event handler for add break button.""" dialog = NewBreakDialog( @@ -412,7 +384,6 @@ def on_window_delete(self, *args): self.config.set("random_order", self.switch_random_order.get_active()) self.config.set("allow_postpone", self.switch_postpone.get_active()) self.config.set("persist_state", self.switch_persist.get_active()) - self.config.set("use_rpc_server", self.switch_rpc_server.get_active()) for plugin in self.config.get("plugins"): if plugin["id"] in self.plugin_switches: plugin["enabled"] = self.plugin_switches[plugin["id"]].get_active() From 0a64b5fec6d94728885b27b1007151c434b84ddd Mon Sep 17 00:00:00 2001 From: deltragon Date: Sat, 2 Aug 2025 20:32:07 +0200 Subject: [PATCH 049/134] remove docs and translations for rpc server --- README.md | 1 - safeeyes/config/locale/safeeyes.pot | 12 ------------ .../io.github.slgobinath.SafeEyes.metainfo.xml | 4 ++-- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f4f08f3f..31ab631a 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,6 @@ This method has the same caveats about icons/window icons as running from source - Smart pause if system is idle - Multi-screen support - Customizable user interface -- RPC API to control externally - Command-line arguments to control the running instance - Customizable using plug-ins diff --git a/safeeyes/config/locale/safeeyes.pot b/safeeyes/config/locale/safeeyes.pot index a560a340..ea0ceee7 100644 --- a/safeeyes/config/locale/safeeyes.pot +++ b/safeeyes/config/locale/safeeyes.pot @@ -73,10 +73,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "" -# RPC not enabled message -msgid "Safe Eyes is running without an RPC server. Turn it on to use command-line arguments." -msgstr "" - # About dialog msgid "Close" msgstr "" @@ -142,14 +138,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml b/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml index 660aa6aa..45d2dab2 100644 --- a/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml +++ b/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml @@ -29,8 +29,8 @@

Remind you to take breaks with exercises to reduce RSI, Disable keyboard during breaks, Notification before and after breaks, Smart pause if system is idle, Multi-screen - support, Customizable user interface, RPC API to control externally, Command-line - arguments to control the running instance, Customizable using plug-ins + support, Customizable user interface, Command-line arguments to control the running + instance, Customizable using plug-ins

From c5d395752d2b064b3bffa5676ea80f2b8099fce0 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sat, 2 Aug 2025 21:13:40 +0200 Subject: [PATCH 050/134] remove rpc messages from translation files --- .../config/locale/ar/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/bg/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/bn/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/ca/LC_MESSAGES/safeeyes.po | 19 ------------------- .../config/locale/cs/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/da/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/de/LC_MESSAGES/safeeyes.po | 17 ----------------- .../locale/en_US/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/eo/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/es/LC_MESSAGES/safeeyes.po | 18 ------------------ .../config/locale/et/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/eu/LC_MESSAGES/safeeyes.po | 17 ----------------- .../config/locale/fa/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/fr/LC_MESSAGES/safeeyes.po | 18 ------------------ .../config/locale/he/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/hi/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/hu/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/id/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/it/LC_MESSAGES/safeeyes.po | 17 ----------------- .../config/locale/kn/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/ko/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/lt/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/lv/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/mk/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/mr/LC_MESSAGES/safeeyes.po | 15 --------------- .../config/locale/nb/LC_MESSAGES/safeeyes.po | 17 ----------------- .../config/locale/nl/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/pl/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/pt/LC_MESSAGES/safeeyes.po | 17 ----------------- .../locale/pt_BR/LC_MESSAGES/safeeyes.po | 17 ----------------- .../config/locale/ru/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/sk/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/sr/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/sv/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/ta/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/tr/LC_MESSAGES/safeeyes.po | 16 ---------------- .../config/locale/ug/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/uk/LC_MESSAGES/safeeyes.po | 16 ---------------- .../locale/uz_Latn/LC_MESSAGES/safeeyes.po | 14 -------------- .../config/locale/vi/LC_MESSAGES/safeeyes.po | 16 ---------------- .../locale/zh_CN/LC_MESSAGES/safeeyes.po | 15 --------------- .../locale/zh_TW/LC_MESSAGES/safeeyes.po | 14 -------------- 42 files changed, 663 deletions(-) diff --git a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po index ed5c9c71..84747bc8 100644 --- a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po @@ -87,12 +87,6 @@ msgstr "أظهر حالة برنامج safeeyes المشتغل حاليا و أ msgid "Safe Eyes is not running" msgstr "SafeEyes لا يعمل" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "يعمل Safe Eyes الآن دون خادوم RPC. شغِّله لكي تستعمل معطيات سطر الأوامر." - # About dialog msgid "Close" msgstr "أغلق" @@ -160,14 +154,6 @@ msgstr "مكّن تأخير الاستراحات" msgid "Persist the internal state" msgstr "ثبّت الحالة الداخلية" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "استعمل خادوم RPC لاستقبال الأوامر في وقت التشغيل" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "قد لا تعمل أوامر سطر الأوامر من غير خادوم RPC" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "الاستراحات الطويلة يجب أن تكون من مضاعفات مدة الاستراحات القصيرة" diff --git a/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po index 04d9fa81..b211b0ab 100644 --- a/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po @@ -85,12 +85,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "Затваряне" @@ -158,14 +152,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po index 170f0f33..9d8894f0 100644 --- a/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po @@ -85,12 +85,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "Safe Eyes চলছে না" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "" @@ -158,14 +152,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po index de791808..e59426d0 100644 --- a/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "Mostra l'estat de la instància de safeeyes en funcionament i surt" msgid "Safe Eyes is not running" msgstr "Safe Eyes no s'està executant" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes s'està executant sense un servidor RPC. Engegueu-lo per usar els " -"arguments de la línia d'ordres." - # About dialog msgid "Close" msgstr "Tanca" @@ -163,17 +155,6 @@ msgstr "Permet posposar les pauses" msgid "Persist the internal state" msgstr "Persisteix l'estat intern" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" -"Empra el servidor RPC per a rebre els comandaments del temps d'execució" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"Sense el servidor RPC, és possible que les ordres a la línia d'ordres no " -"funcionin" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po index 2baf8068..339518e5 100644 --- a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "Popis argumentu příkazového řádku" msgid "Safe Eyes is not running" msgstr "Stavová zpráva" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes je spuštěné s tím, že RPC server je vypnutý. Aby bylo možné " -"používat argumenty příkazového řádku, je třeba RPC server zapnout." - # About dialog msgid "Close" msgstr "Zavřít" @@ -162,14 +154,6 @@ msgstr "Umožnit přeskakování přestávek" msgid "Persist the internal state" msgstr "Zachovat vnitřní stav" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Použít RPC server pro obdržení příkazů za chodu" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Bez RPC serveru, argumenty na příkazovém řádku nemusí fungovat" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Je třeba, aby dlouhé přestávky byly násobkem krátkých přestávek" diff --git a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po index 3d25ce39..87f8ba7c 100644 --- a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "print status for at køre Safe Eyes instans og afslut" msgid "Safe Eyes is not running" msgstr "Safe Eyes kører ikke" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes kører med RPC-server deaktiveret. Aktivér RPC-serveren for at " -"bruge kommandolinjeargumenter." - # About dialog msgid "Close" msgstr "Luk" @@ -160,14 +152,6 @@ msgstr "Tillad at udskyde pauser" msgid "Persist the internal state" msgstr "Fortsætter i interntilstand" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Brug RPC-server til at modtage runtime-kommandoer" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Uden RPC-serveren fungerer kommandolinjekommandoer muligvis ikke" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po index 18469d80..f05c6024 100644 --- a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "Status der laufenden SafeEyes-Instanz ausgeben und beenden" msgid "Safe Eyes is not running" msgstr "Safe Eyes wird nicht ausgeführt" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes läuft ohne RPC-Server. Aktivieren Sie ihn, um Kommandozeilen-" -"Befehle nutzen zu können." - # About dialog msgid "Close" msgstr "Schließen" @@ -163,15 +155,6 @@ msgstr "Aufschieben von Pausen zulassen" msgid "Persist the internal state" msgstr "Internen Zustand beibehalten" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" -"RPC-Server benutzen, um Befehle während der Laufzeit empfangen zu können" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Ohne RPC-Server könnten Kommandozeilen-Befehle nicht funktionieren" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po index 2e7550f3..564143d9 100644 --- a/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "print the status of running safeeyes instance and exit" msgid "Safe Eyes is not running" msgstr "Safe Eyes is not running" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." - # About dialog msgid "Close" msgstr "Close" @@ -163,14 +155,6 @@ msgstr "Allow postponing breaks" msgid "Persist the internal state" msgstr "Persist the internal state" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Use RPC server to receive runtime commands" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Without the RPC server, command-line commands may not work" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Long break interval must be a multiple of short break interval" diff --git a/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po index d3c55acb..57adffe9 100644 --- a/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "montri la staton de la rulanta apero de safeeyes kaj eliri" msgid "Safe Eyes is not running" msgstr "Safe Eyes nun ne rulas" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes estas rulanta sen RPC-servilo. Ŝaltu ĝin por uzi komandliniajn " -"argumentojn." - # About dialog msgid "Close" msgstr "Malfemi" @@ -161,14 +153,6 @@ msgstr "Lasi prokrasti paŭzojn" msgid "Persist the internal state" msgstr "Daŭri la internan staton" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Uzi RPC-servilon por ricevi rultempajn komandojn" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Sen la RPC-servilo, komandliniaj komandoj eble ne funkcias" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Longa paŭza intertempo devas esti oblo de mallonga paŭza intertempo" diff --git a/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po index 4de46a5e..f4d577fc 100644 --- a/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "mostrar el estado del ejemplar de safeeyes en ejecución y salir" msgid "Safe Eyes is not running" msgstr "Safe Eyes no se está ejecutando" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes está funcionando sin un servidor RPC. Actívelo para usar " -"argumentos de línea de órdenes." - # About dialog msgid "Close" msgstr "Cerrar" @@ -162,16 +154,6 @@ msgstr "Permitir posponer los descansos" msgid "Persist the internal state" msgstr "Estado interno persistente" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Utilizar el servidor RPC para recibir órdenes en tiempo de ejecución" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"Sin el servidor RPC, las órdenes ejecutadas en el terminal pueden no " -"funcionar" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po index c0c9c510..214eac38 100644 --- a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "väljasta hetkel töötava safeeyes instantsi staatus ja välju" msgid "Safe Eyes is not running" msgstr "Safe Eyes ei tööta hetkel" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes töötab RPC serverita. Käsurea parameetrite kasutamiseks lülita ta " -"sisse." - # About dialog msgid "Close" msgstr "Sulge" @@ -162,14 +154,6 @@ msgstr "Pauside edasilükkamine lubatud" msgid "Persist the internal state" msgstr "Sisemise seisundi säilitamine" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Kasuta RPC serverit käitusaja käskude vastuvõtuks" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Käsurea parameetrid ei pruugi töötada RPC serverita" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Pika pausi intervall peab olema lühikese pausi pikkuse kordne" diff --git a/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po index 7408e20e..7b474dc4 100644 --- a/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "inprimatu uneko safeeyes instantziaren egoera eta irten" msgid "Safe Eyes is not running" msgstr "Safe Eyes ez dago martxan" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"RPC zerbitzaririk gabe ari da exekutatzen Safe Eyes. Piztu zerbitzaria " -"komando-lerroko argumentuak erabiltzeko." - # About dialog msgid "Close" msgstr "Itxi" @@ -162,15 +154,6 @@ msgstr "Baimendu etenaldiak atzeratzea" msgid "Persist the internal state" msgstr "Eutsi barruko egoerari" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Erabili RPC zerbitzaria exekuzio-denboraren komanduak jasotzeko" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"RPC zerbitzaririk gabe, komandu-lineako komanduek ez dute funtzionatuko" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po index 76a4f4b5..ee8dd2f5 100644 --- a/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "چاپ وضعیت نمونهٔ محافظ چشم در جال اجرا و msgid "Safe Eyes is not running" msgstr "محافظ چشم اجرا نمی‌شود" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"محافظ چشم دارد بدون کارساز RPC اجرا می‌شود. برای استفاده از آرگومان‌های خط " -"فرمان، روشنش کنید." - # About dialog msgid "Close" msgstr "بستن" @@ -162,14 +154,6 @@ msgstr "اجازه به تعویق استراحت‌ها" msgid "Persist the internal state" msgstr "ادامه وضعیت داخلی" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "استفاده از کارساز RPC برای دریافت دستورات زمان اجرا" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "بدون کارساز RPC ممکن است دستورات خط فرمان نکنند" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "دورهٔ استراحت طولانی باید مضربی از دوره‌های استراحت کوتاه باشد" diff --git a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po index 37223754..8573921d 100644 --- a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "imprimer l’état de l’instance safeeyes en cours et fermer" msgid "Safe Eyes is not running" msgstr "Safe Eyes n’est pas en fonction" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes est en fonction alors que le serveur RPC est désactivé. Activez le " -"serveur RPC afin d’utiliser des arguments en ligne de commande." - # About dialog msgid "Close" msgstr "Fermer" @@ -163,16 +155,6 @@ msgstr "Permettre le report des pauses" msgid "Persist the internal state" msgstr "Conserver l’état interne" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Utiliser le serveur RPC pour recevoir des commandes d’exécution" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"Sans le serveur RPC, les commandes en ligne de commande pourraient ne pas " -"fonctionner" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po index 32910ebb..72b7292d 100644 --- a/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "print the status of running safeeyes instance and exit" msgid "Safe Eyes is not running" msgstr "הגנה על העיניים אינה פעילה" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"הגנה על העיניים מופעלת ללא שרת RPC. יש להפעיל אותו כדי להשתמש במשתנים משורת " -"הפקודה." - # About dialog msgid "Close" msgstr "סגירה" @@ -160,14 +152,6 @@ msgstr "לאפשר לעכב הפסקות" msgid "Persist the internal state" msgstr "לשמור על המצב הפנימי" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "להשתמש בשרת RPC כדי לקבל פקודות זמן ריצה" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "ללא שרת RPC, פקודות שורת פקודה עשויות לא לעבוד" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "הפרש ארוך בין הפסקות חייב להיות מכפלות של הפרש קצר בין הפסקות" diff --git a/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po index 9091fcb9..7ca23fdc 100644 --- a/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po @@ -85,12 +85,6 @@ msgstr "सेफ आईज दृष्टांत स्थिति को msgid "Safe Eyes is not running" msgstr "सेफ आईज नहीं चल रहा है" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "बंद करें" @@ -158,14 +152,6 @@ msgstr "ब्रेक स्थगित करने की अनुमत msgid "Persist the internal state" msgstr "आंतरिक अवस्था को जारी रखें" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "लम्बी ब्रेक अंतराल लघु ब्रेक अंतराल के एक बहुमान होना चाहिए" diff --git a/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po index 54387c23..b7dcb913 100644 --- a/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po @@ -85,12 +85,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "Bezárás" @@ -158,14 +152,6 @@ msgstr "Engedi a pihenők elhalasztását" msgid "Persist the internal state" msgstr "Belső állapot elmentése" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Hosszú pihenő fel kell legyen bontva több kicsire" diff --git a/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po index 1efe9a0d..6c619b24 100644 --- a/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "cetak status menjalankan instance safeeyes dan keluar" msgid "Safe Eyes is not running" msgstr "Safe Eyes tidak berjalan" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes berjalan tanpa server RPC. Aktifkan untuk menggunakan command-line " -"arguments." - # About dialog msgid "Close" msgstr "Tutup" @@ -160,14 +152,6 @@ msgstr "Izinkan penundaan istirahat" msgid "Persist the internal state" msgstr "Pertahankan kondisi internal" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Gunakan server RPC untuk menerima perintah runtime" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Tanpa server RPC, perintah command-line mungkin tidak akan berfungsi" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po index 81f1be7a..05ae2fac 100644 --- a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "stampa lo stato di esecuzione di Safe Eyes ed esci" msgid "Safe Eyes is not running" msgstr "Safe Eyes non è avviato" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes è in esecuzione senza un server RPC. Apri Safe Eyes per utilizzare " -"la linea di comando." - # About dialog msgid "Close" msgstr "Chiudi" @@ -163,15 +155,6 @@ msgstr "Consenti di posporre le pause" msgid "Persist the internal state" msgstr "Persistenza dello stato interno" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Utilizza il server RPC per ricevere i comandi" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"Senza il server RPC, i comandi a riga di comando non possono funzionare" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po index 0aa04739..95dfd80f 100644 --- a/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "ಚಾಲನೆಯಲ್ಲಿರುವ ಸೇಫ್ಐಸ್ ಸ್ಥ msgid "Safe Eyes is not running" msgstr "ಸೇಫ್‌ಐಸ್ ನೆಡುಯುತ್ತಿಲ್ಲ" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"RPC ಸರ್ವರ್ ಇಲ್ಲದೆ ಸೇಫ್ಐಸ್ ಚಾಲನೆಯಲ್ಲಿದೆ. ಕಮಾಂಡ್ ಲೈನ್ ಆರ್ಗ್ಯುಮೆಂಟ್‌ಗಳನ್ನು ಬಳಸಲು ಅದನ್ನು ಆನ್ " -"ಮಾಡಿ." - # About dialog msgid "Close" msgstr "ಮುಚ್ಚಿರಿ" @@ -160,14 +152,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po index 52d27051..509b0973 100644 --- a/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "실행 중인 세이프아이즈 객체를 종료하고 상태 출력" msgid "Safe Eyes is not running" msgstr "세이프 아이즈가 실행 중이 아닙니다" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"세이프 아이즈가 RPC 서버없이 실행 중입니다. 명령줄 인수를 사용하려면 켜주세" -"요." - # About dialog msgid "Close" msgstr "닫기" @@ -160,14 +152,6 @@ msgstr "휴식 미루기 허용" msgid "Persist the internal state" msgstr "내부 상태 유지" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "RPC 서버를 사용해 런타임 명령 수신" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "RPC 서버가 없으면 명령줄 명령이 작동하지 않을 수 있습니다" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "긴 휴식 기간은 짧은 휴식 기간의 배수여야 합니다" diff --git a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po index 331653af..97940936 100644 --- a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "išvesti paleisto safeeyes egzemplioriaus būseną ir išeiti" msgid "Safe Eyes is not running" msgstr "Safe Eyes nėra paleista" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes veikia be RPC serverio. Įjunkite jį norėdami naudoti komandų " -"eilutės argumentus." - # About dialog msgid "Close" msgstr "Užverti" @@ -163,14 +155,6 @@ msgstr "Leisti atidėti pertraukas" msgid "Persist the internal state" msgstr "Išlaikyti vidinę būseną" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Naudoti RPC serverį, siekiant gauti vykdymo aplinkos komandas" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Be RPC serverio, komandų eilutės komandos gali neveikti" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po index 6ab1d280..25e3915b 100644 --- a/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po @@ -71,12 +71,6 @@ msgstr "drukāt darbojošās safeeyes instances statusu un iziet" msgid "Safe Eyes is not running" msgstr "Safe Eyes nav palaists" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - msgid "Close" msgstr "Aizvērt" @@ -132,14 +126,6 @@ msgstr "Ļaut atlikt pārtraukumus" msgid "Persist the internal state" msgstr "Saglabāt iekšējo stāvokli" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - msgid "Long break interval must be a multiple of short break interval" msgstr "Garā pārtraukuma laikam jādalās ar īsā pārtraukuma laiku" diff --git a/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po index 24368df1..fb986327 100644 --- a/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po @@ -85,12 +85,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "" @@ -160,14 +154,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po index d8df6f69..48bf4d25 100644 --- a/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po @@ -85,13 +85,6 @@ msgstr "सुरक्षित डोळ्यांची उदाहरण msgid "Safe Eyes is not running" msgstr "सेफ डोळे चालत नाहीत" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"सेफ डोळे आरपीसी सर्व्हरविना चालू आहेत. कमांड-लाइन वितर्क वापरण्यासाठी ते चालू करा." - # About dialog msgid "Close" msgstr "बंद" @@ -159,14 +152,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po index 2a19f4f7..96e59be5 100644 --- a/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po @@ -85,15 +85,6 @@ msgstr "skriv ut status for kjørende Øyetrygg-instans og avslutt" msgid "Safe Eyes is not running" msgstr "Øyetrygg kjører ikke" -# RPC not enabled message -#, fuzzy -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Øyetrygg kjører uten RPC-tjener. Slå den på for å bruke kommandolinje-" -"argumenter." - # About dialog msgid "Close" msgstr "Lukk" @@ -161,14 +152,6 @@ msgstr "Tillat utsettelse av pauser" msgid "Persist the internal state" msgstr "Fortsett i inngangstilstand" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Bruk RPC-tjener for å motta kjøremiljøkommandoer" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Uten RPC-tjeneren, vil ikke kommandolinje-argumenter virke" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Tiden mellom lange pauser må være en inndeling av de små pausene" diff --git a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po index 8d988eea..728d396c 100644 --- a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "de status van de draaiende safeeyes-instantie printen en afsluiten" msgid "Safe Eyes is not running" msgstr "Safe Eyes draait niet" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes draait zonder RPC-server. Schakel dit in om opdrachtregelopties te " -"gebruiken." - # About dialog msgid "Close" msgstr "Sluiten" @@ -163,14 +155,6 @@ msgstr "Overslaan van pauzes toestaan" msgid "Persist the internal state" msgstr "Interne status aanhouden" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "RPC-server gebruiken om opdrachten te ontvangen" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Zonder RPC-server werken opdrachtregelopties mogelijk niet" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po index 5e992c92..ea50c28a 100644 --- a/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "wydrukuj status uruchomionej instancji safeeyes i zakończ" msgid "Safe Eyes is not running" msgstr "Safe Eyes nie jest uruchomiony" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes działa bez serwera RPC. Włącz, aby używać argumentów wiersza " -"polecenia." - # About dialog msgid "Close" msgstr "Zamknij" @@ -163,14 +155,6 @@ msgstr "Pozwól na przełożenie przerw" msgid "Persist the internal state" msgstr "Utrzymaj stan wewnętrzny" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Użyj serwera RPC, aby odbierać polecenia środowiska wykonawczego" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Bez serwera RPC polecenia wiersza polecenia mogą nie działać" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Długa przerwa musi być poprzedzona kilkoma krótkimi" diff --git a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po index cf0340b6..de233af1 100644 --- a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "mostrar o estado de execução do Safe Eyes e sair" msgid "Safe Eyes is not running" msgstr "O Safe Eyes não está em execução" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"O Safe Eyes está em execução sem um servidor RPC (interno). Acionar um " -"servidor RPC para usar argumentos de linha de comando." - # About dialog msgid "Close" msgstr "Fechar" @@ -163,15 +155,6 @@ msgstr "Permitir adiamento de pausas" msgid "Persist the internal state" msgstr "Manter o estado interno" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Utilizar um servidor RPC (interno) para receber comandos de runtime" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"Sem o servidor RPC, os comandos da linha de comandos podem não funcionar" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po index 2e8bdf32..d3ae6a7c 100644 --- a/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "mostrar o estado de execução do safeeyes e sair" msgid "Safe Eyes is not running" msgstr "O Safe Eyes não está em execução" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"O Safe Eyes está em execução sem um servidor RPC ligado. Ligar para usar " -"argumentos de linha de comando." - # About dialog msgid "Close" msgstr "Fechar" @@ -162,15 +154,6 @@ msgstr "Permitir adiamento de pausas" msgid "Persist the internal state" msgstr "Persistir o estado interno" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Utilize o servidor RPC para receber comandos de tempo de execução" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" -"Sem o servidor RPC, os comandos da linha de comandos podem não funcionar" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po index 676c9b68..45fdca19 100644 --- a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "вывести статус запущенного экземпляра msgid "Safe Eyes is not running" msgstr "Safe Eyes не запущен" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Программа Safe Eyes запущена без RPC сервера. Для использования аргументов " -"командной строки запустите RPC сервер." - # About dialog msgid "Close" msgstr "Закрыть" @@ -163,14 +155,6 @@ msgstr "Разрешить откладывание перерывов" msgid "Persist the internal state" msgstr "Сохранять внутреннее состояние" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Используйте RPC сервер для получения команд во время работы приложения" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Без RPC сервера команды из командной строки могут не сработать" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Время длинного перерыва должно быть кратно времени короткого перерыва" diff --git a/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po index d733438d..33be37c1 100644 --- a/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "vypíše, či safeeyes beží a skončí" msgid "Safe Eyes is not running" msgstr "SafeEyes nebeží" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes je spustený bez RPC serveru. Zapnite ho pre použitie argumentov v " -"príkazovom riadku." - # About dialog msgid "Close" msgstr "Zavrieť" @@ -163,14 +155,6 @@ msgstr "Povoliť odkladanie prestávok" msgid "Persist the internal state" msgstr "Zachovať vnútorný stav" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Použiť RPC server na prijatie spúšťacích príkazov" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Bez RPC serveru príkazy v príkazovom riadku nemusia fungovať" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Dlhá prestávka musí byť násobkom krátkej prestávky" diff --git a/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po index 252a81f0..2a27a8b5 100644 --- a/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "Одштампајте статус тренутне сесије safeey msgid "Safe Eyes is not running" msgstr "Safe Eyes није укључен" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes ради без RPC (Remote Procedure Call) сервера. Укључите га како " -"бисте користили командне-линије." - # About dialog msgid "Close" msgstr "Затворите" @@ -163,14 +155,6 @@ msgstr "Дозволи одлагање пауза" msgid "Persist the internal state" msgstr "Задржи интерни статус" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Користите RPC сервер како бисте примили runtime команде" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Без RPC сервера, унос путем командних-линија можда неће бити успешан" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po index 32b94929..1d7ba661 100644 --- a/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "skriv ut status för körning av safeeyes-instansen och avsluta" msgid "Safe Eyes is not running" msgstr "Safe Eyes körs inte" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes kör utan \"RPC\"-server. Slå på den för att kunna använda " -"kommandoradsargument." - # About dialog msgid "Close" msgstr "Stäng" @@ -160,14 +152,6 @@ msgstr "Tillåt att skjuta upp pauser" msgid "Persist the internal state" msgstr "Bevara det interna tillståndet" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Använd RPC-server för att ta emot runtime-kommandon" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Utan RPC-servern kanske kommandoradskommandon inte fungerar" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Lång paus intervall måste vara en multipel av kort paus intervall" diff --git a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po index 2cf73009..0ae40ce5 100644 --- a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "இயங்கும் Safe Eyes நிலையை காண்பி msgid "Safe Eyes is not running" msgstr "Safe Eyes தற்போது இயங்கவில்லை" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes RPC சேவையகமின்றி இயங்குகிறது.முனையக் கட்டளைகளைப் பயன்படுத்த RPC சேவையகத்தை " -"இயக்கவும்." - # About dialog msgid "Close" msgstr "மூடு" @@ -163,14 +155,6 @@ msgstr "இடைவேளையை ஒத்திவைக்க அனும msgid "Persist the internal state" msgstr "உள்ளக நிலையை தக்கவைத்து கொள்ளவும்" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "இயக்க நேர கட்டளைகளைப் பெற RPC சேவையகத்தைப் செயற்படுத்தவும்" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "RPC சேவையகம் இல்லாமல், முனைய கட்டளைகள் இயங்காது" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po index 6c5cb1ec..1d6024da 100644 --- a/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "çalışan Safe Eyes örneğinin durumunu yaz ve çık" msgid "Safe Eyes is not running" msgstr "Safe Eyes çalışmıyor" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes bir RPC sunucusu olmadan çalışıyor. Komut satırı argümanlarını " -"kullanmak için açın." - # About dialog msgid "Close" msgstr "Kapat" @@ -163,14 +155,6 @@ msgstr "Molaları ertelemeye izin ver" msgid "Persist the internal state" msgstr "İç durum kalıcı olsun" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Çalışma zamanı komutlarını almak için RPC sunucusu kullan" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "RPC sunucusu olmadan, komut satırı komutları çalışmayabilir" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Uzun mola aralığı, kısa mola aralığının katı olmalıdır" diff --git a/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po index 303092d2..0aca020a 100644 --- a/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po @@ -84,12 +84,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "" @@ -157,14 +151,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po index e990483b..914d2664 100644 --- a/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po @@ -86,14 +86,6 @@ msgstr "виведення стану виконуваного екземпля msgid "Safe Eyes is not running" msgstr "Safe Eyes не працює" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes працює з відключеним сервером RPC. Увімкніть сервер RPC для " -"використання аргументів вказівкового рядка." - # About dialog msgid "Close" msgstr "Закрити" @@ -161,14 +153,6 @@ msgstr "Дозволити перенесення перерв" msgid "Persist the internal state" msgstr "Утримувати внутрішній стан" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Використовувати RPC-сервер для отримання вказівок виконання" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Без сервера RPC вказівки вказівкового рядка можуть не працювати" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po index 8d218e30..c9ed80e7 100644 --- a/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po @@ -84,12 +84,6 @@ msgstr "" msgid "Safe Eyes is not running" msgstr "" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" - # About dialog msgid "Close" msgstr "" @@ -157,14 +151,6 @@ msgstr "" msgid "Persist the internal state" msgstr "" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "" diff --git a/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po index bd4f7865..53c48ee8 100644 --- a/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po @@ -85,14 +85,6 @@ msgstr "in trạng thái chạy phiên bản Safe Eyes và thoát" msgid "Safe Eyes is not running" msgstr "Safe Eyes không hoạt động" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes đang chạy mà không có máy chủ RPC. Bạn hãy bật nó lên để sử dụng " -"tham số dòng lệnh nhé." - # About dialog msgid "Close" msgstr "Đóng" @@ -163,14 +155,6 @@ msgstr "Cho phép tạm hoãn nghỉ ngơi" msgid "Persist the internal state" msgstr "Duy trì nội trạng thái" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "Sử dụng máy chủ RPC để nhận lệnh thời gian chạy" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "Nếu không có máy chủ RPC, các lệnh dòng lệnh có thể không hoạt động" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "Thời gian nghỉ dài phải là bội số của thời gian nghỉ ngắn" diff --git a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po index d2326fcf..071f34c0 100644 --- a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po @@ -85,13 +85,6 @@ msgstr "显示Safe Eyes运行状态后退出" msgid "Safe Eyes is not running" msgstr "Safe Eyes 没有运行" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "" -"Safe Eyes 正在没有RPC服务器的情况下运行。打开RPC服务器可以使用命令行参数。" - # About dialog msgid "Close" msgstr "关闭" @@ -161,14 +154,6 @@ msgstr "允许推迟休息" msgid "Persist the internal state" msgstr "支持内部状态" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "使用RPC服务器接收运行时命令" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "没有RPC服务器,命令行命令可能无法正常工作" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "长休息的间隔时间必须是短休息间隔时间的倍数" diff --git a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po index 1f4693bc..54a27153 100644 --- a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po @@ -85,12 +85,6 @@ msgstr "顯示目前正在執行的 safeeyes 狀態並離開" msgid "Safe Eyes is not running" msgstr "Safe Eyes 未啟動" -# RPC not enabled message -msgid "" -"Safe Eyes is running without an RPC server. Turn it on to use command-line " -"arguments." -msgstr "Safe Eyes 運行中不使用 RPC 伺服器。使用終端指令參數進行開啟。" - # About dialog msgid "Close" msgstr "關閉" @@ -158,14 +152,6 @@ msgstr "允許推遲休息" msgid "Persist the internal state" msgstr "維持內部計算狀態" -# Settings dialog -msgid "Use RPC server to receive runtime commands" -msgstr "使用 RPC 伺服器來接收運行時指令" - -# Settings dialog -msgid "Without the RPC server, command-line commands may not work" -msgstr "不使用 RPC 伺服器,終端指令可能法作用" - # Settings dialog msgid "Long break interval must be a multiple of short break interval" msgstr "長休息間隔須為短休息間隔的倍數" From 9da380647a64990490ea1cce0a0a515d2145236c Mon Sep 17 00:00:00 2001 From: Sebastian Dorobantu Date: Sun, 3 Aug 2025 18:15:59 +0200 Subject: [PATCH 051/134] add option to postpone breaks in seconds --- safeeyes/config/safeeyes.json | 1 + safeeyes/core.py | 14 ++++++++---- safeeyes/glade/settings_dialog.glade | 34 +++++++++++++++++++++------- safeeyes/ui/settings_dialog.py | 13 ++++++++++- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/safeeyes/config/safeeyes.json b/safeeyes/config/safeeyes.json index 06c05cba..47e20fa8 100644 --- a/safeeyes/config/safeeyes.json +++ b/safeeyes/config/safeeyes.json @@ -11,6 +11,7 @@ "short_break_duration": 15, "persist_state": false, "postpone_duration": 5, + "postpone_unit": "minutes", "shortcut_disable_time": 2, "shortcut_skip": 9, "shortcut_postpone": 65, diff --git a/safeeyes/core.py b/safeeyes/core.py index 386ea1ad..09f71733 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -73,9 +73,11 @@ def initialize(self, config: Config): logging.info("Initialize the core") self.pre_break_warning_time = config.get("pre_break_warning_time") self._break_queue = BreakQueue.create(config, self.context) - self.default_postpone_duration = ( - config.get("postpone_duration") * 60 - ) # Convert to seconds + self.default_postpone_duration = int(config.get("postpone_duration")) + self.postpone_unit = config.get("postpone_unit") + if self.postpone_unit == "minutes": + self.default_postpone_duration *= 60 + self.postpone_duration = self.default_postpone_duration def start(self, next_break_time=-1, reset_breaks=False) -> None: @@ -236,7 +238,11 @@ def __scheduler_job(self) -> None: ) # Wait for the pre break warning period - logging.info("Waiting for %d minutes until next break", (time_to_wait / 60)) + if self.postpone_unit == "minutes": + logging.info("Waiting for %d minutes until next break", (time_to_wait / 60)) + else: + logging.info("Waiting for %d seconds until next break", time_to_wait) + self.__wait_for(time_to_wait) logging.info("Pre-break waiting is over") diff --git a/safeeyes/glade/settings_dialog.glade b/safeeyes/glade/settings_dialog.glade index 93a9735a..03844853 100644 --- a/safeeyes/glade/settings_dialog.glade +++ b/safeeyes/glade/settings_dialog.glade @@ -412,28 +412,46 @@
- 1 5 - 1 + 1 + start + Postponement duration in 1 + + + + start - Postponement duration (in minutes) + center + true + false + 0 + + + + minutes + seconds + + + - 1 - 1 - end - center - 1 adjust_postpone_duration + 1 1 + end + True + True 1 + 1 if-valid + center 1 + 1 diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 76daf7e6..f10996d0 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -80,6 +80,7 @@ def __init__(self, application, config, on_save_settings): self.spin_long_break_interval = builder.get_object("spin_long_break_interval") self.spin_time_to_prepare = builder.get_object("spin_time_to_prepare") self.spin_postpone_duration = builder.get_object("spin_postpone_duration") + self.dropdown_postpone_unit = builder.get_object("dropdown_postpone_unit") self.spin_disable_keyboard_shortcut = builder.get_object( "spin_disable_keyboard_shortcut" ) @@ -130,6 +131,11 @@ def __initialize(self, config): self.spin_long_break_interval.set_value(config.get("long_break_interval")) self.spin_time_to_prepare.set_value(config.get("pre_break_warning_time")) self.spin_postpone_duration.set_value(config.get("postpone_duration")) + # Set the active item in the dropdown based on the postpone unit + if config.get("postpone_unit") == "seconds": + self.dropdown_postpone_unit.set_selected(1) + else: + self.dropdown_postpone_unit.set_selected(0) self.spin_disable_keyboard_shortcut.set_value( config.get("shortcut_disable_time") ) @@ -140,7 +146,7 @@ def __initialize(self, config): self.infobar_long_break_shown = False def __create_break_item(self, break_config, is_short): - """Create an entry for break to be listed in the break tab.""" + """Create an entry for break be listed in the break tab.""" parent_box = self.box_long_breaks if is_short: parent_box = self.box_short_breaks @@ -317,6 +323,8 @@ def on_switch_postpone_activate(self, switch, state): state of the postpone switch. """ self.spin_postpone_duration.set_sensitive(self.switch_postpone.get_active()) + self.dropdown_postpone_unit.set_sensitive(self.switch_postpone.get_active()) + def on_spin_short_break_interval_change(self, spin_button, *value): """Event handler for value change of short break interval.""" @@ -376,6 +384,9 @@ def on_window_delete(self, *args): self.config.set( "postpone_duration", self.spin_postpone_duration.get_value_as_int() ) + self.config.set( + "postpone_unit", self.dropdown_postpone_unit.get_selected_item().get_string() + ) self.config.set( "shortcut_disable_time", self.spin_disable_keyboard_shortcut.get_value_as_int(), From da182ba09286aac7c2d6fe3608e4318ba9902e15 Mon Sep 17 00:00:00 2001 From: Sebastian Dorobantu Date: Mon, 4 Aug 2025 00:55:02 +0200 Subject: [PATCH 052/134] run validate_po.py and ruff things --- safeeyes/config/locale/safeeyes.pot | 9 +++++++++ safeeyes/ui/settings_dialog.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/safeeyes/config/locale/safeeyes.pot b/safeeyes/config/locale/safeeyes.pot index ea0ceee7..13371059 100644 --- a/safeeyes/config/locale/safeeyes.pot +++ b/safeeyes/config/locale/safeeyes.pot @@ -558,3 +558,12 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" + +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index f10996d0..330ff9c1 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -325,7 +325,6 @@ def on_switch_postpone_activate(self, switch, state): self.spin_postpone_duration.set_sensitive(self.switch_postpone.get_active()) self.dropdown_postpone_unit.set_sensitive(self.switch_postpone.get_active()) - def on_spin_short_break_interval_change(self, spin_button, *value): """Event handler for value change of short break interval.""" short_break_interval = self.spin_short_break_interval.get_value_as_int() @@ -385,7 +384,8 @@ def on_window_delete(self, *args): "postpone_duration", self.spin_postpone_duration.get_value_as_int() ) self.config.set( - "postpone_unit", self.dropdown_postpone_unit.get_selected_item().get_string() + "postpone_unit", + self.dropdown_postpone_unit.get_selected_item().get_string(), ) self.config.set( "shortcut_disable_time", From c760f8df01598540ec81b12786d4e6968b4a1a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 3 Aug 2025 21:50:40 +0200 Subject: [PATCH 053/134] Translated using Weblate (Estonian) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/et/ --- safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po index 214eac38..1c86c8d5 100644 --- a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-05-12 09:03+0000\n" -"Last-Translator: Priit Jõerüüt \n" +"PO-Revision-Date: 2025-08-04 20:02+0000\n" +"Last-Translator: Priit Jõerüüt \n" "Language-Team: Estonian \n" "Language: et\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.12-dev\n" +"X-Generator: Weblate 5.13-dev\n" # Short break msgid "Gently close your eyes" @@ -579,12 +579,14 @@ msgstr "" msgid "Customizing the postpone and skip shortcuts does not work on Wayland." msgstr "" +"Edasilükkamise ja vahelejätmise kiirklahvide kohendamine ei toimi Waylandi " +"puhul." msgid "Safe Eyes - Error" -msgstr "" +msgstr "Safe Eyes - viga" msgid "A required plugin is missing dependencies!" -msgstr "" +msgstr "Nõutaval pluginal on sõltuvused puudu!" # Short break #~ msgid "Tightly close your eyes" From b18589a60138ec1dde3df89fa6ebb2289ae02d84 Mon Sep 17 00:00:00 2001 From: Sebastian Dorobantu Date: Mon, 4 Aug 2025 23:55:32 +0200 Subject: [PATCH 054/134] fix: ensure behavior defaults to minutes and correct random typo added --- safeeyes/core.py | 8 ++++---- safeeyes/ui/settings_dialog.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 09f71733..deac1ee9 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -75,7 +75,7 @@ def initialize(self, config: Config): self._break_queue = BreakQueue.create(config, self.context) self.default_postpone_duration = int(config.get("postpone_duration")) self.postpone_unit = config.get("postpone_unit") - if self.postpone_unit == "minutes": + if self.postpone_unit != "seconds": self.default_postpone_duration *= 60 self.postpone_duration = self.default_postpone_duration @@ -238,10 +238,10 @@ def __scheduler_job(self) -> None: ) # Wait for the pre break warning period - if self.postpone_unit == "minutes": - logging.info("Waiting for %d minutes until next break", (time_to_wait / 60)) - else: + if self.postpone_unit == "seconds": logging.info("Waiting for %d seconds until next break", time_to_wait) + else: + logging.info("Waiting for %d minutes until next break", (time_to_wait / 60)) self.__wait_for(time_to_wait) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 330ff9c1..9470d43d 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -146,7 +146,7 @@ def __initialize(self, config): self.infobar_long_break_shown = False def __create_break_item(self, break_config, is_short): - """Create an entry for break be listed in the break tab.""" + """Create an entry for break to be listed in the break tab.""" parent_box = self.box_long_breaks if is_short: parent_box = self.box_short_breaks From efce042e42b351fe0ca43e10319ba07bc5e55052 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 7 Aug 2025 12:08:23 +0200 Subject: [PATCH 055/134] audiblealert: replace aplay with ffplay (ffmpeg) --- safeeyes/plugins/audiblealert/config.json | 4 ++-- safeeyes/plugins/audiblealert/plugin.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/safeeyes/plugins/audiblealert/config.json b/safeeyes/plugins/audiblealert/config.json index ea464895..1f716f1b 100644 --- a/safeeyes/plugins/audiblealert/config.json +++ b/safeeyes/plugins/audiblealert/config.json @@ -6,7 +6,7 @@ }, "dependencies": { "python_modules": [], - "shell_commands": ["aplay"], + "shell_commands": ["ffplay"], "operating_systems": [], "desktop_environments": [], "resources": ["on_pre_break.wav", "on_stop_break.wav"] @@ -24,4 +24,4 @@ "default": true }], "break_override_allowed": true -} \ No newline at end of file +} diff --git a/safeeyes/plugins/audiblealert/plugin.py b/safeeyes/plugins/audiblealert/plugin.py index 23002e10..97203d4f 100644 --- a/safeeyes/plugins/audiblealert/plugin.py +++ b/safeeyes/plugins/audiblealert/plugin.py @@ -42,7 +42,9 @@ def play_sound(resource_name): path = utility.get_resource_path(resource_name) if path is None: return - utility.execute_command("aplay", ["-q", path]) + utility.execute_command( + "ffplay", [path, "-nodisp", "-nostats", "-hide_banner", "-autoexit"] + ) except BaseException: logging.error("Failed to play audible alert %s", resource_name) From 60f59658cb26bf559bc832d5d9ceed72dcb8f6d2 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 7 Aug 2025 12:16:29 +0200 Subject: [PATCH 056/134] audiblealert: add volume control --- safeeyes/plugins/audiblealert/config.json | 10 +++++++++- safeeyes/plugins/audiblealert/plugin.py | 22 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/safeeyes/plugins/audiblealert/config.json b/safeeyes/plugins/audiblealert/config.json index 1f716f1b..ff326793 100644 --- a/safeeyes/plugins/audiblealert/config.json +++ b/safeeyes/plugins/audiblealert/config.json @@ -2,7 +2,7 @@ "meta": { "name": "Audible Alert", "description": "Play audible alert before and after breaks", - "version": "0.0.3" + "version": "0.0.4" }, "dependencies": { "python_modules": [], @@ -22,6 +22,14 @@ "label": "Play audible alert after breaks", "type": "BOOL", "default": true + }, + { + "id": "volume", + "label": "Alert volume", + "type": "INT", + "default": 100, + "max": 100, + "min": 0 }], "break_override_allowed": true } diff --git a/safeeyes/plugins/audiblealert/plugin.py b/safeeyes/plugins/audiblealert/plugin.py index 97203d4f..9efbc0a0 100644 --- a/safeeyes/plugins/audiblealert/plugin.py +++ b/safeeyes/plugins/audiblealert/plugin.py @@ -26,6 +26,7 @@ context = None pre_break_alert = False post_break_alert = False +volume: int = 100 def play_sound(resource_name): @@ -36,14 +37,25 @@ def play_sound(resource_name): resource_name {string} -- name of the wav file resource """ - logging.info("Playing audible alert %s", resource_name) + global volume + + logging.info("Playing audible alert %s at volume %s%%", resource_name, volume) try: # Open the sound file path = utility.get_resource_path(resource_name) if path is None: return utility.execute_command( - "ffplay", [path, "-nodisp", "-nostats", "-hide_banner", "-autoexit"] + "ffplay", + [ + path, + "-nodisp", + "-nostats", + "-hide_banner", + "-autoexit", + "-volume", + str(volume), + ], ) except BaseException: @@ -55,10 +67,16 @@ def init(ctx, safeeyes_config, plugin_config): global context global pre_break_alert global post_break_alert + global volume logging.debug("Initialize Audible Alert plugin") context = ctx pre_break_alert = plugin_config["pre_break_alert"] post_break_alert = plugin_config["post_break_alert"] + volume = int(plugin_config.get("volume", 100)) + if volume > 100: + volume = 100 + if volume < 0: + volume = 0 def on_pre_break(break_obj): From 69693c357da8e23b8dfdfa0a7d065488eb37038d Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 7 Aug 2025 12:33:21 +0200 Subject: [PATCH 057/134] audiblealert: add fallback to other tools --- safeeyes/config/locale/safeeyes.pot | 4 +++ safeeyes/plugins/audiblealert/config.json | 2 +- .../audiblealert/dependency_checker.py | 36 +++++++++++++++++++ safeeyes/plugins/audiblealert/plugin.py | 11 ++++-- 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 safeeyes/plugins/audiblealert/dependency_checker.py diff --git a/safeeyes/config/locale/safeeyes.pot b/safeeyes/config/locale/safeeyes.pot index 13371059..2b991175 100644 --- a/safeeyes/config/locale/safeeyes.pot +++ b/safeeyes/config/locale/safeeyes.pot @@ -567,3 +567,7 @@ msgstr "" msgid "seconds" msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" diff --git a/safeeyes/plugins/audiblealert/config.json b/safeeyes/plugins/audiblealert/config.json index ff326793..1493fa08 100644 --- a/safeeyes/plugins/audiblealert/config.json +++ b/safeeyes/plugins/audiblealert/config.json @@ -6,7 +6,7 @@ }, "dependencies": { "python_modules": [], - "shell_commands": ["ffplay"], + "shell_commands": [], "operating_systems": [], "desktop_environments": [], "resources": ["on_pre_break.wav", "on_stop_break.wav"] diff --git a/safeeyes/plugins/audiblealert/dependency_checker.py b/safeeyes/plugins/audiblealert/dependency_checker.py new file mode 100644 index 00000000..ea357871 --- /dev/null +++ b/safeeyes/plugins/audiblealert/dependency_checker.py @@ -0,0 +1,36 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from safeeyes import utility +from safeeyes.translations import translate as _ + + +def validate(plugin_config, plugin_settings): + commands = ["ffplay", "pw-play"] + exists = False + for command in commands: + if utility.command_exist(command): + exists = True + break + + if not exists: + return _("Please install one of the command-line tools: %s") % ", ".join( + [f"'{command}'" for command in commands] + ) + else: + return None diff --git a/safeeyes/plugins/audiblealert/plugin.py b/safeeyes/plugins/audiblealert/plugin.py index 9efbc0a0..b6e06927 100644 --- a/safeeyes/plugins/audiblealert/plugin.py +++ b/safeeyes/plugins/audiblealert/plugin.py @@ -45,6 +45,11 @@ def play_sound(resource_name): path = utility.get_resource_path(resource_name) if path is None: return + except BaseException: + logging.error("Failed to load resource %s", resource_name) + return + + if utility.command_exist("ffplay"): # ffmpeg utility.execute_command( "ffplay", [ @@ -57,9 +62,9 @@ def play_sound(resource_name): str(volume), ], ) - - except BaseException: - logging.error("Failed to play audible alert %s", resource_name) + elif utility.command_exist("pw-play"): # pipewire + pwvol = volume / 100 # 0 = silent, 1.0 = 100% volume + utility.execute_command("pw-play", ["--volume", str(pwvol), path]) def init(ctx, safeeyes_config, plugin_config): From bd2fddef485481746fdc427ddd369a9b9b4dc5a3 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Tue, 12 Aug 2025 14:08:16 +0200 Subject: [PATCH 058/134] Translated using Weblate (Czech) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/cs/ --- safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po index 339518e5..68774231 100644 --- a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-06-18 10:01+0000\n" +"PO-Revision-Date: 2025-08-13 13:01+0000\n" "Last-Translator: vikdevelop \n" "Language-Team: Czech \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" -"X-Generator: Weblate 5.12.1\n" +"X-Generator: Weblate 5.13-dev\n" # Short break msgid "Gently close your eyes" @@ -584,13 +584,13 @@ msgstr "" "styly vytvořte místo toho nový soubor stylů v '%(new)s'." msgid "Customizing the postpone and skip shortcuts does not work on Wayland." -msgstr "" +msgstr "Přizpůsobení zkratek pro odložení a přeskočení nefunguje na Waylandu." msgid "Safe Eyes - Error" -msgstr "" +msgstr "Chyba - Safe Eyes" msgid "A required plugin is missing dependencies!" -msgstr "" +msgstr "Požadovanému doplňku chybí závislosti!" # Short break #~ msgid "Tightly close your eyes" From 0b305c8303cc22917f7c45879a5fc930e9a925de Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 18:22:03 +0200 Subject: [PATCH 059/134] core: rename __fire_start_break to __do_start_break the __fire methods usually only fire a hook - this one does more than just that --- safeeyes/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index deac1ee9..de5aad93 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -186,7 +186,7 @@ def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: if break_type is not None and self._break_queue.get_break().type != break_type: self._break_queue.next(break_type) - utility.execute_main_thread(self.__fire_start_break) + utility.execute_main_thread(self.__do_start_break) def __scheduler_job(self) -> None: """Scheduler task to execute during every interval.""" @@ -280,13 +280,13 @@ def __wait_until_prepare(self) -> None: self.__wait_for(self.pre_break_warning_time) if not self.running: return - utility.execute_main_thread(self.__fire_start_break) + utility.execute_main_thread(self.__do_start_break) def __postpone_break(self) -> None: self.__wait_for(self.postpone_duration) - utility.execute_main_thread(self.__fire_start_break) + utility.execute_main_thread(self.__do_start_break) - def __fire_start_break(self) -> None: + def __do_start_break(self) -> None: if self._break_queue is None: # This will only be called by methods which check this return From 272be29d915a1841fc61388166000d9e2c2237f9 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 18:24:21 +0200 Subject: [PATCH 060/134] core: refactor: split out __do_pre_break, reorder reorders code in the order it actually gets called, and for future refactoring --- safeeyes/core.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index de5aad93..9c6a3905 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -245,13 +245,7 @@ def __scheduler_job(self) -> None: self.__wait_for(time_to_wait) - logging.info("Pre-break waiting is over") - - if not self.running: - # This can be reached if another thread changed running while __wait_for was - # blocking - return # type: ignore[unreachable] - utility.execute_main_thread(self.__fire_pre_break) + self.__do_pre_break() def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> None: """Pass the next break information to the registered listeners.""" @@ -260,6 +254,16 @@ def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> Non return self.on_update_next_break.fire(self._break_queue.get_break(), next_break_time) + def __do_pre_break(self) -> None: + logging.info("Pre-break waiting is over") + + if not self.running: + # This can be reached if another thread changed running while __wait_for was + # blocking + return + + utility.execute_main_thread(self.__fire_pre_break) + def __fire_pre_break(self) -> None: """Show the notification and start the break after the notification.""" if self._break_queue is None: From 44e4f324e9923966c57ae4c2aa576bc93f4550f9 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 16:30:01 +0200 Subject: [PATCH 061/134] core: refactor __start_break into separate method splits out the parts after sleeping into a __cycle_break_countdown method so it can be later called in a callback this also switches this method from a `time.sleep()` call to using __wait_for like the rest of this module. --- safeeyes/core.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 9c6a3905..4720148c 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -25,6 +25,7 @@ import typing from safeeyes import utility +from safeeyes.model import Break from safeeyes.model import BreakType from safeeyes.model import BreakQueue from safeeyes.model import EventHook @@ -45,6 +46,10 @@ class SafeEyesCore: _break_queue: typing.Optional[BreakQueue] = None + # set while taking a break + _countdown: typing.Optional[int] = 0 + _taking_break: typing.Optional[Break] = None + def __init__(self, context) -> None: """Create an instance of SafeEyesCore and initialize the variables.""" # This event is fired before for a break @@ -326,20 +331,35 @@ def __start_break(self) -> None: return self.context["state"] = State.BREAK break_obj = self._break_queue.get_break() - countdown = break_obj.duration - total_break_time = countdown + self._taking_break = break_obj + self._countdown = break_obj.duration + + self.__cycle_break_countdown() + + def __cycle_break_countdown(self) -> None: + if self._taking_break is None or self._countdown is None: + raise Exception("countdown running without countdown or break") - while ( - countdown + if ( + self._countdown > 0 and self.running and not self.context["skipped"] and not self.context["postponed"] ): + countdown = self._countdown + self._countdown -= 1 + + total_break_time = self._taking_break.duration seconds = total_break_time - countdown self.on_count_down.fire(countdown, seconds) - time.sleep(1) # Sleep for 1 second - countdown -= 1 - utility.execute_main_thread(self.__fire_stop_break) + # Sleep for 1 second + self.__wait_for(1) + utility.start_thread(self.__cycle_break_countdown) + else: + self._countdown = None + self._taking_break = None + + utility.execute_main_thread(self.__fire_stop_break) def __fire_stop_break(self) -> None: # Loop terminated because of timeout (not skipped) -> Close the break alert From 43b3a436b23b9999668dab0e22465ee2689c5db5 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 13:56:39 +0200 Subject: [PATCH 062/134] core: add callback to __wait_for instead of blocking this refactors __wait_for to start a thread and call the passed callback after the timeout in the main thread, instead of blocking the calling thread. this makes it possible to more easily switch out the mechanism that __wait_for uses for scheduling that callback later. --- safeeyes/core.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 4720148c..323c1a60 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -248,9 +248,7 @@ def __scheduler_job(self) -> None: else: logging.info("Waiting for %d minutes until next break", (time_to_wait / 60)) - self.__wait_for(time_to_wait) - - self.__do_pre_break() + self.__wait_for(time_to_wait, self.__do_pre_break) def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> None: """Pass the next break information to the registered listeners.""" @@ -286,16 +284,14 @@ def __wait_until_prepare(self) -> None: "Wait for %d seconds before the break", self.pre_break_warning_time ) # Wait for the pre break warning period - self.__wait_for(self.pre_break_warning_time) - if not self.running: - return - utility.execute_main_thread(self.__do_start_break) + self.__wait_for(self.pre_break_warning_time, self.__do_start_break) def __postpone_break(self) -> None: - self.__wait_for(self.postpone_duration) - utility.execute_main_thread(self.__do_start_break) + self.__wait_for(self.postpone_duration, self.__do_start_break) def __do_start_break(self) -> None: + if not self.running: + return if self._break_queue is None: # This will only be called by methods which check this return @@ -353,8 +349,7 @@ def __cycle_break_countdown(self) -> None: seconds = total_break_time - countdown self.on_count_down.fire(countdown, seconds) # Sleep for 1 second - self.__wait_for(1) - utility.start_thread(self.__cycle_break_countdown) + self.__wait_for(1, self.__cycle_break_countdown) else: self._countdown = None self._taking_break = None @@ -373,11 +368,24 @@ def __fire_stop_break(self) -> None: self.context["postpone_button_disabled"] = False self.__start_next_break() - def __wait_for(self, duration: int) -> None: + def __wait_for( + self, + duration: int, + callback: typing.Callable[[], None], + ) -> None: """Wait until someone wake up or the timeout happens.""" - self.waiting_condition.acquire() - self.waiting_condition.wait(duration) - self.waiting_condition.release() + + def inner() -> None: + self.waiting_condition.acquire() + self.waiting_condition.wait(duration) + self.waiting_condition.release() + + if not self.running: + return + + utility.execute_main_thread(callback) + + utility.start_thread(inner) def __start_next_break(self) -> None: if self._break_queue is None: From 1c5ca1eb73192b6a86b874e68eb30ddcf75680ed Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 17:45:32 +0200 Subject: [PATCH 063/134] core: add __fire_hook method this adds a __fire_hook method for safely firing an event hook on the main thread, and uses it to fire all hooks. this ensures that all hooks are always fired from the main thread in one central place. this makes changing that logic easier later this also fixes the on_count_down hook, which previously wasn't always called from the main thread. __fire_hook also ensures that plugins don't do anything weird like recursively calling methods that would trigger another hook instantly. the implementation of __fire_hook is somewhat clever. it uses a threading.Event to block on and synchronize with the event loop, so we can wait for the plugins' response and return it. since this implementation will later be replaced, it is okay that it is somewhat complicated. --- safeeyes/core.py | 60 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 323c1a60..9bf551dc 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -46,6 +46,9 @@ class SafeEyesCore: _break_queue: typing.Optional[BreakQueue] = None + # set while __fire_hook is running + _firing_hook: bool = False + # set while taking a break _countdown: typing.Optional[int] = 0 _taking_break: typing.Optional[Break] = None @@ -191,7 +194,7 @@ def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: if break_type is not None and self._break_queue.get_break().type != break_type: self._break_queue.next(break_type) - utility.execute_main_thread(self.__do_start_break) + self.__do_start_break() def __scheduler_job(self) -> None: """Scheduler task to execute during every interval.""" @@ -238,9 +241,7 @@ def __scheduler_job(self) -> None: seconds=time_to_wait ) self.context["state"] = State.WAITING - utility.execute_main_thread( - self.__fire_on_update_next_break, self.scheduled_next_break_time - ) + self.__fire_on_update_next_break(self.scheduled_next_break_time) # Wait for the pre break warning period if self.postpone_unit == "seconds": @@ -255,7 +256,9 @@ def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> Non if self._break_queue is None: # This will only be called by methods which check this return - self.on_update_next_break.fire(self._break_queue.get_break(), next_break_time) + self.__fire_hook( + self.on_update_next_break, self._break_queue.get_break(), next_break_time + ) def __do_pre_break(self) -> None: logging.info("Pre-break waiting is over") @@ -265,7 +268,7 @@ def __do_pre_break(self) -> None: # blocking return - utility.execute_main_thread(self.__fire_pre_break) + self.__fire_pre_break() def __fire_pre_break(self) -> None: """Show the notification and start the break after the notification.""" @@ -273,7 +276,8 @@ def __fire_pre_break(self) -> None: # This will only be called by methods which check this return self.context["state"] = State.PRE_BREAK - if not self.on_pre_break.fire(self._break_queue.get_break()): + proceed = self.__fire_hook(self.on_pre_break, self._break_queue.get_break()) + if not proceed: # Plugins wanted to ignore this break self.__start_next_break() return @@ -297,7 +301,8 @@ def __do_start_break(self) -> None: return break_obj = self._break_queue.get_break() # Show the break screen - if not self.on_start_break.fire(break_obj): + proceed = self.__fire_hook(self.on_start_break, break_obj) + if not proceed: # Plugins want to ignore this break self.__start_next_break() return @@ -317,7 +322,7 @@ def __do_start_break(self) -> None: # Wait in user thread utility.start_thread(self.__postpone_break) else: - self.start_break.fire(break_obj) + self.__fire_hook(self.start_break, break_obj) utility.start_thread(self.__start_break) def __start_break(self) -> None: @@ -347,20 +352,20 @@ def __cycle_break_countdown(self) -> None: total_break_time = self._taking_break.duration seconds = total_break_time - countdown - self.on_count_down.fire(countdown, seconds) + self.__fire_hook(self.on_count_down, countdown, seconds) # Sleep for 1 second self.__wait_for(1, self.__cycle_break_countdown) else: self._countdown = None self._taking_break = None - utility.execute_main_thread(self.__fire_stop_break) + self.__fire_stop_break() def __fire_stop_break(self) -> None: # Loop terminated because of timeout (not skipped) -> Close the break alert if not self.context["skipped"] and not self.context["postponed"]: logging.info("Break is terminated automatically") - self.on_stop_break.fire() + self.__fire_hook(self.on_stop_break) # Reset the skipped flag self.context["skipped"] = False @@ -383,10 +388,39 @@ def inner() -> None: if not self.running: return - utility.execute_main_thread(callback) + callback() utility.start_thread(inner) + def __fire_hook( + self, + hook: EventHook, + *args, + **kwargs, + ) -> bool: + if self._firing_hook: + raise Exception("this should not be called reentrantly") + + self._firing_hook = True + + # run hook on main thread, but block the caller until it's done + event = threading.Event() + proceed = False + + def run_method(hook: EventHook, *args, **kwargs) -> None: + nonlocal event + nonlocal proceed + proceed = hook.fire(*args, **kwargs) + event.set() + + utility.execute_main_thread(lambda: run_method(hook, *args, **kwargs)) + + event.wait() + + self._firing_hook = False + + return proceed + def __start_next_break(self) -> None: if self._break_queue is None: # This will only be called by methods which check this From 52e5bb2238731b2bf86e784463f9a1a44e29f982 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 19:02:41 +0200 Subject: [PATCH 064/134] core: __wakeup_scheduler again, centralize the implementation in one method so it can be more easily switched out later. --- safeeyes/core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 9bf551dc..f99691b2 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -113,12 +113,11 @@ def stop(self, is_resting=False) -> None: logging.info("Stop Safe Eyes core") self.paused_time = datetime.datetime.now().timestamp() # Stop the break thread - self.waiting_condition.acquire() self.running = False if self.context["state"] != State.QUIT: self.context["state"] = State.RESTING if (is_resting) else State.STOPPED - self.waiting_condition.notify_all() - self.waiting_condition.release() + + self.__wakeup_scheduler() def skip(self) -> None: """User skipped the break using Skip button.""" @@ -185,10 +184,8 @@ def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: logging.info("Stop the scheduler") # Stop the break thread - self.waiting_condition.acquire() self.running = False - self.waiting_condition.notify_all() - self.waiting_condition.release() + self.__wakeup_scheduler() time.sleep(1) # Wait for 1 sec to ensure the scheduler is dead self.running = True @@ -421,6 +418,12 @@ def run_method(hook: EventHook, *args, **kwargs) -> None: return proceed + def __wakeup_scheduler(self) -> None: + # wakeup scheduler + self.waiting_condition.acquire() + self.waiting_condition.notify_all() + self.waiting_condition.release() + def __start_next_break(self) -> None: if self._break_queue is None: # This will only be called by methods which check this From 03114f30028ca545ce790043b9bb9fcb8df92b79 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 18:50:12 +0200 Subject: [PATCH 065/134] core: refactor: add _take_break_now flag before this, the implementation is complicated and slow. it stops the scheduler completely, waits for a second, and then restarts it at the correct place. this change removes all that and replaces it with setting a _take_break_now flag and waking the scheduler. this flag is checked after every __wait_for call and, if applicable, calls the correct code. this makes taking a break using the -t cli flag or the "Take break now" option noticeably faster. it also clears up the code, making it very clear when and where a break can be taken. --- safeeyes/core.py | 50 +++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index f99691b2..025b2535 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -21,7 +21,6 @@ import datetime import logging import threading -import time import typing from safeeyes import utility @@ -53,6 +52,9 @@ class SafeEyesCore: _countdown: typing.Optional[int] = 0 _taking_break: typing.Optional[Break] = None + # set to true when a break was requested + _take_break_now: bool = False + def __init__(self, context) -> None: """Create an instance of SafeEyesCore and initialize the variables.""" # This event is fired before for a break @@ -154,7 +156,13 @@ def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: return if not self.context["state"] == State.WAITING: return - utility.start_thread(self.__take_break, break_type=break_type) + + if break_type is not None and self._break_queue.get_break().type != break_type: + self._break_queue.next(break_type) + + self._take_break_now = True + + self.__wakeup_scheduler() def has_breaks(self, break_type: typing.Optional[BreakType] = None) -> bool: """Check whether Safe Eyes has breaks or not. @@ -169,30 +177,6 @@ def has_breaks(self, break_type: typing.Optional[BreakType] = None) -> bool: return not self._break_queue.is_empty(break_type) - def __take_break(self, break_type: typing.Optional[BreakType] = None) -> None: - """Show the next break screen.""" - logging.info("Take a break due to external request") - - if self._break_queue is None: - # This will only be called by self.take_break, which checks this - return - - with self.lock: - if not self.running: - return - - logging.info("Stop the scheduler") - - # Stop the break thread - self.running = False - self.__wakeup_scheduler() - time.sleep(1) # Wait for 1 sec to ensure the scheduler is dead - self.running = True - - if break_type is not None and self._break_queue.get_break().type != break_type: - self._break_queue.next(break_type) - self.__do_start_break() - def __scheduler_job(self) -> None: """Scheduler task to execute during every interval.""" if not self.running: @@ -258,6 +242,12 @@ def __fire_on_update_next_break(self, next_break_time: datetime.datetime) -> Non ) def __do_pre_break(self) -> None: + if self._take_break_now: + self._take_break_now = False + logging.info("Take a break due to external request") + self.__do_start_break() + return + logging.info("Pre-break waiting is over") if not self.running: @@ -291,6 +281,10 @@ def __postpone_break(self) -> None: self.__wait_for(self.postpone_duration, self.__do_start_break) def __do_start_break(self) -> None: + if self._take_break_now: + # already taking a break now, ignore + self._take_break_now = False + if not self.running: return if self._break_queue is None: @@ -338,6 +332,10 @@ def __cycle_break_countdown(self) -> None: if self._taking_break is None or self._countdown is None: raise Exception("countdown running without countdown or break") + if self._take_break_now: + logging.warning("Break requested while already taking a break") + self._take_break_now = False + if ( self._countdown > 0 and self.running From 266ec706226d5c7cca0409079b5ad5601c2d2013 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 9 Jun 2025 12:39:40 +0200 Subject: [PATCH 066/134] core: remove lock as it is only used in the main thread --- safeeyes/core.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 025b2535..b1402014 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -70,7 +70,6 @@ def __init__(self, context) -> None: # This event is fired when deciding the next break time self.on_update_next_break = EventHook() self.waiting_condition = threading.Condition() - self.lock = threading.Lock() self.context = context self.context["skipped"] = False self.context["postponed"] = False @@ -95,31 +94,29 @@ def start(self, next_break_time=-1, reset_breaks=False) -> None: if self._break_queue is None: logging.info("No breaks defined, not starting the core") return - with self.lock: - if not self.running: - logging.info("Start Safe Eyes core") - if reset_breaks: - logging.info("Reset breaks to start from the beginning") - self._break_queue.reset() + if not self.running: + logging.info("Start Safe Eyes core") + if reset_breaks: + logging.info("Reset breaks to start from the beginning") + self._break_queue.reset() - self.running = True - self.scheduled_next_break_timestamp = int(next_break_time) - utility.start_thread(self.__scheduler_job) + self.running = True + self.scheduled_next_break_timestamp = int(next_break_time) + utility.start_thread(self.__scheduler_job) def stop(self, is_resting=False) -> None: """Stop Safe Eyes if it is running.""" - with self.lock: - if not self.running: - return + if not self.running: + return - logging.info("Stop Safe Eyes core") - self.paused_time = datetime.datetime.now().timestamp() - # Stop the break thread - self.running = False - if self.context["state"] != State.QUIT: - self.context["state"] = State.RESTING if (is_resting) else State.STOPPED + logging.info("Stop Safe Eyes core") + self.paused_time = datetime.datetime.now().timestamp() + # Stop the break thread + self.running = False + if self.context["state"] != State.QUIT: + self.context["state"] = State.RESTING if (is_resting) else State.STOPPED - self.__wakeup_scheduler() + self.__wakeup_scheduler() def skip(self) -> None: """User skipped the break using Skip button.""" From b5e422f7a45930e5416b04f4c593095ac2d9f64d Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 9 Jun 2025 12:38:24 +0200 Subject: [PATCH 067/134] core: run on main thread with callbacks instead of blocking on separate threads, use GLib timeouts to schedule the callbacks. this allows running everything on the main thread without blocking, and avoiding the need for any kind of complicated synchronization. --- safeeyes/core.py | 86 +++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index b1402014..2df72b9f 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -20,10 +20,8 @@ import datetime import logging -import threading import typing -from safeeyes import utility from safeeyes.model import Break from safeeyes.model import BreakType from safeeyes.model import BreakQueue @@ -31,6 +29,11 @@ from safeeyes.model import State from safeeyes.model import Config +import gi + +gi.require_version("GLib", "2.0") +from gi.repository import GLib + class SafeEyesCore: """Core of Safe Eyes runs the scheduler and notifies the breaks.""" @@ -45,6 +48,10 @@ class SafeEyesCore: _break_queue: typing.Optional[BreakQueue] = None + # set while __wait_for is running + _timeout_id: typing.Optional[int] = None + _callback: typing.Optional[typing.Callable[[], None]] = None + # set while __fire_hook is running _firing_hook: bool = False @@ -69,7 +76,6 @@ def __init__(self, context) -> None: self.on_stop_break = EventHook() # This event is fired when deciding the next break time self.on_update_next_break = EventHook() - self.waiting_condition = threading.Condition() self.context = context self.context["skipped"] = False self.context["postponed"] = False @@ -102,7 +108,7 @@ def start(self, next_break_time=-1, reset_breaks=False) -> None: self.running = True self.scheduled_next_break_timestamp = int(next_break_time) - utility.start_thread(self.__scheduler_job) + self.__scheduler_job() def stop(self, is_resting=False) -> None: """Stop Safe Eyes if it is running.""" @@ -265,7 +271,8 @@ def __fire_pre_break(self) -> None: # Plugins wanted to ignore this break self.__start_next_break() return - utility.start_thread(self.__wait_until_prepare) + + self.__wait_until_prepare() def __wait_until_prepare(self) -> None: logging.info( @@ -308,10 +315,10 @@ def __do_start_break(self) -> None: ) self.__fire_on_update_next_break(self.scheduled_next_break_time) # Wait in user thread - utility.start_thread(self.__postpone_break) + self.__postpone_break() else: self.__fire_hook(self.start_break, break_obj) - utility.start_thread(self.__start_break) + self.__start_break() def __start_break(self) -> None: """Start the break screen.""" @@ -371,18 +378,25 @@ def __wait_for( callback: typing.Callable[[], None], ) -> None: """Wait until someone wake up or the timeout happens.""" + if self._callback is not None or self._timeout_id is not None: + raise Exception("this should not be called reentrantly") - def inner() -> None: - self.waiting_condition.acquire() - self.waiting_condition.wait(duration) - self.waiting_condition.release() + self._callback = callback + self._timeout_id = GLib.timeout_add_seconds(duration, self.__on_wakeup) - if not self.running: - return + def __on_wakeup(self) -> bool: + if self._callback is None or self._timeout_id is None: + raise Exception("Woken up but no callback") - callback() + callback = self._callback + + self._timeout_id = None + self._callback = None + + callback() - utility.start_thread(inner) + # This signals that the callback should only be called once + return GLib.SOURCE_REMOVE def __fire_hook( self, @@ -395,29 +409,35 @@ def __fire_hook( self._firing_hook = True - # run hook on main thread, but block the caller until it's done - event = threading.Event() - proceed = False - - def run_method(hook: EventHook, *args, **kwargs) -> None: - nonlocal event - nonlocal proceed - proceed = hook.fire(*args, **kwargs) - event.set() - - utility.execute_main_thread(lambda: run_method(hook, *args, **kwargs)) - - event.wait() + proceed = hook.fire(*args, **kwargs) self._firing_hook = False return proceed def __wakeup_scheduler(self) -> None: - # wakeup scheduler - self.waiting_condition.acquire() - self.waiting_condition.notify_all() - self.waiting_condition.release() + if (self._callback is None) != (self._timeout_id is None): + # either both are set or none are set + raise Exception("This should never happen") + + if (self._callback is None) and not self._firing_hook: + # neither is set - not running + raise Exception("trying to queue action while core is not running") + elif (self._callback is not None) and self._firing_hook: + # both are set + raise Exception("This should never happen") + + if self._callback is not None and self._timeout_id is not None: + callback = self._callback + + GLib.source_remove(self._timeout_id) + self._timeout_id = None + self._callback = None + + callback() + elif self._firing_hook: + # plugin is running + pass def __start_next_break(self) -> None: if self._break_queue is None: @@ -428,4 +448,4 @@ def __start_next_break(self) -> None: if self.running: # Schedule the break again - utility.start_thread(self.__scheduler_job) + self.__scheduler_job() From 2d18885af45dbfbf01dab9f56b8170586b4d8540 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 25 Jun 2025 18:01:01 +0200 Subject: [PATCH 068/134] safeeyes: application: remove another needless sleep/thread this is no longer needed as calling SafeEyesCore.stop() now runs synchronously on the main thread, so will complete instantly. --- safeeyes/safeeyes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index 56c1e48a..bca9fa16 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -23,7 +23,6 @@ import atexit import logging import typing -from threading import Timer from importlib import metadata import gi @@ -492,8 +491,7 @@ def restart(self, config, set_active=False): self.active = True if self.active and self.safe_eyes_core.has_breaks(): - # 1 sec delay is required to give enough time for core to be stopped - Timer(1.0, self.safe_eyes_core.start).start() + self.safe_eyes_core.start() self.plugins_manager.start() def enable_safeeyes(self, scheduled_next_break_time=-1, reset_breaks=False): From c8c49bc8979c1089027c17ccde00148dc4d3ed18 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 25 Aug 2024 21:06:39 +0200 Subject: [PATCH 069/134] test SafeEyesCore: initial --- safeeyes/tests/test_core.py | 359 ++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 safeeyes/tests/test_core.py diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py new file mode 100644 index 00000000..e921d1c8 --- /dev/null +++ b/safeeyes/tests/test_core.py @@ -0,0 +1,359 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from collections import deque +import datetime +import logging +import pytest + +from safeeyes import core +from safeeyes import model + +import threading +from time import sleep + +from unittest import mock + + +class TestSafeEyesCore: + @pytest.fixture(autouse=True) + def set_time(self, time_machine): + time_machine.move_to(datetime.datetime.fromisoformat("2024-08-25T13:00")) + + @pytest.fixture(autouse=True) + def monkeypatch_translations(self, monkeypatch): + monkeypatch.setattr( + core, "_", lambda message: "translated!: " + message, raising=False + ) + monkeypatch.setattr( + model, "_", lambda message: "translated!: " + message, raising=False + ) + + @pytest.fixture + def sequential_threading(self, monkeypatch): + # executes instantly + # TODO: separate thread? + monkeypatch.setattr( + core.utility, + "execute_main_thread", + lambda target_function, *args, **kwargs: target_function(*args, **kwargs), + ) + + class Handle: + thread = None + task_queue = deque() + running = True + condvar_in = threading.Condition() + condvar_out = threading.Condition() + + def background_thread(self): + while True: + with self.condvar_in: + success = self.condvar_in.wait(1) + if not success: + raise Exception("thread timed out") + + if not self.running: + logging.debug("background task shutdown") + break + + logging.debug("background task woken up") + + if self.task_queue: + (target_function, kwargs) = self.task_queue.popleft() + logging.debug(f"thread started {target_function}") + target_function(**kwargs) + logging.debug(f"thread finished {target_function}") + + with self.condvar_out: + self.condvar_out.notify() + + def sleep(self, time): + if self.thread is threading.current_thread(): + with self.condvar_out: + self.condvar_out.notify() + with self.condvar_in: + success = self.condvar_in.wait(1) + if not success: + raise Exception("thread timed out") + + def utility_start_thread(self, target_function, **kwargs): + self.task_queue.append((target_function, kwargs)) + + if self.thread is None: + self.thread = threading.Thread( + target=self.background_thread, + name="WorkThread", + daemon=False, + kwargs=kwargs, + ) + self.thread.start() + + def next(self): + assert self.thread + + with self.condvar_in: + self.condvar_in.notify() + + def wait(self): + # wait until done: + with self.condvar_out: + success = self.condvar_out.wait(1) + if not success: + raise Exception("thread timed out") + + def stop(self): + self.running = False + with self.condvar_in: + self.condvar_in.notify() + + if self.thread: + self.thread.join(1) + + handle = Handle() + + monkeypatch.setattr(core.utility, "start_thread", handle.utility_start_thread) + + monkeypatch.setattr(core.time, "sleep", lambda time: handle.sleep(time)) + + yield handle + + handle.stop() + + def test_create_empty(self): + context = {} + config = { + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + } + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.initialize(config) + + def test_start_empty(self, sequential_threading): + context = {} + config = { + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + } + on_update_next_break = mock.Mock() + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.on_update_next_break += mock + + safe_eyes_core.initialize(config) + + safe_eyes_core.start() + safe_eyes_core.stop() + + on_update_next_break.assert_not_called() + + def test_start(self, sequential_threading, time_machine): + context = { + "session": {}, + } + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + } + on_update_next_break = mock.Mock() + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.on_update_next_break += on_update_next_break + + safe_eyes_core.initialize(config) + + safe_eyes_core.start() + + # start __scheduler_job + sequential_threading.next() + # FIXME: sleep is needed so code reaches the Condition + sleep(0.1) + assert context["state"] == model.State.WAITING + + on_update_next_break.assert_called_once() + assert isinstance(on_update_next_break.call_args[0][0], model.Break) + assert on_update_next_break.call_args[0][0].name == "translated!: break 1" + on_update_next_break.reset_mock() + + with safe_eyes_core.lock: + time_machine.shift(delta=datetime.timedelta(minutes=15)) + + with safe_eyes_core.waiting_condition: + logging.debug("notify") + safe_eyes_core.waiting_condition.notify_all() + + logging.debug("wait for end of __scheduler_job") + sequential_threading.wait() + logging.debug("done waiting for end of __scheduler_job") + + safe_eyes_core.stop() + assert context["state"] == model.State.STOPPED + + logging.debug("done") + + def test_actual(self, sequential_threading, time_machine): + context = { + "session": {}, + } + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "pre_break_warning_time": 10, + "random_order": False, + "postpone_duration": 5, + } + on_update_next_break = mock.Mock() + on_pre_break = mock.Mock(return_value=True) + on_start_break = mock.Mock(return_value=True) + start_break = mock.Mock() + on_count_down = mock.Mock() + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.on_update_next_break += on_update_next_break + safe_eyes_core.on_pre_break += on_pre_break + safe_eyes_core.on_start_break += on_start_break + safe_eyes_core.start_break += start_break + safe_eyes_core.on_count_down += on_count_down + + safe_eyes_core.initialize(config) + + safe_eyes_core.start() + + # start __scheduler_job + sequential_threading.next() + # FIXME: sleep is needed so code reaches the Condition + sleep(0.1) + assert context["state"] == model.State.WAITING + + on_update_next_break.assert_called_once() + assert isinstance(on_update_next_break.call_args[0][0], model.Break) + assert on_update_next_break.call_args[0][0].name == "translated!: break 1" + on_update_next_break.reset_mock() + + with safe_eyes_core.lock: + time_machine.shift(delta=datetime.timedelta(minutes=15)) + + with safe_eyes_core.waiting_condition: + logging.debug("notify") + safe_eyes_core.waiting_condition.notify_all() + + logging.debug("wait for end of __scheduler_job") + sequential_threading.wait() + logging.debug("done waiting for end of __scheduler_job") + + assert context["state"] == model.State.PRE_BREAK + + on_pre_break.assert_called_once() + assert isinstance(on_pre_break.call_args[0][0], model.Break) + assert on_pre_break.call_args[0][0].name == "translated!: break 1" + on_pre_break.reset_mock() + + # start __wait_until_prepare + sequential_threading.next() + + # FIXME: sleep is needed so code reaches the Condition + sleep(0.1) + with safe_eyes_core.lock: + time_machine.shift(delta=datetime.timedelta(seconds=10)) + + with safe_eyes_core.waiting_condition: + logging.debug("notify") + safe_eyes_core.waiting_condition.notify_all() + + logging.debug("wait for end of __wait_until_prepare") + sequential_threading.wait() + logging.debug("done waiting for end of __wait_until_prepare") + + # start __start_break + sequential_threading.next() + sequential_threading.wait() + + # first sleep in __start_break + sequential_threading.next() + + on_start_break.assert_called_once() + assert isinstance(on_start_break.call_args[0][0], model.Break) + assert on_start_break.call_args[0][0].name == "translated!: break 1" + on_start_break.reset_mock() + + start_break.assert_called_once() + assert isinstance(start_break.call_args[0][0], model.Break) + assert start_break.call_args[0][0].name == "translated!: break 1" + start_break.reset_mock() + + assert context["state"] == model.State.BREAK + + # continue sleep in __start_break + for i in range(config["short_break_duration"] - 1): + sequential_threading.wait() + sequential_threading.next() + + logging.debug("wait for end of __start_break") + sequential_threading.wait() + logging.debug("done waiting for end of __start_break") + + on_count_down.assert_called() + assert on_count_down.call_count == 15 + on_count_down.reset_mock() + + assert context["state"] == model.State.BREAK + + safe_eyes_core.stop() + + on_update_next_break.assert_not_called() + on_pre_break.assert_not_called() + on_start_break.assert_not_called() + start_break.assert_not_called() + assert context["state"] == model.State.STOPPED From a69d1b0bee69e1e2712464ef7c2127974dcfecf2 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 25 Aug 2024 21:07:04 +0200 Subject: [PATCH 070/134] test SafeEyesCore: refactor out run_next_break method --- safeeyes/tests/test_core.py | 206 ++++++++++++++++++++++++++++-------- 1 file changed, 159 insertions(+), 47 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index e921d1c8..2df6452e 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -33,7 +33,9 @@ class TestSafeEyesCore: @pytest.fixture(autouse=True) def set_time(self, time_machine): - time_machine.move_to(datetime.datetime.fromisoformat("2024-08-25T13:00")) + time_machine.move_to( + datetime.datetime.fromisoformat("2024-08-25T13:00:00+00:00"), tick=False + ) @pytest.fixture(autouse=True) def monkeypatch_translations(self, monkeypatch): @@ -45,7 +47,7 @@ def monkeypatch_translations(self, monkeypatch): ) @pytest.fixture - def sequential_threading(self, monkeypatch): + def sequential_threading(self, monkeypatch, time_machine): # executes instantly # TODO: separate thread? monkeypatch.setattr( @@ -61,6 +63,9 @@ class Handle: condvar_in = threading.Condition() condvar_out = threading.Condition() + def __init__(self, time_machine): + self.time_machine = time_machine + def background_thread(self): while True: with self.condvar_in: @@ -87,6 +92,7 @@ def sleep(self, time): if self.thread is threading.current_thread(): with self.condvar_out: self.condvar_out.notify() + self.time_machine.shift(delta=datetime.timedelta(seconds=time)) with self.condvar_in: success = self.condvar_in.wait(1) if not success: @@ -125,7 +131,7 @@ def stop(self): if self.thread: self.thread.join(1) - handle = Handle() + handle = Handle(time_machine=time_machine) monkeypatch.setattr(core.utility, "start_thread", handle.utility_start_thread) @@ -206,7 +212,7 @@ def test_start(self, sequential_threading, time_machine): # start __scheduler_job sequential_threading.next() - # FIXME: sleep is needed so code reaches the Condition + # FIXME: sleep is needed so code reaches the waiting_condition sleep(0.1) assert context["state"] == model.State.WAITING @@ -231,59 +237,48 @@ def test_start(self, sequential_threading, time_machine): logging.debug("done") - def test_actual(self, sequential_threading, time_machine): - context = { - "session": {}, - } - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - {"name": "break 4"}, - ], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "pre_break_warning_time": 10, - "random_order": False, - "postpone_duration": 5, - } + def run_next_break( + self, + sequential_threading, + time_machine, + safe_eyes_core, + context, + break_duration, + break_interval, + pre_break_warning_time, + break_name_translated, + ): + """Run one entire cycle of safe_eyes_core. + + It must be waiting for __scheduler_job to run. (This is the equivalent of + State.WAITING). + That means it must either be just started, or have finished the previous cycle. + """ on_update_next_break = mock.Mock() on_pre_break = mock.Mock(return_value=True) on_start_break = mock.Mock(return_value=True) start_break = mock.Mock() on_count_down = mock.Mock() - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.on_update_next_break += on_update_next_break safe_eyes_core.on_pre_break += on_pre_break safe_eyes_core.on_start_break += on_start_break safe_eyes_core.start_break += start_break safe_eyes_core.on_count_down += on_count_down - safe_eyes_core.initialize(config) - - safe_eyes_core.start() - # start __scheduler_job sequential_threading.next() - # FIXME: sleep is needed so code reaches the Condition + # FIXME: sleep is needed so code reaches the waiting_condition sleep(0.1) assert context["state"] == model.State.WAITING on_update_next_break.assert_called_once() assert isinstance(on_update_next_break.call_args[0][0], model.Break) - assert on_update_next_break.call_args[0][0].name == "translated!: break 1" + assert on_update_next_break.call_args[0][0].name == break_name_translated on_update_next_break.reset_mock() with safe_eyes_core.lock: - time_machine.shift(delta=datetime.timedelta(minutes=15)) + time_machine.shift(delta=datetime.timedelta(minutes=break_interval)) with safe_eyes_core.waiting_condition: logging.debug("notify") @@ -297,16 +292,16 @@ def test_actual(self, sequential_threading, time_machine): on_pre_break.assert_called_once() assert isinstance(on_pre_break.call_args[0][0], model.Break) - assert on_pre_break.call_args[0][0].name == "translated!: break 1" + assert on_pre_break.call_args[0][0].name == break_name_translated on_pre_break.reset_mock() # start __wait_until_prepare sequential_threading.next() - # FIXME: sleep is needed so code reaches the Condition + # FIXME: sleep is needed so code reaches the waiting_condition sleep(0.1) with safe_eyes_core.lock: - time_machine.shift(delta=datetime.timedelta(seconds=10)) + time_machine.shift(delta=datetime.timedelta(seconds=pre_break_warning_time)) with safe_eyes_core.waiting_condition: logging.debug("notify") @@ -325,18 +320,18 @@ def test_actual(self, sequential_threading, time_machine): on_start_break.assert_called_once() assert isinstance(on_start_break.call_args[0][0], model.Break) - assert on_start_break.call_args[0][0].name == "translated!: break 1" + assert on_start_break.call_args[0][0].name == break_name_translated on_start_break.reset_mock() start_break.assert_called_once() assert isinstance(start_break.call_args[0][0], model.Break) - assert start_break.call_args[0][0].name == "translated!: break 1" + assert start_break.call_args[0][0].name == break_name_translated start_break.reset_mock() assert context["state"] == model.State.BREAK # continue sleep in __start_break - for i in range(config["short_break_duration"] - 1): + for i in range(break_duration - 1): sequential_threading.wait() sequential_threading.next() @@ -345,15 +340,132 @@ def test_actual(self, sequential_threading, time_machine): logging.debug("done waiting for end of __start_break") on_count_down.assert_called() - assert on_count_down.call_count == 15 + assert on_count_down.call_count == break_duration on_count_down.reset_mock() assert context["state"] == model.State.BREAK + def assert_datetime(self, string): + if not string.endswith("+00:00"): + string += "+00:00" + assert datetime.datetime.now( + datetime.timezone.utc + ) == datetime.datetime.fromisoformat(string) + + def test_actual(self, sequential_threading, time_machine): + context = { + "session": {}, + } + short_break_duration = 15 # seconds + short_break_interval = 15 # minutes + pre_break_warning_time = 10 # seconds + long_break_duration = 60 # seconds + long_break_interval = 75 # minutes + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + } + + self.assert_datetime("2024-08-25T13:00:00") + + safe_eyes_core = core.SafeEyesCore(context) + + safe_eyes_core.initialize(config) + + safe_eyes_core.start() + + self.run_next_break( + sequential_threading, + time_machine, + safe_eyes_core, + context, + short_break_duration, + short_break_interval, + pre_break_warning_time, + "translated!: break 1", + ) + + self.assert_datetime("2024-08-25T13:15:25") + + self.run_next_break( + sequential_threading, + time_machine, + safe_eyes_core, + context, + short_break_duration, + short_break_interval, + pre_break_warning_time, + "translated!: break 2", + ) + + self.assert_datetime("2024-08-25T13:30:50") + + self.run_next_break( + sequential_threading, + time_machine, + safe_eyes_core, + context, + short_break_duration, + short_break_interval, + pre_break_warning_time, + "translated!: break 3", + ) + + self.assert_datetime("2024-08-25T13:46:15") + + self.run_next_break( + sequential_threading, + time_machine, + safe_eyes_core, + context, + short_break_duration, + short_break_interval, + pre_break_warning_time, + "translated!: break 4", + ) + + self.assert_datetime("2024-08-25T14:01:40") + + self.run_next_break( + sequential_threading, + time_machine, + safe_eyes_core, + context, + long_break_duration, + long_break_interval, + pre_break_warning_time, + "translated!: long break 1", + ) + + # self.assert_datetime("2024-08-25T14:16:40") + + self.run_next_break( + sequential_threading, + time_machine, + safe_eyes_core, + context, + short_break_duration, + short_break_interval, + pre_break_warning_time, + "translated!: break 1", + ) + safe_eyes_core.stop() - on_update_next_break.assert_not_called() - on_pre_break.assert_not_called() - on_start_break.assert_not_called() - start_break.assert_not_called() assert context["state"] == model.State.STOPPED From 565c77c2ea3c2ffce10b1e09d4caf438ae9b1e67 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 25 Aug 2024 22:33:32 +0200 Subject: [PATCH 071/134] test SafeEyesCore: add handling for condvar, add explanation, check exact times --- safeeyes/tests/test_core.py | 199 ++++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 90 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 2df6452e..d84e983c 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -25,7 +25,6 @@ from safeeyes import model import threading -from time import sleep from unittest import mock @@ -48,14 +47,50 @@ def monkeypatch_translations(self, monkeypatch): @pytest.fixture def sequential_threading(self, monkeypatch, time_machine): - # executes instantly - # TODO: separate thread? + """This fixture allows stopping threads at any point. + + It is hard-coded for SafeEyesCore, the handle class returned by the fixture must + be initialized with a SafeEyesCore instance to be patched. + With this, all sleeping/blocking/thread starting calls inside SafeEyesCore are + intercepted, and paused. + Additionally, all threads inside SafeEyesCore run sequentially. + The test code can use the next() method to unpause the thread, + which will run until the next sleeping/blocking/thread starting call, + after which it needs to be woken up using next() again. + The next() method blocks the test code while the thread is running. + """ + # executes instantly, on the same thread + # no need to switch threads, as we don't use any gtk things monkeypatch.setattr( core.utility, "execute_main_thread", lambda target_function, *args, **kwargs: target_function(*args, **kwargs), ) + handle = None + + def utility_start_thread(target_function, **kwargs): + if not handle: + raise Exception("handle must be initialized before first thread") + handle.utility_start_thread(target_function, **kwargs) + + def time_sleep(time): + if not handle: + raise Exception("handle must be initialized before first sleep call") + handle.sleep(time) + + monkeypatch.setattr(core.utility, "start_thread", utility_start_thread) + + monkeypatch.setattr(core.time, "sleep", time_sleep) + + class PatchedCondition(threading.Condition): + def __init__(self, handle): + super().__init__() + self.handle = handle + + def wait(self, timeout): + self.handle.wait_condvar(timeout) + class Handle: thread = None task_queue = deque() @@ -63,8 +98,15 @@ class Handle: condvar_in = threading.Condition() condvar_out = threading.Condition() - def __init__(self, time_machine): + def __init__(self, safe_eyes_core): + nonlocal handle + nonlocal time_machine + if handle: + raise Exception("only one handle is allowed per test call") + handle = self self.time_machine = time_machine + self.safe_eyes_core = safe_eyes_core + self.safe_eyes_core.waiting_condition = PatchedCondition(self) def background_thread(self): while True: @@ -74,11 +116,8 @@ def background_thread(self): raise Exception("thread timed out") if not self.running: - logging.debug("background task shutdown") break - logging.debug("background task woken up") - if self.task_queue: (target_function, kwargs) = self.task_queue.popleft() logging.debug(f"thread started {target_function}") @@ -98,13 +137,25 @@ def sleep(self, time): if not success: raise Exception("thread timed out") + def wait_condvar(self, time): + if self.thread is not threading.current_thread(): + raise Exception("waiting on condition may only happen in thread") + + with self.condvar_out: + self.condvar_out.notify() + self.time_machine.shift(delta=datetime.timedelta(seconds=time)) + with self.condvar_in: + success = self.condvar_in.wait(1) + if not success: + raise Exception("thread timed out") + def utility_start_thread(self, target_function, **kwargs): self.task_queue.append((target_function, kwargs)) if self.thread is None: self.thread = threading.Thread( target=self.background_thread, - name="WorkThread", + name="SequentialThreadingRunner", daemon=False, kwargs=kwargs, ) @@ -116,7 +167,6 @@ def next(self): with self.condvar_in: self.condvar_in.notify() - def wait(self): # wait until done: with self.condvar_out: success = self.condvar_out.wait(1) @@ -131,15 +181,10 @@ def stop(self): if self.thread: self.thread.join(1) - handle = Handle(time_machine=time_machine) + yield Handle - monkeypatch.setattr(core.utility, "start_thread", handle.utility_start_thread) - - monkeypatch.setattr(core.time, "sleep", lambda time: handle.sleep(time)) - - yield handle - - handle.stop() + if handle: + handle.stop() def test_create_empty(self): context = {} @@ -208,12 +253,13 @@ def test_start(self, sequential_threading, time_machine): safe_eyes_core.initialize(config) + sequential_threading_handle = sequential_threading(safe_eyes_core) + safe_eyes_core.start() # start __scheduler_job - sequential_threading.next() - # FIXME: sleep is needed so code reaches the waiting_condition - sleep(0.1) + sequential_threading_handle.next() + assert context["state"] == model.State.WAITING on_update_next_break.assert_called_once() @@ -221,31 +267,20 @@ def test_start(self, sequential_threading, time_machine): assert on_update_next_break.call_args[0][0].name == "translated!: break 1" on_update_next_break.reset_mock() - with safe_eyes_core.lock: - time_machine.shift(delta=datetime.timedelta(minutes=15)) - - with safe_eyes_core.waiting_condition: - logging.debug("notify") - safe_eyes_core.waiting_condition.notify_all() - - logging.debug("wait for end of __scheduler_job") - sequential_threading.wait() - logging.debug("done waiting for end of __scheduler_job") + # wait for end of __scheduler_job - we cannot stop while waiting on the condvar + # this just moves us into waiting for __wait_until_prepare to start + sequential_threading_handle.next() safe_eyes_core.stop() assert context["state"] == model.State.STOPPED - logging.debug("done") - def run_next_break( self, - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, break_duration, - break_interval, - pre_break_warning_time, break_name_translated, ): """Run one entire cycle of safe_eyes_core. @@ -267,9 +302,9 @@ def run_next_break( safe_eyes_core.on_count_down += on_count_down # start __scheduler_job - sequential_threading.next() - # FIXME: sleep is needed so code reaches the waiting_condition - sleep(0.1) + sequential_threading_handle.next() + # wait until it reaches the condvar + assert context["state"] == model.State.WAITING on_update_next_break.assert_called_once() @@ -277,16 +312,9 @@ def run_next_break( assert on_update_next_break.call_args[0][0].name == break_name_translated on_update_next_break.reset_mock() - with safe_eyes_core.lock: - time_machine.shift(delta=datetime.timedelta(minutes=break_interval)) - - with safe_eyes_core.waiting_condition: - logging.debug("notify") - safe_eyes_core.waiting_condition.notify_all() - - logging.debug("wait for end of __scheduler_job") - sequential_threading.wait() - logging.debug("done waiting for end of __scheduler_job") + # continue after condvar + sequential_threading_handle.next() + # end of __scheduler_job assert context["state"] == model.State.PRE_BREAK @@ -296,27 +324,18 @@ def run_next_break( on_pre_break.reset_mock() # start __wait_until_prepare - sequential_threading.next() - - # FIXME: sleep is needed so code reaches the waiting_condition - sleep(0.1) - with safe_eyes_core.lock: - time_machine.shift(delta=datetime.timedelta(seconds=pre_break_warning_time)) + sequential_threading_handle.next() - with safe_eyes_core.waiting_condition: - logging.debug("notify") - safe_eyes_core.waiting_condition.notify_all() - - logging.debug("wait for end of __wait_until_prepare") - sequential_threading.wait() - logging.debug("done waiting for end of __wait_until_prepare") + # wait until it reaches the condvar + # continue after condvar + sequential_threading_handle.next() + # end of __wait_until_prepare # start __start_break - sequential_threading.next() - sequential_threading.wait() + sequential_threading_handle.next() # first sleep in __start_break - sequential_threading.next() + sequential_threading_handle.next() on_start_break.assert_called_once() assert isinstance(on_start_break.call_args[0][0], model.Break) @@ -331,13 +350,11 @@ def run_next_break( assert context["state"] == model.State.BREAK # continue sleep in __start_break - for i in range(break_duration - 1): - sequential_threading.wait() - sequential_threading.next() + for i in range(break_duration - 2): + sequential_threading_handle.next() - logging.debug("wait for end of __start_break") - sequential_threading.wait() - logging.debug("done waiting for end of __start_break") + sequential_threading_handle.next() + # end of __start_break on_count_down.assert_called() assert on_count_down.call_count == break_duration @@ -352,7 +369,7 @@ def assert_datetime(self, string): datetime.timezone.utc ) == datetime.datetime.fromisoformat(string) - def test_actual(self, sequential_threading, time_machine): + def test_full_run_with_defaults(self, sequential_threading, time_machine): context = { "session": {}, } @@ -386,86 +403,88 @@ def test_actual(self, sequential_threading, time_machine): safe_eyes_core = core.SafeEyesCore(context) + sequential_threading_handle = sequential_threading(safe_eyes_core) + safe_eyes_core.initialize(config) safe_eyes_core.start() self.run_next_break( - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, short_break_duration, - short_break_interval, - pre_break_warning_time, "translated!: break 1", ) + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration self.assert_datetime("2024-08-25T13:15:25") self.run_next_break( - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, short_break_duration, - short_break_interval, - pre_break_warning_time, "translated!: break 2", ) self.assert_datetime("2024-08-25T13:30:50") self.run_next_break( - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, short_break_duration, - short_break_interval, - pre_break_warning_time, "translated!: break 3", ) self.assert_datetime("2024-08-25T13:46:15") self.run_next_break( - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, short_break_duration, - short_break_interval, - pre_break_warning_time, "translated!: break 4", ) self.assert_datetime("2024-08-25T14:01:40") self.run_next_break( - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, long_break_duration, - long_break_interval, - pre_break_warning_time, "translated!: long break 1", ) - # self.assert_datetime("2024-08-25T14:16:40") + # Time passed: 16min 10s + # 15min short_break_interval (from previous, as long_break_interval must be + # multiple) + # 10 seconds pre_break_warning_time, 1 minute long_break_duration + self.assert_datetime("2024-08-25T14:17:50") self.run_next_break( - sequential_threading, + sequential_threading_handle, time_machine, safe_eyes_core, context, short_break_duration, - short_break_interval, - pre_break_warning_time, "translated!: break 1", ) + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T14:33:15") + safe_eyes_core.stop() assert context["state"] == model.State.STOPPED From ed13832e1e01b51135adcff989c2ced7acdada7e Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 25 Aug 2024 22:33:49 +0200 Subject: [PATCH 072/134] test SafeEyesCore: add test from https://github.com/slgobinath/SafeEyes/issues/640 --- safeeyes/tests/test_core.py | 112 ++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index d84e983c..2534aac5 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -488,3 +488,115 @@ def test_full_run_with_defaults(self, sequential_threading, time_machine): safe_eyes_core.stop() assert context["state"] == model.State.STOPPED + + def test_long_duration_is_bigger_than_short_interval( + self, sequential_threading, time_machine + ): + """Example taken from https://github.com/slgobinath/SafeEyes/issues/640.""" + context = { + "session": {}, + } + short_break_duration = 300 # seconds = 5min + short_break_interval = 25 # minutes + pre_break_warning_time = 10 # seconds + long_break_duration = 1800 # seconds = 30min + long_break_interval = 100 # minutes + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + } + + self.assert_datetime("2024-08-25T13:00:00") + + safe_eyes_core = core.SafeEyesCore(context) + + sequential_threading_handle = sequential_threading(safe_eyes_core) + + safe_eyes_core.initialize(config) + + safe_eyes_core.start() + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + ) + + # Time passed: 30m 10s + # 25min short_break_interval, 10 seconds pre_break_warning_time, + # 5 minutes short_break_duration + self.assert_datetime("2024-08-25T13:30:10") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 2", + ) + + self.assert_datetime("2024-08-25T14:00:20") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 3", + ) + + self.assert_datetime("2024-08-25T14:30:30") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + long_break_duration, + "translated!: long break 1", + ) + + # Time passed: 55min 10s + # 25min short_break_interval (from previous, as long_break_interval must be + # multiple) + # 10 seconds pre_break_warning_time, 30 minute long_break_duration + self.assert_datetime("2024-08-25T15:25:40") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 4", + ) + + # Time passed: 30m 10s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T15:55:50") + + safe_eyes_core.stop() + + assert context["state"] == model.State.STOPPED From 1b6ed49086b397f84ce8d4514ee6aa50b347796b Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 25 Aug 2024 22:36:38 +0200 Subject: [PATCH 073/134] reorder methods --- safeeyes/tests/test_core.py | 176 ++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 2534aac5..06234e0a 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -186,94 +186,6 @@ def stop(self): if handle: handle.stop() - def test_create_empty(self): - context = {} - config = { - "short_breaks": [], - "long_breaks": [], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - "postpone_duration": 5, - } - safe_eyes_core = core.SafeEyesCore(context) - safe_eyes_core.initialize(config) - - def test_start_empty(self, sequential_threading): - context = {} - config = { - "short_breaks": [], - "long_breaks": [], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - "postpone_duration": 5, - } - on_update_next_break = mock.Mock() - safe_eyes_core = core.SafeEyesCore(context) - safe_eyes_core.on_update_next_break += mock - - safe_eyes_core.initialize(config) - - safe_eyes_core.start() - safe_eyes_core.stop() - - on_update_next_break.assert_not_called() - - def test_start(self, sequential_threading, time_machine): - context = { - "session": {}, - } - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - {"name": "break 4"}, - ], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - "postpone_duration": 5, - } - on_update_next_break = mock.Mock() - safe_eyes_core = core.SafeEyesCore(context) - safe_eyes_core.on_update_next_break += on_update_next_break - - safe_eyes_core.initialize(config) - - sequential_threading_handle = sequential_threading(safe_eyes_core) - - safe_eyes_core.start() - - # start __scheduler_job - sequential_threading_handle.next() - - assert context["state"] == model.State.WAITING - - on_update_next_break.assert_called_once() - assert isinstance(on_update_next_break.call_args[0][0], model.Break) - assert on_update_next_break.call_args[0][0].name == "translated!: break 1" - on_update_next_break.reset_mock() - - # wait for end of __scheduler_job - we cannot stop while waiting on the condvar - # this just moves us into waiting for __wait_until_prepare to start - sequential_threading_handle.next() - - safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED - def run_next_break( self, sequential_threading_handle, @@ -369,6 +281,94 @@ def assert_datetime(self, string): datetime.timezone.utc ) == datetime.datetime.fromisoformat(string) + def test_create_empty(self): + context = {} + config = { + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + } + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.initialize(config) + + def test_start_empty(self, sequential_threading): + context = {} + config = { + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + } + on_update_next_break = mock.Mock() + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.on_update_next_break += mock + + safe_eyes_core.initialize(config) + + safe_eyes_core.start() + safe_eyes_core.stop() + + on_update_next_break.assert_not_called() + + def test_start(self, sequential_threading, time_machine): + context = { + "session": {}, + } + config = { + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + } + on_update_next_break = mock.Mock() + safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core.on_update_next_break += on_update_next_break + + safe_eyes_core.initialize(config) + + sequential_threading_handle = sequential_threading(safe_eyes_core) + + safe_eyes_core.start() + + # start __scheduler_job + sequential_threading_handle.next() + + assert context["state"] == model.State.WAITING + + on_update_next_break.assert_called_once() + assert isinstance(on_update_next_break.call_args[0][0], model.Break) + assert on_update_next_break.call_args[0][0].name == "translated!: break 1" + on_update_next_break.reset_mock() + + # wait for end of __scheduler_job - we cannot stop while waiting on the condvar + # this just moves us into waiting for __wait_until_prepare to start + sequential_threading_handle.next() + + safe_eyes_core.stop() + assert context["state"] == model.State.STOPPED + def test_full_run_with_defaults(self, sequential_threading, time_machine): context = { "session": {}, From cccd6a1b813266d5c3bf05047d6484dbd758bfff Mon Sep 17 00:00:00 2001 From: deltragon Date: Sat, 2 Aug 2025 21:40:33 +0200 Subject: [PATCH 074/134] add dependency on time-machine --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1b2c0505..822c0ca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ types = [ ] tests = [ "pytest==8.3.5", + "time-machine==2.16.0", ] [tool.mypy] From 88babdf08b49603af779b9d146d569bf5efb3325 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 19:13:07 +0200 Subject: [PATCH 075/134] core: add yield point when starting scheduler for the tests --- safeeyes/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 2df72b9f..4b27c3cf 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -448,4 +448,4 @@ def __start_next_break(self) -> None: if self.running: # Schedule the break again - self.__scheduler_job() + self.__wait_for(0, self.__scheduler_job) From 253db1d5bc88291fb89458eff4280ea51b3f2591 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 19:13:07 +0200 Subject: [PATCH 076/134] core: fix tests now that SafeEyesCore is single-threaded, this is much, much easier. --- safeeyes/tests/test_core.py | 229 +++++++++++++----------------------- 1 file changed, 79 insertions(+), 150 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 06234e0a..08f9c826 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -16,19 +16,45 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from collections import deque import datetime -import logging import pytest +import typing from safeeyes import core from safeeyes import model -import threading - from unittest import mock +class SafeEyesCoreHandle: + callback: typing.Optional[typing.Tuple[typing.Callable, int]] = None + + def __init__(self, safe_eyes_core: core.SafeEyesCore, time_machine): + self.time_machine = time_machine + self.safe_eyes_core = safe_eyes_core + + def timeout_add_seconds(self, duration: int, callback: typing.Callable) -> int: + if self.callback is not None: + raise Exception("only one callback supported. need to make this smarter") + self.callback = (callback, duration) + print(f"callback registered for {callback} and {duration}") + return 1 + + def next(self): + assert self.callback + + (callback, duration) = self.callback + self.callback = None + self.time_machine.shift(delta=datetime.timedelta(seconds=duration)) + print(f"shift to {datetime.datetime.now()}") + callback() + + +SequentialThreadingFixture: typing.TypeAlias = typing.Callable[ + [core.SafeEyesCore], SafeEyesCoreHandle +] + + class TestSafeEyesCore: @pytest.fixture(autouse=True) def set_time(self, time_machine): @@ -46,7 +72,9 @@ def monkeypatch_translations(self, monkeypatch): ) @pytest.fixture - def sequential_threading(self, monkeypatch, time_machine): + def sequential_threading( + self, monkeypatch: pytest.MonkeyPatch, time_machine + ) -> typing.Generator[SequentialThreadingFixture]: """This fixture allows stopping threads at any point. It is hard-coded for SafeEyesCore, the handle class returned by the fixture must @@ -61,130 +89,30 @@ def sequential_threading(self, monkeypatch, time_machine): """ # executes instantly, on the same thread # no need to switch threads, as we don't use any gtk things - monkeypatch.setattr( - core.utility, - "execute_main_thread", - lambda target_function, *args, **kwargs: target_function(*args, **kwargs), - ) + handle: typing.Optional["SafeEyesCoreHandle"] = None - handle = None - - def utility_start_thread(target_function, **kwargs): - if not handle: - raise Exception("handle must be initialized before first thread") - handle.utility_start_thread(target_function, **kwargs) - - def time_sleep(time): + def timeout_add_seconds(duration, callback) -> int: if not handle: raise Exception("handle must be initialized before first sleep call") - handle.sleep(time) - - monkeypatch.setattr(core.utility, "start_thread", utility_start_thread) - - monkeypatch.setattr(core.time, "sleep", time_sleep) - - class PatchedCondition(threading.Condition): - def __init__(self, handle): - super().__init__() - self.handle = handle - - def wait(self, timeout): - self.handle.wait_condvar(timeout) - - class Handle: - thread = None - task_queue = deque() - running = True - condvar_in = threading.Condition() - condvar_out = threading.Condition() - - def __init__(self, safe_eyes_core): - nonlocal handle - nonlocal time_machine - if handle: - raise Exception("only one handle is allowed per test call") - handle = self - self.time_machine = time_machine - self.safe_eyes_core = safe_eyes_core - self.safe_eyes_core.waiting_condition = PatchedCondition(self) - - def background_thread(self): - while True: - with self.condvar_in: - success = self.condvar_in.wait(1) - if not success: - raise Exception("thread timed out") - - if not self.running: - break - - if self.task_queue: - (target_function, kwargs) = self.task_queue.popleft() - logging.debug(f"thread started {target_function}") - target_function(**kwargs) - logging.debug(f"thread finished {target_function}") - - with self.condvar_out: - self.condvar_out.notify() - - def sleep(self, time): - if self.thread is threading.current_thread(): - with self.condvar_out: - self.condvar_out.notify() - self.time_machine.shift(delta=datetime.timedelta(seconds=time)) - with self.condvar_in: - success = self.condvar_in.wait(1) - if not success: - raise Exception("thread timed out") - - def wait_condvar(self, time): - if self.thread is not threading.current_thread(): - raise Exception("waiting on condition may only happen in thread") - - with self.condvar_out: - self.condvar_out.notify() - self.time_machine.shift(delta=datetime.timedelta(seconds=time)) - with self.condvar_in: - success = self.condvar_in.wait(1) - if not success: - raise Exception("thread timed out") - - def utility_start_thread(self, target_function, **kwargs): - self.task_queue.append((target_function, kwargs)) - - if self.thread is None: - self.thread = threading.Thread( - target=self.background_thread, - name="SequentialThreadingRunner", - daemon=False, - kwargs=kwargs, - ) - self.thread.start() - - def next(self): - assert self.thread - - with self.condvar_in: - self.condvar_in.notify() - - # wait until done: - with self.condvar_out: - success = self.condvar_out.wait(1) - if not success: - raise Exception("thread timed out") - - def stop(self): - self.running = False - with self.condvar_in: - self.condvar_in.notify() - - if self.thread: - self.thread.join(1) - - yield Handle - - if handle: - handle.stop() + return handle.timeout_add_seconds(duration, callback) + + def source_remove(source_id: int) -> None: + pass + + monkeypatch.setattr(core.GLib, "timeout_add_seconds", timeout_add_seconds) + monkeypatch.setattr(core.GLib, "source_remove", source_remove) + + def create_handle(safe_eyes_core: core.SafeEyesCore) -> SafeEyesCoreHandle: + nonlocal time_machine + nonlocal handle + if handle: + raise Exception("only one handle is allowed per test call") + + handle = SafeEyesCoreHandle(safe_eyes_core, time_machine) + + return handle + + yield create_handle def run_next_break( self, @@ -194,28 +122,36 @@ def run_next_break( context, break_duration, break_name_translated, + initial: bool = False, ): """Run one entire cycle of safe_eyes_core. - It must be waiting for __scheduler_job to run. (This is the equivalent of - State.WAITING). - That means it must either be just started, or have finished the previous cycle. + If initial is True, it must not be started yet. + If initial is False, it must be in the state where __scheduler_job is about to + be called again. + This means it is in the BREAK state, but the break has ended and on_stop_break + was already called. """ on_update_next_break = mock.Mock() on_pre_break = mock.Mock(return_value=True) on_start_break = mock.Mock(return_value=True) start_break = mock.Mock() on_count_down = mock.Mock() + on_stop_break = mock.Mock() safe_eyes_core.on_update_next_break += on_update_next_break safe_eyes_core.on_pre_break += on_pre_break safe_eyes_core.on_start_break += on_start_break safe_eyes_core.start_break += start_break safe_eyes_core.on_count_down += on_count_down + safe_eyes_core.on_stop_break += on_stop_break - # start __scheduler_job - sequential_threading_handle.next() - # wait until it reaches the condvar + if initial: + safe_eyes_core.start() + else: + assert context["state"] == model.State.BREAK + + sequential_threading_handle.next() assert context["state"] == model.State.WAITING @@ -236,19 +172,11 @@ def run_next_break( on_pre_break.reset_mock() # start __wait_until_prepare - sequential_threading_handle.next() - - # wait until it reaches the condvar - # continue after condvar - sequential_threading_handle.next() - # end of __wait_until_prepare - - # start __start_break - sequential_threading_handle.next() - # first sleep in __start_break sequential_threading_handle.next() + assert context["state"] == model.State.BREAK + on_start_break.assert_called_once() assert isinstance(on_start_break.call_args[0][0], model.Break) assert on_start_break.call_args[0][0].name == break_name_translated @@ -262,9 +190,11 @@ def run_next_break( assert context["state"] == model.State.BREAK # continue sleep in __start_break - for i in range(break_duration - 2): + for i in range(break_duration - 1): sequential_threading_handle.next() + assert context["state"] == model.State.BREAK + sequential_threading_handle.next() # end of __start_break @@ -272,6 +202,10 @@ def run_next_break( assert on_count_down.call_count == break_duration on_count_down.reset_mock() + on_stop_break.assert_called() + assert on_stop_break.call_count == 1 + on_stop_break.reset_mock() + assert context["state"] == model.State.BREAK def assert_datetime(self, string): @@ -352,9 +286,6 @@ def test_start(self, sequential_threading, time_machine): safe_eyes_core.start() - # start __scheduler_job - sequential_threading_handle.next() - assert context["state"] == model.State.WAITING on_update_next_break.assert_called_once() @@ -407,8 +338,6 @@ def test_full_run_with_defaults(self, sequential_threading, time_machine): safe_eyes_core.initialize(config) - safe_eyes_core.start() - self.run_next_break( sequential_threading_handle, time_machine, @@ -416,6 +345,7 @@ def test_full_run_with_defaults(self, sequential_threading, time_machine): context, short_break_duration, "translated!: break 1", + initial=True, ) # Time passed: 15min 25s @@ -530,8 +460,6 @@ def test_long_duration_is_bigger_than_short_interval( safe_eyes_core.initialize(config) - safe_eyes_core.start() - self.run_next_break( sequential_threading_handle, time_machine, @@ -539,6 +467,7 @@ def test_long_duration_is_bigger_than_short_interval( context, short_break_duration, "translated!: break 1", + initial=True, ) # Time passed: 30m 10s From 23bcc92a49f1dd2fa84da322964c50ca2dff6450 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 20:35:44 +0200 Subject: [PATCH 077/134] test_core: add types --- safeeyes/tests/test_core.py | 202 ++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 87 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 08f9c826..925eb54c 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -23,13 +23,21 @@ from safeeyes import core from safeeyes import model +from time_machine import TimeMachineFixture + from unittest import mock class SafeEyesCoreHandle: callback: typing.Optional[typing.Tuple[typing.Callable, int]] = None + safe_eyes_core: core.SafeEyesCore + time_machine: TimeMachineFixture - def __init__(self, safe_eyes_core: core.SafeEyesCore, time_machine): + def __init__( + self, + safe_eyes_core: core.SafeEyesCore, + time_machine: TimeMachineFixture, + ): self.time_machine = time_machine self.safe_eyes_core = safe_eyes_core @@ -40,7 +48,7 @@ def timeout_add_seconds(self, duration: int, callback: typing.Callable) -> int: print(f"callback registered for {callback} and {duration}") return 1 - def next(self): + def next(self) -> None: assert self.callback (callback, duration) = self.callback @@ -63,7 +71,7 @@ def set_time(self, time_machine): ) @pytest.fixture(autouse=True) - def monkeypatch_translations(self, monkeypatch): + def monkeypatch_translations(self, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( core, "_", lambda message: "translated!: " + message, raising=False ) @@ -73,7 +81,9 @@ def monkeypatch_translations(self, monkeypatch): @pytest.fixture def sequential_threading( - self, monkeypatch: pytest.MonkeyPatch, time_machine + self, + monkeypatch: pytest.MonkeyPatch, + time_machine: TimeMachineFixture, ) -> typing.Generator[SequentialThreadingFixture]: """This fixture allows stopping threads at any point. @@ -116,12 +126,12 @@ def create_handle(safe_eyes_core: core.SafeEyesCore) -> SafeEyesCoreHandle: def run_next_break( self, - sequential_threading_handle, - time_machine, - safe_eyes_core, + sequential_threading_handle: SafeEyesCoreHandle, + time_machine: TimeMachineFixture, + safe_eyes_core: core.SafeEyesCore, context, - break_duration, - break_name_translated, + break_duration: int, + break_name_translated: str, initial: bool = False, ): """Run one entire cycle of safe_eyes_core. @@ -208,7 +218,7 @@ def run_next_break( assert context["state"] == model.State.BREAK - def assert_datetime(self, string): + def assert_datetime(self, string: str): if not string.endswith("+00:00"): string += "+00:00" assert datetime.datetime.now( @@ -230,18 +240,21 @@ def test_create_empty(self): safe_eyes_core = core.SafeEyesCore(context) safe_eyes_core.initialize(config) - def test_start_empty(self, sequential_threading): - context = {} - config = { - "short_breaks": [], - "long_breaks": [], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - "postpone_duration": 5, - } + def test_start_empty(self, sequential_threading: SequentialThreadingFixture): + context: dict[str, typing.Any] = {} + config = model.Config( + user_config={ + "short_breaks": [], + "long_breaks": [], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) on_update_next_break = mock.Mock() safe_eyes_core = core.SafeEyesCore(context) safe_eyes_core.on_update_next_break += mock @@ -253,29 +266,32 @@ def test_start_empty(self, sequential_threading): on_update_next_break.assert_not_called() - def test_start(self, sequential_threading, time_machine): - context = { + def test_start(self, sequential_threading: SequentialThreadingFixture): + context: dict[str, typing.Any] = { "session": {}, } - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - {"name": "break 4"}, - ], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - "postpone_duration": 5, - } + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": 15, + "long_break_interval": 75, + "long_break_duration": 60, + "short_break_duration": 15, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) on_update_next_break = mock.Mock() safe_eyes_core = core.SafeEyesCore(context) safe_eyes_core.on_update_next_break += on_update_next_break @@ -300,8 +316,12 @@ def test_start(self, sequential_threading, time_machine): safe_eyes_core.stop() assert context["state"] == model.State.STOPPED - def test_full_run_with_defaults(self, sequential_threading, time_machine): - context = { + def test_full_run_with_defaults( + self, + sequential_threading: SequentialThreadingFixture, + time_machine: TimeMachineFixture, + ): + context: dict[str, typing.Any] = { "session": {}, } short_break_duration = 15 # seconds @@ -309,26 +329,29 @@ def test_full_run_with_defaults(self, sequential_threading, time_machine): pre_break_warning_time = 10 # seconds long_break_duration = 60 # seconds long_break_interval = 75 # minutes - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - {"name": "break 4"}, - ], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": short_break_interval, - "long_break_interval": long_break_interval, - "long_break_duration": long_break_duration, - "short_break_duration": short_break_duration, - "pre_break_warning_time": pre_break_warning_time, - "random_order": False, - "postpone_duration": 5, - } + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) self.assert_datetime("2024-08-25T13:00:00") @@ -420,10 +443,12 @@ def test_full_run_with_defaults(self, sequential_threading, time_machine): assert context["state"] == model.State.STOPPED def test_long_duration_is_bigger_than_short_interval( - self, sequential_threading, time_machine + self, + sequential_threading: SequentialThreadingFixture, + time_machine: TimeMachineFixture, ): """Example taken from https://github.com/slgobinath/SafeEyes/issues/640.""" - context = { + context: dict[str, typing.Any] = { "session": {}, } short_break_duration = 300 # seconds = 5min @@ -431,26 +456,29 @@ def test_long_duration_is_bigger_than_short_interval( pre_break_warning_time = 10 # seconds long_break_duration = 1800 # seconds = 30min long_break_interval = 100 # minutes - config = { - "short_breaks": [ - {"name": "break 1"}, - {"name": "break 2"}, - {"name": "break 3"}, - {"name": "break 4"}, - ], - "long_breaks": [ - {"name": "long break 1"}, - {"name": "long break 2"}, - {"name": "long break 3"}, - ], - "short_break_interval": short_break_interval, - "long_break_interval": long_break_interval, - "long_break_duration": long_break_duration, - "short_break_duration": short_break_duration, - "pre_break_warning_time": pre_break_warning_time, - "random_order": False, - "postpone_duration": 5, - } + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) self.assert_datetime("2024-08-25T13:00:00") From e93019e5bb5e07ebb14eef84c56b40f8b0b5eaeb Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 3 Aug 2025 20:35:44 +0200 Subject: [PATCH 078/134] test_core: remove useless test --- safeeyes/tests/test_core.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 925eb54c..96b1a200 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -225,21 +225,6 @@ def assert_datetime(self, string: str): datetime.timezone.utc ) == datetime.datetime.fromisoformat(string) - def test_create_empty(self): - context = {} - config = { - "short_breaks": [], - "long_breaks": [], - "short_break_interval": 15, - "long_break_interval": 75, - "long_break_duration": 60, - "short_break_duration": 15, - "random_order": False, - "postpone_duration": 5, - } - safe_eyes_core = core.SafeEyesCore(context) - safe_eyes_core.initialize(config) - def test_start_empty(self, sequential_threading: SequentialThreadingFixture): context: dict[str, typing.Any] = {} config = model.Config( From 3fb1509c1c099a083bb201438c88677605a4f436 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 22:39:24 +0200 Subject: [PATCH 079/134] donotdisturb: move shared code into method --- safeeyes/plugins/donotdisturb/plugin.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/safeeyes/plugins/donotdisturb/plugin.py b/safeeyes/plugins/donotdisturb/plugin.py index 63d234bf..6c855d59 100644 --- a/safeeyes/plugins/donotdisturb/plugin.py +++ b/safeeyes/plugins/donotdisturb/plugin.py @@ -229,29 +229,24 @@ def _normalize_window_classes(classes_as_str: str): return [w.lower() for w in classes_as_str.split()] -def on_pre_break(break_obj): - """Lifecycle method executes before the pre-break period.""" +def __should_skip_break(pre_break: bool) -> bool: if utility.IS_WAYLAND: if utility.DESKTOP_ENVIRONMENT == "gnome": skip_break = is_idle_inhibited_gnome() else: - skip_break = is_active_window_skipped_wayland(True) + skip_break = is_active_window_skipped_wayland(pre_break) else: - skip_break = is_active_window_skipped_xorg(True) + skip_break = is_active_window_skipped_xorg(pre_break) if dnd_while_on_battery and not skip_break: skip_break = is_on_battery() return skip_break +def on_pre_break(break_obj): + """Lifecycle method executes before the pre-break period.""" + return __should_skip_break(pre_break=True) + + def on_start_break(break_obj): """Lifecycle method executes just before the break.""" - if utility.IS_WAYLAND: - if utility.DESKTOP_ENVIRONMENT == "gnome": - skip_break = is_idle_inhibited_gnome() - else: - skip_break = is_active_window_skipped_wayland(False) - else: - skip_break = is_active_window_skipped_xorg(False) - if dnd_while_on_battery and not skip_break: - skip_break = is_on_battery() - return skip_break + return __should_skip_break(pre_break=False) From 4909fd8d813421be9f7d857e5cc858222c01721d Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 22:52:59 +0200 Subject: [PATCH 080/134] donotdisturb: implement detection on KDE Plasma Wayland This uses a non-standard property on org.freedesktop.Notifications that is present on KDE Plasma. --- .../donotdisturb/dependency_checker.py | 5 ++- safeeyes/plugins/donotdisturb/plugin.py | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/safeeyes/plugins/donotdisturb/dependency_checker.py b/safeeyes/plugins/donotdisturb/dependency_checker.py index 96a00de4..91907f98 100644 --- a/safeeyes/plugins/donotdisturb/dependency_checker.py +++ b/safeeyes/plugins/donotdisturb/dependency_checker.py @@ -23,7 +23,10 @@ def validate(plugin_config, plugin_settings): command = None if utility.IS_WAYLAND: - if utility.DESKTOP_ENVIRONMENT == "gnome": + if ( + utility.DESKTOP_ENVIRONMENT == "gnome" + or utility.DESKTOP_ENVIRONMENT == "kde" + ): return None command = "wlrctl" else: diff --git a/safeeyes/plugins/donotdisturb/plugin.py b/safeeyes/plugins/donotdisturb/plugin.py index 6c855d59..4e1dff91 100644 --- a/safeeyes/plugins/donotdisturb/plugin.py +++ b/safeeyes/plugins/donotdisturb/plugin.py @@ -176,6 +176,32 @@ def is_idle_inhibited_gnome(): return bool(result & 0b1000) +def is_idle_inhibited_kde() -> bool: + """KDE Plasma doesn't work with wlrctl, and there is no way to enumerate + fullscreen windows, but KDE does expose a non-standard Inhibited property on + org.freedesktop.Notifications, which does communicate the Do Not Disturb status + on KDE. + This is also only an approximation, but comes pretty close. + """ + dbus_proxy = Gio.DBusProxy.new_for_bus_sync( + bus_type=Gio.BusType.SESSION, + flags=Gio.DBusProxyFlags.NONE, + info=None, + name="org.freedesktop.Notifications", + object_path="/org/freedesktop/Notifications", + interface_name="org.freedesktop.Notifications", + cancellable=None, + ) + prop = dbus_proxy.get_cached_property("Inhibited") + + if prop is None: + return False + + result = prop.unpack() + + return result + + def _window_class_matches(window_class: str, classes: list) -> bool: return any(map(lambda w: w in classes, window_class.split())) @@ -233,12 +259,18 @@ def __should_skip_break(pre_break: bool) -> bool: if utility.IS_WAYLAND: if utility.DESKTOP_ENVIRONMENT == "gnome": skip_break = is_idle_inhibited_gnome() + elif utility.DESKTOP_ENVIRONMENT == "kde": + skip_break = is_idle_inhibited_kde() else: skip_break = is_active_window_skipped_wayland(pre_break) else: skip_break = is_active_window_skipped_xorg(pre_break) if dnd_while_on_battery and not skip_break: skip_break = is_on_battery() + + if skip_break: + logging.info("Skipping break due to donotdisturb") + return skip_break From b4df993632bb24210f151ac09c873ae8b96695ba Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 18:58:11 +0200 Subject: [PATCH 081/134] document the plugin methods better --- safeeyes/plugin_manager.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/safeeyes/plugin_manager.py b/safeeyes/plugin_manager.py index 35cc4107..9d9e7663 100644 --- a/safeeyes/plugin_manager.py +++ b/safeeyes/plugin_manager.py @@ -24,9 +24,7 @@ |- plugin.py |- icon.png (Optional) -The plugin.py can have following methods but all are optional: - - description() - If a custom description has to be displayed, use this function +The plugin.py can have following lifecycle methods but all are optional: - init(context, safeeyes_config, plugin_config) Initialize the plugin. Will be called after loading and after every changes in configuration @@ -50,6 +48,20 @@ Executes once the plugin.py is loaded as a module - disable() Executes if the plugin is disabled at the runtime by the user + +The plugin.py can additionally have the following methods: + - get_widget_title(break_obj) + Returns title of this plugin's widget on the break screen + If this is used, it must also use get_widget_content to work correctly + - get_widget_content(break_obj) + Returns content of this plugin's widget on the break screen + If this is used, it must also use get_widget_title to work correctly + - get_tray_action(break_obj) -> TrayAction + Display a button on the break screen's tray that triggers an action + +This method is unused: + - description() + If a custom description has to be displayed, use this function """ import importlib From 301be24c3b860d0acb7079e19cf081a4c970ce5e Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 19:27:58 +0200 Subject: [PATCH 082/134] add types to TrayAction, make icon_path optional --- safeeyes/model.py | 40 ++++++++++++++++++++++++++-------------- safeeyes/utility.py | 5 ++++- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/safeeyes/model.py b/safeeyes/model.py index 9d02e70a..116d1af4 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -474,21 +474,26 @@ def __ne__(self, config): class TrayAction: """Data object wrapping name, icon and action.""" - def __init__(self, name, icon, action, system_icon): + __toolbar_buttons: list[Gtk.Button] + + def __init__( + self, name: str, icon: str, action: typing.Callable, system_icon: bool + ) -> None: self.name = name self.__icon = icon self.action = action self.system_icon = system_icon self.__toolbar_buttons = [] - def get_icon(self): - if self.system_icon: - image = Gtk.Image.new_from_icon_name(self.__icon) - return image - else: + def get_icon(self) -> Gtk.Image: + if not self.system_icon: image = utility.load_and_scale_image(self.__icon, 16, 16) - image.show() - return image + if image is not None: + image.show() + return image + + image = Gtk.Image.new_from_icon_name(self.__icon) + return image def add_toolbar_button(self, button): self.__toolbar_buttons.append(button) @@ -499,12 +504,19 @@ def reset(self): self.__toolbar_buttons.clear() @classmethod - def build(cls, name, icon_path, icon_id, action): - image = utility.load_and_scale_image(icon_path, 12, 12) - if image is None: - return TrayAction(name, icon_id, action, True) - else: - return TrayAction(name, icon_path, action, False) + def build( + cls, + name: str, + icon_path: typing.Optional[str], + icon_id: str, + action: typing.Callable, + ) -> "TrayAction": + if icon_path is not None: + image = utility.load_and_scale_image(icon_path, 12, 12) + if image is not None: + return TrayAction(name, icon_path, action, False) + + return TrayAction(name, icon_id, action, True) @dataclass diff --git a/safeeyes/utility.py b/safeeyes/utility.py index 28875471..c8908438 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -31,6 +31,7 @@ import shutil import subprocess import threading +import typing from logging.handlers import RotatingFileHandler from pathlib import Path @@ -732,7 +733,9 @@ def create_gtk_builder(glade_file): return builder -def load_and_scale_image(path, width, height): +def load_and_scale_image( + path: str, width: int, height: int +) -> typing.Optional[Gtk.Image]: if not os.path.isfile(path): return None pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( From b3e7c98a93bf7cf5bdfc349b50b49c0b398fde34 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 22:00:18 +0200 Subject: [PATCH 083/134] break screen: make toolbar focusable, fix glade structure --- safeeyes/glade/break_screen.glade | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/safeeyes/glade/break_screen.glade b/safeeyes/glade/break_screen.glade index 94d63915..1ccd3106 100644 --- a/safeeyes/glade/break_screen.glade +++ b/safeeyes/glade/break_screen.glade @@ -25,9 +25,6 @@ 0 0 - - - 1 1 @@ -151,7 +148,6 @@ toolbar - 0 end start From 5fb7bad0b0552827a3731cf77d87c237566c8cbf Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 22:01:31 +0200 Subject: [PATCH 084/134] add support for tray actions that don't disappear on click --- safeeyes/model.py | 13 ++++++++++--- safeeyes/ui/break_screen.py | 10 ++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/safeeyes/model.py b/safeeyes/model.py index 116d1af4..fd322036 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -477,13 +477,19 @@ class TrayAction: __toolbar_buttons: list[Gtk.Button] def __init__( - self, name: str, icon: str, action: typing.Callable, system_icon: bool + self, + name: str, + icon: str, + action: typing.Callable, + system_icon: bool, + single_use: bool, ) -> None: self.name = name self.__icon = icon self.action = action self.system_icon = system_icon self.__toolbar_buttons = [] + self.single_use = single_use def get_icon(self) -> Gtk.Image: if not self.system_icon: @@ -510,13 +516,14 @@ def build( icon_path: typing.Optional[str], icon_id: str, action: typing.Callable, + single_use: bool = True, ) -> "TrayAction": if icon_path is not None: image = utility.load_and_scale_image(icon_path, 12, 12) if image is not None: - return TrayAction(name, icon_path, action, False) + return TrayAction(name, icon_path, action, False, single_use) - return TrayAction(name, icon_id, action, True) + return TrayAction(name, icon_id, action, True, single_use) @dataclass diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index aeacff15..158adc42 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -23,6 +23,7 @@ import gi from safeeyes import utility +from safeeyes.model import TrayAction from safeeyes.translations import translate as _ import Xlib from Xlib.display import Display @@ -139,13 +140,14 @@ def close(self): # Destroy other windows if exists GLib.idle_add(lambda: self.__destroy_all_screens()) - def __tray_action(self, button, tray_action): + def __tray_action(self, button, tray_action: TrayAction): """Tray action handler. - Hides all toolbar buttons for this action and call the action - provided by the plugin. + Hides all toolbar buttons for this action, if it is single use, + and call the action provided by the plugin. """ - tray_action.reset() + if tray_action.single_use: + tray_action.reset() tray_action.action() def __show_break_screen(self, message, image_path, widget, tray_actions): From e0d38fabfb06565e99f8aa8d664335123af55a00 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 22:02:14 +0200 Subject: [PATCH 085/134] add support for plugin returning multiple tray actions --- safeeyes/plugin_manager.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/safeeyes/plugin_manager.py b/safeeyes/plugin_manager.py index 9d9e7663..d9e27a23 100644 --- a/safeeyes/plugin_manager.py +++ b/safeeyes/plugin_manager.py @@ -56,8 +56,8 @@ - get_widget_content(break_obj) Returns content of this plugin's widget on the break screen If this is used, it must also use get_widget_title to work correctly - - get_tray_action(break_obj) -> TrayAction - Display a button on the break screen's tray that triggers an action + - get_tray_action(break_obj) -> TrayAction | list[TrayAction] + Display button(s) on the break screen's tray that triggers an action This method is unused: - description() @@ -70,7 +70,7 @@ import sys from safeeyes import utility -from safeeyes.model import PluginDependency, RequiredPluginException +from safeeyes.model import Break, PluginDependency, RequiredPluginException, TrayAction sys.path.append(os.path.abspath(utility.SYSTEM_PLUGINS_DIR)) sys.path.append(os.path.abspath(utility.USER_PLUGINS_DIR)) @@ -219,15 +219,19 @@ def get_break_screen_widgets(self, break_obj): continue return widget.strip() - def get_break_screen_tray_actions(self, break_obj): + def get_break_screen_tray_actions(self, break_obj: Break) -> list[TrayAction]: """Return Tray Actions.""" actions = [] for plugin in self.__plugins.values(): action = plugin.call_plugin_method_break_obj( "get_tray_action", 1, break_obj ) - if action: + if isinstance(action, TrayAction): actions.append(action) + elif isinstance(action, list): + for a in action: + if isinstance(a, TrayAction): + actions.append(a) return actions From 5bc3e16854cd277cd31e1d21d534a413cba203a2 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 22 Jun 2025 22:04:01 +0200 Subject: [PATCH 086/134] screensaver: add tray action to lock screen now --- safeeyes/plugins/screensaver/plugin.py | 37 +++++++++++--- .../resource/rotation-lock-symbolic.svg | 51 +++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 safeeyes/plugins/screensaver/resource/rotation-lock-symbolic.svg diff --git a/safeeyes/plugins/screensaver/plugin.py b/safeeyes/plugins/screensaver/plugin.py index 5e75b16c..c8b910b4 100644 --- a/safeeyes/plugins/screensaver/plugin.py +++ b/safeeyes/plugins/screensaver/plugin.py @@ -33,6 +33,7 @@ min_seconds = 0 seconds_passed = 0 tray_icon_path = None +icon_lock_later_path = None def __lock_screen_command(): @@ -102,21 +103,29 @@ def __lock_screen_command(): return None -def __lock_screen(): +def __lock_screen_later(): global user_locked_screen user_locked_screen = True +def __lock_screen_now() -> None: + utility.execute_command(lock_screen_command) + + def init(ctx, safeeyes_config, plugin_config): """Initialize the screensaver plugin.""" global context global lock_screen_command global min_seconds global tray_icon_path + global icon_lock_later_path logging.debug("Initialize Screensaver plugin") context = ctx min_seconds = plugin_config["min_seconds"] tray_icon_path = os.path.join(plugin_config["path"], "resource/lock.png") + icon_lock_later_path = os.path.join( + plugin_config["path"], "resource/rotation-lock-symbolic.svg" + ) if plugin_config["command"]: lock_screen_command = plugin_config["command"].split() else: @@ -147,10 +156,22 @@ def on_stop_break(): min_seconds. """ if user_locked_screen or (lock_screen and seconds_passed >= min_seconds): - utility.execute_command(lock_screen_command) - - -def get_tray_action(break_obj): - return TrayAction.build( - "Lock screen", tray_icon_path, "dialog-password", __lock_screen - ) + __lock_screen_now() + + +def get_tray_action(break_obj) -> list[TrayAction]: + return [ + TrayAction.build( + "Lock screen now", + tray_icon_path, + "system-lock-screen", + __lock_screen_now, + single_use=False, + ), + TrayAction.build( + "Lock screen after break", + icon_lock_later_path, + "dialog-password", + __lock_screen_later, + ), + ] diff --git a/safeeyes/plugins/screensaver/resource/rotation-lock-symbolic.svg b/safeeyes/plugins/screensaver/resource/rotation-lock-symbolic.svg new file mode 100644 index 00000000..d2a86cf1 --- /dev/null +++ b/safeeyes/plugins/screensaver/resource/rotation-lock-symbolic.svg @@ -0,0 +1,51 @@ + + + + + + + + + + From f83d1207e44e8ca81e40a16aca3e676595544447 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 10:46:11 +0200 Subject: [PATCH 087/134] update translations --- safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ .../config/locale/en_US/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ .../config/locale/pt_BR/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ .../config/locale/uz_Latn/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ .../config/locale/zh_CN/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ .../config/locale/zh_TW/LC_MESSAGES/safeeyes.po | 13 +++++++++++++ 42 files changed, 546 insertions(+) diff --git a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po index 84747bc8..7dd381ad 100644 --- a/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ar/LC_MESSAGES/safeeyes.po @@ -598,6 +598,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "أغلق عينيك بشدّة" diff --git a/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po index b211b0ab..f09f3316 100644 --- a/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/bg/LC_MESSAGES/safeeyes.po @@ -578,6 +578,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Затворете плътно очи" diff --git a/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po index 9d8894f0..e8d13fe4 100644 --- a/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/bn/LC_MESSAGES/safeeyes.po @@ -577,3 +577,16 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" + +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" diff --git a/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po index e59426d0..a1494aa2 100644 --- a/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ca/LC_MESSAGES/safeeyes.po @@ -592,6 +592,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Tanqueu fortament els ulls" diff --git a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po index 68774231..1fbaf50d 100644 --- a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po @@ -592,6 +592,19 @@ msgstr "Chyba - Safe Eyes" msgid "A required plugin is missing dependencies!" msgstr "Požadovanému doplňku chybí závislosti!" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Zavřete oči" diff --git a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po index 87f8ba7c..fac45d64 100644 --- a/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/da/LC_MESSAGES/safeeyes.po @@ -579,6 +579,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Luk øjnene tæt" diff --git a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po index f05c6024..c3286c42 100644 --- a/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/de/LC_MESSAGES/safeeyes.po @@ -598,6 +598,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Augen fest schließen" diff --git a/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po index 564143d9..ae5d3838 100644 --- a/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/en_US/LC_MESSAGES/safeeyes.po @@ -581,6 +581,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Tightly close your eyes" diff --git a/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po index 57adffe9..150d9ba0 100644 --- a/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/eo/LC_MESSAGES/safeeyes.po @@ -581,6 +581,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Streĉe malfermu viajn okulojn" diff --git a/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po index f4d577fc..13207282 100644 --- a/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/es/LC_MESSAGES/safeeyes.po @@ -592,6 +592,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Cierre fuertemente los ojos" diff --git a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po index 1c86c8d5..2da24ad9 100644 --- a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po @@ -588,6 +588,19 @@ msgstr "Safe Eyes - viga" msgid "A required plugin is missing dependencies!" msgstr "Nõutaval pluginal on sõltuvused puudu!" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Sulge silmad" diff --git a/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po index 7b474dc4..450ee04e 100644 --- a/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/eu/LC_MESSAGES/safeeyes.po @@ -585,6 +585,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Itxi begiak indarrez" diff --git a/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po index ee8dd2f5..456276cb 100644 --- a/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fa/LC_MESSAGES/safeeyes.po @@ -582,6 +582,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "محکم چشمانتان را ببندید" diff --git a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po index 8573921d..7caa8483 100644 --- a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po @@ -595,6 +595,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Fermez bien vos yeux" diff --git a/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po index 72b7292d..3a6e3e8e 100644 --- a/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/he/LC_MESSAGES/safeeyes.po @@ -580,6 +580,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "לעצום עיניים היטב" diff --git a/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po index 7ca23fdc..f95a3086 100644 --- a/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/hi/LC_MESSAGES/safeeyes.po @@ -578,6 +578,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "कसकर अपनी आँखें बंद करें" diff --git a/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po index b7dcb913..77b95ad9 100644 --- a/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/hu/LC_MESSAGES/safeeyes.po @@ -580,6 +580,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Szorosan csukd be a szemed" diff --git a/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po index 6c619b24..9557ec78 100644 --- a/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/id/LC_MESSAGES/safeeyes.po @@ -581,6 +581,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Tutup rapat matamu" diff --git a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po index 05ae2fac..32f8ed55 100644 --- a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po @@ -593,6 +593,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Strizza gli occhi" diff --git a/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po index 95dfd80f..e83ff12c 100644 --- a/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/kn/LC_MESSAGES/safeeyes.po @@ -575,6 +575,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "ಕಣ್ಣನ್ನು ಗಟ್ಟಿಯಾಗಿ ಮುಚ್ಚಿರಿ" diff --git a/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po index 509b0973..9fb2e01c 100644 --- a/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ko/LC_MESSAGES/safeeyes.po @@ -575,6 +575,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "눈을 꼭 감으세요" diff --git a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po index 97940936..18ccfa2b 100644 --- a/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lt/LC_MESSAGES/safeeyes.po @@ -593,6 +593,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Stipriai užsimerkite" diff --git a/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po index 25e3915b..2f2752d9 100644 --- a/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/lv/LC_MESSAGES/safeeyes.po @@ -497,6 +497,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + #~ msgid "Tightly close your eyes" #~ msgstr "Cieši aizver acis" diff --git a/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po index fb986327..26b7e2e0 100644 --- a/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/mk/LC_MESSAGES/safeeyes.po @@ -579,3 +579,16 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" + +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" diff --git a/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po index 48bf4d25..7c83b098 100644 --- a/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/mr/LC_MESSAGES/safeeyes.po @@ -578,6 +578,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "डोळे घट्ट बंद करा" diff --git a/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po index 96e59be5..93013d73 100644 --- a/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nb/LC_MESSAGES/safeeyes.po @@ -582,6 +582,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Lukk øyene dine godt" diff --git a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po index 728d396c..82169122 100644 --- a/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/nl/LC_MESSAGES/safeeyes.po @@ -592,6 +592,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Sluit je ogen goed" diff --git a/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po index ea50c28a..7b43c805 100644 --- a/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pl/LC_MESSAGES/safeeyes.po @@ -586,6 +586,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Dokładnie zamknij oczy" diff --git a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po index de233af1..0b1d8187 100644 --- a/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt/LC_MESSAGES/safeeyes.po @@ -589,6 +589,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Fechar os olhos com força" diff --git a/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po index d3ae6a7c..e5d18fd4 100644 --- a/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/pt_BR/LC_MESSAGES/safeeyes.po @@ -587,6 +587,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Feche firmemente seus olhos" diff --git a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po index 45fdca19..96fe1add 100644 --- a/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ru/LC_MESSAGES/safeeyes.po @@ -594,6 +594,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Плотно закройте глаза" diff --git a/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po index 33be37c1..868579fc 100644 --- a/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sk/LC_MESSAGES/safeeyes.po @@ -588,6 +588,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Pevne zatvor oči" diff --git a/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po index 2a27a8b5..eaf20aeb 100644 --- a/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sr/LC_MESSAGES/safeeyes.po @@ -591,6 +591,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Чврсто затворите очи" diff --git a/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po index 1d7ba661..4c826838 100644 --- a/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/sv/LC_MESSAGES/safeeyes.po @@ -580,6 +580,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Stäng ögonen tätt" diff --git a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po index 0ae40ce5..02a79873 100644 --- a/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ta/LC_MESSAGES/safeeyes.po @@ -588,6 +588,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "உங்கள் கண்களை இறுக்கமாக மூடுங்கள்" diff --git a/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po index 1d6024da..c65c22f2 100644 --- a/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/tr/LC_MESSAGES/safeeyes.po @@ -587,6 +587,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Gözlerinizi sıkıca kapatın" diff --git a/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po index 0aca020a..ed66aa91 100644 --- a/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/ug/LC_MESSAGES/safeeyes.po @@ -573,3 +573,16 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" + +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" diff --git a/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po index 914d2664..d60b9416 100644 --- a/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/uk/LC_MESSAGES/safeeyes.po @@ -587,6 +587,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Щільно заплющіть очі" diff --git a/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po index c9ed80e7..19ef1fc8 100644 --- a/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/uz_Latn/LC_MESSAGES/safeeyes.po @@ -573,3 +573,16 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" + +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" diff --git a/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po index 53c48ee8..4c24f160 100644 --- a/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/vi/LC_MESSAGES/safeeyes.po @@ -581,6 +581,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "Nhắm chặt mắt lại" diff --git a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po index 071f34c0..bcac5ae7 100644 --- a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po @@ -579,6 +579,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "闭上眼睛休息一下" diff --git a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po index 54a27153..058532d3 100644 --- a/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_TW/LC_MESSAGES/safeeyes.po @@ -577,6 +577,19 @@ msgstr "" msgid "A required plugin is missing dependencies!" msgstr "" +msgid "Postponement duration in" +msgstr "" + +msgid "minutes" +msgstr "" + +msgid "seconds" +msgstr "" + +#, python-format +msgid "Please install one of the command-line tools: %s" +msgstr "" + # Short break #~ msgid "Tightly close your eyes" #~ msgstr "閉上您的眼睛休息一會" From 38fadf0d6fd3a4311e87249b2f27cc2a48174f20 Mon Sep 17 00:00:00 2001 From: lalala Date: Mon, 18 Aug 2025 02:18:30 +0200 Subject: [PATCH 088/134] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (136 of 136 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/zh_Hans/ --- safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po index 071f34c0..550504b1 100644 --- a/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/zh_CN/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-06-20 06:03+0000\n" +"PO-Revision-Date: 2025-08-18 08:50+0000\n" "Last-Translator: lalala \n" "Language-Team: Chinese (Simplified Han script) \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.12.1\n" +"X-Generator: Weblate 5.13\n" # Short break msgid "Gently close your eyes" @@ -571,13 +571,13 @@ msgstr "" "样式表。" msgid "Customizing the postpone and skip shortcuts does not work on Wayland." -msgstr "" +msgstr "自定义推迟和跳过快捷键在 Wayland 上不起作用。" msgid "Safe Eyes - Error" -msgstr "" +msgstr "Safe Eyes - 错误" msgid "A required plugin is missing dependencies!" -msgstr "" +msgstr "所需插件缺少依赖项!" # Short break #~ msgid "Tightly close your eyes" From 009f1fbb86dea2bd26a2c2eb2723f974d528ebb3 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 15:01:46 +0200 Subject: [PATCH 089/134] settings dialog: reset button: fix config change This was missed in 0b7e9887429c5fced789487f6dad61b9b9534397 (#730). --- safeeyes/ui/settings_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 9470d43d..4f6e9cac 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -184,11 +184,11 @@ def __create_break_item(self, break_config, is_short): def on_reset_menu_clicked(self, button): self.popover.hide() - def __confirmation_dialog_response(dialog, result): + def __confirmation_dialog_response(dialog, result) -> None: response_id = dialog.choose_finish(result) if response_id == 1: utility.reset_config() - self.config = Config() + self.config = Config.load() # Remove breaks from the container self.__clear_children(self.box_short_breaks) self.__clear_children(self.box_long_breaks) From e99d4247fc47c92ee2cf0eb1c981aff2d54e59d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 18 Aug 2025 20:40:03 +0200 Subject: [PATCH 090/134] Translated using Weblate (Estonian) Currently translated at 100.0% (140 of 140 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/et/ --- safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po index 2da24ad9..3bbaa11c 100644 --- a/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/et/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-08-04 20:02+0000\n" +"PO-Revision-Date: 2025-08-19 19:02+0000\n" "Last-Translator: Priit Jõerüüt \n" "Language-Team: Estonian \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.13-dev\n" +"X-Generator: Weblate 5.13\n" # Short break msgid "Gently close your eyes" @@ -589,17 +589,17 @@ msgid "A required plugin is missing dependencies!" msgstr "Nõutaval pluginal on sõltuvused puudu!" msgid "Postponement duration in" -msgstr "" +msgstr "Edasilükkamise kestus" msgid "minutes" -msgstr "" +msgstr "minuti pärast" msgid "seconds" -msgstr "" +msgstr "sekundi pärast" #, python-format msgid "Please install one of the command-line tools: %s" -msgstr "" +msgstr "Palun paigalda mõni käsureaprogrammidest: %s" # Short break #~ msgid "Tightly close your eyes" From dd7601c6387059cca3d0e87287b9f1ce99f1770b Mon Sep 17 00:00:00 2001 From: AO Localisation Lab Date: Tue, 19 Aug 2025 01:56:34 +0200 Subject: [PATCH 091/134] Translated using Weblate (French) Currently translated at 100.0% (140 of 140 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/fr/ --- .../config/locale/fr/LC_MESSAGES/safeeyes.po | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po index 7caa8483..fcf0ddc0 100644 --- a/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/fr/LC_MESSAGES/safeeyes.po @@ -6,8 +6,9 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2024-11-12 23:00+0000\n" -"Last-Translator: AO Localisation Lab \n" +"PO-Revision-Date: 2025-08-19 19:02+0000\n" +"Last-Translator: AO Localisation Lab " +"\n" "Language-Team: French \n" "Language: fr\n" @@ -15,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 5.9-dev\n" +"X-Generator: Weblate 5.13\n" # Short break msgid "Gently close your eyes" @@ -585,28 +586,33 @@ msgid "" "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new " "stylesheet in '%(new)s' instead." msgstr "" +"Une ancienne feuille de style a été trouvée dans « %(old)s », elle sera " +"ignorée. Pour des styles personnalisés, créez plutôt une nouvelle feuille de " +"style dans « %(new)s »." msgid "Customizing the postpone and skip shortcuts does not work on Wayland." msgstr "" +"La personnalisation des raccourcis pour reporter et ignorer ne fonctionne " +"pas sous Wayland." msgid "Safe Eyes - Error" -msgstr "" +msgstr "Safe Eyes – Erreur" msgid "A required plugin is missing dependencies!" -msgstr "" +msgstr "Des dépendances manquent pour un greffon nécessaire" msgid "Postponement duration in" -msgstr "" +msgstr "Durée du report en" msgid "minutes" -msgstr "" +msgstr "minutes" msgid "seconds" -msgstr "" +msgstr "secondes" #, python-format msgid "Please install one of the command-line tools: %s" -msgstr "" +msgstr "Installez l’un des outils de ligne de commande : %s" # Short break #~ msgid "Tightly close your eyes" From 1912a40325f6f374b22d3d8fb50671f63b1fa587 Mon Sep 17 00:00:00 2001 From: cas9 Date: Wed, 20 Aug 2025 10:23:12 +0200 Subject: [PATCH 092/134] Translated using Weblate (Italian) Currently translated at 100.0% (140 of 140 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/it/ --- .../config/locale/it/LC_MESSAGES/safeeyes.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po index 32f8ed55..a450c357 100644 --- a/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/it/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-07-13 16:01+0000\n" +"PO-Revision-Date: 2025-08-21 09:01+0000\n" "Last-Translator: cas9 \n" "Language-Team: Italian \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.13-dev\n" +"X-Generator: Weblate 5.13\n" # Short break msgid "Gently close your eyes" @@ -585,26 +585,26 @@ msgstr "" "personalizzati, crea un nuovo foglio di stile in %(new)s invece." msgid "Customizing the postpone and skip shortcuts does not work on Wayland." -msgstr "" +msgstr "Personalizzare le scorciatoie postponi e salta non funziona in Wayland." msgid "Safe Eyes - Error" -msgstr "" +msgstr "Safe Eyes - Errore" msgid "A required plugin is missing dependencies!" -msgstr "" +msgstr "Un plugin necessario manca di dipendenze!" msgid "Postponement duration in" -msgstr "" +msgstr "Durata del rinvio in" msgid "minutes" -msgstr "" +msgstr "minuti" msgid "seconds" -msgstr "" +msgstr "secondi" #, python-format msgid "Please install one of the command-line tools: %s" -msgstr "" +msgstr "Prego installa uno degli strumenti da riga di comando: %s" # Short break #~ msgid "Tightly close your eyes" From a1b375fc227957bd007258bbc8b3450e842b67d3 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Wed, 20 Aug 2025 15:56:20 +0200 Subject: [PATCH 093/134] Translated using Weblate (Czech) Currently translated at 100.0% (140 of 140 strings) Translation: Safe Eyes/Translations Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/cs/ --- safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po index 1fbaf50d..e2e00f81 100644 --- a/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po +++ b/safeeyes/config/locale/cs/LC_MESSAGES/safeeyes.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2025-08-13 13:01+0000\n" +"PO-Revision-Date: 2025-08-21 09:01+0000\n" "Last-Translator: vikdevelop \n" "Language-Team: Czech \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" -"X-Generator: Weblate 5.13-dev\n" +"X-Generator: Weblate 5.13\n" # Short break msgid "Gently close your eyes" @@ -593,17 +593,17 @@ msgid "A required plugin is missing dependencies!" msgstr "Požadovanému doplňku chybí závislosti!" msgid "Postponement duration in" -msgstr "" +msgstr "Doba odkladu v" msgid "minutes" -msgstr "" +msgstr "minutách" msgid "seconds" -msgstr "" +msgstr "vteřinách" #, python-format msgid "Please install one of the command-line tools: %s" -msgstr "" +msgstr "Nainstalujte prosím jeden z těchto nástrojů příkazového řádku:%s" # Short break #~ msgid "Tightly close your eyes" From 40dd51fd20431e81a6233dab014bdb3f79952702 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:13:05 +0200 Subject: [PATCH 094/134] tests: core: drop debug prints --- safeeyes/tests/test_core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 96b1a200..5549604f 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -45,7 +45,6 @@ def timeout_add_seconds(self, duration: int, callback: typing.Callable) -> int: if self.callback is not None: raise Exception("only one callback supported. need to make this smarter") self.callback = (callback, duration) - print(f"callback registered for {callback} and {duration}") return 1 def next(self) -> None: @@ -54,7 +53,6 @@ def next(self) -> None: (callback, duration) = self.callback self.callback = None self.time_machine.shift(delta=datetime.timedelta(seconds=duration)) - print(f"shift to {datetime.datetime.now()}") callback() From a642103deb04eb8369eb7e9a3be336e1755921b8 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:13:05 +0200 Subject: [PATCH 095/134] tests: core: properly unregister callback this is needed for stopping and restarting core --- safeeyes/tests/test_core.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 5549604f..369bd09b 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -47,6 +47,11 @@ def timeout_add_seconds(self, duration: int, callback: typing.Callable) -> int: self.callback = (callback, duration) return 1 + def source_remove(self, source_id: int) -> None: + if self.callback is None: + raise Exception("no callback registered") + self.callback = None + def next(self) -> None: assert self.callback @@ -105,7 +110,9 @@ def timeout_add_seconds(duration, callback) -> int: return handle.timeout_add_seconds(duration, callback) def source_remove(source_id: int) -> None: - pass + if not handle: + raise Exception("handle must be initialized before first call") + handle.source_remove(source_id) monkeypatch.setattr(core.GLib, "timeout_add_seconds", timeout_add_seconds) monkeypatch.setattr(core.GLib, "source_remove", source_remove) From 40fbdb825bd4d06808fb86b4ca1f5a2c443a2078 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:13:05 +0200 Subject: [PATCH 096/134] tests: core: add tests for idling refactor run_next_break into another method that allows continuing after idling --- safeeyes/tests/test_core.py | 337 ++++++++++++++++++++++++++++++++++-- 1 file changed, 327 insertions(+), 10 deletions(-) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 369bd09b..d245e1e9 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -148,18 +148,8 @@ def run_next_break( was already called. """ on_update_next_break = mock.Mock() - on_pre_break = mock.Mock(return_value=True) - on_start_break = mock.Mock(return_value=True) - start_break = mock.Mock() - on_count_down = mock.Mock() - on_stop_break = mock.Mock() safe_eyes_core.on_update_next_break += on_update_next_break - safe_eyes_core.on_pre_break += on_pre_break - safe_eyes_core.on_start_break += on_start_break - safe_eyes_core.start_break += start_break - safe_eyes_core.on_count_down += on_count_down - safe_eyes_core.on_stop_break += on_stop_break if initial: safe_eyes_core.start() @@ -175,6 +165,36 @@ def run_next_break( assert on_update_next_break.call_args[0][0].name == break_name_translated on_update_next_break.reset_mock() + self.run_next_break_from_waiting_state( + sequential_threading_handle, + safe_eyes_core, + context, + break_duration, + break_name_translated, + ) + + def run_next_break_from_waiting_state( + self, + sequential_threading_handle: SafeEyesCoreHandle, + safe_eyes_core: core.SafeEyesCore, + context, + break_duration: int, + break_name_translated: str, + ) -> None: + on_pre_break = mock.Mock(return_value=True) + on_start_break = mock.Mock(return_value=True) + start_break = mock.Mock() + on_count_down = mock.Mock() + on_stop_break = mock.Mock() + + safe_eyes_core.on_pre_break += on_pre_break + safe_eyes_core.on_start_break += on_start_break + safe_eyes_core.start_break += start_break + safe_eyes_core.on_count_down += on_count_down + safe_eyes_core.on_stop_break += on_stop_break + + assert context["state"] == model.State.WAITING + # continue after condvar sequential_threading_handle.next() # end of __scheduler_job @@ -547,3 +567,300 @@ def test_long_duration_is_bigger_than_short_interval( safe_eyes_core.stop() assert context["state"] == model.State.STOPPED + + def test_idle( + self, + sequential_threading: SequentialThreadingFixture, + time_machine: TimeMachineFixture, + ): + """Test idling for short amount of time.""" + context: dict[str, typing.Any] = { + "session": {}, + } + short_break_duration = 15 # seconds + short_break_interval = 15 # minutes + pre_break_warning_time = 10 # seconds + long_break_duration = 60 # seconds + long_break_interval = 75 # minutes + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) + + self.assert_datetime("2024-08-25T13:00:00") + + safe_eyes_core = core.SafeEyesCore(context) + + sequential_threading_handle = sequential_threading(safe_eyes_core) + + safe_eyes_core.initialize(config) + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + initial=True, + ) + + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T13:15:25") + + # idle, simulate behaviour of smartpause plugin + idle_seconds = 30 + idle_period = datetime.timedelta(seconds=idle_seconds) + + safe_eyes_core.stop(is_resting=True) + + assert context["state"] == model.State.RESTING + + time_machine.shift(delta=idle_period) + + assert safe_eyes_core.scheduled_next_break_time is not None + next_break = safe_eyes_core.scheduled_next_break_time + idle_period + + safe_eyes_core.start(next_break_time=next_break.timestamp()) + + self.assert_datetime("2024-08-25T13:15:55") + + self.run_next_break_from_waiting_state( + sequential_threading_handle, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 2", + ) + + self.assert_datetime("2024-08-25T13:31:20") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 3", + ) + + self.assert_datetime("2024-08-25T13:46:45") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 4", + ) + + self.assert_datetime("2024-08-25T14:02:10") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + long_break_duration, + "translated!: long break 1", + ) + + # Time passed: 16min 10s + # 15min short_break_interval (from previous, as long_break_interval must be + # multiple) + # 10 seconds pre_break_warning_time, 1 minute long_break_duration + self.assert_datetime("2024-08-25T14:18:20") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + ) + + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T14:33:45") + + safe_eyes_core.stop() + + assert context["state"] == model.State.STOPPED + + def test_idle_skip_long( + self, + sequential_threading: SequentialThreadingFixture, + time_machine: TimeMachineFixture, + ): + """Test idling for longer than long break time.""" + context: dict[str, typing.Any] = { + "session": {}, + } + short_break_duration = 15 # seconds + short_break_interval = 15 # minutes + pre_break_warning_time = 10 # seconds + long_break_duration = 60 # seconds + long_break_interval = 75 # minutes + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) + + self.assert_datetime("2024-08-25T13:00:00") + + safe_eyes_core = core.SafeEyesCore(context) + + sequential_threading_handle = sequential_threading(safe_eyes_core) + + safe_eyes_core.initialize(config) + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + initial=True, + ) + + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T13:15:25") + + # idle, simulate behaviour of smartpause plugin + idle_seconds = 65 + idle_period = datetime.timedelta(seconds=idle_seconds) + + safe_eyes_core.stop(is_resting=True) + + assert context["state"] == model.State.RESTING + + time_machine.shift(delta=idle_period) + + assert safe_eyes_core.scheduled_next_break_time is not None + next_break = safe_eyes_core.scheduled_next_break_time + idle_period + + safe_eyes_core.start(next_break_time=next_break.timestamp()) + + self.assert_datetime("2024-08-25T13:16:30") + + self.run_next_break_from_waiting_state( + sequential_threading_handle, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 2", + ) + + self.assert_datetime("2024-08-25T13:31:55") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 3", + ) + + self.assert_datetime("2024-08-25T13:47:20") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 4", + ) + + self.assert_datetime("2024-08-25T14:02:45") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + ) + + self.assert_datetime("2024-08-25T14:18:10") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + long_break_duration, + "translated!: long break 1", + ) + + # Time passed: 16min 10s + # 15min short_break_interval (from previous, as long_break_interval must be + # multiple) + # 10 seconds pre_break_warning_time, 1 minute long_break_duration + self.assert_datetime("2024-08-25T14:34:20") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 2", + ) + + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T14:49:45") + + safe_eyes_core.stop() + + assert context["state"] == model.State.STOPPED From 2c222d0b45a64e9c5e0cf3caaa0a326a9a83be63 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:29:15 +0200 Subject: [PATCH 097/134] test: core: add broken idle test when idling for longer than the long break duration, right when the next long break is coming up, the skipping doesn't work properly - it skips all short breaks --- safeeyes/tests/test_core.py | 148 ++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index d245e1e9..3b77283d 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -864,3 +864,151 @@ def test_idle_skip_long( safe_eyes_core.stop() assert context["state"] == model.State.STOPPED + + def test_idle_skip_long_before_long( + self, + sequential_threading: SequentialThreadingFixture, + time_machine: TimeMachineFixture, + ): + """Test idling for longer than long break time, right before the next long + break. + + FIXME: this test is broken, it should not work this way. Instead of just + skipping the long break, it skips all the short breaks too. + """ + context: dict[str, typing.Any] = { + "session": {}, + } + short_break_duration = 15 # seconds + short_break_interval = 15 # minutes + pre_break_warning_time = 10 # seconds + long_break_duration = 60 # seconds + long_break_interval = 75 # minutes + config = model.Config( + user_config={ + "short_breaks": [ + {"name": "break 1"}, + {"name": "break 2"}, + {"name": "break 3"}, + {"name": "break 4"}, + ], + "long_breaks": [ + {"name": "long break 1"}, + {"name": "long break 2"}, + {"name": "long break 3"}, + ], + "short_break_interval": short_break_interval, + "long_break_interval": long_break_interval, + "long_break_duration": long_break_duration, + "short_break_duration": short_break_duration, + "pre_break_warning_time": pre_break_warning_time, + "random_order": False, + "postpone_duration": 5, + }, + system_config={}, + ) + + self.assert_datetime("2024-08-25T13:00:00") + + safe_eyes_core = core.SafeEyesCore(context) + + sequential_threading_handle = sequential_threading(safe_eyes_core) + + safe_eyes_core.initialize(config) + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + initial=True, + ) + + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T13:15:25") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 2", + ) + + self.assert_datetime("2024-08-25T13:30:50") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 3", + ) + + self.assert_datetime("2024-08-25T13:46:15") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 4", + ) + + self.assert_datetime("2024-08-25T14:01:40") + + # idle, simulate behaviour of smartpause plugin + idle_seconds = 65 + idle_period = datetime.timedelta(seconds=idle_seconds) + + safe_eyes_core.stop(is_resting=True) + + assert context["state"] == model.State.RESTING + + time_machine.shift(delta=idle_period) + + assert safe_eyes_core.scheduled_next_break_time is not None + next_break = safe_eyes_core.scheduled_next_break_time + idle_period + + safe_eyes_core.start(next_break_time=next_break.timestamp()) + + self.assert_datetime("2024-08-25T14:02:45") + + self.run_next_break_from_waiting_state( + sequential_threading_handle, + safe_eyes_core, + context, + long_break_duration, + "translated!: long break 1", + ) + + # Time passed: 16min 10s + # 15min short_break_interval (from previous, as long_break_interval must be + # multiple) + # 10 seconds pre_break_warning_time, 1 minute long_break_duration + self.assert_datetime("2024-08-25T15:18:55") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 1", + ) + + # Time passed: 15min 25s + # 15min short_break_interval, 10 seconds pre_break_warning_time, + # 15 seconds short_break_duration + self.assert_datetime("2024-08-25T15:34:20") + + safe_eyes_core.stop() + + assert context["state"] == model.State.STOPPED From f61257d529d8142f60c664e70a1c4eee548f69a9 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:39:17 +0200 Subject: [PATCH 098/134] tests: core: fix error when logging --- safeeyes/tests/test_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index 3b77283d..d1517b20 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -299,6 +299,7 @@ def test_start(self, sequential_threading: SequentialThreadingFixture): "short_break_duration": 15, "random_order": False, "postpone_duration": 5, + "pre_break_warning_time": 10, # seconds }, system_config={}, ) From 117441028094f0cc9015e614ce15e8b626d185a6 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:44:38 +0200 Subject: [PATCH 099/134] core: start/enable_safeeyes: remove unused reset_breaks param --- safeeyes/core.py | 5 +---- safeeyes/safeeyes.py | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index 4b27c3cf..eb770472 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -95,16 +95,13 @@ def initialize(self, config: Config): self.postpone_duration = self.default_postpone_duration - def start(self, next_break_time=-1, reset_breaks=False) -> None: + def start(self, next_break_time=-1) -> None: """Start Safe Eyes is it is not running already.""" if self._break_queue is None: logging.info("No breaks defined, not starting the core") return if not self.running: logging.info("Start Safe Eyes core") - if reset_breaks: - logging.info("Reset breaks to start from the beginning") - self._break_queue.reset() self.running = True self.scheduled_next_break_timestamp = int(next_break_time) diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index bca9fa16..b1c67655 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -237,8 +237,8 @@ def do_startup(self): self.show_about ) self.context["api"]["enable_safeeyes"] = ( - lambda next_break_time=-1, reset_breaks=False: utility.execute_main_thread( - self.enable_safeeyes, next_break_time, reset_breaks + lambda next_break_time=-1: utility.execute_main_thread( + self.enable_safeeyes, next_break_time ) ) self.context["api"]["disable_safeeyes"] = ( @@ -494,7 +494,7 @@ def restart(self, config, set_active=False): self.safe_eyes_core.start() self.plugins_manager.start() - def enable_safeeyes(self, scheduled_next_break_time=-1, reset_breaks=False): + def enable_safeeyes(self, scheduled_next_break_time=-1): """Listen to tray icon enable action and send the signal to core.""" if ( not self.required_plugin_dialog_active @@ -502,7 +502,7 @@ def enable_safeeyes(self, scheduled_next_break_time=-1, reset_breaks=False): and self.safe_eyes_core.has_breaks() ): self.active = True - self.safe_eyes_core.start(scheduled_next_break_time, reset_breaks) + self.safe_eyes_core.start(scheduled_next_break_time) self.plugins_manager.start() def disable_safeeyes(self, status=None, is_resting=False): From c6f6a958ed7191d201bd8afee2870928d93e336c Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:46:38 +0200 Subject: [PATCH 100/134] model: BreakQueue: rename reset to skip_long_break --- safeeyes/core.py | 2 +- safeeyes/model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/safeeyes/core.py b/safeeyes/core.py index eb770472..5e6ae0f1 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -201,7 +201,7 @@ def __scheduler_job(self) -> None: paused_duration, ) # Skip the next long break - self._break_queue.reset() + self._break_queue.skip_long_break() if self.context["postponed"]: # Previous break was postponed diff --git a/safeeyes/model.py b/safeeyes/model.py index fd322036..7bcd2a58 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -249,7 +249,7 @@ def __set_next_break(self, break_type: typing.Optional[BreakType] = None) -> Non self.__current_break = break_obj self.context["session"]["break"] = self.__current_break.name - def reset(self) -> None: + def skip_long_break(self) -> None: if self.__short_queue: for break_object in self.__short_queue: break_object.time = self.__short_break_time From e0eb988d20b0910a868838dd7f59d4c45f32bf47 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:46:38 +0200 Subject: [PATCH 101/134] tests: model: add broken test for skipping long break --- safeeyes/tests/test_model.py | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index 52a7b9e6..ccc2536c 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -384,6 +384,67 @@ def test_full_next_break(self, monkeypatch: pytest.MonkeyPatch) -> None: assert bq.next().name == "translated!: break 4" assert bq.next().name == "translated!: long break 1" + def test_skip_long_break(self, monkeypatch: pytest.MonkeyPatch) -> None: + bq = self.get_bq_full(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: break 1" + assert not bq.is_long_break() + + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 1" + assert bq.is_long_break() + assert bq.next().name == "translated!: break 1" + assert not bq.is_long_break() + assert bq.next().name == "translated!: break 2" + + bq.skip_long_break() + + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: long break 2" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: long break 3" + + def test_skip_long_break_before_long_break( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + bq = self.get_bq_full(monkeypatch) + + next = bq.get_break() + assert next.name == "translated!: break 1" + assert not bq.is_long_break() + + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 1" + assert bq.is_long_break() + assert bq.next().name == "translated!: break 1" + assert not bq.is_long_break() + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 2" + + assert bq.get_break().name == "translated!: long break 2" + + bq.skip_long_break() + + assert bq.get_break().name == "translated!: long break 2" + + assert bq.next().name == "translated!: break 1" + assert bq.next().name == "translated!: break 2" + assert bq.next().name == "translated!: break 3" + assert bq.next().name == "translated!: break 4" + assert bq.next().name == "translated!: long break 3" + def test_full_next_break_random(self, monkeypatch: pytest.MonkeyPatch) -> None: random_seed = 5 bq = self.get_bq_full(monkeypatch, random_seed) From f54317d9a480e8cd050882b955a0c126c10876bb Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 12:46:38 +0200 Subject: [PATCH 102/134] BreakQueue: skip upcoming long break right away --- safeeyes/model.py | 23 ++++++++++----- safeeyes/tests/test_core.py | 56 +++++++++++++++++++++++++++--------- safeeyes/tests/test_model.py | 3 +- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/safeeyes/model.py b/safeeyes/model.py index 7bcd2a58..5d5bbe49 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -250,13 +250,22 @@ def __set_next_break(self, break_type: typing.Optional[BreakType] = None) -> Non self.context["session"]["break"] = self.__current_break.name def skip_long_break(self) -> None: - if self.__short_queue: - for break_object in self.__short_queue: - break_object.time = self.__short_break_time - - if self.__long_queue: - for break_object in self.__long_queue: - break_object.time = self.__long_break_time + if not (self.__short_queue and self.__long_queue): + return + + for break_object in self.__short_queue: + break_object.time = self.__short_break_time + + for break_object in self.__long_queue: + break_object.time = self.__long_break_time + + if self.__current_break.type == BreakType.LONG_BREAK: + # Note: this skips the long break, meaning the following long break + # won't be the current one, but the next one after + # we could decrement the __current_long counter, but then we'd need to + # handle wraparound and possibly randomizing, which seems complicated + self.__current_break = self.__next_short() + self.context["session"]["break"] = self.__current_break.name def is_empty(self, break_type: BreakType) -> bool: """Check if the given break type is empty or not.""" diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index d1517b20..abea61af 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -874,8 +874,7 @@ def test_idle_skip_long_before_long( """Test idling for longer than long break time, right before the next long break. - FIXME: this test is broken, it should not work this way. Instead of just - skipping the long break, it skips all the short breaks too. + This used to skip all the short breaks too. """ context: dict[str, typing.Any] = { "session": {}, @@ -986,15 +985,11 @@ def test_idle_skip_long_before_long( sequential_threading_handle, safe_eyes_core, context, - long_break_duration, - "translated!: long break 1", + short_break_duration, + "translated!: break 1", ) - # Time passed: 16min 10s - # 15min short_break_interval (from previous, as long_break_interval must be - # multiple) - # 10 seconds pre_break_warning_time, 1 minute long_break_duration - self.assert_datetime("2024-08-25T15:18:55") + self.assert_datetime("2024-08-25T14:18:10") self.run_next_break( sequential_threading_handle, @@ -1002,13 +997,46 @@ def test_idle_skip_long_before_long( safe_eyes_core, context, short_break_duration, - "translated!: break 1", + "translated!: break 2", ) - # Time passed: 15min 25s - # 15min short_break_interval, 10 seconds pre_break_warning_time, - # 15 seconds short_break_duration - self.assert_datetime("2024-08-25T15:34:20") + self.assert_datetime("2024-08-25T14:33:35") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 3", + ) + + self.assert_datetime("2024-08-25T14:49:00") + + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + short_break_duration, + "translated!: break 4", + ) + + self.assert_datetime("2024-08-25T15:04:25") + + # note that long break 1 was skipped, and we went directly to long break 2 + # there's a note in BreakQueue.skip_long_break, we could fix it if needed, but + # it seems too much effort to be worth it right now + self.run_next_break( + sequential_threading_handle, + time_machine, + safe_eyes_core, + context, + long_break_duration, + "translated!: long break 2", + ) + + self.assert_datetime("2024-08-25T15:20:35") safe_eyes_core.stop() diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index ccc2536c..f0d219db 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -437,9 +437,8 @@ def test_skip_long_break_before_long_break( bq.skip_long_break() - assert bq.get_break().name == "translated!: long break 2" + assert bq.get_break().name == "translated!: break 1" - assert bq.next().name == "translated!: break 1" assert bq.next().name == "translated!: break 2" assert bq.next().name == "translated!: break 3" assert bq.next().name == "translated!: break 4" From 3ed63246822b8d13ebb331246bf0f6ee3bf42fd3 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 17:32:36 +0200 Subject: [PATCH 103/134] settings: dont duplicate the active plugin config per setting and use a more understandable key --- safeeyes/ui/settings_dialog.py | 24 +++++++++--------------- safeeyes/utility.py | 4 ++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 4f6e9cac..8cae2849 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -421,7 +421,7 @@ def __init__(self, application, config): self.__load_int_item( setting["label"], setting["id"], - setting["safeeyes_config"], + config["active_plugin_config"], setting.get("min", 0), setting.get("max", 120), ) @@ -429,13 +429,13 @@ def __init__(self, application, config): elif setting["type"].upper() == "TEXT": box_settings.append( self.__load_text_item( - setting["label"], setting["id"], setting["safeeyes_config"] + setting["label"], setting["id"], config["active_plugin_config"] ) ) elif setting["type"].upper() == "BOOL": box_settings.append( self.__load_bool_item( - setting["label"], setting["id"], setting["safeeyes_config"] + setting["label"], setting["id"], config["active_plugin_config"] ) ) @@ -450,9 +450,7 @@ def __load_int_item(self, name, key, settings, min_value, max_value): spin_value.set_value(settings[key]) box = builder.get_object("box") box.set_visible(True) - self.property_controls.append( - {"key": key, "settings": settings, "value": spin_value.get_value} - ) + self.property_controls.append({"key": key, "value": spin_value.get_value}) return box def __load_text_item(self, name, key, settings): @@ -463,9 +461,7 @@ def __load_text_item(self, name, key, settings): txt_value.set_text(settings[key]) box = builder.get_object("box") box.set_visible(True) - self.property_controls.append( - {"key": key, "settings": settings, "value": txt_value.get_text} - ) + self.property_controls.append({"key": key, "value": txt_value.get_text}) return box def __load_bool_item(self, name, key, settings): @@ -476,17 +472,15 @@ def __load_bool_item(self, name, key, settings): switch_value.set_active(settings[key]) box = builder.get_object("box") box.set_visible(True) - self.property_controls.append( - {"key": key, "settings": settings, "value": switch_value.get_active} - ) + self.property_controls.append({"key": key, "value": switch_value.get_active}) return box def on_window_delete(self, *args): """Event handler for Properties dialog close action.""" for property_control in self.property_controls: - property_control["settings"][property_control["key"]] = property_control[ - "value" - ]() + self.config["active_plugin_config"][property_control["key"]] = ( + property_control["value"]() + ) self.window.destroy() def show(self): diff --git a/safeeyes/utility.py b/safeeyes/utility.py index c8908438..990aa1f2 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -259,8 +259,8 @@ def load_plugins_config(safeeyes_config): config["id"] = plugin["id"] config["icon"] = icon config["enabled"] = plugin["enabled"] - for setting in config["settings"]: - setting["safeeyes_config"] = plugin["settings"] + config["active_plugin_config"] = plugin.get("settings") + configs.append(config) return configs From 946aef46c76a2b4f85eb7aee58ff6cbd33d6b7f6 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 21 Aug 2025 14:44:44 +0200 Subject: [PATCH 104/134] settings: dialogs: set transient_for instead of application --- safeeyes/ui/settings_dialog.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 8cae2849..e6e27e08 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -288,7 +288,7 @@ def __create_plugin_item(self, plugin_config): def __show_plugins_properties_dialog(self, plugin_config): """Show the PluginProperties dialog.""" - dialog = PluginSettingsDialog(self.application, plugin_config) + dialog = PluginSettingsDialog(self.window, plugin_config) dialog.show() def __disable_errored_plugin(self, button, plugin_config): @@ -301,7 +301,7 @@ def __show_break_properties_dialog( ): """Show the BreakProperties dialog.""" dialog = BreakSettingsDialog( - self.application, + self.window, break_config, is_short, parent, @@ -355,7 +355,7 @@ def on_info_bar_long_break_close(self, infobar, *user_data): def add_break(self, button) -> None: """Event handler for add break button.""" dialog = NewBreakDialog( - self.application, + self.window, self.config, lambda is_short, break_config: self.__create_break_item( break_config, is_short @@ -406,13 +406,13 @@ def on_window_delete(self, *args): class PluginSettingsDialog: """Builds a settings dialog based on the configuration of a plugin.""" - def __init__(self, application, config): + def __init__(self, parent, config): self.config = config self.property_controls = [] builder = utility.create_gtk_builder(SETTINGS_DIALOG_PLUGIN_GLADE) self.window = builder.get_object("dialog_settings_plugin") - self.window.set_application(application) + self.window.set_transient_for(parent) box_settings = builder.get_object("box_settings") self.window.set_title(_("Plugin Settings")) for setting in config.get("settings"): @@ -493,7 +493,7 @@ class BreakSettingsDialog: def __init__( self, - application, + parent, break_config, is_short, parent_config, @@ -512,7 +512,7 @@ def __init__( builder = utility.create_gtk_builder(SETTINGS_DIALOG_BREAK_GLADE) self.window = builder.get_object("dialog_settings_break") - self.window.set_application(application) + self.window.set_transient_for(parent) self.txt_break = builder.get_object("txt_break") self.switch_override_interval = builder.get_object("switch_override_interval") self.switch_override_duration = builder.get_object("switch_override_duration") @@ -692,13 +692,13 @@ def show(self): class NewBreakDialog: """Builds a new break dialog.""" - def __init__(self, application, parent_config, on_add): + def __init__(self, parent, parent_config, on_add): self.parent_config = parent_config self.on_add = on_add builder = utility.create_gtk_builder(SETTINGS_DIALOG_NEW_BREAK_GLADE) self.window = builder.get_object("dialog_new_break") - self.window.set_application(application) + self.window.set_transient_for(parent) self.txt_break = builder.get_object("txt_break") self.cmb_type = builder.get_object("cmb_type") list_types = builder.get_object("lst_break_types") From 8cbcc7b54f38459329a5f4b0995896aefedf6363 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 17:30:58 +0200 Subject: [PATCH 105/134] refactor: settings: split plugin item into separate class --- safeeyes/ui/settings_dialog.py | 126 +++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 52 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index e6e27e08..b5836024 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -59,7 +59,7 @@ def __init__(self, application, config, on_save_settings): self.application = application self.config = config self.on_save_settings = on_save_settings - self.plugin_switches = {} + self.plugin_items = {} self.plugin_map = {} self.last_short_break_interval = config.get("short_break_interval") self.initializing = True @@ -238,64 +238,26 @@ def __confirmation_dialog_response(dialog, result): def __create_plugin_item(self, plugin_config): """Create an entry for plugin to be listed in the plugin tab.""" - builder = utility.create_gtk_builder(SETTINGS_PLUGIN_ITEM_GLADE) - lbl_plugin_name = builder.get_object("lbl_plugin_name") - lbl_plugin_description = builder.get_object("lbl_plugin_description") - switch_enable = builder.get_object("switch_enable") - btn_properties = builder.get_object("btn_properties") - lbl_plugin_name.set_label(_(plugin_config["meta"]["name"])) - switch_enable.set_active(plugin_config["enabled"]) - if plugin_config["error"]: - message = plugin_config["meta"]["dependency_description"] - if isinstance(message, PluginDependency): - lbl_plugin_description.set_label(_(message.message)) - btn_plugin_extra_link = builder.get_object("btn_plugin_extra_link") - btn_plugin_extra_link.set_label(_("Click here for more information")) - btn_plugin_extra_link.set_uri(message.link) - btn_plugin_extra_link.set_visible(True) - else: - lbl_plugin_description.set_label(_(message)) - lbl_plugin_name.set_sensitive(False) - lbl_plugin_description.set_sensitive(False) - switch_enable.set_sensitive(False) - btn_properties.set_sensitive(False) - if plugin_config["enabled"]: - btn_disable_errored = builder.get_object("btn_disable_errored") - btn_disable_errored.set_visible(True) - btn_disable_errored.connect( - "clicked", - lambda button: self.__disable_errored_plugin(button, plugin_config), - ) + box = PluginItem( + plugin_config, + on_properties=lambda: self.__show_plugins_properties_dialog(plugin_config), + ) + + self.plugin_items[plugin_config["id"]] = box - else: - lbl_plugin_description.set_label(_(plugin_config["meta"]["description"])) - if plugin_config["settings"]: - btn_properties.set_sensitive(True) - btn_properties.connect( - "clicked", - lambda button: self.__show_plugins_properties_dialog(plugin_config), - ) - else: - btn_properties.set_sensitive(False) - self.plugin_switches[plugin_config["id"]] = switch_enable if plugin_config.get("break_override_allowed", False): self.plugin_map[plugin_config["id"]] = plugin_config["meta"]["name"] - if plugin_config["icon"]: - builder.get_object("img_plugin_icon").set_from_file(plugin_config["icon"]) - box = builder.get_object("box") - box.set_visible(True) - return box + + gbox = box.box + + gbox.set_visible(True) + return gbox def __show_plugins_properties_dialog(self, plugin_config): """Show the PluginProperties dialog.""" dialog = PluginSettingsDialog(self.window, plugin_config) dialog.show() - def __disable_errored_plugin(self, button, plugin_config): - """Permanently disable errored plugin.""" - button.set_sensitive(False) - self.plugin_switches[plugin_config["id"]].set_active(False) - def __show_break_properties_dialog( self, break_config, is_short, parent, on_close, on_add, on_remove ): @@ -396,13 +358,73 @@ def on_window_delete(self, *args): self.config.set("allow_postpone", self.switch_postpone.get_active()) self.config.set("persist_state", self.switch_persist.get_active()) for plugin in self.config.get("plugins"): - if plugin["id"] in self.plugin_switches: - plugin["enabled"] = self.plugin_switches[plugin["id"]].get_active() + if plugin["id"] in self.plugin_items: + plugin["enabled"] = self.plugin_items[plugin["id"]].is_enabled() self.on_save_settings(self.config) # Call the provided save method self.window.destroy() +class PluginItem: + def __init__(self, plugin_config, on_properties): + super().__init__() + + self.on_properties = on_properties + self.plugin_config = plugin_config + + builder = utility.create_gtk_builder(SETTINGS_PLUGIN_ITEM_GLADE) + self.lbl_plugin_name = builder.get_object("lbl_plugin_name") + self.lbl_plugin_description = builder.get_object("lbl_plugin_description") + self.switch_enable = builder.get_object("switch_enable") + self.btn_properties = builder.get_object("btn_properties") + self.lbl_plugin_name.set_label(_(plugin_config["meta"]["name"])) + self.switch_enable.set_active(plugin_config["enabled"]) + + if plugin_config["error"]: + message = plugin_config["meta"]["dependency_description"] + if isinstance(message, PluginDependency): + self.lbl_plugin_description.set_label(_(message.message)) + self.btn_plugin_extra_link = builder.get_object("btn_plugin_extra_link") + self.btn_plugin_extra_link.set_uri(message.link) + self.btn_plugin_extra_link.set_visible(True) + else: + self.lbl_plugin_description.set_label(_(message)) + self.lbl_plugin_name.set_sensitive(False) + self.lbl_plugin_description.set_sensitive(False) + self.switch_enable.set_sensitive(False) + self.btn_properties.set_sensitive(False) + if plugin_config["enabled"]: + self.btn_disable_errored = builder.get_object("btn_disable_errored") + self.btn_disable_errored.set_visible(True) + self.btn_disable_errored.connect("clicked", self.on_disable_errored) + else: + self.lbl_plugin_description.set_label( + _(plugin_config["meta"]["description"]) + ) + if plugin_config["settings"]: + self.btn_properties.set_sensitive(True) + self.btn_properties.connect("clicked", self.on_properties_clicked) + else: + self.btn_properties.set_sensitive(False) + + if plugin_config["icon"]: + builder.get_object("img_plugin_icon").set_from_file(plugin_config["icon"]) + + self.box = builder.get_object("box") + + def is_enabled(self): + return self.switch_enable.get_active() + + def on_disable_errored(self, button): + """Permanently disable errored plugin.""" + self.btn_disable_errored.set_sensitive(False) + self.switch_enable.set_active(False) + + def on_properties_clicked(self, button): + if not self.plugin_config["error"] and self.plugin_config["settings"]: + self.on_properties() + + class PluginSettingsDialog: """Builds a settings dialog based on the configuration of a plugin.""" From 51a1e00490ecab3022361a1a52821ad05a553e18 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 17:30:58 +0200 Subject: [PATCH 106/134] refactor: settings: split break item into separate class --- safeeyes/ui/settings_dialog.py | 54 +++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index b5836024..b06b1a6e 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -150,36 +150,30 @@ def __create_break_item(self, break_config, is_short): parent_box = self.box_long_breaks if is_short: parent_box = self.box_short_breaks - builder = utility.create_gtk_builder(SETTINGS_BREAK_ITEM_GLADE) - box = builder.get_object("box") - lbl_name = builder.get_object("lbl_name") - lbl_name.set_label(_(break_config["name"])) - btn_properties = builder.get_object("btn_properties") - btn_properties.connect( - "clicked", - lambda button: self.__show_break_properties_dialog( + + box = BreakItem( + break_name=break_config["name"], + on_properties=lambda: self.__show_break_properties_dialog( break_config, is_short, self.config, - lambda cfg: lbl_name.set_label(_(cfg["name"])), + lambda cfg: box.set_break_name(cfg["name"]), lambda is_short, break_config: self.__create_break_item( break_config, is_short ), lambda: parent_box.remove(box), ), - ) - btn_delete = builder.get_object("btn_delete") - btn_delete.connect( - "clicked", - lambda button: self.__delete_break( + on_delete=lambda: self.__delete_break( break_config, is_short, lambda: parent_box.remove(box), ), ) - box.set_visible(True) - parent_box.append(box) - return box + + gbox = box.box + + gbox.set_visible(True) + parent_box.append(gbox) def on_reset_menu_clicked(self, button): self.popover.hide() @@ -365,6 +359,32 @@ def on_window_delete(self, *args): self.window.destroy() +class BreakItem: + def __init__(self, break_name, on_properties, on_delete): + super().__init__() + + self.on_properties = on_properties + self.on_delete = on_delete + + builder = utility.create_gtk_builder(SETTINGS_BREAK_ITEM_GLADE) + self.box = builder.get_object("box") + self.lbl_name = builder.get_object("lbl_name") + self.lbl_name.set_label(_(break_name)) + self.btn_properties = builder.get_object("btn_properties") + self.btn_properties.connect("clicked", self.on_properties_clicked) + self.btn_delete = builder.get_object("btn_delete") + self.btn_delete.connect("clicked", self.on_delete_clicked) + + def set_break_name(self, break_name): + self.lbl_name.set_label(_(break_name)) + + def on_properties_clicked(self, button): + self.on_properties() + + def on_delete_clicked(self, button): + self.on_delete() + + class PluginItem: def __init__(self, plugin_config, on_properties): super().__init__() From b267c2631042584d81a27ad41262b5d6ac0ab6d2 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 21 Aug 2025 14:07:57 +0200 Subject: [PATCH 107/134] settings: plugin settings items: split out common code --- safeeyes/ui/settings_dialog.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index b06b1a6e..fa5ed88a 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -459,27 +459,25 @@ def __init__(self, parent, config): self.window.set_title(_("Plugin Settings")) for setting in config.get("settings"): if setting["type"].upper() == "INT": - box_settings.append( - self.__load_int_item( - setting["label"], - setting["id"], - config["active_plugin_config"], - setting.get("min", 0), - setting.get("max", 120), - ) + box = self.__load_int_item( + setting["label"], + setting["id"], + config["active_plugin_config"], + setting.get("min", 0), + setting.get("max", 120), ) elif setting["type"].upper() == "TEXT": - box_settings.append( - self.__load_text_item( - setting["label"], setting["id"], config["active_plugin_config"] - ) + box = self.__load_text_item( + setting["label"], setting["id"], config["active_plugin_config"] ) elif setting["type"].upper() == "BOOL": - box_settings.append( - self.__load_bool_item( - setting["label"], setting["id"], config["active_plugin_config"] - ) + box = self.__load_bool_item( + setting["label"], setting["id"], config["active_plugin_config"] ) + else: + continue + + box_settings.append(box) self.window.connect("close-request", self.on_window_delete) From 1699f3ba636f7c995945d3fd359867cd3cb3e7c0 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 17:32:36 +0200 Subject: [PATCH 108/134] settings: refactor plugin settings items into classes --- safeeyes/ui/settings_dialog.py | 95 +++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index fa5ed88a..a975e628 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -445,6 +445,49 @@ def on_properties_clicked(self, button): self.on_properties() +class IntItem: + def __init__(self, name, value, min_value, max_value): + super().__init__() + + builder = utility.create_gtk_builder(SETTINGS_ITEM_INT_GLADE) + builder.get_object("lbl_name").set_label(_(name)) + self.spin_value = builder.get_object("spin_value") + self.spin_value.set_range(min_value, max_value) + self.spin_value.set_value(value) + self.box = builder.get_object("box") + + def get_value(self): + return self.spin_value.get_value() + + +class TextItem: + def __init__(self, name, value): + super().__init__() + + builder = utility.create_gtk_builder(SETTINGS_ITEM_TEXT_GLADE) + builder.get_object("lbl_name").set_label(_(name)) + self.txt_value = builder.get_object("txt_value") + self.txt_value.set_text(value) + self.box = builder.get_object("box") + + def get_value(self): + return self.txt_value.get_text() + + +class BoolItem: + def __init__(self, name, value): + super().__init__() + + builder = utility.create_gtk_builder(SETTINGS_ITEM_BOOL_GLADE) + builder.get_object("lbl_name").set_label(_(name)) + self.switch_value = builder.get_object("switch_value") + self.switch_value.set_active(value) + self.box = builder.get_object("box") + + def get_value(self): + return self.switch_value.get_active() + + class PluginSettingsDialog: """Builds a settings dialog based on the configuration of a plugin.""" @@ -459,67 +502,33 @@ def __init__(self, parent, config): self.window.set_title(_("Plugin Settings")) for setting in config.get("settings"): if setting["type"].upper() == "INT": - box = self.__load_int_item( + box = IntItem( setting["label"], - setting["id"], - config["active_plugin_config"], + config["active_plugin_config"][setting["id"]], setting.get("min", 0), setting.get("max", 120), ) elif setting["type"].upper() == "TEXT": - box = self.__load_text_item( - setting["label"], setting["id"], config["active_plugin_config"] + box = TextItem( + setting["label"], config["active_plugin_config"][setting["id"]] ) elif setting["type"].upper() == "BOOL": - box = self.__load_bool_item( - setting["label"], setting["id"], config["active_plugin_config"] + box = BoolItem( + setting["label"], config["active_plugin_config"][setting["id"]] ) else: continue - box_settings.append(box) + self.property_controls.append({"key": setting["id"], "box": box}) + box_settings.append(box.box) self.window.connect("close-request", self.on_window_delete) - def __load_int_item(self, name, key, settings, min_value, max_value): - """Load the UI control for int property.""" - builder = utility.create_gtk_builder(SETTINGS_ITEM_INT_GLADE) - builder.get_object("lbl_name").set_label(_(name)) - spin_value = builder.get_object("spin_value") - spin_value.set_range(min_value, max_value) - spin_value.set_value(settings[key]) - box = builder.get_object("box") - box.set_visible(True) - self.property_controls.append({"key": key, "value": spin_value.get_value}) - return box - - def __load_text_item(self, name, key, settings): - """Load the UI control for text property.""" - builder = utility.create_gtk_builder(SETTINGS_ITEM_TEXT_GLADE) - builder.get_object("lbl_name").set_label(_(name)) - txt_value = builder.get_object("txt_value") - txt_value.set_text(settings[key]) - box = builder.get_object("box") - box.set_visible(True) - self.property_controls.append({"key": key, "value": txt_value.get_text}) - return box - - def __load_bool_item(self, name, key, settings): - """Load the UI control for boolean property.""" - builder = utility.create_gtk_builder(SETTINGS_ITEM_BOOL_GLADE) - builder.get_object("lbl_name").set_label(_(name)) - switch_value = builder.get_object("switch_value") - switch_value.set_active(settings[key]) - box = builder.get_object("box") - box.set_visible(True) - self.property_controls.append({"key": key, "value": switch_value.get_active}) - return box - def on_window_delete(self, *args): """Event handler for Properties dialog close action.""" for property_control in self.property_controls: self.config["active_plugin_config"][property_control["key"]] = ( - property_control["value"]() + property_control["box"].get_value() ) self.window.destroy() From f5e76f3cb95a224fcf63c49b7e6d23aad3742c7c Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 22:41:58 +0200 Subject: [PATCH 109/134] break screen: split window instances into separate class --- safeeyes/ui/break_screen.py | 189 +++++++++++++++++++++--------------- 1 file changed, 113 insertions(+), 76 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 158adc42..2e133aab 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -39,16 +39,14 @@ class BreakScreen: - """The fullscreen window which prevents users from using the computer. + """The fullscreen windows which prevent users from using the computer. - This class reads the break_screen.glade and build the user - interface. + This class creates and manages the fullscreen windows for every monitor. """ def __init__(self, application, context, on_skipped, on_postponed): self.application = application self.context = context - self.count_labels = [] self.x11_display = None self.enable_postpone = False self.enable_shortcut = False @@ -100,11 +98,6 @@ def postpone_break(self): self.on_postponed() self.close() - def on_window_delete(self, *args): - """Window close event handler.""" - logging.info("Closing the break screen") - self.close() - def on_skip_clicked(self, button): """Skip button press event handler.""" self.skip_break() @@ -140,16 +133,6 @@ def close(self): # Destroy other windows if exists GLib.idle_add(lambda: self.__destroy_all_screens()) - def __tray_action(self, button, tray_action: TrayAction): - """Tray action handler. - - Hides all toolbar buttons for this action, if it is single use, - and call the action provided by the plugin. - """ - if tray_action.single_use: - tray_action.reset() - tray_action.action() - def __show_break_screen(self, message, image_path, widget, tray_actions): """Show an empty break screen on all screens.""" # Lock the keyboard @@ -171,12 +154,20 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): i = 0 for monitor in monitors: - builder = Gtk.Builder() - builder.add_from_file(BREAK_SCREEN_GLADE) + w = BreakScreenWindow( + self.application, + message, + image_path, + widget, + tray_actions, + lambda: self.close(), + self.show_postpone_button, + self.on_postpone_clicked, + self.show_skip_button, + self.on_skip_clicked, + ) - window = builder.get_object("window_main") - window.set_application(self.application) - window.connect("close-request", self.on_window_delete) + window = w.window if self.context["is_wayland"]: # Note: in theory, this could also be used on X11 @@ -187,54 +178,8 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): window.add_controller(controller) window.set_title("SafeEyes-" + str(i)) - lbl_message = builder.get_object("lbl_message") - lbl_count = builder.get_object("lbl_count") - lbl_widget = builder.get_object("lbl_widget") - img_break = builder.get_object("img_break") - box_buttons = builder.get_object("box_buttons") - toolbar = builder.get_object("toolbar") - - for tray_action in tray_actions: - # TODO: apparently, this would be better served with an icon theme - # + Gtk.button.new_from_icon_name - icon = tray_action.get_icon() - toolbar_button = Gtk.Button() - toolbar_button.set_child(icon) - tray_action.add_toolbar_button(toolbar_button) - toolbar_button.connect( - "clicked", - lambda button, action: self.__tray_action(button, action), - tray_action, - ) - toolbar_button.set_tooltip_text(_(tray_action.name)) - toolbar.append(toolbar_button) - toolbar_button.show() - - # Add the buttons - if self.show_postpone_button: - # Add postpone button - btn_postpone = Gtk.Button.new_with_label(_("Postpone")) - btn_postpone.get_style_context().add_class("btn_postpone") - btn_postpone.connect("clicked", self.on_postpone_clicked) - btn_postpone.set_visible(True) - box_buttons.append(btn_postpone) - - if self.show_skip_button: - # Add the skip button - btn_skip = Gtk.Button.new_with_label(_("Skip")) - btn_skip.get_style_context().add_class("btn_skip") - btn_skip.connect("clicked", self.on_skip_clicked) - btn_skip.set_visible(True) - box_buttons.append(btn_skip) - - # Set values - if image_path: - img_break.set_from_file(image_path) - lbl_message.set_label(message) - lbl_widget.set_markup(widget) - - self.windows.append(window) - self.count_labels.append(lbl_count) + + self.windows.append(w) if self.context["desktop"] == "kde": # Fix flickering screen in KDE by setting opacity to 1 @@ -259,8 +204,8 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): def __update_count_down(self, count): """Update the countdown on all break screens.""" - for label in self.count_labels: - label.set_text(count) + for window in self.windows: + window.set_count_down(count) def __window_set_keep_above_x11(self, window): """Use EWMH hints to keep window above and on all desktops.""" @@ -353,6 +298,98 @@ def __release_keyboard_x11(self): def __destroy_all_screens(self): """Close all the break screens.""" for win in self.windows: - win.destroy() + win.window.destroy() del self.windows[:] - del self.count_labels[:] + + +class BreakScreenWindow: + """This class manages the UI for the break screen window. + + Each instance is a single window, covering a single monitor. + """ + + def __init__( + self, + application, + message, + image_path, + widget, + tray_actions, + on_close, + show_postpone, + on_postpone, + show_skip, + on_skip, + ): + self.on_close = on_close + + builder = Gtk.Builder() + builder.add_from_file(BREAK_SCREEN_GLADE) + + self.window = builder.get_object("window_main") + self.window.set_application(application) + self.window.connect("close-request", self.on_window_delete) + + lbl_message = builder.get_object("lbl_message") + self.lbl_count = builder.get_object("lbl_count") + lbl_widget = builder.get_object("lbl_widget") + img_break = builder.get_object("img_break") + box_buttons = builder.get_object("box_buttons") + toolbar = builder.get_object("toolbar") + + for tray_action in tray_actions: + # TODO: apparently, this would be better served with an icon theme + # + Gtk.button.new_from_icon_name + icon = tray_action.get_icon() + toolbar_button = Gtk.Button() + toolbar_button.set_child(icon) + tray_action.add_toolbar_button(toolbar_button) + toolbar_button.connect( + "clicked", + lambda button, action: self.__tray_action(button, action), + tray_action, + ) + toolbar_button.set_tooltip_text(_(tray_action.name)) + toolbar.append(toolbar_button) + toolbar_button.show() + + # Add the buttons + if show_postpone: + # Add postpone button + btn_postpone = Gtk.Button.new_with_label(_("Postpone")) + btn_postpone.get_style_context().add_class("btn_postpone") + btn_postpone.connect("clicked", on_postpone) + btn_postpone.set_visible(True) + box_buttons.append(btn_postpone) + + if show_skip: + # Add the skip button + btn_skip = Gtk.Button.new_with_label(_("Skip")) + btn_skip.get_style_context().add_class("btn_skip") + btn_skip.connect("clicked", on_skip) + btn_skip.set_visible(True) + box_buttons.append(btn_skip) + + # Set values + if image_path: + img_break.set_from_file(image_path) + lbl_message.set_label(message) + lbl_widget.set_markup(widget) + + def set_count_down(self, count): + self.lbl_count.set_text(count) + + def __tray_action(self, button, tray_action: TrayAction): + """Tray action handler. + + Hides all toolbar buttons for this action and call the action + provided by the plugin. + """ + if tray_action.single_use: + tray_action.reset() + tray_action.action() + + def on_window_delete(self, *args): + """Window close event handler.""" + logging.info("Closing the break screen") + self.on_close() From 74db8262b7eb49e7174ec5fc171dd4036fd2fbff Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 15 Jul 2024 18:43:07 +0200 Subject: [PATCH 110/134] about dialog: migrate from Gtk.Builder to Gtk.Template --- safeeyes/glade/about_dialog.glade | 6 ++++-- safeeyes/ui/about_dialog.py | 34 +++++++++++++++++++------------ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/safeeyes/glade/about_dialog.glade b/safeeyes/glade/about_dialog.glade index 27570bf3..cf235a85 100644 --- a/safeeyes/glade/about_dialog.glade +++ b/safeeyes/glade/about_dialog.glade @@ -35,10 +35,11 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. - + diff --git a/safeeyes/ui/about_dialog.py b/safeeyes/ui/about_dialog.py index 5914d44a..ca698ccd 100644 --- a/safeeyes/ui/about_dialog.py +++ b/safeeyes/ui/about_dialog.py @@ -19,6 +19,10 @@ """This module creates the AboutDialog which shows the version and license.""" import os +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk from safeeyes import utility from safeeyes.translations import translate as _ @@ -26,7 +30,8 @@ ABOUT_DIALOG_GLADE = os.path.join(utility.BIN_DIRECTORY, "glade/about_dialog.glade") -class AboutDialog: +@Gtk.Template(filename=ABOUT_DIALOG_GLADE) +class AboutDialog(Gtk.ApplicationWindow): """AboutDialog reads the about_dialog.glade and build the user interface using that file. @@ -34,33 +39,36 @@ class AboutDialog: license and the GitHub url. """ - def __init__(self, application, version): - builder = utility.create_gtk_builder(ABOUT_DIALOG_GLADE) - self.window = builder.get_object("window_about") - self.window.set_application(application) + __gtype_name__ = "AboutDialog" - self.window.connect("close-request", self.on_window_delete) - builder.get_object("btn_close").connect("clicked", self.on_close_clicked) + lbl_decription = Gtk.Template.Child() + lbl_license = Gtk.Template.Child() + lbl_app_name = Gtk.Template.Child() + + def __init__(self, application, version): + super().__init__(application=application) - builder.get_object("lbl_decription").set_label( + self.lbl_decription.set_label( _( "Safe Eyes protects your eyes from eye strain (asthenopia) by reminding" " you to take breaks while you're working long hours at the computer" ) ) - builder.get_object("lbl_license").set_label(_("License") + ":") + self.lbl_license.set_label(_("License") + ":") # Set the version at the runtime - builder.get_object("lbl_app_name").set_label("Safe Eyes " + version) + self.lbl_app_name.set_label("Safe Eyes " + version) def show(self): """Show the About dialog.""" - self.window.present() + self.present() + @Gtk.Template.Callback() def on_window_delete(self, *args): """Window close event handler.""" - self.window.destroy() + self.destroy() + @Gtk.Template.Callback() def on_close_clicked(self, *args): """Close button click event handler.""" - self.window.destroy() + self.destroy() From aab4f57fb2675b5cfeeec1091f8b0a1f7c49c627 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 4 Aug 2024 22:26:16 +0200 Subject: [PATCH 111/134] gtk template: required plugin dialog --- safeeyes/glade/required_plugin_dialog.glade | 9 ++- safeeyes/safeeyes.py | 1 + safeeyes/ui/required_plugin_dialog.py | 63 ++++++++++----------- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/safeeyes/glade/required_plugin_dialog.glade b/safeeyes/glade/required_plugin_dialog.glade index d9c05954..c97f3cb6 100644 --- a/safeeyes/glade/required_plugin_dialog.glade +++ b/safeeyes/glade/required_plugin_dialog.glade @@ -19,12 +19,13 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index b1c67655..508fa3e1 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -358,6 +358,7 @@ def show_required_plugin_dialog(self, error: RequiredPluginException): error.get_message(), self.quit, lambda: self.disable_plugin(plugin_id), + application=self, ) dialog.show() diff --git a/safeeyes/ui/required_plugin_dialog.py b/safeeyes/ui/required_plugin_dialog.py index ebfbd5d7..e635df0a 100644 --- a/safeeyes/ui/required_plugin_dialog.py +++ b/safeeyes/ui/required_plugin_dialog.py @@ -21,6 +21,10 @@ """ import os +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk from safeeyes import utility from safeeyes.model import PluginDependency @@ -31,62 +35,53 @@ ) -class RequiredPluginDialog: +@Gtk.Template(filename=REQUIRED_PLUGIN_DIALOG_GLADE) +class RequiredPluginDialog(Gtk.ApplicationWindow): """RequiredPluginDialog shows an error when a plugin has required dependencies. """ - def __init__(self, plugin_id, plugin_name, message, on_quit, on_disable_plugin): - self.on_quit = on_quit - self.on_disable_plugin = on_disable_plugin + __gtype_name__ = "RequiredPluginDialog" - builder = utility.create_gtk_builder(REQUIRED_PLUGIN_DIALOG_GLADE) - self.window = builder.get_object("window_required_plugin") + lbl_header = Gtk.Template.Child() + lbl_message = Gtk.Template.Child() + btn_extra_link = Gtk.Template.Child() - self.window.connect("close-request", self.on_window_delete) - builder.get_object("btn_close").connect("clicked", self.on_close_clicked) - builder.get_object("btn_disable_plugin").connect( - "clicked", self.on_disable_plugin_clicked - ) + def __init__( + self, plugin_id, plugin_name, message, on_quit, on_disable_plugin, application + ): + super().__init__(application=application) - builder.get_object("lbl_header").set_label( - _("The required plugin '%s' is missing dependencies!") % _(plugin_name) - ) - - builder.get_object("lbl_main").set_label( - _( - "Please install the dependencies or disable the plugin. To hide this" - " message, you can also deactivate the plugin in the settings." - ) - ) + self.on_quit = on_quit + self.on_disable_plugin = on_disable_plugin - builder.get_object("btn_close").set_label(_("Quit")) - builder.get_object("btn_disable_plugin").set_label( - _("Disable plugin temporarily") + self.lbl_header.set_label( + _("The required plugin '%s' is missing dependencies!") % _(plugin_name) ) if isinstance(message, PluginDependency): - builder.get_object("lbl_message").set_label(_(message.message)) - btn_extra_link = builder.get_object("btn_extra_link") - btn_extra_link.set_label(_("Click here for more information")) - btn_extra_link.set_uri(message.link) - btn_extra_link.set_visible(True) + self.lbl_message.set_label(_(message.message)) + self.btn_extra_link.set_uri(message.link) + self.btn_extra_link.set_visible(True) else: - builder.get_object("lbl_message").set_label(_(message)) + self.lbl_message.set_label(_(message)) def show(self): """Show the dialog.""" - self.window.present() + self.present() + @Gtk.Template.Callback() def on_window_delete(self, *args): """Window close event handler.""" - self.window.destroy() + self.destroy() self.on_quit() + @Gtk.Template.Callback() def on_close_clicked(self, *args): - self.window.destroy() + self.destroy() self.on_quit() + @Gtk.Template.Callback() def on_disable_plugin_clicked(self, *args): - self.window.destroy() + self.destroy() self.on_disable_plugin() From a26fedf16f86bd3c9f114ed90cd0fb54835508c5 Mon Sep 17 00:00:00 2001 From: deltragon Date: Sun, 4 Aug 2024 22:55:14 +0200 Subject: [PATCH 112/134] gtk template: settings dialog (main window) --- safeeyes/glade/settings_dialog.glade | 14 ++++- safeeyes/ui/settings_dialog.py | 90 ++++++++++++---------------- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/safeeyes/glade/settings_dialog.glade b/safeeyes/glade/settings_dialog.glade index 03844853..116e3122 100644 --- a/safeeyes/glade/settings_dialog.glade +++ b/safeeyes/glade/settings_dialog.glade @@ -19,7 +19,7 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + 15 @@ -77,6 +77,7 @@ 1 1 1 + @@ -85,11 +86,12 @@ - + diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index a975e628..9b9baf75 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -52,11 +52,34 @@ SETTINGS_ITEM_BOOL_GLADE = os.path.join(utility.BIN_DIRECTORY, "glade/item_bool.glade") -class SettingsDialog: +@Gtk.Template(filename=SETTINGS_DIALOG_GLADE) +class SettingsDialog(Gtk.ApplicationWindow): """Create and initialize SettingsDialog instance.""" + __gtype_name__ = "SettingsDialog" + + box_short_breaks = Gtk.Template.Child() + box_long_breaks = Gtk.Template.Child() + box_plugins = Gtk.Template.Child() + popover = Gtk.Template.Child() + + spin_short_break_duration = Gtk.Template.Child() + spin_long_break_duration = Gtk.Template.Child() + spin_short_break_interval = Gtk.Template.Child() + spin_long_break_interval = Gtk.Template.Child() + spin_time_to_prepare = Gtk.Template.Child() + spin_postpone_duration = Gtk.Template.Child() + dropdown_postpone_unit = Gtk.Template.Child() + spin_disable_keyboard_shortcut = Gtk.Template.Child() + switch_strict_break = Gtk.Template.Child() + switch_random_order = Gtk.Template.Child() + switch_postpone = Gtk.Template.Child() + switch_persist = Gtk.Template.Child() + info_bar_long_break = Gtk.Template.Child() + def __init__(self, application, config, on_save_settings): - self.application = application + super().__init__(application=application) + self.config = config self.on_save_settings = on_save_settings self.plugin_items = {} @@ -65,53 +88,11 @@ def __init__(self, application, config, on_save_settings): self.initializing = True self.infobar_long_break_shown = False - builder = utility.create_gtk_builder(SETTINGS_DIALOG_GLADE) - - self.window = builder.get_object("window_settings") - self.window.set_application(application) - self.box_short_breaks = builder.get_object("box_short_breaks") - self.box_long_breaks = builder.get_object("box_long_breaks") - self.box_plugins = builder.get_object("box_plugins") - self.popover = builder.get_object("popover") - - self.spin_short_break_duration = builder.get_object("spin_short_break_duration") - self.spin_long_break_duration = builder.get_object("spin_long_break_duration") - self.spin_short_break_interval = builder.get_object("spin_short_break_interval") - self.spin_long_break_interval = builder.get_object("spin_long_break_interval") - self.spin_time_to_prepare = builder.get_object("spin_time_to_prepare") - self.spin_postpone_duration = builder.get_object("spin_postpone_duration") - self.dropdown_postpone_unit = builder.get_object("dropdown_postpone_unit") - self.spin_disable_keyboard_shortcut = builder.get_object( - "spin_disable_keyboard_shortcut" - ) - self.switch_strict_break = builder.get_object("switch_strict_break") - self.switch_random_order = builder.get_object("switch_random_order") - self.switch_postpone = builder.get_object("switch_postpone") - self.switch_persist = builder.get_object("switch_persist") - self.info_bar_long_break = builder.get_object("info_bar_long_break") self.info_bar_long_break.hide() - self.window.connect("close-request", self.on_window_delete) - builder.get_object("reset_menu").connect("clicked", self.on_reset_menu_clicked) - self.spin_short_break_interval.connect( - "value-changed", self.on_spin_short_break_interval_change - ) - self.info_bar_long_break.connect("close", self.on_info_bar_long_break_close) - self.info_bar_long_break.connect("response", self.on_info_bar_long_break_close) - self.spin_long_break_interval.connect( - "value-changed", self.on_spin_long_break_interval_change - ) - builder.get_object("btn_add_break").connect("clicked", self.add_break) - # Set the current values of input fields self.__initialize(config) - # Add event listener to postpone switch - self.switch_postpone.connect("state-set", self.on_switch_postpone_activate) - self.on_switch_postpone_activate( - self.switch_postpone, self.switch_postpone.get_active() - ) - self.initializing = False def __initialize(self, config): @@ -175,6 +156,7 @@ def __create_break_item(self, break_config, is_short): gbox.set_visible(True) parent_box.append(gbox) + @Gtk.Template.Callback() def on_reset_menu_clicked(self, button): self.popover.hide() @@ -201,7 +183,7 @@ def __confirmation_dialog_response(dialog, result) -> None: messagedialog.set_cancel_button(0) messagedialog.set_default_button(0) - messagedialog.choose(self.window, None, __confirmation_dialog_response) + messagedialog.choose(self, None, __confirmation_dialog_response) def __clear_children(self, widget): while widget.get_last_child() is not None: @@ -228,7 +210,7 @@ def __confirmation_dialog_response(dialog, result): messagedialog.set_cancel_button(0) messagedialog.set_default_button(0) - messagedialog.choose(self.window, None, __confirmation_dialog_response) + messagedialog.choose(self, None, __confirmation_dialog_response) def __create_plugin_item(self, plugin_config): """Create an entry for plugin to be listed in the plugin tab.""" @@ -249,7 +231,7 @@ def __create_plugin_item(self, plugin_config): def __show_plugins_properties_dialog(self, plugin_config): """Show the PluginProperties dialog.""" - dialog = PluginSettingsDialog(self.window, plugin_config) + dialog = PluginSettingsDialog(self, plugin_config) dialog.show() def __show_break_properties_dialog( @@ -257,7 +239,7 @@ def __show_break_properties_dialog( ): """Show the BreakProperties dialog.""" dialog = BreakSettingsDialog( - self.window, + self, break_config, is_short, parent, @@ -270,8 +252,9 @@ def __show_break_properties_dialog( def show(self): """Show the SettingsDialog.""" - self.window.present() + super().show() + @Gtk.Template.Callback() def on_switch_postpone_activate(self, switch, state): """Event handler to the state change of the postpone switch. @@ -281,6 +264,7 @@ def on_switch_postpone_activate(self, switch, state): self.spin_postpone_duration.set_sensitive(self.switch_postpone.get_active()) self.dropdown_postpone_unit.set_sensitive(self.switch_postpone.get_active()) + @Gtk.Template.Callback() def on_spin_short_break_interval_change(self, spin_button, *value): """Event handler for value change of short break interval.""" short_break_interval = self.spin_short_break_interval.get_value_as_int() @@ -298,20 +282,23 @@ def on_spin_short_break_interval_change(self, spin_button, *value): self.infobar_long_break_shown = True self.info_bar_long_break.show() + @Gtk.Template.Callback() def on_spin_long_break_interval_change(self, spin_button, *value): """Event handler for value change of long break interval.""" if not self.initializing and not self.infobar_long_break_shown: self.infobar_long_break_shown = True self.info_bar_long_break.show() + @Gtk.Template.Callback() def on_info_bar_long_break_close(self, infobar, *user_data): """Event handler for info bar close action.""" self.info_bar_long_break.hide() + @Gtk.Template.Callback() def add_break(self, button) -> None: """Event handler for add break button.""" dialog = NewBreakDialog( - self.window, + self, self.config, lambda is_short, break_config: self.__create_break_item( break_config, is_short @@ -319,6 +306,7 @@ def add_break(self, button) -> None: ) dialog.show() + @Gtk.Template.Callback() def on_window_delete(self, *args): """Event handler for Settings dialog close action.""" self.config.set( @@ -356,7 +344,7 @@ def on_window_delete(self, *args): plugin["enabled"] = self.plugin_items[plugin["id"]].is_enabled() self.on_save_settings(self.config) # Call the provided save method - self.window.destroy() + self.destroy() class BreakItem: From 73eb8b07c0454a25b4ff60167c82f001f8aef3fe Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 15:14:01 +0200 Subject: [PATCH 113/134] gtk template: settings dialog (subdialogs) --- safeeyes/glade/new_break.glade | 9 +- safeeyes/glade/settings_break.glade | 11 ++- safeeyes/glade/settings_plugin.glade | 7 +- safeeyes/ui/settings_dialog.py | 122 ++++++++++++--------------- 4 files changed, 72 insertions(+), 77 deletions(-) diff --git a/safeeyes/glade/new_break.glade b/safeeyes/glade/new_break.glade index 88bff455..935a616a 100644 --- a/safeeyes/glade/new_break.glade +++ b/safeeyes/glade/new_break.glade @@ -19,7 +19,7 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + 100 @@ -40,7 +40,7 @@ - + diff --git a/safeeyes/glade/settings_break.glade b/safeeyes/glade/settings_break.glade index 77830a36..179ac6a9 100644 --- a/safeeyes/glade/settings_break.glade +++ b/safeeyes/glade/settings_break.glade @@ -19,7 +19,7 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + 1 @@ -47,7 +47,7 @@ - + diff --git a/safeeyes/glade/settings_plugin.glade b/safeeyes/glade/settings_plugin.glade index 1a6c8fb1..071393e2 100644 --- a/safeeyes/glade/settings_plugin.glade +++ b/safeeyes/glade/settings_plugin.glade @@ -19,9 +19,9 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 9b9baf75..2d9700b8 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -476,18 +476,20 @@ def get_value(self): return self.switch_value.get_active() -class PluginSettingsDialog: +@Gtk.Template(filename=SETTINGS_DIALOG_PLUGIN_GLADE) +class PluginSettingsDialog(Gtk.Window): """Builds a settings dialog based on the configuration of a plugin.""" + __gtype_name__ = "PluginSettingsDialog" + + box_settings = Gtk.Template.Child() + def __init__(self, parent, config): + super().__init__(transient_for=parent) + self.config = config self.property_controls = [] - builder = utility.create_gtk_builder(SETTINGS_DIALOG_PLUGIN_GLADE) - self.window = builder.get_object("dialog_settings_plugin") - self.window.set_transient_for(parent) - box_settings = builder.get_object("box_settings") - self.window.set_title(_("Plugin Settings")) for setting in config.get("settings"): if setting["type"].upper() == "INT": box = IntItem( @@ -508,26 +510,39 @@ def __init__(self, parent, config): continue self.property_controls.append({"key": setting["id"], "box": box}) - box_settings.append(box.box) - - self.window.connect("close-request", self.on_window_delete) + self.box_settings.append(box.box) + @Gtk.Template.Callback() def on_window_delete(self, *args): """Event handler for Properties dialog close action.""" for property_control in self.property_controls: self.config["active_plugin_config"][property_control["key"]] = ( property_control["box"].get_value() ) - self.window.destroy() + self.destroy() def show(self): """Show the Properties dialog.""" - self.window.present() + self.present() -class BreakSettingsDialog: +@Gtk.Template(filename=SETTINGS_DIALOG_BREAK_GLADE) +class BreakSettingsDialog(Gtk.Window): """Builds a settings dialog based on the configuration of a plugin.""" + __gtype_name__ = "BreakSettingsDialog" + + txt_break = Gtk.Template.Child() + switch_override_interval = Gtk.Template.Child() + switch_override_duration = Gtk.Template.Child() + switch_override_plugins = Gtk.Template.Child() + spin_interval = Gtk.Template.Child() + spin_duration = Gtk.Template.Child() + btn_image = Gtk.Template.Child() + cmb_type = Gtk.Template.Child() + grid_plugins = Gtk.Template.Child() + lst_break_types = Gtk.Template.Child() + def __init__( self, parent, @@ -539,6 +554,8 @@ def __init__( on_add, on_remove, ): + super().__init__(transient_for=parent) + self.break_config = break_config self.parent_config = parent_config self.plugin_check_buttons = {} @@ -547,34 +564,16 @@ def __init__( self.on_add = on_add self.on_remove = on_remove - builder = utility.create_gtk_builder(SETTINGS_DIALOG_BREAK_GLADE) - self.window = builder.get_object("dialog_settings_break") - self.window.set_transient_for(parent) - self.txt_break = builder.get_object("txt_break") - self.switch_override_interval = builder.get_object("switch_override_interval") - self.switch_override_duration = builder.get_object("switch_override_duration") - self.switch_override_plugins = builder.get_object("switch_override_plugins") - self.spin_interval = builder.get_object("spin_interval") - self.spin_duration = builder.get_object("spin_duration") - self.btn_image = builder.get_object("btn_image") - self.cmb_type = builder.get_object("cmb_type") - - grid_plugins = builder.get_object("grid_plugins") - list_types = builder.get_object("lst_break_types") - interval_overriden = break_config.get("interval", None) is not None duration_overriden = break_config.get("duration", None) is not None plugins_overriden = break_config.get("plugins", None) is not None # Set the values - self.window.set_title(_("Break Settings")) self.txt_break.set_text(_(break_config["name"])) self.switch_override_interval.set_active(interval_overriden) self.switch_override_duration.set_active(duration_overriden) self.switch_override_plugins.set_active(plugins_overriden) self.cmb_type.set_active(0 if is_short else 1) - list_types[0][0] = _(list_types[0][0]) - list_types[1][0] = _(list_types[1][0]) if interval_overriden: self.spin_interval.set_value(break_config["interval"]) @@ -596,7 +595,7 @@ def __init__( for plugin_id in plugin_map.keys(): chk_button = Gtk.CheckButton.new_with_label(_(plugin_map[plugin_id])) self.plugin_check_buttons[plugin_id] = chk_button - grid_plugins.attach(chk_button, row, col, 1, 1) + self.grid_plugins.attach(chk_button, row, col, 1, 1) if plugins_overriden: chk_button.set_active(plugin_id in break_config["plugins"]) else: @@ -613,18 +612,6 @@ def __init__( image = Gtk.Image.new_from_pixbuf(pixbuf) self.btn_image.set_child(image) - self.window.connect("close-request", self.on_window_delete) - self.btn_image.connect("clicked", self.select_image) - - self.switch_override_interval.connect( - "state-set", self.on_switch_override_interval_activate - ) - self.switch_override_duration.connect( - "state-set", self.on_switch_override_duration_activate - ) - self.switch_override_plugins.connect( - "state-set", self.on_switch_override_plugins_activate - ) self.on_switch_override_interval_activate( self.switch_override_interval, self.switch_override_interval.get_active() ) @@ -635,19 +622,23 @@ def __init__( self.switch_override_plugins, self.switch_override_plugins.get_active() ) + @Gtk.Template.Callback() def on_switch_override_interval_activate(self, switch_button, state): """switch_override_interval state change event handler.""" self.spin_interval.set_sensitive(state) + @Gtk.Template.Callback() def on_switch_override_duration_activate(self, switch_button, state): """switch_override_duration state change event handler.""" self.spin_duration.set_sensitive(state) + @Gtk.Template.Callback() def on_switch_override_plugins_activate(self, switch_button, state): """switch_override_plugins state change event handler.""" for chk_box in self.plugin_check_buttons.values(): chk_box.set_sensitive(state) + @Gtk.Template.Callback() def select_image(self, button): """Show a file chooser dialog and let the user to select an image.""" dialog = Gtk.FileDialog() @@ -661,7 +652,7 @@ def select_image(self, button): filters.append(png_filter) dialog.set_filters(filters) - dialog.open(self.window, None, self.select_image_callback) + dialog.open(self, None, self.select_image_callback) def select_image_callback(self, dialog, result): response = None @@ -683,6 +674,7 @@ def select_image_callback(self, dialog, result): self.break_config.pop("image", None) self.btn_image.set_icon_name("gtk-missing-image") + @Gtk.Template.Callback() def on_window_delete(self, *args): """Event handler for Properties dialog close action.""" break_name = self.txt_break.get_text().strip() @@ -719,41 +711,34 @@ def on_window_delete(self, *args): self.on_add(not self.is_short, self.break_config) else: self.on_close(self.break_config) - self.window.destroy() + self.destroy() def show(self): """Show the Properties dialog.""" - self.window.present() + self.present() -class NewBreakDialog: +@Gtk.Template(filename=SETTINGS_DIALOG_NEW_BREAK_GLADE) +class NewBreakDialog(Gtk.Window): """Builds a new break dialog.""" - def __init__(self, parent, parent_config, on_add): - self.parent_config = parent_config - self.on_add = on_add - - builder = utility.create_gtk_builder(SETTINGS_DIALOG_NEW_BREAK_GLADE) - self.window = builder.get_object("dialog_new_break") - self.window.set_transient_for(parent) - self.txt_break = builder.get_object("txt_break") - self.cmb_type = builder.get_object("cmb_type") - list_types = builder.get_object("lst_break_types") + __gtype_name__ = "NewBreakDialog" - list_types[0][0] = _(list_types[0][0]) - list_types[1][0] = _(list_types[1][0]) + txt_break = Gtk.Template.Child() + cmb_type = Gtk.Template.Child() - self.window.connect("close-request", self.on_window_delete) - builder.get_object("btn_discard").connect("clicked", self.discard) - builder.get_object("btn_save").connect("clicked", self.save) + def __init__(self, parent, parent_config, on_add): + super().__init__(transient_for=parent) - # Set the values - self.window.set_title(_("New Break")) + self.parent_config = parent_config + self.on_add = on_add + @Gtk.Template.Callback() def discard(self, button): """Close the dialog.""" - self.window.destroy() + self.destroy() + @Gtk.Template.Callback() def save(self, button): """Event handler for Properties dialog close action.""" break_config = {"name": self.txt_break.get_text().strip()} @@ -764,12 +749,13 @@ def save(self, button): else: self.parent_config.get("long_breaks").append(break_config) self.on_add(False, break_config) - self.window.destroy() + self.destroy() + @Gtk.Template.Callback() def on_window_delete(self, *args): """Event handler for dialog close action.""" - self.window.destroy() + self.destroy() def show(self): """Show the Properties dialog.""" - self.window.present() + self.present() From b4240915943e7be50b52111ef1948d1c4a516710 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 17:30:58 +0200 Subject: [PATCH 114/134] gtk template: settings - plugin and break items --- safeeyes/glade/item_break.glade | 8 +++-- safeeyes/glade/item_plugin.glade | 8 +++-- safeeyes/ui/settings_dialog.py | 56 +++++++++++++++----------------- 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/safeeyes/glade/item_break.glade b/safeeyes/glade/item_break.glade index 17d01a0d..39ff4e89 100644 --- a/safeeyes/glade/item_break.glade +++ b/safeeyes/glade/item_break.glade @@ -19,9 +19,9 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/glade/item_plugin.glade b/safeeyes/glade/item_plugin.glade index 6edf781c..90060608 100644 --- a/safeeyes/glade/item_plugin.glade +++ b/safeeyes/glade/item_plugin.glade @@ -19,9 +19,9 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 2d9700b8..da970d98 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -151,10 +151,8 @@ def __create_break_item(self, break_config, is_short): ), ) - gbox = box.box - - gbox.set_visible(True) - parent_box.append(gbox) + box.set_visible(True) + parent_box.append(box) @Gtk.Template.Callback() def on_reset_menu_clicked(self, button): @@ -224,10 +222,8 @@ def __create_plugin_item(self, plugin_config): if plugin_config.get("break_override_allowed", False): self.plugin_map[plugin_config["id"]] = plugin_config["meta"]["name"] - gbox = box.box - - gbox.set_visible(True) - return gbox + box.set_visible(True) + return box def __show_plugins_properties_dialog(self, plugin_config): """Show the PluginProperties dialog.""" @@ -347,44 +343,50 @@ def on_window_delete(self, *args): self.destroy() -class BreakItem: +@Gtk.Template(filename=SETTINGS_BREAK_ITEM_GLADE) +class BreakItem(Gtk.Box): + __gtype_name__ = "BreakItem" + + lbl_name = Gtk.Template.Child() + def __init__(self, break_name, on_properties, on_delete): super().__init__() self.on_properties = on_properties self.on_delete = on_delete - builder = utility.create_gtk_builder(SETTINGS_BREAK_ITEM_GLADE) - self.box = builder.get_object("box") - self.lbl_name = builder.get_object("lbl_name") self.lbl_name.set_label(_(break_name)) - self.btn_properties = builder.get_object("btn_properties") - self.btn_properties.connect("clicked", self.on_properties_clicked) - self.btn_delete = builder.get_object("btn_delete") - self.btn_delete.connect("clicked", self.on_delete_clicked) def set_break_name(self, break_name): self.lbl_name.set_label(_(break_name)) + @Gtk.Template.Callback() def on_properties_clicked(self, button): self.on_properties() + @Gtk.Template.Callback() def on_delete_clicked(self, button): self.on_delete() -class PluginItem: +@Gtk.Template(filename=SETTINGS_PLUGIN_ITEM_GLADE) +class PluginItem(Gtk.Box): + __gtype_name__ = "PluginItem" + + lbl_plugin_name = Gtk.Template.Child() + lbl_plugin_description = Gtk.Template.Child() + switch_enable = Gtk.Template.Child() + btn_properties = Gtk.Template.Child() + btn_disable_errored = Gtk.Template.Child() + btn_plugin_extra_link = Gtk.Template.Child() + img_plugin_icon = Gtk.Template.Child() + def __init__(self, plugin_config, on_properties): super().__init__() self.on_properties = on_properties self.plugin_config = plugin_config - builder = utility.create_gtk_builder(SETTINGS_PLUGIN_ITEM_GLADE) - self.lbl_plugin_name = builder.get_object("lbl_plugin_name") - self.lbl_plugin_description = builder.get_object("lbl_plugin_description") - self.switch_enable = builder.get_object("switch_enable") - self.btn_properties = builder.get_object("btn_properties") self.lbl_plugin_name.set_label(_(plugin_config["meta"]["name"])) self.switch_enable.set_active(plugin_config["enabled"]) @@ -392,7 +394,6 @@ def __init__(self, plugin_config, on_properties): message = plugin_config["meta"]["dependency_description"] if isinstance(message, PluginDependency): self.lbl_plugin_description.set_label(_(message.message)) - self.btn_plugin_extra_link = builder.get_object("btn_plugin_extra_link") self.btn_plugin_extra_link.set_uri(message.link) self.btn_plugin_extra_link.set_visible(True) else: @@ -402,32 +403,29 @@ def __init__(self, plugin_config, on_properties): self.switch_enable.set_sensitive(False) self.btn_properties.set_sensitive(False) if plugin_config["enabled"]: - self.btn_disable_errored = builder.get_object("btn_disable_errored") self.btn_disable_errored.set_visible(True) - self.btn_disable_errored.connect("clicked", self.on_disable_errored) else: self.lbl_plugin_description.set_label( _(plugin_config["meta"]["description"]) ) if plugin_config["settings"]: self.btn_properties.set_sensitive(True) - self.btn_properties.connect("clicked", self.on_properties_clicked) else: self.btn_properties.set_sensitive(False) if plugin_config["icon"]: - builder.get_object("img_plugin_icon").set_from_file(plugin_config["icon"]) - - self.box = builder.get_object("box") + self.img_plugin_icon.set_from_file(plugin_config["icon"]) def is_enabled(self): return self.switch_enable.get_active() + @Gtk.Template.Callback() def on_disable_errored(self, button): """Permanently disable errored plugin.""" self.btn_disable_errored.set_sensitive(False) self.switch_enable.set_active(False) + @Gtk.Template.Callback() def on_properties_clicked(self, button): if not self.plugin_config["error"] and self.plugin_config["settings"]: self.on_properties() From aa50b7089ccf652c9e8e85f643493c555f2e0f61 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 17:32:36 +0200 Subject: [PATCH 115/134] gtk template: settings - plugin settings items --- safeeyes/glade/item_bool.glade | 6 ++--- safeeyes/glade/item_int.glade | 6 ++--- safeeyes/glade/item_text.glade | 6 ++--- safeeyes/ui/settings_dialog.py | 41 +++++++++++++++++++++------------- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/safeeyes/glade/item_bool.glade b/safeeyes/glade/item_bool.glade index 799ed20f..c036fdd2 100644 --- a/safeeyes/glade/item_bool.glade +++ b/safeeyes/glade/item_bool.glade @@ -19,9 +19,9 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/glade/item_int.glade b/safeeyes/glade/item_int.glade index 31cd8581..83c40cd8 100644 --- a/safeeyes/glade/item_int.glade +++ b/safeeyes/glade/item_int.glade @@ -19,14 +19,14 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + 100 1 10 - + diff --git a/safeeyes/glade/item_text.glade b/safeeyes/glade/item_text.glade index aad81a14..44bca9b7 100644 --- a/safeeyes/glade/item_text.glade +++ b/safeeyes/glade/item_text.glade @@ -19,9 +19,9 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index da970d98..ed62fd3a 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -431,44 +431,53 @@ def on_properties_clicked(self, button): self.on_properties() -class IntItem: +@Gtk.Template(filename=SETTINGS_ITEM_INT_GLADE) +class IntItem(Gtk.Box): + __gtype_name__ = "IntItem" + + lbl_name = Gtk.Template.Child() + spin_value = Gtk.Template.Child() + def __init__(self, name, value, min_value, max_value): super().__init__() - builder = utility.create_gtk_builder(SETTINGS_ITEM_INT_GLADE) - builder.get_object("lbl_name").set_label(_(name)) - self.spin_value = builder.get_object("spin_value") + self.lbl_name.set_label(_(name)) self.spin_value.set_range(min_value, max_value) self.spin_value.set_value(value) - self.box = builder.get_object("box") def get_value(self): return self.spin_value.get_value() -class TextItem: +@Gtk.Template(filename=SETTINGS_ITEM_TEXT_GLADE) +class TextItem(Gtk.Box): + __gtype_name__ = "TextItem" + + lbl_name = Gtk.Template.Child() + txt_value = Gtk.Template.Child() + def __init__(self, name, value): super().__init__() - builder = utility.create_gtk_builder(SETTINGS_ITEM_TEXT_GLADE) - builder.get_object("lbl_name").set_label(_(name)) - self.txt_value = builder.get_object("txt_value") + self.lbl_name.set_label(_(name)) self.txt_value.set_text(value) - self.box = builder.get_object("box") def get_value(self): return self.txt_value.get_text() -class BoolItem: +@Gtk.Template(filename=SETTINGS_ITEM_BOOL_GLADE) +class BoolItem(Gtk.Box): + __gtype_name__ = "BoolItem" + + lbl_name = Gtk.Template.Child() + switch_value = Gtk.Template.Child() + def __init__(self, name, value): super().__init__() - builder = utility.create_gtk_builder(SETTINGS_ITEM_BOOL_GLADE) - builder.get_object("lbl_name").set_label(_(name)) - self.switch_value = builder.get_object("switch_value") + self.lbl_name.set_label(_(name)) self.switch_value.set_active(value) - self.box = builder.get_object("box") def get_value(self): return self.switch_value.get_active() @@ -508,7 +517,7 @@ def __init__(self, parent, config): continue self.property_controls.append({"key": setting["id"], "box": box}) - self.box_settings.append(box.box) + self.box_settings.append(box) @Gtk.Template.Callback() def on_window_delete(self, *args): From ef748f42d60ef4fa749a9c62a86495a8a5e6cb6f Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 7 Aug 2024 22:41:58 +0200 Subject: [PATCH 116/134] gtk template: break screen --- safeeyes/glade/break_screen.glade | 7 +++-- safeeyes/ui/break_screen.py | 49 +++++++++++++++---------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/safeeyes/glade/break_screen.glade b/safeeyes/glade/break_screen.glade index 1ccd3106..c3214d9a 100644 --- a/safeeyes/glade/break_screen.glade +++ b/safeeyes/glade/break_screen.glade @@ -18,12 +18,13 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 2e133aab..6af55628 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -154,7 +154,7 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): i = 0 for monitor in monitors: - w = BreakScreenWindow( + window = BreakScreenWindow( self.application, message, image_path, @@ -167,8 +167,6 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): self.on_skip_clicked, ) - window = w.window - if self.context["is_wayland"]: # Note: in theory, this could also be used on X11 # however, that already has its own implementation below @@ -179,7 +177,7 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): window.set_title("SafeEyes-" + str(i)) - self.windows.append(w) + self.windows.append(window) if self.context["desktop"] == "kde": # Fix flickering screen in KDE by setting opacity to 1 @@ -298,16 +296,26 @@ def __release_keyboard_x11(self): def __destroy_all_screens(self): """Close all the break screens.""" for win in self.windows: - win.window.destroy() + win.destroy() del self.windows[:] -class BreakScreenWindow: +@Gtk.Template(filename=BREAK_SCREEN_GLADE) +class BreakScreenWindow(Gtk.Window): """This class manages the UI for the break screen window. Each instance is a single window, covering a single monitor. """ + __gtype_name__ = "BreakScreenWindow" + + lbl_message = Gtk.Template.Child() + lbl_count = Gtk.Template.Child() + lbl_widget = Gtk.Template.Child() + img_break = Gtk.Template.Child() + box_buttons = Gtk.Template.Child() + toolbar = Gtk.Template.Child() + def __init__( self, application, @@ -321,21 +329,9 @@ def __init__( show_skip, on_skip, ): - self.on_close = on_close + super().__init__(application=application) - builder = Gtk.Builder() - builder.add_from_file(BREAK_SCREEN_GLADE) - - self.window = builder.get_object("window_main") - self.window.set_application(application) - self.window.connect("close-request", self.on_window_delete) - - lbl_message = builder.get_object("lbl_message") - self.lbl_count = builder.get_object("lbl_count") - lbl_widget = builder.get_object("lbl_widget") - img_break = builder.get_object("img_break") - box_buttons = builder.get_object("box_buttons") - toolbar = builder.get_object("toolbar") + self.on_close = on_close for tray_action in tray_actions: # TODO: apparently, this would be better served with an icon theme @@ -350,7 +346,7 @@ def __init__( tray_action, ) toolbar_button.set_tooltip_text(_(tray_action.name)) - toolbar.append(toolbar_button) + self.toolbar.append(toolbar_button) toolbar_button.show() # Add the buttons @@ -360,7 +356,7 @@ def __init__( btn_postpone.get_style_context().add_class("btn_postpone") btn_postpone.connect("clicked", on_postpone) btn_postpone.set_visible(True) - box_buttons.append(btn_postpone) + self.box_buttons.append(btn_postpone) if show_skip: # Add the skip button @@ -368,13 +364,13 @@ def __init__( btn_skip.get_style_context().add_class("btn_skip") btn_skip.connect("clicked", on_skip) btn_skip.set_visible(True) - box_buttons.append(btn_skip) + self.box_buttons.append(btn_skip) # Set values if image_path: - img_break.set_from_file(image_path) - lbl_message.set_label(message) - lbl_widget.set_markup(widget) + self.img_break.set_from_file(image_path) + self.lbl_message.set_label(message) + self.lbl_widget.set_markup(widget) def set_count_down(self, count): self.lbl_count.set_text(count) @@ -389,6 +385,7 @@ def __tray_action(self, button, tray_action: TrayAction): tray_action.reset() tray_action.action() + @Gtk.Template.Callback() def on_window_delete(self, *args): """Window close event handler.""" logging.info("Closing the break screen") From 8deda2288ded33941cbe5a439d155f284e8c1ba7 Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 24 Apr 2025 14:03:56 +0200 Subject: [PATCH 117/134] about dialog: add types --- safeeyes/ui/about_dialog.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/safeeyes/ui/about_dialog.py b/safeeyes/ui/about_dialog.py index ca698ccd..d4d230bc 100644 --- a/safeeyes/ui/about_dialog.py +++ b/safeeyes/ui/about_dialog.py @@ -41,11 +41,11 @@ class AboutDialog(Gtk.ApplicationWindow): __gtype_name__ = "AboutDialog" - lbl_decription = Gtk.Template.Child() - lbl_license = Gtk.Template.Child() - lbl_app_name = Gtk.Template.Child() + lbl_decription: Gtk.Label = Gtk.Template.Child() + lbl_license: Gtk.Label = Gtk.Template.Child() + lbl_app_name: Gtk.Label = Gtk.Template.Child() - def __init__(self, application, version): + def __init__(self, application: Gtk.Application, version: str): super().__init__(application=application) self.lbl_decription.set_label( @@ -59,16 +59,16 @@ def __init__(self, application, version): # Set the version at the runtime self.lbl_app_name.set_label("Safe Eyes " + version) - def show(self): + def show(self) -> None: """Show the About dialog.""" self.present() @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Window close event handler.""" self.destroy() @Gtk.Template.Callback() - def on_close_clicked(self, *args): + def on_close_clicked(self, *args) -> None: """Close button click event handler.""" self.destroy() From ec87407381326c18e87557cc31795b5b33106478 Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 13:54:34 +0200 Subject: [PATCH 118/134] required plugin dialog: add types --- safeeyes/safeeyes.py | 3 +-- safeeyes/ui/required_plugin_dialog.py | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index 508fa3e1..59818225 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -346,14 +346,13 @@ def show_settings(self): ) settings_dialog.show() - def show_required_plugin_dialog(self, error: RequiredPluginException): + def show_required_plugin_dialog(self, error: RequiredPluginException) -> None: self.required_plugin_dialog_active = True logging.info("Show RequiredPlugin dialog") plugin_id = error.get_plugin_id() dialog = RequiredPluginDialog( - error.get_plugin_id(), error.get_plugin_name(), error.get_message(), self.quit, diff --git a/safeeyes/ui/required_plugin_dialog.py b/safeeyes/ui/required_plugin_dialog.py index e635df0a..33df3e5a 100644 --- a/safeeyes/ui/required_plugin_dialog.py +++ b/safeeyes/ui/required_plugin_dialog.py @@ -22,6 +22,7 @@ import os import gi +import typing gi.require_version("Gtk", "4.0") from gi.repository import Gtk @@ -43,12 +44,17 @@ class RequiredPluginDialog(Gtk.ApplicationWindow): __gtype_name__ = "RequiredPluginDialog" - lbl_header = Gtk.Template.Child() - lbl_message = Gtk.Template.Child() - btn_extra_link = Gtk.Template.Child() + lbl_header: Gtk.Label = Gtk.Template.Child() + lbl_message: Gtk.Label = Gtk.Template.Child() + btn_extra_link: Gtk.LinkButton = Gtk.Template.Child() def __init__( - self, plugin_id, plugin_name, message, on_quit, on_disable_plugin, application + self, + plugin_name: str, + message: typing.Union[str, PluginDependency], + on_quit: typing.Callable[[], None], + on_disable_plugin: typing.Callable[[], None], + application: Gtk.Application, ): super().__init__(application=application) @@ -61,27 +67,28 @@ def __init__( if isinstance(message, PluginDependency): self.lbl_message.set_label(_(message.message)) - self.btn_extra_link.set_uri(message.link) + if message.link is not None: + self.btn_extra_link.set_uri(message.link) self.btn_extra_link.set_visible(True) else: self.lbl_message.set_label(_(message)) - def show(self): + def show(self) -> None: """Show the dialog.""" self.present() @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Window close event handler.""" self.destroy() self.on_quit() @Gtk.Template.Callback() - def on_close_clicked(self, *args): + def on_close_clicked(self, *args) -> None: self.destroy() self.on_quit() @Gtk.Template.Callback() - def on_disable_plugin_clicked(self, *args): + def on_disable_plugin_clicked(self, *args) -> None: self.destroy() self.on_disable_plugin() From feaa1ee10d0472451e0007f9a6668b69e9388f2a Mon Sep 17 00:00:00 2001 From: deltragon Date: Mon, 18 Aug 2025 14:00:19 +0200 Subject: [PATCH 119/134] settings: add types --- safeeyes/ui/settings_dialog.py | 252 +++++++++++++++++++-------------- 1 file changed, 144 insertions(+), 108 deletions(-) diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index ed62fd3a..0419c400 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -18,6 +18,7 @@ import math import os +import typing import gi from safeeyes import utility @@ -58,26 +59,35 @@ class SettingsDialog(Gtk.ApplicationWindow): __gtype_name__ = "SettingsDialog" - box_short_breaks = Gtk.Template.Child() - box_long_breaks = Gtk.Template.Child() - box_plugins = Gtk.Template.Child() - popover = Gtk.Template.Child() - - spin_short_break_duration = Gtk.Template.Child() - spin_long_break_duration = Gtk.Template.Child() - spin_short_break_interval = Gtk.Template.Child() - spin_long_break_interval = Gtk.Template.Child() - spin_time_to_prepare = Gtk.Template.Child() - spin_postpone_duration = Gtk.Template.Child() - dropdown_postpone_unit = Gtk.Template.Child() - spin_disable_keyboard_shortcut = Gtk.Template.Child() - switch_strict_break = Gtk.Template.Child() - switch_random_order = Gtk.Template.Child() - switch_postpone = Gtk.Template.Child() - switch_persist = Gtk.Template.Child() - info_bar_long_break = Gtk.Template.Child() - - def __init__(self, application, config, on_save_settings): + box_short_breaks: Gtk.Box = Gtk.Template.Child() + box_long_breaks: Gtk.Box = Gtk.Template.Child() + box_plugins: Gtk.Box = Gtk.Template.Child() + popover: Gtk.MenuButton = Gtk.Template.Child() + + spin_short_break_duration: Gtk.SpinButton = Gtk.Template.Child() + spin_long_break_duration: Gtk.SpinButton = Gtk.Template.Child() + spin_short_break_interval: Gtk.SpinButton = Gtk.Template.Child() + spin_long_break_interval: Gtk.SpinButton = Gtk.Template.Child() + spin_time_to_prepare: Gtk.SpinButton = Gtk.Template.Child() + spin_postpone_duration: Gtk.SpinButton = Gtk.Template.Child() + dropdown_postpone_unit: Gtk.DropDown = Gtk.Template.Child() + spin_disable_keyboard_shortcut: Gtk.SpinButton = Gtk.Template.Child() + switch_strict_break: Gtk.Switch = Gtk.Template.Child() + switch_random_order: Gtk.Switch = Gtk.Template.Child() + switch_postpone: Gtk.Switch = Gtk.Template.Child() + switch_persist: Gtk.Switch = Gtk.Template.Child() + info_bar_long_break: Gtk.InfoBar = Gtk.Template.Child() + + plugin_items: dict[str, "PluginItem"] + plugin_map: dict[str, str] + config: Config + + def __init__( + self, + application: Gtk.Application, + config: Config, + on_save_settings: typing.Callable[[Config], None], + ): super().__init__(application=application) self.config = config @@ -95,7 +105,7 @@ def __init__(self, application, config, on_save_settings): self.initializing = False - def __initialize(self, config): + def __initialize(self, config: Config) -> None: # Don't show infobar for changes made internally self.infobar_long_break_shown = True for short_break in config.get("short_breaks"): @@ -126,23 +136,23 @@ def __initialize(self, config): self.switch_persist.set_active(config.get("persist_state")) self.infobar_long_break_shown = False - def __create_break_item(self, break_config, is_short): + def __create_break_item(self, break_config: dict, is_short: bool) -> None: """Create an entry for break to be listed in the break tab.""" parent_box = self.box_long_breaks if is_short: parent_box = self.box_short_breaks - box = BreakItem( + box: "BreakItem" = BreakItem( break_name=break_config["name"], on_properties=lambda: self.__show_break_properties_dialog( break_config, is_short, self.config, - lambda cfg: box.set_break_name(cfg["name"]), - lambda is_short, break_config: self.__create_break_item( + on_close=lambda cfg: box.set_break_name(cfg["name"]), + on_add=lambda is_short, break_config: self.__create_break_item( break_config, is_short ), - lambda: parent_box.remove(box), + on_remove=lambda: parent_box.remove(box), ), on_delete=lambda: self.__delete_break( break_config, @@ -155,7 +165,7 @@ def __create_break_item(self, break_config, is_short): parent_box.append(box) @Gtk.Template.Callback() - def on_reset_menu_clicked(self, button): + def on_reset_menu_clicked(self, button: Gtk.Button) -> None: self.popover.hide() def __confirmation_dialog_response(dialog, result) -> None: @@ -183,14 +193,16 @@ def __confirmation_dialog_response(dialog, result) -> None: messagedialog.choose(self, None, __confirmation_dialog_response) - def __clear_children(self, widget): - while widget.get_last_child() is not None: - widget.remove(widget.get_last_child()) + def __clear_children(self, widget: Gtk.Box) -> None: + while (child := widget.get_last_child()) is not None: + widget.remove(child) - def __delete_break(self, break_config, is_short, on_remove): + def __delete_break( + self, break_config: dict, is_short: bool, on_remove: typing.Callable[[], None] + ) -> None: """Remove the break after a confirmation.""" - def __confirmation_dialog_response(dialog, result): + def __confirmation_dialog_response(dialog, result) -> None: response_id = dialog.choose_finish(result) if response_id == 1: if is_short: @@ -210,7 +222,7 @@ def __confirmation_dialog_response(dialog, result): messagedialog.choose(self, None, __confirmation_dialog_response) - def __create_plugin_item(self, plugin_config): + def __create_plugin_item(self, plugin_config: dict) -> "PluginItem": """Create an entry for plugin to be listed in the plugin tab.""" box = PluginItem( plugin_config, @@ -225,14 +237,20 @@ def __create_plugin_item(self, plugin_config): box.set_visible(True) return box - def __show_plugins_properties_dialog(self, plugin_config): + def __show_plugins_properties_dialog(self, plugin_config: dict) -> None: """Show the PluginProperties dialog.""" dialog = PluginSettingsDialog(self, plugin_config) dialog.show() def __show_break_properties_dialog( - self, break_config, is_short, parent, on_close, on_add, on_remove - ): + self, + break_config: dict, + is_short: bool, + parent: Config, + on_close: typing.Callable[[dict], None], + on_add: typing.Callable[[bool, dict], None], + on_remove: typing.Callable[[], None], + ) -> None: """Show the BreakProperties dialog.""" dialog = BreakSettingsDialog( self, @@ -246,12 +264,12 @@ def __show_break_properties_dialog( ) dialog.show() - def show(self): + def show(self) -> None: """Show the SettingsDialog.""" super().show() @Gtk.Template.Callback() - def on_switch_postpone_activate(self, switch, state): + def on_switch_postpone_activate(self, switch, state) -> None: """Event handler to the state change of the postpone switch. Enable or disable the self.spin_postpone_duration based on the @@ -261,7 +279,7 @@ def on_switch_postpone_activate(self, switch, state): self.dropdown_postpone_unit.set_sensitive(self.switch_postpone.get_active()) @Gtk.Template.Callback() - def on_spin_short_break_interval_change(self, spin_button, *value): + def on_spin_short_break_interval_change(self, spin_button, *value) -> None: """Event handler for value change of short break interval.""" short_break_interval = self.spin_short_break_interval.get_value_as_int() long_break_interval = self.spin_long_break_interval.get_value_as_int() @@ -279,14 +297,14 @@ def on_spin_short_break_interval_change(self, spin_button, *value): self.info_bar_long_break.show() @Gtk.Template.Callback() - def on_spin_long_break_interval_change(self, spin_button, *value): + def on_spin_long_break_interval_change(self, spin_button, *value) -> None: """Event handler for value change of long break interval.""" if not self.initializing and not self.infobar_long_break_shown: self.infobar_long_break_shown = True self.info_bar_long_break.show() @Gtk.Template.Callback() - def on_info_bar_long_break_close(self, infobar, *user_data): + def on_info_bar_long_break_close(self, infobar, *user_data) -> None: """Event handler for info bar close action.""" self.info_bar_long_break.hide() @@ -303,7 +321,7 @@ def add_break(self, button) -> None: dialog.show() @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Event handler for Settings dialog close action.""" self.config.set( "short_break_duration", self.spin_short_break_duration.get_value_as_int() @@ -325,7 +343,11 @@ def on_window_delete(self, *args): ) self.config.set( "postpone_unit", - self.dropdown_postpone_unit.get_selected_item().get_string(), + # the model is a GtkStringList - so get_selected_item will return a + # StringObject + typing.cast( + Gtk.StringObject, self.dropdown_postpone_unit.get_selected_item() + ).get_string(), ) self.config.set( "shortcut_disable_time", @@ -347,9 +369,14 @@ def on_window_delete(self, *args): class BreakItem(Gtk.Box): __gtype_name__ = "BreakItem" - lbl_name = Gtk.Template.Child() + lbl_name: Gtk.Label = Gtk.Template.Child() - def __init__(self, break_name, on_properties, on_delete): + def __init__( + self, + break_name: str, + on_properties: typing.Callable[[], None], + on_delete: typing.Callable[[], None], + ): super().__init__() self.on_properties = on_properties @@ -357,15 +384,15 @@ def __init__(self, break_name, on_properties, on_delete): self.lbl_name.set_label(_(break_name)) - def set_break_name(self, break_name): + def set_break_name(self, break_name: str) -> None: self.lbl_name.set_label(_(break_name)) @Gtk.Template.Callback() - def on_properties_clicked(self, button): + def on_properties_clicked(self, button) -> None: self.on_properties() @Gtk.Template.Callback() - def on_delete_clicked(self, button): + def on_delete_clicked(self, button) -> None: self.on_delete() @@ -373,15 +400,15 @@ def on_delete_clicked(self, button): class PluginItem(Gtk.Box): __gtype_name__ = "PluginItem" - lbl_plugin_name = Gtk.Template.Child() - lbl_plugin_description = Gtk.Template.Child() - switch_enable = Gtk.Template.Child() - btn_properties = Gtk.Template.Child() - btn_disable_errored = Gtk.Template.Child() - btn_plugin_extra_link = Gtk.Template.Child() - img_plugin_icon = Gtk.Template.Child() + lbl_plugin_name: Gtk.Label = Gtk.Template.Child() + lbl_plugin_description: Gtk.Label = Gtk.Template.Child() + switch_enable: Gtk.Switch = Gtk.Template.Child() + btn_properties: Gtk.Button = Gtk.Template.Child() + btn_disable_errored: Gtk.Button = Gtk.Template.Child() + btn_plugin_extra_link: Gtk.LinkButton = Gtk.Template.Child() + img_plugin_icon: Gtk.Image = Gtk.Template.Child() - def __init__(self, plugin_config, on_properties): + def __init__(self, plugin_config: dict, on_properties: typing.Callable[[], None]): super().__init__() self.on_properties = on_properties @@ -394,7 +421,8 @@ def __init__(self, plugin_config, on_properties): message = plugin_config["meta"]["dependency_description"] if isinstance(message, PluginDependency): self.lbl_plugin_description.set_label(_(message.message)) - self.btn_plugin_extra_link.set_uri(message.link) + if message.link is not None: + self.btn_plugin_extra_link.set_uri(message.link) self.btn_plugin_extra_link.set_visible(True) else: self.lbl_plugin_description.set_label(_(message)) @@ -416,17 +444,17 @@ def __init__(self, plugin_config, on_properties): if plugin_config["icon"]: self.img_plugin_icon.set_from_file(plugin_config["icon"]) - def is_enabled(self): + def is_enabled(self) -> bool: return self.switch_enable.get_active() @Gtk.Template.Callback() - def on_disable_errored(self, button): + def on_disable_errored(self, button) -> None: """Permanently disable errored plugin.""" self.btn_disable_errored.set_sensitive(False) self.switch_enable.set_active(False) @Gtk.Template.Callback() - def on_properties_clicked(self, button): + def on_properties_clicked(self, button) -> None: if not self.plugin_config["error"] and self.plugin_config["settings"]: self.on_properties() @@ -435,17 +463,17 @@ def on_properties_clicked(self, button): class IntItem(Gtk.Box): __gtype_name__ = "IntItem" - lbl_name = Gtk.Template.Child() - spin_value = Gtk.Template.Child() + lbl_name: Gtk.Label = Gtk.Template.Child() + spin_value: Gtk.SpinButton = Gtk.Template.Child() - def __init__(self, name, value, min_value, max_value): + def __init__(self, name: str, value: float, min_value: float, max_value: float): super().__init__() self.lbl_name.set_label(_(name)) self.spin_value.set_range(min_value, max_value) self.spin_value.set_value(value) - def get_value(self): + def get_value(self) -> float: return self.spin_value.get_value() @@ -453,16 +481,16 @@ def get_value(self): class TextItem(Gtk.Box): __gtype_name__ = "TextItem" - lbl_name = Gtk.Template.Child() - txt_value = Gtk.Template.Child() + lbl_name: Gtk.Label = Gtk.Template.Child() + txt_value: Gtk.Entry = Gtk.Template.Child() - def __init__(self, name, value): + def __init__(self, name: str, value: str): super().__init__() self.lbl_name.set_label(_(name)) self.txt_value.set_text(value) - def get_value(self): + def get_value(self) -> str: return self.txt_value.get_text() @@ -470,16 +498,16 @@ def get_value(self): class BoolItem(Gtk.Box): __gtype_name__ = "BoolItem" - lbl_name = Gtk.Template.Child() - switch_value = Gtk.Template.Child() + lbl_name: Gtk.Label = Gtk.Template.Child() + switch_value: Gtk.Switch = Gtk.Template.Child() - def __init__(self, name, value): + def __init__(self, name: str, value: bool): super().__init__() self.lbl_name.set_label(_(name)) self.switch_value.set_active(value) - def get_value(self): + def get_value(self) -> bool: return self.switch_value.get_active() @@ -489,15 +517,16 @@ class PluginSettingsDialog(Gtk.Window): __gtype_name__ = "PluginSettingsDialog" - box_settings = Gtk.Template.Child() + box_settings: Gtk.Box = Gtk.Template.Child() - def __init__(self, parent, config): + def __init__(self, parent: Gtk.Window, config: typing.Any): super().__init__(transient_for=parent) self.config = config self.property_controls = [] for setting in config.get("settings"): + box: typing.Union[IntItem, BoolItem, TextItem] if setting["type"].upper() == "INT": box = IntItem( setting["label"], @@ -520,7 +549,7 @@ def __init__(self, parent, config): self.box_settings.append(box) @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Event handler for Properties dialog close action.""" for property_control in self.property_controls: self.config["active_plugin_config"][property_control["key"]] = ( @@ -528,7 +557,7 @@ def on_window_delete(self, *args): ) self.destroy() - def show(self): + def show(self) -> None: """Show the Properties dialog.""" self.present() @@ -539,27 +568,27 @@ class BreakSettingsDialog(Gtk.Window): __gtype_name__ = "BreakSettingsDialog" - txt_break = Gtk.Template.Child() - switch_override_interval = Gtk.Template.Child() - switch_override_duration = Gtk.Template.Child() - switch_override_plugins = Gtk.Template.Child() - spin_interval = Gtk.Template.Child() - spin_duration = Gtk.Template.Child() - btn_image = Gtk.Template.Child() - cmb_type = Gtk.Template.Child() - grid_plugins = Gtk.Template.Child() - lst_break_types = Gtk.Template.Child() + txt_break: Gtk.Entry = Gtk.Template.Child() + switch_override_interval: Gtk.Switch = Gtk.Template.Child() + switch_override_duration: Gtk.Switch = Gtk.Template.Child() + switch_override_plugins: Gtk.Switch = Gtk.Template.Child() + spin_interval: Gtk.SpinButton = Gtk.Template.Child() + spin_duration: Gtk.SpinButton = Gtk.Template.Child() + btn_image: Gtk.Button = Gtk.Template.Child() + cmb_type: Gtk.ComboBox = Gtk.Template.Child() + grid_plugins: Gtk.Grid = Gtk.Template.Child() + lst_break_types: Gtk.ComboBox = Gtk.Template.Child() def __init__( self, - parent, - break_config, - is_short, - parent_config, - plugin_map, - on_close, - on_add, - on_remove, + parent: Gtk.Window, + break_config: dict, + is_short: bool, + parent_config: Config, + plugin_map: dict[str, str], + on_close: typing.Callable[[dict], None], + on_add: typing.Callable[[bool, dict], None], + on_remove: typing.Callable[[], None], ): super().__init__(transient_for=parent) @@ -630,23 +659,23 @@ def __init__( ) @Gtk.Template.Callback() - def on_switch_override_interval_activate(self, switch_button, state): + def on_switch_override_interval_activate(self, switch_button, state) -> None: """switch_override_interval state change event handler.""" self.spin_interval.set_sensitive(state) @Gtk.Template.Callback() - def on_switch_override_duration_activate(self, switch_button, state): + def on_switch_override_duration_activate(self, switch_button, state) -> None: """switch_override_duration state change event handler.""" self.spin_duration.set_sensitive(state) @Gtk.Template.Callback() - def on_switch_override_plugins_activate(self, switch_button, state): + def on_switch_override_plugins_activate(self, switch_button, state) -> None: """switch_override_plugins state change event handler.""" for chk_box in self.plugin_check_buttons.values(): chk_box.set_sensitive(state) @Gtk.Template.Callback() - def select_image(self, button): + def select_image(self, button) -> None: """Show a file chooser dialog and let the user to select an image.""" dialog = Gtk.FileDialog() dialog.set_title(_("Please select an image")) @@ -661,7 +690,9 @@ def select_image(self, button): dialog.open(self, None, self.select_image_callback) - def select_image_callback(self, dialog, result): + def select_image_callback( + self, dialog: Gtk.FileDialog, result: Gio.AsyncResult + ) -> None: response = None try: @@ -682,7 +713,7 @@ def select_image_callback(self, dialog, result): self.btn_image.set_icon_name("gtk-missing-image") @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Event handler for Properties dialog close action.""" break_name = self.txt_break.get_text().strip() if break_name: @@ -720,7 +751,7 @@ def on_window_delete(self, *args): self.on_close(self.break_config) self.destroy() - def show(self): + def show(self) -> None: """Show the Properties dialog.""" self.present() @@ -731,22 +762,27 @@ class NewBreakDialog(Gtk.Window): __gtype_name__ = "NewBreakDialog" - txt_break = Gtk.Template.Child() - cmb_type = Gtk.Template.Child() + txt_break: Gtk.Entry = Gtk.Template.Child() + cmb_type: Gtk.ComboBox = Gtk.Template.Child() - def __init__(self, parent, parent_config, on_add): + def __init__( + self, + parent: Gtk.Window, + parent_config: Config, + on_add: typing.Callable[[bool, dict], None], + ): super().__init__(transient_for=parent) self.parent_config = parent_config self.on_add = on_add @Gtk.Template.Callback() - def discard(self, button): + def discard(self, button) -> None: """Close the dialog.""" self.destroy() @Gtk.Template.Callback() - def save(self, button): + def save(self, button) -> None: """Event handler for Properties dialog close action.""" break_config = {"name": self.txt_break.get_text().strip()} @@ -759,10 +795,10 @@ def save(self, button): self.destroy() @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Event handler for dialog close action.""" self.destroy() - def show(self): + def show(self) -> None: """Show the Properties dialog.""" self.present() From 8bc91d0881d99ad336b53ee5aa1a76c67d79d469 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 20 Aug 2025 11:03:24 +0200 Subject: [PATCH 120/134] break screen: add types --- safeeyes/ui/break_screen.py | 117 ++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 6af55628..12901387 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -20,10 +20,11 @@ import logging import os import time +import typing import gi from safeeyes import utility -from safeeyes.model import TrayAction +from safeeyes.model import Break, Config, TrayAction from safeeyes.translations import translate as _ import Xlib from Xlib.display import Display @@ -44,7 +45,15 @@ class BreakScreen: This class creates and manages the fullscreen windows for every monitor. """ - def __init__(self, application, context, on_skipped, on_postponed): + windows: list["BreakScreenWindow"] + + def __init__( + self, + application: Gtk.Application, + context, + on_skipped: typing.Callable[[], None], + on_postponed: typing.Callable[[], None], + ): self.application = application self.context = context self.x11_display = None @@ -64,7 +73,7 @@ def __init__(self, application, context, on_skipped, on_postponed): if not self.context["is_wayland"]: self.x11_display = Display() - def initialize(self, config): + def initialize(self, config: Config) -> None: """Initialize the internal properties from configuration.""" logging.info("Initialize the break screen") self.enable_postpone = config.get("allow_postpone", False) @@ -84,7 +93,7 @@ def initialize(self, config): self.shortcut_disable_time = config.get("shortcut_disable_time", 2) self.strict_break = config.get("strict_break", False) - def skip_break(self): + def skip_break(self) -> None: """Skip the break from the break screen.""" logging.info("User skipped the break") # Must call on_skipped before close to lock screen before closing the break @@ -92,28 +101,30 @@ def skip_break(self): self.on_skipped() self.close() - def postpone_break(self): + def postpone_break(self) -> None: """Postpone the break from the break screen.""" logging.info("User postponed the break") self.on_postponed() self.close() - def on_skip_clicked(self, button): + def on_skip_clicked(self, button) -> None: """Skip button press event handler.""" self.skip_break() - def on_postpone_clicked(self, button): + def on_postpone_clicked(self, button) -> None: """Postpone button press event handler.""" self.postpone_break() - def show_count_down(self, countdown, seconds): + def show_count_down(self, countdown: int, seconds: int) -> None: """Show/update the count down on all screens.""" self.enable_shortcut = self.shortcut_disable_time <= seconds mins, secs = divmod(countdown, 60) timeformat = "{:02d}:{:02d}".format(mins, secs) GLib.idle_add(lambda: self.__update_count_down(timeformat)) - def show_message(self, break_obj, widget, tray_actions=[]): + def show_message( + self, break_obj: Break, widget: str, tray_actions: list[TrayAction] = [] + ) -> None: """Show the break screen with the given message on all displays.""" message = break_obj.name image_path = break_obj.image @@ -122,7 +133,7 @@ def show_message(self, break_obj, widget, tray_actions=[]): lambda: self.__show_break_screen(message, image_path, widget, tray_actions) ) - def close(self): + def close(self) -> None: """Hide the break screen from active window and destroy all other windows. """ @@ -133,14 +144,24 @@ def close(self): # Destroy other windows if exists GLib.idle_add(lambda: self.__destroy_all_screens()) - def __show_break_screen(self, message, image_path, widget, tray_actions): + def __show_break_screen( + self, + message: str, + image_path: typing.Optional[str], + widget: str, + tray_actions: list[TrayAction], + ) -> None: """Show an empty break screen on all screens.""" # Lock the keyboard if not self.context["is_wayland"]: utility.start_thread(self.__lock_keyboard_x11) display = Gdk.Display.get_default() - monitors = display.get_monitors() + + if display is None: + raise Exception("display not found") + + monitors = typing.cast(typing.Sequence[Gdk.Monitor], display.get_monitors()) logging.info("Show break screens in %d display(s)", len(monitors)) skip_button_disabled = self.context.get("skip_button_disabled", False) @@ -196,17 +217,22 @@ def __show_break_screen(self, message, image_path, widget, tray_actions): if self.context["is_wayland"]: # this may or may not be granted by the window system - window.get_surface().inhibit_system_shortcuts(None) + surface = window.get_surface() + if surface is not None: + typing.cast(Gdk.Toplevel, surface).inhibit_system_shortcuts(None) i = i + 1 - def __update_count_down(self, count): + def __update_count_down(self, count: str) -> None: """Update the countdown on all break screens.""" for window in self.windows: window.set_count_down(count) - def __window_set_keep_above_x11(self, window): + def __window_set_keep_above_x11(self, window: "BreakScreenWindow") -> None: """Use EWMH hints to keep window above and on all desktops.""" + if self.x11_display is None: + return + NET_WM_STATE = self.x11_display.intern_atom("_NET_WM_STATE") NET_WM_STATE_ABOVE = self.x11_display.intern_atom("_NET_WM_STATE_ABOVE") NET_WM_STATE_STICKY = self.x11_display.intern_atom("_NET_WM_STATE_STICKY") @@ -216,7 +242,12 @@ def __window_set_keep_above_x11(self, window): # See https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html#id-1.6.8 root_window = self.x11_display.screen().root - xid = GdkX11.X11Surface.get_xid(window.get_surface()) + surface = window.get_surface() + + if surface is None or not isinstance(surface, GdkX11.X11Surface): + return + + xid = GdkX11.X11Surface.get_xid(surface) root_window.send_event( Xlib.protocol.event.ClientMessage( @@ -240,11 +271,14 @@ def __window_set_keep_above_x11(self, window): self.x11_display.sync() - def __lock_keyboard_x11(self): + def __lock_keyboard_x11(self) -> None: """Lock the keyboard to prevent the user from using keyboard shortcuts. (X11 only) """ + if self.x11_display is None: + return + logging.info("Lock the keyboard") self.lock_keyboard = True @@ -275,7 +309,9 @@ def __lock_keyboard_x11(self): # Reduce the CPU usage by sleeping for a second time.sleep(1) - def on_key_pressed_wayland(self, event_controller_key, keyval, keycode, state): + def on_key_pressed_wayland( + self, event_controller_key, keyval, keycode, state + ) -> bool: if self.enable_shortcut: if keyval == Gdk.KEY_space and self.show_postpone_button: self.postpone_break() @@ -286,14 +322,17 @@ def on_key_pressed_wayland(self, event_controller_key, keyval, keycode, state): return False - def __release_keyboard_x11(self): + def __release_keyboard_x11(self) -> None: """Release the locked keyboard.""" + if self.x11_display is None: + return + logging.info("Unlock the keyboard") self.lock_keyboard = False self.x11_display.ungrab_keyboard(X.CurrentTime) self.x11_display.flush() - def __destroy_all_screens(self): + def __destroy_all_screens(self) -> None: """Close all the break screens.""" for win in self.windows: win.destroy() @@ -309,25 +348,25 @@ class BreakScreenWindow(Gtk.Window): __gtype_name__ = "BreakScreenWindow" - lbl_message = Gtk.Template.Child() - lbl_count = Gtk.Template.Child() - lbl_widget = Gtk.Template.Child() - img_break = Gtk.Template.Child() - box_buttons = Gtk.Template.Child() - toolbar = Gtk.Template.Child() + lbl_message: Gtk.Label = Gtk.Template.Child() + lbl_count: Gtk.Label = Gtk.Template.Child() + lbl_widget: Gtk.Label = Gtk.Template.Child() + img_break: Gtk.Image = Gtk.Template.Child() + box_buttons: Gtk.Box = Gtk.Template.Child() + toolbar: Gtk.Box = Gtk.Template.Child() def __init__( self, - application, - message, - image_path, - widget, - tray_actions, - on_close, - show_postpone, - on_postpone, - show_skip, - on_skip, + application: Gtk.Application, + message: str, + image_path: typing.Optional[str], + widget: str, + tray_actions: list[TrayAction], + on_close: typing.Callable[[], None], + show_postpone: bool, + on_postpone: typing.Callable[[Gtk.Button], None], + show_skip: bool, + on_skip: typing.Callable[[Gtk.Button], None], ): super().__init__(application=application) @@ -372,10 +411,10 @@ def __init__( self.lbl_message.set_label(message) self.lbl_widget.set_markup(widget) - def set_count_down(self, count): + def set_count_down(self, count: str) -> None: self.lbl_count.set_text(count) - def __tray_action(self, button, tray_action: TrayAction): + def __tray_action(self, button, tray_action: TrayAction) -> None: """Tray action handler. Hides all toolbar buttons for this action and call the action @@ -386,7 +425,7 @@ def __tray_action(self, button, tray_action: TrayAction): tray_action.action() @Gtk.Template.Callback() - def on_window_delete(self, *args): + def on_window_delete(self, *args) -> None: """Window close event handler.""" logging.info("Closing the break screen") self.on_close() From b885db02885e3024baa83308b772c6a82432e3af Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 8 Aug 2024 22:53:57 +0200 Subject: [PATCH 121/134] remove unused create_gtk_builder helper --- safeeyes/utility.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/safeeyes/utility.py b/safeeyes/utility.py index 990aa1f2..32b6a18f 100644 --- a/safeeyes/utility.py +++ b/safeeyes/utility.py @@ -713,26 +713,6 @@ def open_session(): return session -def create_gtk_builder(glade_file): - """Create a Gtk builder and load the glade file.""" - from safeeyes.translations import translate as _ - - builder = Gtk.Builder() - builder.set_translation_domain("safeeyes") - builder.add_from_file(glade_file) - # Tranlslate all sub components - for obj in builder.get_objects(): - if hasattr(obj, "get_label"): - label = obj.get_label() - if label is not None: - obj.set_label(_(label)) - elif hasattr(obj, "get_title"): - title = obj.get_title() - if title is not None: - obj.set_title(_(title)) - return builder - - def load_and_scale_image( path: str, width: int, height: int ) -> typing.Optional[Gtk.Image]: From 513cc59e1d23c893521e0fd475bde6f19a22cffb Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 4 Jun 2025 14:57:00 +0200 Subject: [PATCH 122/134] typing: add Context to gradually type --- safeeyes/context.py | 132 +++++++++++++++++++++++++++++++++++ safeeyes/model.py | 20 ++++-- safeeyes/safeeyes.py | 56 +++++---------- safeeyes/tests/test_model.py | 33 +++++---- 4 files changed, 181 insertions(+), 60 deletions(-) create mode 100644 safeeyes/context.py diff --git a/safeeyes/context.py b/safeeyes/context.py new file mode 100644 index 00000000..f4092df0 --- /dev/null +++ b/safeeyes/context.py @@ -0,0 +1,132 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2025 Mel Dafert + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from collections.abc import MutableMapping +import datetime +import typing + +from safeeyes import utility +from safeeyes.model import State + +if typing.TYPE_CHECKING: + from safeeyes.safeeyes import SafeEyes + + +class API: + _application: "SafeEyes" + + def __init__( + self, + application: "SafeEyes", + ) -> None: + self._application = application + + def __getitem__(self, key: str) -> typing.Callable: + """This is soft-deprecated - it is preferred to access the property.""" + return getattr(self, key) + + def show_settings(self) -> None: + utility.execute_main_thread(self._application.show_settings) + + def show_about(self) -> None: + utility.execute_main_thread(self._application.show_about) + + def enable_safeeyes(self, next_break_time=-1) -> None: + utility.execute_main_thread(self._application.enable_safeeyes, next_break_time) + + def disable_safeeyes(self, status=None, is_resting=False) -> None: + utility.execute_main_thread( + self._application.disable_safeeyes, status, is_resting + ) + + def status(self) -> str: + return self._application.status() + + def quit(self) -> None: + utility.execute_main_thread(self._application.quit) + + def take_break(self, break_type=None) -> None: + self._application.take_break(break_type) + + def has_breaks(self, break_type=None) -> bool: + return self._application.safe_eyes_core.has_breaks(break_type) + + def postpone(self, duration=-1) -> None: + self._application.safe_eyes_core.postpone(duration) + + def get_break_time(self, break_type=None) -> typing.Optional[datetime.datetime]: + return self._application.safe_eyes_core.get_break_time(break_type) + + +class Context(MutableMapping): + version: str + api: API + desktop: str + is_wayland: bool + locale: str + session: dict[str, typing.Any] + state: State + + ext: dict + + def __init__( + self, + api: API, + locale: str, + version: str, + session: dict[str, typing.Any], + ) -> None: + self.version = version + self.desktop = utility.desktop_environment() + self.is_wayland = utility.is_wayland() + self.locale = locale + self.session = session + self.state = State.START + self.api = api + + self.ext = {} + + def __setitem__(self, key: str, value: typing.Any) -> None: + """This is soft-deprecated - it is preferred to access the property.""" + if hasattr(self, key): + setattr(self, key, value) + return + + self.ext[key] = value + + def __getitem__(self, key: str) -> typing.Any: + """This is soft-deprecated - it is preferred to access the property.""" + if hasattr(self, key): + return getattr(self, key) + + return self.ext[key] + + def __delitem__(self, key: str) -> None: + """This is soft-deprecated - it is preferred to access the property.""" + if hasattr(self, key): + raise Exception("cannot delete property") + + del self.ext[key] + + def __len__(self) -> int: + """This is soft-deprecated.""" + return len(self.ext) + + def __iter__(self) -> typing.Iterator[typing.Any]: + """This is soft-deprecated.""" + return iter(self.ext) diff --git a/safeeyes/model.py b/safeeyes/model.py index 5d5bbe49..aabfbe24 100644 --- a/safeeyes/model.py +++ b/safeeyes/model.py @@ -38,6 +38,9 @@ from safeeyes import utility from safeeyes.translations import translate as _ +if typing.TYPE_CHECKING: + from safeeyes.context import Context + class BreakType(Enum): """Type of Safe Eyes breaks.""" @@ -105,9 +108,12 @@ class BreakQueue: __is_random_order: bool __long_queue: typing.Optional[list[Break]] __short_queue: typing.Optional[list[Break]] + context: "Context" @classmethod - def create(cls, config: "Config", context) -> typing.Optional["BreakQueue"]: + def create( + cls, config: "Config", context: "Context" + ) -> typing.Optional["BreakQueue"]: short_break_time = config.get("short_break_interval") long_break_time = config.get("long_break_interval") is_random_order = config.get("random_order") @@ -142,7 +148,7 @@ def create(cls, config: "Config", context) -> typing.Optional["BreakQueue"]: def __init__( self, - context, + context: "Context", short_break_time: int, long_break_time: int, is_random_order: bool, @@ -166,7 +172,7 @@ def __init__( self.__set_next_break() # Restore the last break from session - last_break = context["session"].get("break") + last_break = context.session.get("break") if last_break is not None: current_break = self.get_break() if last_break != current_break.name: @@ -247,7 +253,7 @@ def __set_next_break(self, break_type: typing.Optional[BreakType] = None) -> Non break_obj = self.__next_short() self.__current_break = break_obj - self.context["session"]["break"] = self.__current_break.name + self.context.session["break"] = self.__current_break.name def skip_long_break(self) -> None: if not (self.__short_queue and self.__long_queue): @@ -265,7 +271,7 @@ def skip_long_break(self) -> None: # we could decrement the __current_long counter, but then we'd need to # handle wraparound and possibly randomizing, which seems complicated self.__current_break = self.__next_short() - self.context["session"]["break"] = self.__current_break.name + self.context.session["break"] = self.__current_break.name def is_empty(self, break_type: BreakType) -> bool: """Check if the given break type is empty or not.""" @@ -283,7 +289,7 @@ def __next_short(self) -> Break: raise Exception("this may only be called when there are short breaks") break_obj = shorts[self.__current_short] - self.context["break_type"] = "short" + self.context.ext["break_type"] = "short" # Update the index to next self.__current_short = (self.__current_short + 1) % len(shorts) @@ -297,7 +303,7 @@ def __next_long(self) -> Break: raise Exception("this may only be called when there are long breaks") break_obj = longs[self.__current_long] - self.context["break_type"] = "long" + self.context.ext["break_type"] = "long" # Update the index to next self.__current_long = (self.__current_long + 1) % len(longs) diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index 59818225..aca49a76 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -22,11 +22,10 @@ import atexit import logging -import typing from importlib import metadata import gi -from safeeyes import utility +from safeeyes import context, utility from safeeyes.ui.about_dialog import AboutDialog from safeeyes.ui.break_screen import BreakScreen from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog @@ -47,19 +46,20 @@ class SafeEyes(Gtk.Application): required_plugin_dialog_active = False retry_errored_plugins_count = 0 + context: context.Context + break_screen: BreakScreen + safe_eyes_core: SafeEyesCore + plugins_manager: PluginManager + system_locale: str - def __init__(self, system_locale, config) -> None: + def __init__(self, system_locale: str, config) -> None: super().__init__( application_id="io.github.slgobinath.SafeEyes", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, ) self.active = False - self.break_screen = None - self.safe_eyes_core = None self.config = config - self.context: typing.Any = {} - self.plugins_manager = None self.settings_dialog_active = False self._status = "" self.system_locale = system_locale @@ -219,39 +219,23 @@ def do_command_line(self, command_line): return 0 - def do_startup(self): + def do_startup(self) -> None: Gtk.Application.do_startup(self) logging.info("Starting up Application") # Initialize the Safe Eyes Context - self.context["version"] = SAFE_EYES_VERSION - self.context["desktop"] = utility.desktop_environment() - self.context["is_wayland"] = utility.is_wayland() - self.context["locale"] = self.system_locale - self.context["api"] = {} - self.context["api"]["show_settings"] = lambda: utility.execute_main_thread( - self.show_settings - ) - self.context["api"]["show_about"] = lambda: utility.execute_main_thread( - self.show_about - ) - self.context["api"]["enable_safeeyes"] = ( - lambda next_break_time=-1: utility.execute_main_thread( - self.enable_safeeyes, next_break_time - ) - ) - self.context["api"]["disable_safeeyes"] = ( - lambda status=None, is_resting=False: utility.execute_main_thread( - self.disable_safeeyes, status, is_resting - ) - ) - self.context["api"]["status"] = self.status - self.context["api"]["quit"] = lambda: utility.execute_main_thread(self.quit) if self.config.get("persist_state"): - self.context["session"] = utility.open_session() + session = utility.open_session() else: - self.context["session"] = {"plugin": {}} + session = {"plugin": {}} + + self.context = context.Context( + api=context.API(self), + locale=self.system_locale, + version=SAFE_EYES_VERSION, + session=session, + ) # Initialize the theme self._initialize_styles() @@ -269,10 +253,6 @@ def do_startup(self): self.safe_eyes_core.on_stop_break += self.stop_break self.safe_eyes_core.on_update_next_break += self.update_next_break self.safe_eyes_core.initialize(self.config) - self.context["api"]["take_break"] = self.take_break - self.context["api"]["has_breaks"] = self.safe_eyes_core.has_breaks - self.context["api"]["postpone"] = self.safe_eyes_core.postpone - self.context["api"]["get_break_time"] = self.safe_eyes_core.get_break_time try: self.plugins_manager.init(self.context, self.config) @@ -289,7 +269,7 @@ def do_startup(self): and self.safe_eyes_core.has_breaks() ): self.active = True - self.context["state"] = State.START + self.context.state = State.START self.plugins_manager.start() # Call the start method of all plugins self.safe_eyes_core.start() self.handle_system_suspend() diff --git a/safeeyes/tests/test_model.py b/safeeyes/tests/test_model.py index f0d219db..0be8c516 100644 --- a/safeeyes/tests/test_model.py +++ b/safeeyes/tests/test_model.py @@ -19,7 +19,8 @@ import pytest import random import typing -from safeeyes import model +from unittest import mock +from safeeyes import context, model class TestBreak: @@ -65,9 +66,11 @@ def test_create_empty(self) -> None: system_config={}, ) - context: dict[str, typing.Any] = {} + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) - bq = model.BreakQueue.create(config, context) + bq = model.BreakQueue.create(config, ctx) assert bq is None @@ -98,11 +101,11 @@ def get_bq_only_short( system_config={}, ) - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) - bq = model.BreakQueue.create(config, context) + bq = model.BreakQueue.create(config, ctx) assert bq is not None @@ -135,11 +138,11 @@ def get_bq_only_long( system_config={}, ) - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) - bq = model.BreakQueue.create(config, context) + bq = model.BreakQueue.create(config, ctx) assert bq is not None @@ -177,11 +180,11 @@ def get_bq_full( system_config={}, ) - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) - bq = model.BreakQueue.create(config, context) + bq = model.BreakQueue.create(config, ctx) assert bq is not None From d375a4cf064b5ddc560d616734b8d755f95ad4b5 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 4 Jun 2025 16:24:20 +0200 Subject: [PATCH 123/134] add context to core and model --- safeeyes/context.py | 5 ++ safeeyes/core.py | 51 ++++++----- safeeyes/tests/test_core.py | 163 ++++++++++++++++++------------------ 3 files changed, 113 insertions(+), 106 deletions(-) diff --git a/safeeyes/context.py b/safeeyes/context.py index f4092df0..4da92bfe 100644 --- a/safeeyes/context.py +++ b/safeeyes/context.py @@ -82,6 +82,11 @@ class Context(MutableMapping): session: dict[str, typing.Any] state: State + skipped: bool = False + postponed: bool = False + skip_button_disabled: bool = False + postpone_button_disabled: bool = False + ext: dict def __init__( diff --git a/safeeyes/core.py b/safeeyes/core.py index 5e6ae0f1..ab9eb9c4 100644 --- a/safeeyes/core.py +++ b/safeeyes/core.py @@ -29,6 +29,8 @@ from safeeyes.model import State from safeeyes.model import Config +from safeeyes.context import Context + import gi gi.require_version("GLib", "2.0") @@ -45,6 +47,7 @@ class SafeEyesCore: postpone_duration: int = 0 default_postpone_duration: int = 0 pre_break_warning_time: int = 0 + context: Context _break_queue: typing.Optional[BreakQueue] = None @@ -62,7 +65,7 @@ class SafeEyesCore: # set to true when a break was requested _take_break_now: bool = False - def __init__(self, context) -> None: + def __init__(self, context: Context) -> None: """Create an instance of SafeEyesCore and initialize the variables.""" # This event is fired before for a break self.on_pre_break = EventHook() @@ -77,11 +80,7 @@ def __init__(self, context) -> None: # This event is fired when deciding the next break time self.on_update_next_break = EventHook() self.context = context - self.context["skipped"] = False - self.context["postponed"] = False - self.context["skip_button_disabled"] = False - self.context["postpone_button_disabled"] = False - self.context["state"] = State.WAITING + self.context.state = State.WAITING def initialize(self, config: Config): """Initialize the internal properties from configuration.""" @@ -116,14 +115,14 @@ def stop(self, is_resting=False) -> None: self.paused_time = datetime.datetime.now().timestamp() # Stop the break thread self.running = False - if self.context["state"] != State.QUIT: - self.context["state"] = State.RESTING if (is_resting) else State.STOPPED + if self.context.state != State.QUIT: + self.context.state = State.RESTING if (is_resting) else State.STOPPED self.__wakeup_scheduler() def skip(self) -> None: """User skipped the break using Skip button.""" - self.context["skipped"] = True + self.context.skipped = True def postpone(self, duration=-1) -> None: """User postponed the break using Postpone button.""" @@ -132,7 +131,7 @@ def postpone(self, duration=-1) -> None: else: self.postpone_duration = self.default_postpone_duration logging.debug("Postpone the break for %d seconds", self.postpone_duration) - self.context["postponed"] = True + self.context.postponed = True def get_break_time( self, break_type: typing.Optional[BreakType] = None @@ -154,7 +153,7 @@ def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """ if self._break_queue is None: return - if not self.context["state"] == State.WAITING: + if not self.context.state == State.WAITING: return if break_type is not None and self._break_queue.get_break().type != break_type: @@ -189,7 +188,7 @@ def __scheduler_job(self) -> None: current_time = datetime.datetime.now() current_timestamp = current_time.timestamp() - if self.context["state"] == State.RESTING and self.paused_time > -1: + if self.context.state == State.RESTING and self.paused_time > -1: # Safe Eyes was resting paused_duration = int(current_timestamp - self.paused_time) self.paused_time = -1 @@ -203,11 +202,11 @@ def __scheduler_job(self) -> None: # Skip the next long break self._break_queue.skip_long_break() - if self.context["postponed"]: + if self.context.postponed: # Previous break was postponed logging.info("Prepare for postponed break") time_to_wait = self.postpone_duration - self.context["postponed"] = False + self.context.postponed = False elif current_timestamp < self.scheduled_next_break_timestamp: # Non-standard break was set. time_to_wait = round( @@ -221,7 +220,7 @@ def __scheduler_job(self) -> None: self.scheduled_next_break_time = current_time + datetime.timedelta( seconds=time_to_wait ) - self.context["state"] = State.WAITING + self.context.state = State.WAITING self.__fire_on_update_next_break(self.scheduled_next_break_time) # Wait for the pre break warning period @@ -262,7 +261,7 @@ def __fire_pre_break(self) -> None: if self._break_queue is None: # This will only be called by methods which check this return - self.context["state"] = State.PRE_BREAK + self.context.state = State.PRE_BREAK proceed = self.__fire_hook(self.on_pre_break, self._break_queue.get_break()) if not proceed: # Plugins wanted to ignore this break @@ -298,9 +297,9 @@ def __do_start_break(self) -> None: # Plugins want to ignore this break self.__start_next_break() return - if self.context["postponed"]: + if self.context.postponed: # Plugins want to postpone this break - self.context["postponed"] = False + self.context.postponed = False if self.scheduled_next_break_time is None: raise Exception("this should never happen") @@ -322,7 +321,7 @@ def __start_break(self) -> None: if self._break_queue is None: # This will only be called by methods which check this return - self.context["state"] = State.BREAK + self.context.state = State.BREAK break_obj = self._break_queue.get_break() self._taking_break = break_obj self._countdown = break_obj.duration @@ -340,8 +339,8 @@ def __cycle_break_countdown(self) -> None: if ( self._countdown > 0 and self.running - and not self.context["skipped"] - and not self.context["postponed"] + and not self.context.skipped + and not self.context.postponed ): countdown = self._countdown self._countdown -= 1 @@ -359,14 +358,14 @@ def __cycle_break_countdown(self) -> None: def __fire_stop_break(self) -> None: # Loop terminated because of timeout (not skipped) -> Close the break alert - if not self.context["skipped"] and not self.context["postponed"]: + if not self.context.skipped and not self.context.postponed: logging.info("Break is terminated automatically") self.__fire_hook(self.on_stop_break) # Reset the skipped flag - self.context["skipped"] = False - self.context["skip_button_disabled"] = False - self.context["postpone_button_disabled"] = False + self.context.skipped = False + self.context.skip_button_disabled = False + self.context.postpone_button_disabled = False self.__start_next_break() def __wait_for( @@ -440,7 +439,7 @@ def __start_next_break(self) -> None: if self._break_queue is None: # This will only be called by methods which check this return - if not self.context["postponed"]: + if not self.context.postponed: self._break_queue.next() if self.running: diff --git a/safeeyes/tests/test_core.py b/safeeyes/tests/test_core.py index abea61af..6efed3d3 100644 --- a/safeeyes/tests/test_core.py +++ b/safeeyes/tests/test_core.py @@ -20,6 +20,7 @@ import pytest import typing +from safeeyes import context from safeeyes import core from safeeyes import model @@ -134,7 +135,7 @@ def run_next_break( sequential_threading_handle: SafeEyesCoreHandle, time_machine: TimeMachineFixture, safe_eyes_core: core.SafeEyesCore, - context, + ctx: context.Context, break_duration: int, break_name_translated: str, initial: bool = False, @@ -154,11 +155,11 @@ def run_next_break( if initial: safe_eyes_core.start() else: - assert context["state"] == model.State.BREAK + assert ctx["state"] == model.State.BREAK sequential_threading_handle.next() - assert context["state"] == model.State.WAITING + assert ctx["state"] == model.State.WAITING on_update_next_break.assert_called_once() assert isinstance(on_update_next_break.call_args[0][0], model.Break) @@ -168,7 +169,7 @@ def run_next_break( self.run_next_break_from_waiting_state( sequential_threading_handle, safe_eyes_core, - context, + ctx, break_duration, break_name_translated, ) @@ -177,7 +178,7 @@ def run_next_break_from_waiting_state( self, sequential_threading_handle: SafeEyesCoreHandle, safe_eyes_core: core.SafeEyesCore, - context, + ctx: context.Context, break_duration: int, break_name_translated: str, ) -> None: @@ -193,13 +194,13 @@ def run_next_break_from_waiting_state( safe_eyes_core.on_count_down += on_count_down safe_eyes_core.on_stop_break += on_stop_break - assert context["state"] == model.State.WAITING + assert ctx["state"] == model.State.WAITING # continue after condvar sequential_threading_handle.next() # end of __scheduler_job - assert context["state"] == model.State.PRE_BREAK + assert ctx["state"] == model.State.PRE_BREAK on_pre_break.assert_called_once() assert isinstance(on_pre_break.call_args[0][0], model.Break) @@ -210,7 +211,7 @@ def run_next_break_from_waiting_state( # first sleep in __start_break sequential_threading_handle.next() - assert context["state"] == model.State.BREAK + assert ctx["state"] == model.State.BREAK on_start_break.assert_called_once() assert isinstance(on_start_break.call_args[0][0], model.Break) @@ -222,13 +223,13 @@ def run_next_break_from_waiting_state( assert start_break.call_args[0][0].name == break_name_translated start_break.reset_mock() - assert context["state"] == model.State.BREAK + assert ctx["state"] == model.State.BREAK # continue sleep in __start_break for i in range(break_duration - 1): sequential_threading_handle.next() - assert context["state"] == model.State.BREAK + assert ctx["state"] == model.State.BREAK sequential_threading_handle.next() # end of __start_break @@ -241,7 +242,7 @@ def run_next_break_from_waiting_state( assert on_stop_break.call_count == 1 on_stop_break.reset_mock() - assert context["state"] == model.State.BREAK + assert ctx["state"] == model.State.BREAK def assert_datetime(self, string: str): if not string.endswith("+00:00"): @@ -251,7 +252,9 @@ def assert_datetime(self, string: str): ) == datetime.datetime.fromisoformat(string) def test_start_empty(self, sequential_threading: SequentialThreadingFixture): - context: dict[str, typing.Any] = {} + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) config = model.Config( user_config={ "short_breaks": [], @@ -266,7 +269,7 @@ def test_start_empty(self, sequential_threading: SequentialThreadingFixture): system_config={}, ) on_update_next_break = mock.Mock() - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) safe_eyes_core.on_update_next_break += mock safe_eyes_core.initialize(config) @@ -277,9 +280,9 @@ def test_start_empty(self, sequential_threading: SequentialThreadingFixture): on_update_next_break.assert_not_called() def test_start(self, sequential_threading: SequentialThreadingFixture): - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) config = model.Config( user_config={ "short_breaks": [ @@ -304,7 +307,7 @@ def test_start(self, sequential_threading: SequentialThreadingFixture): system_config={}, ) on_update_next_break = mock.Mock() - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) safe_eyes_core.on_update_next_break += on_update_next_break safe_eyes_core.initialize(config) @@ -313,7 +316,7 @@ def test_start(self, sequential_threading: SequentialThreadingFixture): safe_eyes_core.start() - assert context["state"] == model.State.WAITING + assert ctx["state"] == model.State.WAITING on_update_next_break.assert_called_once() assert isinstance(on_update_next_break.call_args[0][0], model.Break) @@ -325,16 +328,16 @@ def test_start(self, sequential_threading: SequentialThreadingFixture): sequential_threading_handle.next() safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED + assert ctx["state"] == model.State.STOPPED def test_full_run_with_defaults( self, sequential_threading: SequentialThreadingFixture, time_machine: TimeMachineFixture, ): - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) short_break_duration = 15 # seconds short_break_interval = 15 # minutes pre_break_warning_time = 10 # seconds @@ -366,7 +369,7 @@ def test_full_run_with_defaults( self.assert_datetime("2024-08-25T13:00:00") - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) sequential_threading_handle = sequential_threading(safe_eyes_core) @@ -376,7 +379,7 @@ def test_full_run_with_defaults( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", initial=True, @@ -391,7 +394,7 @@ def test_full_run_with_defaults( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -402,7 +405,7 @@ def test_full_run_with_defaults( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 3", ) @@ -413,7 +416,7 @@ def test_full_run_with_defaults( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 4", ) @@ -424,7 +427,7 @@ def test_full_run_with_defaults( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, long_break_duration, "translated!: long break 1", ) @@ -439,7 +442,7 @@ def test_full_run_with_defaults( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", ) @@ -451,7 +454,7 @@ def test_full_run_with_defaults( safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED + assert ctx["state"] == model.State.STOPPED def test_long_duration_is_bigger_than_short_interval( self, @@ -459,9 +462,9 @@ def test_long_duration_is_bigger_than_short_interval( time_machine: TimeMachineFixture, ): """Example taken from https://github.com/slgobinath/SafeEyes/issues/640.""" - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) short_break_duration = 300 # seconds = 5min short_break_interval = 25 # minutes pre_break_warning_time = 10 # seconds @@ -493,7 +496,7 @@ def test_long_duration_is_bigger_than_short_interval( self.assert_datetime("2024-08-25T13:00:00") - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) sequential_threading_handle = sequential_threading(safe_eyes_core) @@ -503,7 +506,7 @@ def test_long_duration_is_bigger_than_short_interval( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", initial=True, @@ -518,7 +521,7 @@ def test_long_duration_is_bigger_than_short_interval( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -529,7 +532,7 @@ def test_long_duration_is_bigger_than_short_interval( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 3", ) @@ -540,7 +543,7 @@ def test_long_duration_is_bigger_than_short_interval( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, long_break_duration, "translated!: long break 1", ) @@ -555,7 +558,7 @@ def test_long_duration_is_bigger_than_short_interval( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 4", ) @@ -567,7 +570,7 @@ def test_long_duration_is_bigger_than_short_interval( safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED + assert ctx["state"] == model.State.STOPPED def test_idle( self, @@ -575,9 +578,9 @@ def test_idle( time_machine: TimeMachineFixture, ): """Test idling for short amount of time.""" - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) short_break_duration = 15 # seconds short_break_interval = 15 # minutes pre_break_warning_time = 10 # seconds @@ -609,7 +612,7 @@ def test_idle( self.assert_datetime("2024-08-25T13:00:00") - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) sequential_threading_handle = sequential_threading(safe_eyes_core) @@ -619,7 +622,7 @@ def test_idle( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", initial=True, @@ -636,7 +639,7 @@ def test_idle( safe_eyes_core.stop(is_resting=True) - assert context["state"] == model.State.RESTING + assert ctx["state"] == model.State.RESTING time_machine.shift(delta=idle_period) @@ -650,7 +653,7 @@ def test_idle( self.run_next_break_from_waiting_state( sequential_threading_handle, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -661,7 +664,7 @@ def test_idle( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 3", ) @@ -672,7 +675,7 @@ def test_idle( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 4", ) @@ -683,7 +686,7 @@ def test_idle( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, long_break_duration, "translated!: long break 1", ) @@ -698,7 +701,7 @@ def test_idle( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", ) @@ -710,7 +713,7 @@ def test_idle( safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED + assert ctx["state"] == model.State.STOPPED def test_idle_skip_long( self, @@ -718,9 +721,9 @@ def test_idle_skip_long( time_machine: TimeMachineFixture, ): """Test idling for longer than long break time.""" - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) short_break_duration = 15 # seconds short_break_interval = 15 # minutes pre_break_warning_time = 10 # seconds @@ -752,7 +755,7 @@ def test_idle_skip_long( self.assert_datetime("2024-08-25T13:00:00") - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) sequential_threading_handle = sequential_threading(safe_eyes_core) @@ -762,7 +765,7 @@ def test_idle_skip_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", initial=True, @@ -779,7 +782,7 @@ def test_idle_skip_long( safe_eyes_core.stop(is_resting=True) - assert context["state"] == model.State.RESTING + assert ctx["state"] == model.State.RESTING time_machine.shift(delta=idle_period) @@ -793,7 +796,7 @@ def test_idle_skip_long( self.run_next_break_from_waiting_state( sequential_threading_handle, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -804,7 +807,7 @@ def test_idle_skip_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 3", ) @@ -815,7 +818,7 @@ def test_idle_skip_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 4", ) @@ -826,7 +829,7 @@ def test_idle_skip_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", ) @@ -837,7 +840,7 @@ def test_idle_skip_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, long_break_duration, "translated!: long break 1", ) @@ -852,7 +855,7 @@ def test_idle_skip_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -864,7 +867,7 @@ def test_idle_skip_long( safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED + assert ctx["state"] == model.State.STOPPED def test_idle_skip_long_before_long( self, @@ -876,9 +879,9 @@ def test_idle_skip_long_before_long( This used to skip all the short breaks too. """ - context: dict[str, typing.Any] = { - "session": {}, - } + ctx = context.Context( + api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={} + ) short_break_duration = 15 # seconds short_break_interval = 15 # minutes pre_break_warning_time = 10 # seconds @@ -910,7 +913,7 @@ def test_idle_skip_long_before_long( self.assert_datetime("2024-08-25T13:00:00") - safe_eyes_core = core.SafeEyesCore(context) + safe_eyes_core = core.SafeEyesCore(ctx) sequential_threading_handle = sequential_threading(safe_eyes_core) @@ -920,7 +923,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", initial=True, @@ -935,7 +938,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -946,7 +949,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 3", ) @@ -957,7 +960,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 4", ) @@ -970,7 +973,7 @@ def test_idle_skip_long_before_long( safe_eyes_core.stop(is_resting=True) - assert context["state"] == model.State.RESTING + assert ctx["state"] == model.State.RESTING time_machine.shift(delta=idle_period) @@ -984,7 +987,7 @@ def test_idle_skip_long_before_long( self.run_next_break_from_waiting_state( sequential_threading_handle, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 1", ) @@ -995,7 +998,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 2", ) @@ -1006,7 +1009,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 3", ) @@ -1017,7 +1020,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, short_break_duration, "translated!: break 4", ) @@ -1031,7 +1034,7 @@ def test_idle_skip_long_before_long( sequential_threading_handle, time_machine, safe_eyes_core, - context, + ctx, long_break_duration, "translated!: long break 2", ) @@ -1040,4 +1043,4 @@ def test_idle_skip_long_before_long( safe_eyes_core.stop() - assert context["state"] == model.State.STOPPED + assert ctx["state"] == model.State.STOPPED From b7adbf09760ecead392f9848b2a466f850ed2e4c Mon Sep 17 00:00:00 2001 From: deltragon Date: Thu, 21 Aug 2025 12:32:29 +0200 Subject: [PATCH 124/134] typing: context: add to break screen --- safeeyes/ui/break_screen.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 12901387..49f5abcb 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -24,6 +24,7 @@ import gi from safeeyes import utility +from safeeyes.context import Context from safeeyes.model import Break, Config, TrayAction from safeeyes.translations import translate as _ import Xlib @@ -50,7 +51,7 @@ class BreakScreen: def __init__( self, application: Gtk.Application, - context, + context: Context, on_skipped: typing.Callable[[], None], on_postponed: typing.Callable[[], None], ): @@ -70,7 +71,7 @@ def __init__( self.show_skip_button = False self.show_postpone_button = False - if not self.context["is_wayland"]: + if not self.context.is_wayland: self.x11_display = Display() def initialize(self, config: Config) -> None: @@ -80,7 +81,7 @@ def initialize(self, config: Config) -> None: self.keycode_shortcut_postpone = config.get("shortcut_postpone", 65) self.keycode_shortcut_skip = config.get("shortcut_skip", 9) - if self.context["is_wayland"] and ( + if self.context.is_wayland and ( self.keycode_shortcut_postpone != 65 or self.keycode_shortcut_skip != 9 ): logging.warning( @@ -138,7 +139,7 @@ def close(self) -> None: windows. """ logging.info("Close the break screen(s)") - if not self.context["is_wayland"]: + if not self.context.is_wayland: self.__release_keyboard_x11() # Destroy other windows if exists @@ -153,7 +154,7 @@ def __show_break_screen( ) -> None: """Show an empty break screen on all screens.""" # Lock the keyboard - if not self.context["is_wayland"]: + if not self.context.is_wayland: utility.start_thread(self.__lock_keyboard_x11) display = Gdk.Display.get_default() @@ -188,7 +189,7 @@ def __show_break_screen( self.on_skip_clicked, ) - if self.context["is_wayland"]: + if self.context.is_wayland: # Note: in theory, this could also be used on X11 # however, that already has its own implementation below controller = Gtk.EventControllerKey() @@ -200,7 +201,7 @@ def __show_break_screen( self.windows.append(window) - if self.context["desktop"] == "kde": + if self.context.desktop == "kde": # Fix flickering screen in KDE by setting opacity to 1 window.set_opacity(0.9) @@ -212,10 +213,10 @@ def __show_break_screen( # shortcut window.set_focus(None) - if not self.context["is_wayland"]: + if not self.context.is_wayland: self.__window_set_keep_above_x11(window) - if self.context["is_wayland"]: + if self.context.is_wayland: # this may or may not be granted by the window system surface = window.get_surface() if surface is not None: From d545223eb3e94f7141df2f7356b291e2280474e1 Mon Sep 17 00:00:00 2001 From: deltragon Date: Wed, 4 Jun 2025 14:57:00 +0200 Subject: [PATCH 125/134] typing: add Context to smartpause plugin --- safeeyes/plugins/smartpause/plugin.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 55c308fd..682a08ac 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -21,6 +21,7 @@ import typing from safeeyes.model import State +from safeeyes.context import Context from .interface import IdleMonitorInterface from .gnome_dbus import IdleMonitorGnomeDBus @@ -31,7 +32,7 @@ Safe Eyes smart pause plugin """ -context = None +context: Context idle_time: float = 0 enable_safeeyes = None disable_safeeyes = None @@ -60,7 +61,7 @@ def _on_idle() -> None: global smart_pause_activated global idle_start_time - if context["state"] == State.WAITING: # type: ignore[index] + if context["state"] == State.WAITING: smart_pause_activated = True idle_start_time = datetime.datetime.now() - datetime.timedelta( seconds=idle_time @@ -73,15 +74,12 @@ def _on_resumed() -> None: global smart_pause_activated global idle_start_time - if ( - context["state"] == State.RESTING # type: ignore[index] - and idle_start_time is not None - ): + if context["state"] == State.RESTING and idle_start_time is not None: logging.info("Resume Safe Eyes due to user activity") smart_pause_activated = False idle_period = datetime.datetime.now() - idle_start_time idle_seconds = idle_period.total_seconds() - context["idle_period"] = idle_seconds # type: ignore[index] + context["idle_period"] = idle_seconds if idle_seconds < short_break_interval: # Credit back the idle time if next_break_time is not None: @@ -281,7 +279,7 @@ def disable() -> None: global idle_monitor # Remove the idle_period - context.pop("idle_period", None) # type: ignore[union-attr] + context.pop("idle_period", None) if idle_monitor is not None: idle_monitor.stop() From 6fb6d6123e02508b491864cb3b8b60b6273d4fb7 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 12:21:58 +0200 Subject: [PATCH 126/134] trayicon: remove unused context arg --- safeeyes/plugins/trayicon/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index af9dfa0f..6fc885bb 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -192,7 +192,7 @@ class DBusMenuService(DBusService): # TODO: replace dict here with more exact typing for item idToItems: dict[str, dict] = {} - def __init__(self, session_bus, context, items): + def __init__(self, session_bus, items): super().__init__( interface_info=MENU_NODE_INFO, object_path=self.DBUS_SERVICE_PATH, @@ -374,7 +374,7 @@ class StatusNotifierItemService(DBusService): ItemIsMenu = True Menu = None - def __init__(self, session_bus, context, menu_items): + def __init__(self, session_bus, menu_items): super().__init__( interface_info=SNI_NODE_INFO, object_path=self.DBUS_SERVICE_PATH, @@ -383,7 +383,7 @@ def __init__(self, session_bus, context, menu_items): self.bus = session_bus - self._menu = DBusMenuService(session_bus, context, menu_items) + self._menu = DBusMenuService(session_bus, menu_items) self.Menu = self._menu.DBUS_SERVICE_PATH def register(self): @@ -453,7 +453,7 @@ def __init__(self, context, plugin_config): session_bus = Gio.bus_get_sync(Gio.BusType.SESSION) self.sni_service = StatusNotifierItemService( - session_bus, context, menu_items=self.get_items() + session_bus, menu_items=self.get_items() ) self.sni_service.register() From e9521b230701e9cd309d7439a5fc25621d5be52a Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 12:20:35 +0200 Subject: [PATCH 127/134] settings: xdg activation receive an xdg activation token from the trayicon and use it to activate the settings window. this is needed to bring the settings window to the front. --- safeeyes/context.py | 4 ++-- safeeyes/plugins/trayicon/plugin.py | 13 +++++++++++-- safeeyes/safeeyes.py | 19 ++++++++++++------- safeeyes/ui/settings_dialog.py | 2 +- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/safeeyes/context.py b/safeeyes/context.py index 4da92bfe..4af1ea20 100644 --- a/safeeyes/context.py +++ b/safeeyes/context.py @@ -40,8 +40,8 @@ def __getitem__(self, key: str) -> typing.Callable: """This is soft-deprecated - it is preferred to access the property.""" return getattr(self, key) - def show_settings(self) -> None: - utility.execute_main_thread(self._application.show_settings) + def show_settings(self, activation_token: typing.Optional[str] = None) -> None: + utility.execute_main_thread(self._application.show_settings, activation_token) def show_about(self) -> None: utility.execute_main_thread(self._application.show_about) diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index 6fc885bb..24d5a9c0 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -53,6 +53,10 @@ + + + + @@ -374,6 +378,8 @@ class StatusNotifierItemService(DBusService): ItemIsMenu = True Menu = None + last_activation_token: typing.Optional[str] = None + def __init__(self, session_bus, menu_items): super().__init__( interface_info=SNI_NODE_INFO, @@ -424,6 +430,9 @@ def set_xayatanalabel(self, label): self.emit_signal("XAyatanaNewLabel", (label, "")) + def ProvideXdgActivationToken(self, token: str) -> None: + self.last_activation_token = token + class TrayIcon: """Create and show the tray icon along with the tray menu.""" @@ -662,12 +671,12 @@ def quit_safe_eyes(self): self.idle_condition.release() self.quit() - def show_settings(self): + def show_settings(self) -> None: """Handle Settings menu action. This action shows the Settings dialog. """ - self.on_show_settings() + self.on_show_settings(self.sni_service.last_activation_token) def show_about(self): """Handle About menu action. diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index aca49a76..77cf0950 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -23,6 +23,7 @@ import atexit import logging from importlib import metadata +import typing import gi from safeeyes import context, utility @@ -52,6 +53,8 @@ class SafeEyes(Gtk.Application): plugins_manager: PluginManager system_locale: str + _settings_dialog: typing.Optional[SettingsDialog] = None + def __init__(self, system_locale: str, config) -> None: super().__init__( application_id="io.github.slgobinath.SafeEyes", @@ -60,7 +63,6 @@ def __init__(self, system_locale: str, config) -> None: self.active = False self.config = config - self.settings_dialog_active = False self._status = "" self.system_locale = system_locale @@ -314,17 +316,20 @@ def _retry_errored_plugins(self): GLib.timeout_add_seconds(timeout, self._retry_errored_plugins) - def show_settings(self): + def show_settings(self, activation_token: typing.Optional[str] = None) -> None: """Listen to tray icon Settings action and send the signal to Settings dialog. """ - if not self.settings_dialog_active: + if self._settings_dialog is None: logging.info("Show Settings dialog") - self.settings_dialog_active = True - settings_dialog = SettingsDialog( + self._settings_dialog = SettingsDialog( self, self.config.clone(), self.save_settings ) - settings_dialog.show() + + if activation_token is not None: + self._settings_dialog.set_startup_id(activation_token) + + self._settings_dialog.show() def show_required_plugin_dialog(self, error: RequiredPluginException) -> None: self.required_plugin_dialog_active = True @@ -435,7 +440,7 @@ def save_settings(self, config): """Listen to Settings dialog Save action and write to the config file. """ - self.settings_dialog_active = False + self._settings_dialog = None if self.config == config: # Config is not modified diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py index 0419c400..4db846ca 100644 --- a/safeeyes/ui/settings_dialog.py +++ b/safeeyes/ui/settings_dialog.py @@ -266,7 +266,7 @@ def __show_break_properties_dialog( def show(self) -> None: """Show the SettingsDialog.""" - super().show() + self.present() @Gtk.Template.Callback() def on_switch_postpone_activate(self, switch, state) -> None: From 051670ebbe4c50330d72b3fd681337dda1616424 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 12:45:13 +0200 Subject: [PATCH 128/134] about dialog: xdg activation --- safeeyes/context.py | 4 ++-- safeeyes/plugins/trayicon/plugin.py | 4 ++-- safeeyes/safeeyes.py | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/safeeyes/context.py b/safeeyes/context.py index 4af1ea20..b76883aa 100644 --- a/safeeyes/context.py +++ b/safeeyes/context.py @@ -43,8 +43,8 @@ def __getitem__(self, key: str) -> typing.Callable: def show_settings(self, activation_token: typing.Optional[str] = None) -> None: utility.execute_main_thread(self._application.show_settings, activation_token) - def show_about(self) -> None: - utility.execute_main_thread(self._application.show_about) + def show_about(self, activation_token: typing.Optional[str] = None) -> None: + utility.execute_main_thread(self._application.show_about, activation_token) def enable_safeeyes(self, next_break_time=-1) -> None: utility.execute_main_thread(self._application.enable_safeeyes, next_break_time) diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index 24d5a9c0..8905f82b 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -678,12 +678,12 @@ def show_settings(self) -> None: """ self.on_show_settings(self.sni_service.last_activation_token) - def show_about(self): + def show_about(self) -> None: """Handle About menu action. This action shows the About dialog. """ - self.on_show_about() + self.on_show_about(self.sni_service.last_activation_token) def next_break_time(self, dateTime): """Update the next break time to be displayed in the menu and diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index 77cf0950..ff917cd7 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -358,12 +358,16 @@ def disable_plugin(self, plugin_id): self.restart(config, set_active=True) - def show_about(self): + def show_about(self, activation_token: typing.Optional[str] = None): """Listen to tray icon About action and send the signal to About dialog. """ logging.info("Show About dialog") about_dialog = AboutDialog(self, SAFE_EYES_VERSION) + + if activation_token is not None: + about_dialog.set_startup_id(activation_token) + about_dialog.show() def quit(self): From 14c56017335708d9ef04e3e746a9b118b019d7b7 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 12:58:48 +0200 Subject: [PATCH 129/134] add types, remove unused global --- safeeyes/plugins/trayicon/plugin.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index 8905f82b..3d978479 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -24,6 +24,7 @@ from gi.repository import Gio, GLib import logging from safeeyes import utility +from safeeyes.context import Context from safeeyes.translations import translate as _ import threading import typing @@ -32,7 +33,6 @@ Safe Eyes tray icon plugin """ -context = None tray_icon = None safeeyes_config = None @@ -440,16 +440,16 @@ class TrayIcon: _animation_timeout_id: typing.Optional[int] = None _animation_icon_enabled: bool = False - def __init__(self, context, plugin_config): + def __init__(self, context: Context, plugin_config): self.context = context - self.on_show_settings = context["api"]["show_settings"] - self.on_show_about = context["api"]["show_about"] - self.quit = context["api"]["quit"] - self.enable_safeeyes = context["api"]["enable_safeeyes"] - self.disable_safeeyes = context["api"]["disable_safeeyes"] - self.take_break = context["api"]["take_break"] - self.has_breaks = context["api"]["has_breaks"] - self.get_break_time = context["api"]["get_break_time"] + self.on_show_settings = context.api.show_settings + self.on_show_about = context.api.show_about + self.quit = context.api.quit + self.enable_safeeyes = context.api.enable_safeeyes + self.disable_safeeyes = context.api.disable_safeeyes + self.take_break = context.api.take_break + self.has_breaks = context.api.has_breaks + self.get_break_time = context.api.get_break_time self.plugin_config = plugin_config self.date_time = None self.active = True @@ -830,14 +830,12 @@ def stop_animation(self) -> None: def init(ctx, safeeyes_cfg, plugin_config): """Initialize the tray icon.""" - global context global tray_icon global safeeyes_config logging.debug("Initialize Tray Icon plugin") - context = ctx safeeyes_config = safeeyes_cfg if not tray_icon: - tray_icon = TrayIcon(context, plugin_config) + tray_icon = TrayIcon(ctx, plugin_config) else: tray_icon.initialize(plugin_config) From d702cb6452c81228f496392cb8492c8f9ea1b0ab Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 13:24:41 +0200 Subject: [PATCH 130/134] break screen: x11 keyboard locking: move threading concentrate all the threading logic within the one method. everything else runs on the main thread --- safeeyes/ui/break_screen.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 49f5abcb..4e452a88 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -143,7 +143,7 @@ def close(self) -> None: self.__release_keyboard_x11() # Destroy other windows if exists - GLib.idle_add(lambda: self.__destroy_all_screens()) + self.__destroy_all_screens() def __show_break_screen( self, @@ -298,18 +298,21 @@ def __lock_keyboard_x11(self) -> None: event.detail == self.keycode_shortcut_skip and self.show_skip_button ): - self.skip_break() + utility.execute_main_thread(lambda: self.skip_break()) break elif ( event.detail == self.keycode_shortcut_postpone and self.show_postpone_button ): - self.postpone_break() + utility.execute_main_thread(lambda: self.postpone_break()) break else: # Reduce the CPU usage by sleeping for a second time.sleep(1) + self.x11_display.ungrab_keyboard(X.CurrentTime) + self.x11_display.flush() + def on_key_pressed_wayland( self, event_controller_key, keyval, keycode, state ) -> bool: @@ -325,13 +328,8 @@ def on_key_pressed_wayland( def __release_keyboard_x11(self) -> None: """Release the locked keyboard.""" - if self.x11_display is None: - return - logging.info("Unlock the keyboard") self.lock_keyboard = False - self.x11_display.ungrab_keyboard(X.CurrentTime) - self.x11_display.flush() def __destroy_all_screens(self) -> None: """Close all the break screens.""" From 8ee16670c5b34eaea9e1badc8f5dc51ccc4dc0a8 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 13:36:57 +0200 Subject: [PATCH 131/134] move another main thread call to the context/api --- safeeyes/context.py | 6 +++--- safeeyes/safeeyes.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/safeeyes/context.py b/safeeyes/context.py index b76883aa..e92d5128 100644 --- a/safeeyes/context.py +++ b/safeeyes/context.py @@ -21,7 +21,7 @@ import typing from safeeyes import utility -from safeeyes.model import State +from safeeyes.model import BreakType, State if typing.TYPE_CHECKING: from safeeyes.safeeyes import SafeEyes @@ -60,8 +60,8 @@ def status(self) -> str: def quit(self) -> None: utility.execute_main_thread(self._application.quit) - def take_break(self, break_type=None) -> None: - self._application.take_break(break_type) + def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: + utility.execute_main_thread(self._application.take_break, break_type) def has_breaks(self, break_type=None) -> bool: return self._application.safe_eyes_core.has_breaks(break_type) diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py index ff917cd7..c3cca8da 100644 --- a/safeeyes/safeeyes.py +++ b/safeeyes/safeeyes.py @@ -30,7 +30,7 @@ from safeeyes.ui.about_dialog import AboutDialog from safeeyes.ui.break_screen import BreakScreen from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog -from safeeyes.model import State, RequiredPluginException +from safeeyes.model import BreakType, State, RequiredPluginException from safeeyes.translations import translate as _ from safeeyes.plugin_manager import PluginManager from safeeyes.core import SafeEyesCore @@ -536,9 +536,9 @@ def stop_break(self): self.plugins_manager.stop_break() return True - def take_break(self, break_type=None): + def take_break(self, break_type: typing.Optional[BreakType] = None) -> None: """Take a break now.""" - utility.execute_main_thread(self.safe_eyes_core.take_break, break_type) + self.safe_eyes_core.take_break(break_type) def status(self): """Return the status of Safe Eyes.""" From 1a080100e6090b14388612a548dd1ea077d4a946 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 13:42:33 +0200 Subject: [PATCH 132/134] break screen: remove now unneeded idle calls --- safeeyes/ui/break_screen.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py index 4e452a88..a7c49009 100644 --- a/safeeyes/ui/break_screen.py +++ b/safeeyes/ui/break_screen.py @@ -33,7 +33,6 @@ gi.require_version("Gtk", "4.0") from gi.repository import Gdk -from gi.repository import GLib from gi.repository import Gtk from gi.repository import GdkX11 @@ -121,7 +120,7 @@ def show_count_down(self, countdown: int, seconds: int) -> None: self.enable_shortcut = self.shortcut_disable_time <= seconds mins, secs = divmod(countdown, 60) timeformat = "{:02d}:{:02d}".format(mins, secs) - GLib.idle_add(lambda: self.__update_count_down(timeformat)) + self.__update_count_down(timeformat) def show_message( self, break_obj: Break, widget: str, tray_actions: list[TrayAction] = [] @@ -130,9 +129,7 @@ def show_message( message = break_obj.name image_path = break_obj.image self.enable_shortcut = self.shortcut_disable_time <= 0 - GLib.idle_add( - lambda: self.__show_break_screen(message, image_path, widget, tray_actions) - ) + self.__show_break_screen(message, image_path, widget, tray_actions) def close(self) -> None: """Hide the break screen from active window and destroy all other From 3a626cb386a631ad5e9cb7d021012aa3a3c1cad7 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 13:42:33 +0200 Subject: [PATCH 133/134] trayicon: switch from thread to timer --- safeeyes/plugins/trayicon/plugin.py | 50 +++++++++++++---------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py index 3d978479..d0251e9b 100644 --- a/safeeyes/plugins/trayicon/plugin.py +++ b/safeeyes/plugins/trayicon/plugin.py @@ -26,7 +26,6 @@ from safeeyes import utility from safeeyes.context import Context from safeeyes.translations import translate as _ -import threading import typing """ @@ -440,6 +439,8 @@ class TrayIcon: _animation_timeout_id: typing.Optional[int] = None _animation_icon_enabled: bool = False + _resume_timeout_id: typing.Optional[int] = None + def __init__(self, context: Context, plugin_config): self.context = context self.on_show_settings = context.api.show_settings @@ -454,8 +455,6 @@ def __init__(self, context: Context, plugin_config): self.date_time = None self.active = True self.wakeup_time = None - self.idle_condition = threading.Condition() - self.lock = threading.Lock() self.allow_disabling = plugin_config["allow_disabling"] self.menu_locked = False @@ -663,12 +662,9 @@ def quit_safe_eyes(self): This action terminates the application. """ - with self.lock: - self.active = True - # Notify all schedulers - self.idle_condition.acquire() - self.idle_condition.notify_all() - self.idle_condition.release() + self.active = True + self.__clear_resume_timer() + self.quit() def show_settings(self) -> None: @@ -720,13 +716,9 @@ def on_enable_clicked(self): This action enables the application if it is currently disabled. """ if not self.active: - with self.lock: - self.enable_ui() - self.enable_safeeyes() - # Notify all schedulers - self.idle_condition.acquire() - self.idle_condition.notify_all() - self.idle_condition.release() + self.enable_ui() + self.enable_safeeyes() + self.__clear_resume_timer() def on_disable_clicked(self, time_to_wait): """Handle the menu actions of all the sub menus of 'Disable Safe Eyes'. @@ -746,7 +738,9 @@ def on_disable_clicked(self, time_to_wait): ) info = _("Disabled until %s") % utility.format_time(self.wakeup_time) self.disable_safeeyes(info) - utility.start_thread(self.__schedule_resume, time_minutes=time_to_wait) + self._resume_timeout_id = GLib.timeout_add_seconds( + time_to_wait * 60, self.__resume + ) self.update_menu() def lock_menu(self): @@ -783,17 +777,19 @@ def enable_ui(self): self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled") self.update_menu() - def __schedule_resume(self, time_minutes): - """Schedule a local timer to enable Safe Eyes after the given - timeout. - """ - self.idle_condition.acquire() - self.idle_condition.wait(time_minutes * 60) # Convert to seconds - self.idle_condition.release() + def __resume(self): + """Reenable Safe Eyes after the given timeout.""" + if not self.active: + self.on_enable_clicked() + + self._resume_timeout_id = None + + return GLib.SOURCE_REMOVE - with self.lock: - if not self.active: - utility.execute_main_thread(self.on_enable_clicked) + def __clear_resume_timer(self): + if self._resume_timeout_id is not None: + GLib.source_remove(self._resume_timeout_id) + self._resume_timeout_id = None def start_animation(self) -> None: if self._animation_timeout_id is not None: From c397e7cadac4ae1e74623aad63438ac3fc7e9281 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 22 Aug 2025 13:13:01 +0200 Subject: [PATCH 134/134] update version to 3.0.0b4 --- debian/changelog | 21 +++++++++++++++++++ pyproject.toml | 4 ++-- safeeyes/glade/about_dialog.glade | 2 +- ...io.github.slgobinath.SafeEyes.metainfo.xml | 1 + 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index d7e96cc8..ebfa6309 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,24 @@ +safeeyes (3.0.0b3) noble; urgency=medium + + * Wayland support: break screen shortcuts, window activation, donotdisturb + detection + + * Feature: Add option to postpone breaks by seconds rather than minutes + + * Feature: screensaver: add tray action to lock screen now + + * smartpause: Performance/Battery life improvements + + * replace RPC server with native GTK commandline integration + + * Internal refactoring to improve thread safety + + * Internal: automated tests using pytest + + * Internal: typechecking improvement + + -- Mel Dafert Fri, 22 Aug 2025 11:30:00 +0000 + safeeyes (3.0.0b3) noble; urgency=medium * Re-release due to broken github action diff --git a/pyproject.toml b/pyproject.toml index 822c0ca2..e21ab0e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "safeeyes" -version = "3.0.0b3" +version = "3.0.0b4" description = "Protect your eyes from eye strain using this continuous breaks reminder." keywords = ["linux utility health eye-strain safe-eyes"] readme = "README.md" @@ -31,7 +31,7 @@ requires-python = ">=3.10" [project.urls] Homepage = "https://github.com/slgobinath/SafeEyes" -Downloads = "https://github.com/slgobinath/SafeEyes/archive/v3.0.0b3.tar.gz" +Downloads = "https://github.com/slgobinath/SafeEyes/archive/v3.0.0b4.tar.gz" [project.scripts] safeeyes = "safeeyes.__main__:main" diff --git a/safeeyes/glade/about_dialog.glade b/safeeyes/glade/about_dialog.glade index cf235a85..0dbb0d40 100644 --- a/safeeyes/glade/about_dialog.glade +++ b/safeeyes/glade/about_dialog.glade @@ -64,7 +64,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.center 10 10 - Safe Eyes 3.0.0b3 + Safe Eyes 3.0.0b4 center 1 1 diff --git a/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml b/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml index 45d2dab2..ad759fc7 100644 --- a/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml +++ b/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml @@ -53,6 +53,7 @@ https://slgobinath.github.io/SafeEyes/ +