├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── plugin-request.md ├── pull_request_template.md └── workflows │ ├── build-deb.yml │ ├── lint.yml │ ├── mypy.yml │ ├── release.yml │ └── translations.yml ├── .gitignore ├── .reuse └── dep5 ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LICENSES └── GPL-3.0-or-later.txt ├── MANIFEST.in ├── README.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── rules └── safeeyes.install ├── pyproject.toml ├── ruff.toml ├── safeeyes ├── __init__.py ├── __main__.py ├── config │ ├── locale │ │ ├── ar │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── bg │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── bn │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── ca │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── cs │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── da │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── de │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── en_US │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── eo │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── es │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── et │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── eu │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── fa │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── fr │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── he │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── hi │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── hu │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── id │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── it │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── kn │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── ko │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── lt │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── lv │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── mk │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── mr │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── nb │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── nl │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── pl │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── pt │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── pt_BR │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── ru │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── safeeyes.pot │ │ ├── sk │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── sr │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── sv │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── ta │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── tr │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── ug │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── uk │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── uz_Latn │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── vi │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ ├── zh_CN │ │ │ └── LC_MESSAGES │ │ │ │ └── safeeyes.po │ │ └── zh_TW │ │ │ └── LC_MESSAGES │ │ │ └── safeeyes.po │ ├── safeeyes.json │ └── style │ │ └── safeeyes_style.css ├── core.py ├── glade │ ├── about_dialog.glade │ ├── break_screen.glade │ ├── item_bool.glade │ ├── item_break.glade │ ├── item_int.glade │ ├── item_plugin.glade │ ├── item_text.glade │ ├── new_break.glade │ ├── required_plugin_dialog.glade │ ├── settings_break.glade │ ├── settings_dialog.glade │ └── settings_plugin.glade ├── model.py ├── platform │ ├── icons │ │ └── hicolor │ │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ └── io.github.slgobinath.SafeEyes.png │ │ │ ├── 16x16 │ │ │ ├── apps │ │ │ │ └── io.github.slgobinath.SafeEyes.png │ │ │ └── status │ │ │ │ ├── io.github.slgobinath.SafeEyes-disabled.png │ │ │ │ ├── io.github.slgobinath.SafeEyes-enabled.png │ │ │ │ └── io.github.slgobinath.SafeEyes-timer.png │ │ │ ├── 24x24 │ │ │ ├── apps │ │ │ │ └── io.github.slgobinath.SafeEyes.png │ │ │ └── status │ │ │ │ ├── io.github.slgobinath.SafeEyes-disabled.png │ │ │ │ ├── io.github.slgobinath.SafeEyes-enabled.png │ │ │ │ └── io.github.slgobinath.SafeEyes-timer.png │ │ │ ├── 32x32 │ │ │ ├── apps │ │ │ │ └── io.github.slgobinath.SafeEyes.png │ │ │ └── status │ │ │ │ ├── io.github.slgobinath.SafeEyes-disabled.png │ │ │ │ └── io.github.slgobinath.SafeEyes-enabled.png │ │ │ ├── 48x48 │ │ │ ├── apps │ │ │ │ └── io.github.slgobinath.SafeEyes.png │ │ │ └── status │ │ │ │ ├── io.github.slgobinath.SafeEyes-disabled.png │ │ │ │ └── io.github.slgobinath.SafeEyes-enabled.png │ │ │ └── 64x64 │ │ │ └── apps │ │ │ └── io.github.slgobinath.SafeEyes.png │ ├── io.github.slgobinath.SafeEyes.desktop │ └── io.github.slgobinath.SafeEyes.metainfo.xml ├── plugin_manager.py ├── plugins │ ├── audiblealert │ │ ├── config.json │ │ ├── icon.png │ │ └── plugin.py │ ├── donotdisturb │ │ ├── config.json │ │ ├── dependency_checker.py │ │ ├── icon.png │ │ └── plugin.py │ ├── healthstats │ │ ├── config.json │ │ ├── dependency_checker.py │ │ ├── icon.png │ │ └── plugin.py │ ├── limitconsecutiveskipping │ │ ├── config.json │ │ ├── icon.png │ │ └── plugin.py │ ├── mediacontrol │ │ ├── config.json │ │ ├── icon.png │ │ ├── plugin.py │ │ └── resource │ │ │ └── pause.png │ ├── notification │ │ ├── config.json │ │ ├── icon.png │ │ └── plugin.py │ ├── screensaver │ │ ├── config.json │ │ ├── icon.png │ │ ├── plugin.py │ │ └── resource │ │ │ └── lock.png │ ├── smartpause │ │ ├── config.json │ │ ├── dependency_checker.py │ │ ├── ext_idle_notify.py │ │ ├── icon.png │ │ └── plugin.py │ └── trayicon │ │ ├── config.json │ │ ├── dependency_checker.py │ │ ├── icon.png │ │ └── plugin.py ├── resource │ ├── ic_plugin.png │ ├── ic_warning.png │ ├── on_pre_break.wav │ └── on_stop_break.wav ├── rpc.py ├── safeeyes.py ├── ui │ ├── __init__.py │ ├── about_dialog.py │ ├── break_screen.py │ ├── required_plugin_dialog.py │ └── settings_dialog.py └── utility.py ├── setup.py ├── update-po.sh └── validate_po.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.{py}] 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits. 2 | 3 | # format using ruff 4 | 5bac6fe9a3b9b95abbbb364a48b5aad7c915e4ed 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Ubuntu 20.04, Manjaro] 25 | - Desktop Env [e.g. Gnome, KDE] 26 | - Version [e.g. 2.0.3] 27 | 28 | **Flatpak issues**: If you experience any issue with flatpak, first please ensure that the bug is present in the [native package](https://github.com/slgobinath/SafeEyes?tab=readme-ov-file#installation-guide), and it is not a flatpak-only bug. Flatpak-only bugs should be reported at https://github.com/flathub/io.github.slgobinath.SafeEyes. (**Please erase this paragraph before creating the bug report**) 29 | 30 | **Debug Log** 31 | Run the Safe Eyes using `safeeyes --debug` command attach the ~/safeeyes.log` file. 32 | 33 | **Configuration** 34 | Attach the configuration file, usually found in `~/.config/safeeyes/safeeyes.json`. 35 | 36 | **Screenshots** 37 | If applicable, add screenshots to help explain your problem. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/plugin-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Plugin request 3 | about: Suggest an idea for a plugin 4 | title: '' 5 | labels: plugin 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your plugin request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the plugin you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Describe your changes here, and link to related issues if available. 4 | 5 | Reminder to run `python validate_po.py --extract` if you have added new translatable strings. 6 | 7 | Reminder to run `ruff check`, `ruff format`, and `mypy safeeyes` to ensure coding standards are followed and types are correct. 8 | -------------------------------------------------------------------------------- /.github/workflows/build-deb.yml: -------------------------------------------------------------------------------- 1 | name: Build Debian Package 2 | 3 | on: 4 | push: 5 | branches: [ release ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-deb: 10 | name: Build Debian Package 11 | runs-on: ubuntu-24.04 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Install dependencies 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y build-essential python3-stdeb fakeroot dpkg-dev debhelper dh-python python3 python3-packaging python3-setuptools 21 | 22 | - name: Build .deb package 23 | run: | 24 | DPKG_DEB_COMPRESSOR_TYPE=xz dpkg-buildpackage -us -uc -nc 25 | mv ../*.deb . 26 | 27 | - name: Upload .deb to GitHub Release 28 | if: github.event_name == 'release' 29 | uses: softprops/action-gh-release@v2 30 | with: 31 | files: "*.deb" 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Save .deb as workflow artifact 36 | if: github.event_name != 'release' 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: deb-package 40 | path: "*.deb" -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint-ruff: 7 | runs-on: ubuntu-latest 8 | name: ruff 9 | steps: 10 | - name: Check out source repository 11 | uses: actions/checkout@v3 12 | - uses: astral-sh/ruff-action@v3 13 | # this runs `ruff check` 14 | - run: ruff format --check 15 | # this runs `ruff format --check`, additionally 16 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yml: -------------------------------------------------------------------------------- 1 | # Workflow to run mypy 2 | # Adapted from the CPython mypy action 3 | name: mypy 4 | 5 | on: [push, pull_request] 6 | 7 | permissions: 8 | contents: read 9 | 10 | env: 11 | UV_SYSTEM_PYTHON: 1 12 | PIP_DISABLE_PIP_VERSION_CHECK: 1 13 | FORCE_COLOR: 1 14 | TERM: xterm-256color # needed for FORCE_COLOR to work on mypy on Ubuntu, see https://github.com/python/mypy/issues/13817 15 | 16 | jobs: 17 | mypy: 18 | name: mypy 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | - uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5.4.0 25 | with: 26 | enable-cache: true 27 | cache-dependency-glob: '' 28 | cache-suffix: '3.13' 29 | - name: Setup Python 30 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 31 | with: 32 | python-version: '3.13' 33 | - run: | 34 | sudo apt-get update 35 | sudo apt-get install -y libwayland-dev libcairo2-dev libgirepository-2.0-dev 36 | - run: uv pip install -r pyproject.toml 37 | - run: uv pip install --group types 38 | - run: mypy safeeyes 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [ release ] 5 | 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | token: ${{ secrets.GH_API_SECRET }} 15 | 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: "3.10" 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build wheel 25 | sudo apt-get update 26 | sudo apt-get install -y gettext 27 | 28 | - name: Get Current Version 29 | uses: SebRollen/toml-action@v1.2.0 30 | with: 31 | file: "pyproject.toml" 32 | field: project.version 33 | id: get_current_version 34 | 35 | - name: Build Python Package 36 | run: rm -rf build *.egg-info/ && python3 -m build 37 | 38 | - name: Create Tag 39 | uses: mathieudutour/github-tag-action@v6.1 40 | with: 41 | custom_tag: "${{steps.get_current_version.outputs.value}}" 42 | github_token: ${{ secrets.GH_API_SECRET }} 43 | 44 | - name: Build Changelog 45 | id: build_changelog 46 | uses: mikepenz/release-changelog-builder-action@v3.4.0 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GH_API_SECRET }} 49 | 50 | - name: Create Release 51 | uses: softprops/action-gh-release@v1 52 | with: 53 | tag_name: 'v${{steps.get_current_version.outputs.value}}' 54 | body: ${{steps.build_changelog.outputs.changelog}} 55 | token: ${{ secrets.GH_API_SECRET }} 56 | 57 | - name: Publish to PyPi 58 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # 1.12.4 59 | with: 60 | password: ${{ secrets.PYPI_API_TOKEN }} 61 | 62 | - name: Sync Release with Master 63 | run: | 64 | git fetch origin 65 | git checkout release 66 | git pull origin release 67 | git checkout master 68 | git pull origin master 69 | git merge release --ff-only 70 | git push origin master 71 | -------------------------------------------------------------------------------- /.github/workflows/translations.yml: -------------------------------------------------------------------------------- 1 | name: Check translations 2 | 3 | # Drop permissions to minimum for security 4 | permissions: 5 | contents: read 6 | 7 | on: 8 | pull_request: 9 | push: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | check_translations: 14 | name: Check translations 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python 3.11 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.11 23 | 24 | - name: Install runtime dependencies 25 | run: | 26 | python3 -m pip install --upgrade pip setuptools wheel 27 | python3 -m pip install polib 28 | sudo apt-get update 29 | sudo apt-get install -y gettext 30 | 31 | - name: Check translations 32 | run: | 33 | python3 validate_po.py 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Glade temporary files 10 | *.glade~ 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | # *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # Visual Studio Code settings 100 | .vscode/ 101 | 102 | safeeyes/config/locale/*/LC_MESSAGES/safeeyes.po~ 103 | 104 | node_modules/ 105 | 106 | # PyCharm 107 | .idea 108 | 109 | # Debian Build 110 | .pybuild/ 111 | debian/safeeyes 112 | debian/.debhelper 113 | debian/files 114 | debian/safeeyes.substvars 115 | debian/safeeyes.prerm.debhelper 116 | debian/safeeyes.postinst.debhelper -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: SafeEyes 3 | Upstream-Contact: Gobinath Loganathan 4 | Source: https://slgobinath.github.io/SafeEyes/ 5 | 6 | Files: * 7 | Copyright: 2016-2024, Gobinath Loganathan 8 | 2019, Jorge Maldonado Ventura 9 | 2016-2024, Multiple Authors 10 | License: GPL-3.0+ 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSES/GPL-3.0-or-later.txt 2 | include README.md 3 | 4 | graft safeeyes 5 | 6 | global-exclude *.py[cod] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Safe Eyes 4 | 5 | [![Release](https://img.shields.io/github/v/release/slgobinath/SafeEyes)](https://github.com/slgobinath/SafeEyes/releases) 6 | [![PyPI version](https://badge.fury.io/py/safeeyes.svg)](https://badge.fury.io/py/safeeyes) 7 | [![Debian](https://badges.debian.net/badges/debian/unstable/safeeyes/version.svg)](https://packages.debian.org/unstable/safeeyes) 8 | [![AUR](https://img.shields.io/aur/version/safeeyes)](https://aur.archlinux.org/packages/safeeyes) 9 | [![Flathub](https://img.shields.io/flathub/v/io.github.slgobinath.SafeEyes)](https://flathub.org/apps/details/io.github.slgobinath.SafeEyes) 10 | [![Translation status](https://hosted.weblate.org/widgets/safe-eyes/-/translations/svg-badge.svg)](https://hosted.weblate.org/engage/safe-eyes/?utm_source=widget) 11 | [![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech) 12 | 13 | Protect your eyes from eye strain using this simple and beautiful, yet extensible break reminder. 14 | 15 | Visit the official site: https://slgobinath.github.io/SafeEyes/ for more details. 16 | 17 | ## Safe Eyes command-line arguments 18 | 19 | ```text 20 | usage: safeeyes [-h] [-a | -d | -e | -q | -s | -t] [--debug] [--version] 21 | 22 | Safe Eyes protects your eyes from eye strain (asthenopia) by reminding you to 23 | take breaks while you're working long hours at the computer. 24 | 25 | optional arguments: 26 | -h, --help show this help message and exit 27 | -a, --about show the about dialog 28 | -d, --disable disable the currently running safeeyes instance 29 | -e, --enable enable the currently running safeeyes instance 30 | -q, --quit quit the running safeeyes instance and exit 31 | -s, --settings show the settings dialog 32 | -t, --take-break take a break now 33 | --debug start safeeyes in debug mode 34 | --status print the status of running safeeyes instance and exit 35 | --version show program's version number and exit 36 | ``` 37 | 38 | ## Installation guide 39 | 40 | Safe Eyes is available on the official repositories of many popular the distributions. 41 | 42 | 43 | Packaging status 44 | 45 | 46 | It is also available in Ubuntu PPA, Arch AUR and Python PyPI. You can choose any installation source and install on any Linux system with Python 3. 47 | 48 | 49 | ### Ubuntu, Linux Mint and other Ubuntu Derivatives 50 | 51 | The [Official PPA for Safe Eyes](https://launchpad.net/~safeeyes-team/+archive/ubuntu/safeeyes) hosts the latest version of safeeyes **for Ubuntu 22.04 and above**. 52 | ```bash 53 | sudo add-apt-repository ppa:safeeyes-team/safeeyes 54 | sudo apt update 55 | sudo apt install safeeyes 56 | ``` 57 | 58 | On older versions of Ubuntu, an older version of Safe Eyes is available on the official repositories. 59 | ```bash 60 | sudo apt install safeeyes 61 | ``` 62 | 63 | ### Arch 64 | 65 | ```bash 66 | yay -S safeeyes 67 | ``` 68 | 69 | ### Gentoo 70 | 71 | ```bash 72 | sudo emerge -av x11-misc/safeeyes 73 | ``` 74 | 75 | ### Debian 76 | 77 | ```bash 78 | sudo apt-get install safeeyes 79 | ``` 80 | 81 | ### Fedora 82 | Available on the [praiskup/safeeyes](https://copr.fedorainfracloud.org/coprs/praiskup/safeeyes/) COPR maintained by @praiksup 83 | 84 | ```bash 85 | sudo dnf -y copr enable praiskup/safeeyes 86 | sudo dnf -y install python3-safeeyes 87 | ``` 88 | For smart pause plugin, you may have to install the latest xprintidle from: [alonid/xprintidle](https://copr.fedorainfracloud.org/coprs/alonid/xprintidle/) 89 | 90 | ### OpenSUSE Tumbleweed 91 | 92 | ```bash 93 | sudo zypper refresh 94 | sudo zypper install safeeyes 95 | ``` 96 | 97 | ### Alpine Linux 98 | 99 | ```bash 100 | sudo apk add safeeyes 101 | ``` 102 | 103 | ### Chrome OS 104 | [Enable the Linux container](https://support.google.com/chromebook/answer/9145439?hl=en) (which is actually Debian), and install Safe Eyes with 105 | ``` 106 | sudo apt install safeeyes 107 | ``` 108 | While no tray icon is available, if you run the app, it will function in the background and will show breaks as usual. You can also change the settings by clicking on the Safe Eyes icon from the menu while the app is running, or by running the command `safeeyes -s`. 109 | 110 | ### Flatpak 111 | **Warning**: Many plugins and features don't work well in the flatpak. We recommend that you use one of the native packages listed above. Flatpak-only bugs should be reported at https://github.com/flathub/io.github.slgobinath.SafeEyes. 112 | ```bash 113 | flatpak install flathub io.github.slgobinath.SafeEyes 114 | ``` 115 | 116 | ### Other Linux & Run from source 117 | 118 | Ensure to meet the following dependencies: 119 | 120 | - gir1.2-notify-0.7 121 | - python3-babel 122 | - python3-croniter 123 | - python3-psutil 124 | - python3-packaging 125 | - python3-xlib 126 | - xprintidle (optional) 127 | - wlrctl (wayland optional) 128 | - Python 3.10+ 129 | 130 | **To install Safe Eyes:** 131 | 132 | ```bash 133 | sudo pip3 install safeeyes 134 | ``` 135 | 136 | After installation, restart your system to update the icons, 137 | 138 | **To run from source:** 139 | 140 | ```bash 141 | git clone https://github.com/slgobinath/SafeEyes.git 142 | cd SafeEyes 143 | python3 -m safeeyes 144 | ``` 145 | 146 | Safe Eyes installers install the required icons to `/usr/share/icons/hicolor`. When you run Safe Eyes from source without, some icons may not appear. 147 | 148 | Note that on Wayland, this may still not be enough to get window icons working properly, as Wayland requires the .desktop file to match the running application, which is hard to do when running from source. If at all possible, prefer using an installed package. 149 | 150 | 151 | ### Install in a virtual environment 152 | 153 | Some Linux systems like CentOS do not have matching dependencies available in their repository. In such systems, you can install and use Safe Eyes in a Python virtual environment. 154 | 155 | 1. Install the necessary dependencies for CentOS 7 156 | 157 | ```bash 158 | sudo yum install python3-devel dbus dbus-devel cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel gobject-introspection-devel cairo-gobject-devel 159 | ``` 160 | 161 | 2. Create a virtual environment in your home folder 162 | 163 | ```bash 164 | mkdir ~/safeeyes 165 | cd ~/safeeyes/ 166 | 167 | python3 -m venv venv 168 | source venv/bin/activate 169 | pip3 install safeeyes 170 | ``` 171 | 172 | 3. Start Safe Eyes from the terminal 173 | 174 | ```bash 175 | cd ~/safeeyes & source venv/bin/activate 176 | python3 -m safeeyes 177 | ``` 178 | 179 | For more details, please check the issue: [#329](https://github.com/slgobinath/SafeEyes/issues/329) 180 | 181 | This method has the same caveats about icons/window icons as running from source. 182 | 183 | ## Features 184 | 185 | - Remind you to take breaks with exercises to reduce RSI 186 | - Disable keyboard during breaks 187 | - Notification before and after breaks 188 | - Smart pause if system is idle 189 | - Multi-screen support 190 | - Customizable user interface 191 | - RPC API to control externally 192 | - Command-line arguments to control the running instance 193 | - Customizable using plug-ins 194 | 195 | ## Third-party Plugins 196 | 197 | Thirdparty plugins are available at another GitHub repository: [safeeyes-plugins](https://github.com/slgobinath/safeeyes-plugins). More details about how to write your own plugin and how to install third-party plugin are available there. 198 | 199 | ## Local development 200 | 201 | When adding new translatable strings in the source code, make sure to run `python validate_po.py --extract` to add them to the translation template. You will need to install `python3-polib` for this. 202 | 203 | Examples for translatable strings are `_("This is a string")` in Python code, or `This is a label` in Glade/xml files. 204 | 205 | To ensure the new strings are well-formed, you can use `python validate_po.py --validate`. 206 | 207 | To ensure that the coding and formatting guidelines are followed, install [ruff](https://docs.astral.sh/ruff/) and run `ruff check` and `ruff format --check` to check for issues, as well as `ruff check --fix` and `ruff format` to autofix them. 208 | 209 | To ensure that any types are correct, install [mypy](https://github.com/python/mypy) and run `mypy safeeyes`. 210 | 211 | The last three checks are also run in CI, so a PR must pass all the tests for it to be mmerged. 212 | 213 | ## How to Release? 214 | 215 | 0. Run `update-po.sh` to generate new translation files (which will be eventually updated by translators). Commit and push the changes to the master branch. 216 | 1. Checkout the latest commits from the `master` branch 217 | 2. Run `python3 -m safeeyes` to make sure nothing is broken 218 | 3. Update the Safe Eyes version in the following places (Open the project in VSCode and search for the current version): 219 | - [pyproject.toml](https://github.com/slgobinath/SafeEyes/blob/master/pyproject.toml#L4) 220 | - [pyproject.toml](https://github.com/slgobinath/SafeEyes/blob/master/pyproject.toml#L35) 221 | - [io.github.slgobinath.SafeEyes.metainfo.xml](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml#L56) 222 | - [about_dialog.glade](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/glade/about_dialog.glade#L74) 223 | 4. Update the [changelog](https://github.com/slgobinath/SafeEyes/blob/master/debian/changelog) (for Ubuntu PPA release) 224 | 5. Commit the changes to `master` 225 | 6. Create a pull-request from `master` to `release` 226 | 7. Merge the PR to release **with merge commit** (Important to merge with merge commit) 227 | 228 | ## How you can help improving translation of Safe Eyes 229 | 230 | First check if translations for your language are already available on [Weblate](https://hosted.weblate.org/engage/safe-eyes/), which is the cloud based translation platform we use. 231 | 232 | - If the language is already there, feel free to add new translations or improve the existing ones. 233 | - If it is not there, please [open an issue](https://github.com/slgobinath/SafeEyes/issues) in Github so that we can add your language to Weblate. 234 | 235 | ## License 236 | 237 | GNU General Public License v3 238 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | safeeyes (3.0.0b3) noble; urgency=medium 2 | 3 | * Re-release due to broken github action 4 | 5 | -- Mel Dafert Mon, 05 May 2025 13:30:00 +0000 6 | 7 | safeeyes (3.0.0b2) noble; urgency=medium 8 | 9 | * Re-release due to broken github action 10 | 11 | -- Mel Dafert Mon, 05 May 2025 12:35:00 +0000 12 | 13 | safeeyes (3.0.0b1) noble; urgency=medium 14 | 15 | * Update to GTK4 16 | 17 | * Improved Wayland support (keyboard locking, idle detection, window icons, 18 | fullscreen) 19 | 20 | * Internal refactoring (linting, formatting, typechecking on CI) 21 | 22 | * Improved handling of custom user stylesheets: always fall back to internal 23 | styles, with a lower priority 24 | 25 | * Translations 26 | 27 | -- Mel Dafert Mon, 05 May 2025 11:30:00 +0000 28 | 29 | safeeyes (2.2.3) jammy; urgency=medium 30 | 31 | * Translations 32 | 33 | * Make config and stylesheet files non-executable 34 | 35 | * Retry loading trayicon plugin to fix races on startup 36 | 37 | * Fix "Show next break time in tray icon" for desktops support tray icons 38 | with labels 39 | 40 | * Fix next break time in tray icon when disabling SafeEyes 41 | 42 | * Make SafeEyes compatible with Python 3.6 again 43 | 44 | * Add 'hyprland' to the list of recognized desktop sessions 45 | 46 | * Remove hard dependency on X11 47 | 48 | -- Mel Dafert Wed, 25 Dec 2024 11:46:00 +0000 49 | 50 | safeeyes (2.2.2) jammy; urgency=medium 51 | 52 | * Fixed translations 53 | 54 | * Fixed GTK startup error 55 | 56 | * Fixed frozen break screen if safeeyes is closed right before a break 57 | 58 | * Fixed safeeyes not working if stylesheet is missing 59 | 60 | * Replaced "tightly close your eyes" with "gently close your eyes" 61 | 62 | -- Archisman Panigrahi Mon, 5 Aug 2024 18:04:23 -0400 63 | 64 | safeeyes (2.2.1) jammy; urgency=medium 65 | 66 | * Make limit consecutive skips to default 67 | 68 | * Add list of contributors to About page 69 | 70 | -- Archisman Panigrahi Sun, 14 Jul 2024 16:15:54 -0400 71 | 72 | safeeyes (2.2.0) jammy; urgency=medium 73 | 74 | * Limit consecutive skips 75 | 76 | -- Archisman Panigrahi Sun, 14 Jul 2024 09:30:12 -0400 77 | 78 | safeeyes (2.1.9) noble; urgency=medium 79 | 80 | * Fix crash in smartpause 81 | 82 | -- Gobinath Loganathan Tue, 18 Jun 2024 20:25:00 -0400 83 | 84 | safeeyes (2.1.8) noble; urgency=medium 85 | 86 | * Support Python 3.12 87 | 88 | -- Archisman Panigrahi Mon, 29 Apr 2024 02:09:48 -0400 89 | 90 | safeeyes (2.1.6-0) lunar; urgency=high 91 | * Support Python 3.11 92 | 93 | * Minor bug fixes 94 | 95 | * Fix the ecd ..rror if there is no long break 96 | 97 | * Add wayland support for do not disturb me plugin 98 | 99 | * Fix missing icons and add random order of breaks 100 | 101 | * Create desktop entries from the code 102 | 103 | * Add Python 3.8 and 3.9 support 104 | 105 | * Redesigned settings dialog 106 | 107 | * Add tray action buttons to the break screen 108 | 109 | * Allow long short breaks for promodo mode 110 | 111 | * Audible alert break before and DND while on battery 112 | 113 | * Fix no long breaks 114 | 115 | * Fix issues with locales 116 | 117 | * Add new command line option: --status 118 | 119 | * Fix multi-display issue 120 | 121 | * Fix hard coded time in notification 122 | 123 | * Completely redesigned architecture and user interface 124 | 125 | * Show next break time in tray icon in supported environments 126 | 127 | * Enable keyboard shortcuts 128 | 129 | * Fix screen flickering in KDE 130 | 131 | * Show the break type in the notification 132 | 133 | * Make pyaudio optional 134 | 135 | * Support postponing the break 136 | 137 | * Handle configuration update efficiently 138 | 139 | * Move to Python 3 140 | 141 | * Add plugin support 142 | 143 | * Add custom breaks 144 | 145 | * Add optional break image feature 146 | 147 | * Add lock screen support 148 | 149 | * Prevent disabling Safe Eyes after notification 150 | 151 | * Fix random crash 152 | 153 | * Use system language if available 154 | 155 | * Fix disable menu issue in Elementary OS 156 | 157 | * Fix long breaks in hours 158 | 159 | * Fix locale time format issue 160 | 161 | * Advanced configurations and disable for given time 162 | 163 | * Optional audible alert and pause Safe Eyes if system is idle 164 | 165 | * Bug fix for no breaks after 1st one 166 | 167 | * Add about dialog and language selection to Settings dialog 168 | 169 | * Fixing bug in multiscreen support 170 | 171 | * Add next break information to tray menu 172 | 173 | * Support translation 174 | 175 | * Adding multiscreen support & handling system suspend 176 | 177 | * Fixing bug in Ubuntu MATE environment 178 | 179 | * Removing apscheduler dependency 180 | 181 | * Fixing seconds instead of minutes bug 182 | 183 | * Bug fixes for Ubuntu 14.04 and keyboard lock during break 184 | 185 | * Reducing minimal Python requirement 186 | 187 | * Fixing appindicator version mismatch 188 | 189 | * Fixing apscheduler version mismatch 190 | 191 | * Initial release 192 | 193 | -- Gobinath Loganathan Sat, 15 Oct 2016 06:28:40 +0530 194 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: safeeyes 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Gobinath Loganathan 5 | Build-Depends: debhelper (>= 10), dh-python, python3, python3-packaging, python3-setuptools 6 | Standards-Version: 3.9.6 7 | X-Python3-Version: >= 3.10 8 | Homepage: https://github.com/slgobinath/SafeEyes/ 9 | 10 | Package: safeeyes 11 | Architecture: all 12 | Depends: ${misc:Depends}, ${python3:Depends}, 13 | python3 (>= 3.10.0), 14 | python3-xlib, 15 | python3-babel, 16 | x11-utils, 17 | xprintidle, 18 | alsa-utils, 19 | python3-psutil, 20 | python3-croniter, 21 | python3-packaging, 22 | gir1.2-notify-0.7, 23 | gir1.2-gtk-4.0 24 | Description: Prevent eye strain with Safe Eyes – an essential screen break reminder. 25 | 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. 26 | . 27 | Features: 28 | - Short breaks with eye exercises 29 | - Long breaks to change physical position and to warm up 30 | - Strict break for those who are addicted to computer 31 | - Do not disturb when working with full-screen applications( Eg: Watching movies) 32 | - Notifications before every break 33 | - Optional audible alert at the end of break 34 | - Option to lock screen after long breaks 35 | - Smart pause and resume based on system idle time 36 | - Multi-monitor support 37 | - Plugins to utilize Safe Eyes 38 | - Elegant and customizable design 39 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: uget-chrome-wrapper 3 | Source: https://github.com/slgobinath/SafeEyes/ 4 | 5 | Files: * 6 | Copyright: 2016 Gobinath Loganathan 7 | License: GPL-3.0+ 8 | 9 | License: GPL-3.0+ 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | . 15 | This package is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | . 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | . 23 | On Debian systems, the complete text of the GNU General 24 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | %: 3 | dh $@ --with python3 --buildsystem=pybuild 4 | -------------------------------------------------------------------------------- /debian/safeeyes.install: -------------------------------------------------------------------------------- 1 | safeeyes/platform/io.github.slgobinath.SafeEyes.desktop usr/share/applications 2 | safeeyes/platform/icons/hicolor/128x128/apps/io.github.slgobinath.SafeEyes.png usr/share/icons/hicolor/128x128/apps 3 | safeeyes/platform/icons/hicolor/16x16/apps/io.github.slgobinath.SafeEyes.png usr/share/icons/hicolor/16x16/apps 4 | safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-disabled.png usr/share/icons/hicolor/16x16/status 5 | safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-enabled.png usr/share/icons/hicolor/16x16/status 6 | safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-timer.png usr/share/icons/hicolor/16x16/status 7 | safeeyes/platform/icons/hicolor/24x24/apps/io.github.slgobinath.SafeEyes.png usr/share/icons/hicolor/24x24/apps 8 | safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-disabled.png usr/share/icons/hicolor/24x24/status 9 | safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-enabled.png usr/share/icons/hicolor/24x24/status 10 | safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-timer.png usr/share/icons/hicolor/24x24/status 11 | safeeyes/platform/icons/hicolor/32x32/apps/io.github.slgobinath.SafeEyes.png usr/share/icons/hicolor/32x32/apps 12 | safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-disabled.png usr/share/icons/hicolor/32x32/status 13 | safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-enabled.png usr/share/icons/hicolor/32x32/status 14 | safeeyes/platform/icons/hicolor/48x48/apps/io.github.slgobinath.SafeEyes.png usr/share/icons/hicolor/48x48/apps 15 | safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-disabled.png usr/share/icons/hicolor/48x48/status 16 | safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-enabled.png usr/share/icons/hicolor/48x48/status 17 | safeeyes/platform/icons/hicolor/64x64/apps/io.github.slgobinath.SafeEyes.png usr/share/icons/hicolor/64x64/apps -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "safeeyes" 3 | version = "3.0.0b3" 4 | description = "Protect your eyes from eye strain using this continuous breaks reminder." 5 | keywords = ["linux utility health eye-strain safe-eyes"] 6 | readme = "README.md" 7 | authors = [ 8 | {name = "Gobinath Loganathan", email = "slgobinath@gmail.com"}, 9 | ] 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: X11 Applications :: GTK", 13 | "Environment :: Other Environment", 14 | "Intended Audience :: End Users/Desktop", 15 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 16 | "Operating System :: POSIX :: Linux", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Topic :: Utilities", 22 | ] 23 | dependencies = [ 24 | "pywayland", 25 | "PyGObject", 26 | "babel", 27 | "psutil", 28 | "packaging", 29 | "python-xlib", 30 | ] 31 | requires-python = ">=3.10" 32 | 33 | [project.urls] 34 | Homepage = "https://github.com/slgobinath/SafeEyes" 35 | Downloads = "https://github.com/slgobinath/SafeEyes/archive/v3.0.0b3.tar.gz" 36 | 37 | [project.scripts] 38 | safeeyes = "safeeyes.__main__:main" 39 | 40 | [project.optional-dependencies] 41 | healthstats = ["croniter"] 42 | 43 | [build-system] 44 | requires = ["setuptools"] 45 | build-backend = "setuptools.build_meta" 46 | 47 | [tool.setuptools.packages.find] 48 | include=["safeeyes*"] 49 | 50 | [dependency-groups] 51 | dev = [ 52 | {include-group = "lint"}, 53 | {include-group = "scripts"}, 54 | {include-group = "types"} 55 | ] 56 | lint = [ 57 | "ruff==0.11.2" 58 | ] 59 | scripts = [ 60 | "polib==1.2.0" 61 | ] 62 | types = [ 63 | "mypy==1.15.0", 64 | "PyGObject-stubs==2.13.0", 65 | "types-croniter==5.0.1.20250322", 66 | "types-psutil==7.0.0.20250218", 67 | "types-python-xlib==0.33.0.20240407" 68 | ] 69 | 70 | [tool.mypy] 71 | # catch typos in configuration file 72 | warn_unused_configs = true 73 | disallow_any_unimported = true 74 | #check_untyped_defs = true 75 | warn_unused_ignores = true 76 | warn_unreachable = true 77 | enable_error_code = [ 78 | "ignore-without-code", 79 | "possibly-undefined" 80 | ] 81 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py310" 2 | 3 | # gettext 4 | builtins = ["_"] 5 | 6 | [lint] 7 | select = ["E", "W", "F", "D2", "D3", "D4"] 8 | ignore = [ 9 | # PyGObject needs require_version before imports 10 | "E402", 11 | 12 | # these are disabled for now, but should probably be cleaned up at some point 13 | "D205", "D401", "D404", 14 | ] 15 | 16 | [lint.pydocstyle] 17 | convention = "numpy" 18 | -------------------------------------------------------------------------------- /safeeyes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/__init__.py -------------------------------------------------------------------------------- /safeeyes/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2016 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Safe Eyes is a utility to remind you to take break frequently to protect 20 | your eyes from eye strain. 21 | """ 22 | 23 | import argparse 24 | import gettext 25 | import locale 26 | import logging 27 | import signal 28 | import sys 29 | 30 | import psutil 31 | from safeeyes import utility 32 | from safeeyes.model import Config 33 | from safeeyes.safeeyes import SafeEyes 34 | from safeeyes.safeeyes import SAFE_EYES_VERSION 35 | from safeeyes.rpc import RPCClient 36 | 37 | gettext.install("safeeyes", utility.LOCALE_PATH) 38 | 39 | 40 | def __running(): 41 | """Check if SafeEyes is already running.""" 42 | process_count = 0 43 | current_user = psutil.Process().username() 44 | for proc in psutil.process_iter(): 45 | if not proc.cmdline: 46 | continue 47 | try: 48 | # Check if safeeyes is in process arguments 49 | if callable(proc.cmdline): 50 | # Latest psutil has cmdline function 51 | cmd_line = proc.cmdline() 52 | else: 53 | # In older versions cmdline was a list object 54 | cmd_line = proc.cmdline 55 | if ("python3" in cmd_line[0] or "python" in cmd_line[0]) and ( 56 | "safeeyes" in cmd_line[1] or "safeeyes" in cmd_line 57 | ): 58 | if proc.username() == current_user: 59 | process_count += 1 60 | if process_count > 1: 61 | return True 62 | 63 | # Ignore if process does not exist or does not have command line args 64 | except (IndexError, psutil.NoSuchProcess): 65 | pass 66 | return False 67 | 68 | 69 | def main(): 70 | """Start the Safe Eyes.""" 71 | system_locale = gettext.translation( 72 | "safeeyes", 73 | localedir=utility.LOCALE_PATH, 74 | languages=[utility.system_locale(), "en_US"], 75 | fallback=True, 76 | ) 77 | system_locale.install() 78 | try: 79 | # locale.bindtextdomain is required for Glade files 80 | locale.bindtextdomain("safeeyes", utility.LOCALE_PATH) 81 | except AttributeError: 82 | logging.warning( 83 | "installed python's gettext module does not support locale.bindtextdomain." 84 | " locale.bindtextdomain is required for Glade files" 85 | ) 86 | 87 | parser = argparse.ArgumentParser(prog="safeeyes") 88 | group = parser.add_mutually_exclusive_group() 89 | group.add_argument( 90 | "-a", "--about", help=_("show the about dialog"), action="store_true" 91 | ) 92 | group.add_argument( 93 | "-d", 94 | "--disable", 95 | help=_("disable the currently running safeeyes instance"), 96 | action="store_true", 97 | ) 98 | group.add_argument( 99 | "-e", 100 | "--enable", 101 | help=_("enable the currently running safeeyes instance"), 102 | action="store_true", 103 | ) 104 | group.add_argument( 105 | "-q", 106 | "--quit", 107 | help=_("quit the running safeeyes instance and exit"), 108 | action="store_true", 109 | ) 110 | group.add_argument( 111 | "-s", "--settings", help=_("show the settings dialog"), action="store_true" 112 | ) 113 | group.add_argument( 114 | "-t", "--take-break", help=_("Take a break now").lower(), action="store_true" 115 | ) 116 | parser.add_argument( 117 | "--debug", help=_("start safeeyes in debug mode"), action="store_true" 118 | ) 119 | parser.add_argument( 120 | "--status", 121 | help=_("print the status of running safeeyes instance and exit"), 122 | action="store_true", 123 | ) 124 | parser.add_argument( 125 | "--version", action="version", version="%(prog)s " + SAFE_EYES_VERSION 126 | ) 127 | args = parser.parse_args() 128 | 129 | # Initialize the logging 130 | utility.initialize_logging(args.debug) 131 | utility.initialize_platform() 132 | config = Config() 133 | utility.cleanup_old_user_stylesheet() 134 | 135 | if __running(): 136 | logging.info("Safe Eyes is already running") 137 | if not config.get("use_rpc_server", True): 138 | # RPC sever is disabled 139 | print( 140 | _( 141 | "Safe Eyes is running without an RPC server. Turn it on to use" 142 | " command-line arguments." 143 | ) 144 | ) 145 | sys.exit(0) 146 | return 147 | rpc_client = RPCClient(config.get("rpc_port")) 148 | if args.about: 149 | rpc_client.show_about() 150 | elif args.disable: 151 | rpc_client.disable_safeeyes() 152 | elif args.enable: 153 | rpc_client.enable_safeeyes() 154 | elif args.settings: 155 | rpc_client.show_settings() 156 | elif args.take_break: 157 | rpc_client.take_break() 158 | elif args.status: 159 | print(rpc_client.status()) 160 | elif args.quit: 161 | rpc_client.quit() 162 | else: 163 | # Default behavior is opening settings 164 | rpc_client.show_settings() 165 | sys.exit(0) 166 | else: 167 | if args.status: 168 | print(_("Safe Eyes is not running")) 169 | sys.exit(0) 170 | elif not args.quit: 171 | logging.info("Starting Safe Eyes") 172 | safe_eyes = SafeEyes(system_locale, config, args) 173 | safe_eyes.start() 174 | 175 | 176 | if __name__ == "__main__": 177 | signal.signal(signal.SIGINT, signal.SIG_DFL) # Handle Ctrl + C 178 | main() 179 | -------------------------------------------------------------------------------- /safeeyes/config/locale/safeeyes.pot: -------------------------------------------------------------------------------- 1 | # SAFE EYES ENGLISH TRANSLATION. 2 | # Copyright (C) 2017 Gobinath 3 | # Gobinath slgobinath@gmail.com, 2017. 4 | # 5 | msgid "" 6 | msgstr "" 7 | 8 | # Short break 9 | msgid "Gently close your eyes" 10 | msgstr "" 11 | 12 | # Short break 13 | msgid "Roll your eyes a few times to each side" 14 | msgstr "" 15 | 16 | # Short break 17 | msgid "Rotate your eyes in clockwise direction" 18 | msgstr "" 19 | 20 | # Short break 21 | msgid "Rotate your eyes in counterclockwise direction" 22 | msgstr "" 23 | 24 | # Short break 25 | msgid "Blink your eyes" 26 | msgstr "" 27 | 28 | # Short break 29 | msgid "Focus on a point in the far distance" 30 | msgstr "" 31 | 32 | # Short break 33 | msgid "Have some water" 34 | msgstr "" 35 | 36 | # Long break 37 | msgid "Walk for a while" 38 | msgstr "" 39 | 40 | # Long break 41 | msgid "Lean back at your seat and relax" 42 | msgstr "" 43 | 44 | # Commandline arg description 45 | msgid "show the about dialog" 46 | msgstr "" 47 | 48 | # Commandline arg description 49 | msgid "disable the currently running safeeyes instance" 50 | msgstr "" 51 | 52 | # Commandline arg description 53 | msgid "enable the currently running safeeyes instance" 54 | msgstr "" 55 | 56 | # Commandline arg description 57 | msgid "quit the running safeeyes instance and exit" 58 | msgstr "" 59 | 60 | # Commandline arg description 61 | msgid "show the settings dialog" 62 | msgstr "" 63 | 64 | # Commandline arg description 65 | msgid "start safeeyes in debug mode" 66 | msgstr "" 67 | 68 | # Commandline arg description 69 | msgid "print the status of running safeeyes instance and exit" 70 | msgstr "" 71 | 72 | # Status message 73 | msgid "Safe Eyes is not running" 74 | msgstr "" 75 | 76 | # RPC not enabled message 77 | msgid "Safe Eyes is running without an RPC server. Turn it on to use command-line arguments." 78 | msgstr "" 79 | 80 | # About dialog 81 | msgid "Close" 82 | msgstr "" 83 | 84 | # Description in about dialog 85 | # Safe Eyes protects your eyes from eye strain (asthenopia) by reminding you to take breaks while you're working long hours at the computer 86 | msgid "Safe Eyes protects your eyes from eye strain (asthenopia) by reminding you to take breaks while you're working long hours at the computer" 87 | msgstr "" 88 | 89 | # About dialog 90 | msgid "License" 91 | msgstr "" 92 | 93 | # About dialog 94 | msgid "List of Contributors" 95 | msgstr "" 96 | 97 | # About dialog 98 | msgid "Help us translate this app" 99 | msgstr "" 100 | 101 | # Break screen 102 | msgid "Skip" 103 | msgstr "" 104 | 105 | # Break screen 106 | msgid "Postpone" 107 | msgstr "" 108 | 109 | # Settings dialog 110 | msgid "Break duration (in seconds)" 111 | msgstr "" 112 | 113 | # Settings dialog 114 | msgid "Interval between two breaks (in minutes)" 115 | msgstr "" 116 | 117 | # Settings dialog 118 | msgid "Time to prepare for a break (in seconds)" 119 | msgstr "" 120 | 121 | # Settings dialog 122 | msgid "Keyboard shortcuts disabled period (in seconds)" 123 | msgstr "" 124 | 125 | # Settings dialog 126 | msgid "Postponement duration (in minutes)" 127 | msgstr "" 128 | 129 | # Settings dialog 130 | msgid "Show breaks in random order" 131 | msgstr "" 132 | 133 | # Settings dialog 134 | msgid "Strict break (No way to skip breaks)" 135 | msgstr "" 136 | 137 | # Settings dialog 138 | msgid "Allow postponing breaks" 139 | msgstr "" 140 | 141 | # Settings dialog 142 | msgid "Persist the internal state" 143 | msgstr "" 144 | 145 | # Settings dialog 146 | msgid "Use RPC server to receive runtime commands" 147 | msgstr "" 148 | 149 | # Settings dialog 150 | msgid "Without the RPC server, command-line commands may not work" 151 | msgstr "" 152 | 153 | # Settings dialog 154 | msgid "Long break interval must be a multiple of short break interval" 155 | msgstr "" 156 | 157 | # Settings dialog 158 | msgid "Reset" 159 | msgstr "" 160 | 161 | # Settings dialog 162 | msgid "Are you sure you want to reset all settings to default?" 163 | msgstr "" 164 | 165 | # Settings dialog 166 | msgid "Options" 167 | msgstr "" 168 | 169 | # Settings dialog 170 | msgid "Short Breaks" 171 | msgstr "" 172 | 173 | # Settings dialog 174 | msgid "Long Breaks" 175 | msgstr "" 176 | 177 | # Settings dialog 178 | msgid "Delete" 179 | msgstr "" 180 | 181 | # Settings dialog 182 | msgid "Are you sure you want to delete this break?" 183 | msgstr "" 184 | 185 | # Settings dialog 186 | msgid "You can't undo this action." 187 | msgstr "" 188 | 189 | # Settings dialog 190 | msgid "Break" 191 | msgstr "" 192 | 193 | # Settings dialog 194 | msgid "Breaks" 195 | msgstr "" 196 | 197 | # Settings dialog 198 | msgid "Plugins" 199 | msgstr "" 200 | 201 | # Settings dialog 202 | msgid "Type" 203 | msgstr "" 204 | 205 | # Settings dialog 206 | msgid "Short" 207 | msgstr "" 208 | 209 | # Settings dialog 210 | msgid "Long" 211 | msgstr "" 212 | 213 | # Settings dialog 214 | msgid "Image" 215 | msgstr "" 216 | 217 | # Settings dialog 218 | msgid "Select" 219 | msgstr "" 220 | 221 | # Settings dialog 222 | msgid "Please select an image" 223 | msgstr "" 224 | 225 | # Settings dialog 226 | msgid "Duration" 227 | msgstr "" 228 | 229 | # Settings dialog 230 | msgid "Time to wait" 231 | msgstr "" 232 | 233 | # Settings dialog 234 | msgid "Override" 235 | msgstr "" 236 | 237 | # Settings dialog 238 | msgid "Time (in seconds)" 239 | msgstr "" 240 | 241 | # Settings dialog 242 | msgid "Time (in minutes)" 243 | msgstr "" 244 | 245 | # Settings dialog 246 | msgid "Break Settings" 247 | msgstr "" 248 | 249 | # Settings dialog 250 | msgid "Plugin Settings" 251 | msgstr "" 252 | 253 | # Settings dialog 254 | #, python-format 255 | msgid "Plugin does not support %s desktop environment" 256 | msgstr "" 257 | 258 | # Settings dialog 259 | #, python-format 260 | msgid "Please install the Python module '%s'" 261 | msgstr "" 262 | 263 | # Settings dialog 264 | #, python-format 265 | msgid "Please install the command-line tool '%s'" 266 | msgstr "" 267 | 268 | # Settings dialog 269 | msgid "Invalid cron expression '%s'" 270 | msgstr "" 271 | 272 | # Settings dialog 273 | #, python-format 274 | msgid "Please add the resource %(resource)s to %(config_resource)s directory" 275 | msgstr "" 276 | 277 | # Settings dialog 278 | msgid "New Break" 279 | msgstr "" 280 | 281 | # Settings dialog 282 | msgid "Remove" 283 | msgstr "" 284 | 285 | # Settings dialog 286 | msgid "Discard" 287 | msgstr "" 288 | 289 | # Settings dialog 290 | msgid "Save" 291 | msgstr "" 292 | 293 | # plugin/audiblealert 294 | msgid "Audible Alert" 295 | msgstr "" 296 | 297 | # plugin/audiblealert 298 | msgid "Play audible alert before and after breaks" 299 | msgstr "" 300 | 301 | # plugin/audiblealert 302 | msgid "Play audible alert before breaks" 303 | msgstr "" 304 | 305 | # plugin/audiblealert 306 | msgid "Play audible alert after breaks" 307 | msgstr "" 308 | 309 | # plugin/donotdisturb 310 | msgid "Do Not Disturb" 311 | msgstr "" 312 | 313 | # plugin/donotdisturb 314 | msgid "Skip break if the active window is in fullscreen mode" 315 | msgstr "" 316 | 317 | # plugin/donotdisturb 318 | msgid "Do not interrupt these windows anytime" 319 | msgstr "" 320 | 321 | # plugin/donotdisturb 322 | msgid "Interrupt these windows regardless of their state" 323 | msgstr "" 324 | 325 | # plugin/donotdisturb 326 | msgid "Switch the interruptible windows to normal mode" 327 | msgstr "" 328 | 329 | # plugin/donotdisturb 330 | msgid "Do not disturb while on battery" 331 | msgstr "" 332 | 333 | # plugin/healthstats 334 | msgid "Health Statistics" 335 | msgstr "" 336 | 337 | # plugin/healthstats 338 | msgid "Show statistics based on how you use Safe Eyes" 339 | msgstr "" 340 | 341 | # plugin/healthstats 342 | msgid "Statistics reset interval (cron expression)" 343 | msgstr "" 344 | 345 | # plugin/notification 346 | msgid "Notification" 347 | msgstr "" 348 | 349 | # plugin/notification 350 | msgid "Show a system notification before breaks" 351 | msgstr "" 352 | 353 | # plugin/notification 354 | #, python-format 355 | msgid "Ready for a short break in %s seconds" 356 | msgstr "" 357 | 358 | # plugin/notification 359 | #, python-format 360 | msgid "Ready for a long break in %s seconds" 361 | msgstr "" 362 | 363 | # plugin/screensaver 364 | msgid "Screensaver" 365 | msgstr "" 366 | 367 | # plugin/screensaver 368 | msgid "Lock the screen after long breaks by starting screensaver" 369 | msgstr "" 370 | 371 | # plugin/screensaver 372 | msgid "Custom screensaver command" 373 | msgstr "" 374 | 375 | # plugin/screensaver 376 | msgid "Minimum seconds to skip without screensaver" 377 | msgstr "" 378 | 379 | # plugin/screensaver 380 | msgid "Lock screen" 381 | msgstr "" 382 | 383 | # plugin/smartpause 384 | msgid "Smart Pause" 385 | msgstr "" 386 | 387 | # plugin/smartpause 388 | msgid "Pause Safe Eyes if the system is idle" 389 | msgstr "" 390 | 391 | # plugin/smartpause 392 | msgid "Minimum idle time to pause Safe Eyes (in seconds)" 393 | msgstr "" 394 | 395 | # plugin/smartpause 396 | msgid "Interpret idle time equivalent to upcoming break duration as a break" 397 | msgstr "" 398 | 399 | # plugin/smartpause 400 | msgid "Postpone the next break until the system becomes idle" 401 | msgstr "" 402 | 403 | #: plugins/trayicon 404 | msgid "Tray Icon" 405 | msgstr "" 406 | 407 | #: plugins/trayicon 408 | msgid "Show a tray icon in the notification area" 409 | msgstr "" 410 | 411 | #: plugins/trayicon 412 | msgid "Show next break time in tray icon" 413 | msgstr "" 414 | 415 | #: plugins/trayicon 416 | msgid "Allow disabling Safe Eyes" 417 | msgstr "" 418 | 419 | #: plugins/trayicon 420 | msgid "About" 421 | msgstr "" 422 | 423 | #: plugins/trayicon 424 | msgid "Disable Safe Eyes" 425 | msgstr "" 426 | 427 | #: plugins/trayicon 428 | #, python-format 429 | msgid "Disabled until %s" 430 | msgstr "" 431 | 432 | #: plugins/trayicon 433 | msgid "Disabled until restart" 434 | msgstr "" 435 | 436 | #: plugins/trayicon 437 | msgid "Enable Safe Eyes" 438 | msgstr "" 439 | 440 | #: plugins/trayicon 441 | #, python-format 442 | msgid "For %(num)d Hour" 443 | msgid_plural "For %(num)d Hours" 444 | msgstr[0] "" 445 | msgstr[1] "" 446 | 447 | #: plugins/trayicon 448 | #, python-format 449 | msgid "For %(num)d Minute" 450 | msgid_plural "For %(num)d Minutes" 451 | msgstr[0] "" 452 | msgstr[1] "" 453 | 454 | #: plugins/trayicon 455 | #, python-format 456 | msgid "For %(num)d Second" 457 | msgid_plural "For %(num)d Seconds" 458 | msgstr[0] "" 459 | msgstr[1] "" 460 | 461 | #: plugins/trayicon 462 | #, python-format 463 | msgid "Next break at %s" 464 | msgstr "" 465 | 466 | #: plugins/trayicon 467 | msgid "No Breaks Available" 468 | msgstr "" 469 | 470 | #: plugins/trayicon 471 | msgid "Settings" 472 | msgstr "" 473 | 474 | #: plugins/trayicon 475 | msgid "Take a break now" 476 | msgstr "" 477 | 478 | #: plugins/trayicon 479 | msgid "Any break" 480 | msgstr "" 481 | 482 | #: plugins/trayicon 483 | msgid "Short break" 484 | msgstr "" 485 | 486 | #: plugins/trayicon 487 | msgid "Long break" 488 | msgstr "" 489 | 490 | #: plugins/trayicon 491 | msgid "Until restart" 492 | msgstr "" 493 | 494 | #: plugins/trayicon 495 | msgid "Quit" 496 | msgstr "" 497 | 498 | # plugin/mediacontrol 499 | msgid "Media Control" 500 | msgstr "" 501 | 502 | # plugin/mediacontrol 503 | msgid "Pause media players from the break screen" 504 | msgstr "" 505 | 506 | # plugin/mediacontrol 507 | msgid "Pause media" 508 | msgstr "" 509 | 510 | # plugin/limitconsecutiveskipping 511 | msgid "Limit Consecutive Skipping" 512 | msgstr "" 513 | 514 | # plugin/limitconsecutiveskipping 515 | msgid "How many skips or postpones are allowed in a row" 516 | msgstr "" 517 | 518 | # plugin/limitconsecutiveskipping 519 | msgid "Limit how many breaks can be skipped or postponed in a row" 520 | msgstr "" 521 | 522 | # plugin/limitconsecutiveskipping 523 | #, python-format 524 | msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" 525 | msgstr "" 526 | 527 | # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop 528 | msgid "RSI Prevention" 529 | msgstr "" 530 | 531 | msgid "Please install service providing tray icons for your desktop environment." 532 | msgstr "" 533 | 534 | #, python-format 535 | msgid "Next long break at %s" 536 | msgstr "" 537 | 538 | #, python-format 539 | msgid "Next breaks at %(short)s/%(long)s" 540 | msgstr "" 541 | 542 | #, python-format 543 | msgid "The required plugin '%s' is missing dependencies!" 544 | msgstr "" 545 | 546 | msgid "Please install the dependencies or disable the plugin. To hide this message, you can also deactivate the plugin in the settings." 547 | msgstr "" 548 | 549 | msgid "Click here for more information" 550 | msgstr "" 551 | 552 | msgid "Disable plugin temporarily" 553 | msgstr "" 554 | 555 | msgid "Disable permanently" 556 | msgstr "" 557 | 558 | msgid "License:" 559 | msgstr "" 560 | 561 | #, python-format 562 | msgid "Old stylesheet found at '%(old)s', ignoring. For custom styles, create a new stylesheet in '%(new)s' instead." 563 | msgstr "" 564 | -------------------------------------------------------------------------------- /safeeyes/config/safeeyes.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "config_version": "6.0.3" 4 | }, 5 | "random_order": true, 6 | "allow_postpone": false, 7 | "short_break_interval": 15, 8 | "long_break_interval": 75, 9 | "long_break_duration": 60, 10 | "pre_break_warning_time": 10, 11 | "short_break_duration": 15, 12 | "persist_state": false, 13 | "postpone_duration": 5, 14 | "use_rpc_server": true, 15 | "rpc_port": 7200, 16 | "shortcut_disable_time": 2, 17 | "shortcut_skip": 9, 18 | "shortcut_postpone": 65, 19 | "strict_break": false, 20 | "short_breaks": [{ 21 | "name": "Gently close your eyes" 22 | }, 23 | { 24 | "name": "Roll your eyes a few times to each side" 25 | }, 26 | { 27 | "name": "Rotate your eyes in clockwise direction" 28 | }, 29 | { 30 | "name": "Rotate your eyes in counterclockwise direction" 31 | }, 32 | { 33 | "name": "Blink your eyes" 34 | }, 35 | { 36 | "name": "Focus on a point in the far distance" 37 | }, 38 | { 39 | "name": "Have some water" 40 | } 41 | ], 42 | "long_breaks": [{ 43 | "name": "Walk for a while" 44 | }, 45 | { 46 | "name": "Lean back at your seat and relax" 47 | } 48 | ], 49 | "plugins": [{ 50 | "id": "donotdisturb", 51 | "enabled": true, 52 | "version": "0.0.2", 53 | "settings": { 54 | "skip_break_windows": "", 55 | "take_break_windows": "", 56 | "unfullscreen": true, 57 | "while_on_battery": false 58 | } 59 | }, 60 | { 61 | "id": "notification", 62 | "enabled": true, 63 | "version": "0.0.1" 64 | }, 65 | { 66 | "id": "audiblealert", 67 | "enabled": true, 68 | "version": "0.0.3", 69 | "settings": { 70 | "pre_break_alert": true, 71 | "post_break_alert": true 72 | } 73 | }, 74 | { 75 | "id": "trayicon", 76 | "enabled": true, 77 | "version": "0.0.3", 78 | "settings": { 79 | "show_time_in_tray": false, 80 | "show_long_time_in_tray": false, 81 | "allow_disabling": true, 82 | "disable_options": [{ 83 | "time": 30, 84 | "unit": "minute" 85 | }, 86 | { 87 | "time": 1, 88 | "unit": "hour" 89 | }, 90 | { 91 | "time": 2, 92 | "unit": "hour" 93 | }, 94 | { 95 | "time": 3, 96 | "unit": "hour" 97 | } 98 | ] 99 | } 100 | }, 101 | { 102 | "id": "smartpause", 103 | "enabled": true, 104 | "version": "0.0.3", 105 | "settings": { 106 | "idle_time": 5, 107 | "postpone_if_active": false 108 | } 109 | }, 110 | { 111 | "id": "screensaver", 112 | "enabled": true, 113 | "version": "0.0.2", 114 | "settings": { 115 | "command": "", 116 | "min_seconds": 3 117 | } 118 | }, 119 | { 120 | "id": "healthstats", 121 | "enabled": false, 122 | "version": "0.0.2", 123 | "settings": { 124 | "statistics_reset_cron": "0 0 * * *" 125 | } 126 | }, 127 | { 128 | "id": "mediacontrol", 129 | "enabled": true, 130 | "version": "0.0.1" 131 | }, 132 | { 133 | "id": "limitconsecutiveskipping", 134 | "enabled": true, 135 | "version": "0.0.1", 136 | "settings": { 137 | "number_of_allowed_skips_in_a_row": 2 138 | } 139 | } 140 | ] 141 | } 142 | -------------------------------------------------------------------------------- /safeeyes/config/style/safeeyes_style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Safe Eyes is a utility to remind you to take break frequently 3 | * to protect your eyes from eye strain. 4 | 5 | * Copyright (C) 2016 Gobinath 6 | 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | .window_main { 22 | background: black; 23 | opacity: 0.9; 24 | border-color: transparent; 25 | } 26 | 27 | .btn_skip { 28 | color: white; 29 | font-size: 10pt; 30 | border-radius: 25px; 31 | padding-top: 10px; 32 | padding-bottom: 10px; 33 | padding-left: 25px; 34 | padding-right: 25px; 35 | border-color: white; 36 | background: transparent; 37 | border-width: 2px; 38 | border-image: none; 39 | } 40 | 41 | .btn_skip:hover { 42 | background: white; 43 | color: black; 44 | } 45 | 46 | .btn_postpone { 47 | color: white; 48 | font-size: 10pt; 49 | border-radius: 25px; 50 | padding-top: 10px; 51 | padding-bottom: 10px; 52 | padding-left: 25px; 53 | padding-right: 25px; 54 | border-color: white; 55 | background: transparent; 56 | border-width: 2px; 57 | border-image: none; 58 | } 59 | 60 | .btn_postpone:hover { 61 | background: white; 62 | color: black; 63 | } 64 | 65 | .lbl_message { 66 | font-size: 22pt; 67 | color: white; 68 | font-weight: bold; 69 | } 70 | 71 | .lbl_count { 72 | font-size: 12pt; 73 | color: white; 74 | } 75 | 76 | .lbl_widget { 77 | font-size: 9pt; 78 | color: white; 79 | } 80 | 81 | .btn_circle { 82 | border-radius: 25px; 83 | } 84 | 85 | .lbl_plugin_name { 86 | font-weight: bold; 87 | } 88 | 89 | .lbl_plugin_description { 90 | color: #9B9B9B; 91 | } 92 | 93 | .btn_plugin_extra_link { 94 | padding-left: 0px; 95 | } 96 | 97 | .info_bar_long_break { 98 | opacity: 0.9; 99 | border-radius: 5px; 100 | } 101 | 102 | .warn_bar_rpc_server { 103 | opacity: 0.9; 104 | border-radius: 5px; 105 | } 106 | 107 | .toolbar { 108 | background: black; 109 | opacity: 0.9; 110 | border-color: transparent; 111 | } 112 | 113 | .btn_menu { 114 | border-width: 0px; 115 | border-radius: 0px; 116 | border-image: None; 117 | background: white; 118 | border-color: transparent; 119 | } 120 | 121 | .btn_menu:hover { 122 | border-width: 0px; 123 | border-radius: 0px; 124 | border-image: None; 125 | background: whitesmoke; 126 | border-color: transparent; 127 | } 128 | -------------------------------------------------------------------------------- /safeeyes/glade/about_dialog.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | This program is free software: you can redistribute it and/or modify 26 | it under the terms of the GNU General Public License as published by 27 | the Free Software Foundation, either version 3 of the License, or 28 | (at your option) any later version. 29 | 30 | This program is distributed in the hope that it will be useful, 31 | but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | GNU General Public License for more details. 34 | 35 | You should have received a copy of the GNU General Public License 36 | along with this program. If not, see <https://www.gnu.org/licenses/>. 37 | 38 | 39 | Safe Eyes 40 | 0 41 | io.github.slgobinath.SafeEyes 42 | 43 | 44 | 1 45 | 5 46 | 5 47 | 5 48 | 5 49 | 1 50 | 1 51 | vertical 52 | top 53 | 54 | 55 | 1 56 | start 57 | 1 58 | vertical 59 | 60 | 61 | 1 62 | center 63 | center 64 | 10 65 | 10 66 | Safe Eyes 3.0.0b3 67 | center 68 | 1 69 | 1 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 1 79 | 4 80 | Safe Eyes protects your eyes from eye strain (asthenopia) by reminding you to take breaks while you're working long hours at the computer 81 | fill 82 | 1 83 | 60 84 | 60 85 | 86 | 87 | 88 | 89 | 1 90 | start 91 | center 92 | 10 93 | License: 94 | 95 | 96 | 97 | 98 | 1 99 | 1 100 | 1 101 | 1 102 | 0 103 | word 104 | text_buffer_license 105 | 0 106 | 107 | 108 | 109 | 110 | https://slgobinath.github.io/SafeEyes 111 | 1 112 | 1 113 | 0 114 | 1 115 | center 116 | 0 117 | https://slgobinath.github.io/SafeEyes 118 | 119 | 120 | 121 | 122 | 123 | 124 | 1 125 | 5 126 | 5 127 | 128 | 129 | 130 | 131 | 1 132 | start 133 | 5 134 | 135 | 136 | List of Contributors 137 | 1 138 | 1 139 | 0 140 | 1 141 | center 142 | https://github.com/slgobinath/SafeEyes/graphs/contributors?type=a 143 | 144 | 145 | 146 | 147 | Close 148 | 1 149 | 1 150 | 1 151 | 1 152 | 1 153 | 154 | 155 | 156 | 157 | Help us translate this app 158 | True 159 | True 160 | False 161 | True 162 | center 163 | 0 164 | https://github.com/slgobinath/SafeEyes?tab=readme-ov-file#how-you-can-help-improving-translation-of-safe-eyes 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /safeeyes/glade/break_screen.glade: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 24 | io.github.slgobinath.SafeEyes 25 | 0 26 | 0 27 | 28 | 29 | 30 | 31 | 32 | 1 33 | 1 34 | 35 | 36 | vertical 37 | 10 38 | 39 | 40 | center 41 | center 42 | 10 43 | 44 | 45 | 46 | 0 47 | 0 48 | 49 | 50 | 51 | 52 | 53 | center 54 | center 55 | 1 56 | 15 57 | 58 | 59 | Hello World 60 | center 61 | 64 | 65 | 0 66 | 0 67 | 3 68 | 69 | 70 | 71 | 72 | 73 | center 74 | 00 75 | 78 | 79 | 1 80 | 2 81 | 82 | 83 | 84 | 85 | 86 | center 87 | 50 88 | 1 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 1 97 | 3 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 0 124 | 1 125 | 3 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 1 134 | Widget 135 | 0.25 136 | 139 | 140 | 141 | 142 | 0 143 | 1 144 | 2 145 | 146 | 147 | 148 | 149 | 150 | vertical 151 | 152 | 153 | toolbar 154 | 0 155 | end 156 | start 157 | 160 | 161 | 162 | 163 | 164 | 1 165 | 166 | 167 | 170 | 171 | 0 172 | 0 173 | 174 | 175 | 176 | 177 | 178 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /safeeyes/glade/item_bool.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | 1 26 | 5 27 | 5 28 | 5 29 | 5 30 | 10 31 | 1 32 | 33 | 34 | 1 35 | center 36 | start 37 | label 38 | 0 39 | 40 | 41 | 42 | 43 | 1 44 | 1 45 | end 46 | center 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /safeeyes/glade/item_break.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | 1 26 | 5 27 | 5 28 | 5 29 | 5 30 | 3 31 | 0 32 | 33 | 34 | 1 35 | center 36 | label 37 | 0 38 | 1 39 | 1 40 | 41 | 42 | 43 | 44 | 1 45 | 1 46 | 1 47 | center 48 | center 49 | gtk-properties 50 | 53 | 54 | 55 | 56 | 57 | 1 58 | 1 59 | center 60 | center 61 | edit-delete 62 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /safeeyes/glade/item_int.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | 100 26 | 1 27 | 10 28 | 29 | 30 | 1 31 | 5 32 | 5 33 | 5 34 | 5 35 | 10 36 | 1 37 | 38 | 39 | start 40 | 1 41 | center 42 | label 43 | 0 44 | 45 | 46 | 47 | 48 | 1 49 | 1 50 | end 51 | center 52 | adjustment_value 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /safeeyes/glade/item_plugin.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | 1 26 | 5 27 | 5 28 | 5 29 | 5 30 | 0 31 | 32 | 33 | 1 34 | center 35 | center 36 | gtk-about 37 | 38 | 39 | 40 | 41 | 1 42 | 5 43 | vertical 44 | 1 45 | 1 46 | 47 | 48 | 1 49 | start 50 | end 51 | Plugin Name 52 | 0.05000000074505806 53 | 1 54 | 1 55 | 1 56 | 59 | 60 | 61 | 62 | 63 | 1 64 | start 65 | start 66 | Plugin Description 67 | 0.05000000074505806 68 | 0 69 | 1 70 | 1 71 | 74 | 75 | 76 | 77 | 78 | Click here for more information 79 | https://slgobinath.github.io/SafeEyes 80 | 0 81 | 0 82 | 5 83 | 5 84 | start 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 1 95 | 5 96 | 97 | 98 | 1 99 | 1 100 | end 101 | center 102 | 103 | 104 | 105 | 106 | False 107 | center 108 | center 109 | gtk-cancel 110 | Disable permanently 111 | 114 | 115 | 116 | 117 | 118 | 1 119 | 1 120 | 1 121 | center 122 | center 123 | gtk-properties 124 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /safeeyes/glade/item_text.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | 1 26 | 5 27 | 5 28 | 5 29 | 5 30 | 10 31 | 1 32 | 33 | 34 | 1 35 | center 36 | start 37 | label 38 | 0 39 | 40 | 41 | 42 | 43 | 1 44 | 1 45 | end 46 | center 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /safeeyes/glade/new_break.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | 100 26 | 1 27 | 10 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Short 37 | 38 | 39 | Long 40 | 41 | 42 | 43 | 44 | New Break 45 | 0 46 | 1 47 | 500 48 | 50 49 | 1 50 | io.github.slgobinath.SafeEyes 51 | 52 | 53 | 1 54 | 10 55 | 10 56 | 10 57 | 10 58 | vertical 59 | 5 60 | 61 | 62 | 1 63 | 64 | 65 | 1 66 | 12 67 | 10 68 | 5 69 | 10 70 | vertical 71 | 3 72 | 73 | 74 | 1 75 | 1 76 | center 77 | 64 78 | 79 | 80 | 81 | 82 | 1 83 | 10 84 | 1 85 | 86 | 87 | 1 88 | start 89 | center 90 | Type 91 | 92 | 93 | 94 | 95 | end 96 | 1 97 | lst_break_types 98 | 0 99 | 0 100 | 101 | 102 | 103 | 0 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 1 115 | Break 116 | 117 | 118 | 119 | 120 | 121 | 122 | 1 123 | 10 124 | 1 125 | top 126 | end 127 | 128 | 129 | Discard 130 | 1 131 | 1 132 | 1 133 | 134 | 135 | 136 | 137 | Save 138 | 1 139 | 1 140 | 1 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /safeeyes/glade/required_plugin_dialog.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | Safe Eyes - Error 26 | 0 27 | io.github.slgobinath.SafeEyes 28 | 29 | 30 | 5 31 | 5 32 | 5 33 | 5 34 | 1 35 | 1 36 | vertical 37 | top 38 | 39 | 40 | start 41 | 1 42 | vertical 43 | 44 | 45 | center 46 | 10 47 | 10 48 | A required plugin is missing dependencies! 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 5 58 | 5 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Click here for more information 68 | https://slgobinath.github.io/SafeEyes 69 | 0 70 | 71 | 72 | 73 | 74 | 5 75 | 5 76 | 5 77 | 5 78 | 1 79 | center 80 | 60 81 | Please install the dependencies or disable the plugin. To hide this message, you can also deactivate the plugin in the settings. 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 5 92 | 5 93 | 94 | 95 | 96 | 97 | start 98 | end 99 | 5 100 | 1 101 | 1 102 | 5 103 | 104 | 105 | Quit 106 | 1 107 | 1 108 | 109 | 110 | 111 | 112 | Disable plugin temporarily 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /safeeyes/glade/settings_plugin.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | Plugin Settings 26 | 0 27 | 1 28 | 400 29 | 10 30 | 1 31 | io.github.slgobinath.SafeEyes 32 | 33 | 34 | 1 35 | 10 36 | 10 37 | vertical 38 | 15 39 | 1 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/128x128/apps/io.github.slgobinath.SafeEyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/128x128/apps/io.github.slgobinath.SafeEyes.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/16x16/apps/io.github.slgobinath.SafeEyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/16x16/apps/io.github.slgobinath.SafeEyes.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-disabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-enabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-timer.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/24x24/apps/io.github.slgobinath.SafeEyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/24x24/apps/io.github.slgobinath.SafeEyes.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-disabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-enabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-timer.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/32x32/apps/io.github.slgobinath.SafeEyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/32x32/apps/io.github.slgobinath.SafeEyes.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-disabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-enabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/48x48/apps/io.github.slgobinath.SafeEyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/48x48/apps/io.github.slgobinath.SafeEyes.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-disabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-enabled.png -------------------------------------------------------------------------------- /safeeyes/platform/icons/hicolor/64x64/apps/io.github.slgobinath.SafeEyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/platform/icons/hicolor/64x64/apps/io.github.slgobinath.SafeEyes.png -------------------------------------------------------------------------------- /safeeyes/platform/io.github.slgobinath.SafeEyes.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Safe Eyes 3 | Comment=Protect your eyes from eye strain 4 | Comment[ca]=Protegiu-vos els ulls de la fatiga visual 5 | Comment[cs]=Chraňte své oči před únavou 6 | Comment[de]=Schützt die Augen vor Überanstrengung 7 | Comment[es]=Protege tus ojos de la fatiga ocular 8 | Comment[et]=Kaitse oma silmi väsimuse eest 9 | Comment[fa]=محافظت چشم هااز ضعیف شدن 10 | Comment[fr]=Protégez vos yeux de la fatigue 11 | Comment[ge]=დაიცავით თქვენი თვალები დაღლილობისაგან 12 | Comment[hi]=तनाव से आंखों की रक्षा 13 | Comment[hu]=Protect your eyes from eye strain 14 | Comment[id]=Melindungi mata Anda dari kelelahan 15 | Comment[it]=Proteggi i tuoi occhi dalla stanchezza 16 | Comment[lt]=Apsaugokite savo akis nuo įtampos 17 | Comment[lv]=Aizsargājiet savas acis no pārslodzes 18 | Comment[mk]=Заштитете се од замор на очите 19 | Comment[pl]=Chroń oczy przed zmęczeniem 20 | Comment[pt]=Proteja seus olhos da tensão ocular 21 | Comment[ru]=Защитите свои глаза от зрительного перенапряжения 22 | Comment[sk]=Chráňte svoje oči pred únavou 23 | Comment[ta]=உங்கள் கண்களை சோர்வடையாது பாதுகாத்திடுங்கள் 24 | Comment[tr]=Gözünüzü yorgunluğa karşı koruyun 25 | Comment[uk]=Захистіть свої очі від втоми 26 | Comment[vi]=Bảo vệ đôi mắt của bạn khỏi sự mệt mỏi 27 | GenericName=RSI Prevention 28 | Exec=safeeyes 29 | Icon=io.github.slgobinath.SafeEyes 30 | StartupNotify=false 31 | Terminal=false 32 | Type=Application 33 | Categories=Utility; 34 | -------------------------------------------------------------------------------- /safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.slgobinath.SafeEyes 4 | 5 | Safe Eyes 6 | 7 | 8 | Gobinath 9 | 10 | 11 | A FOSS tool for Linux users to reduce and prevent repetitive strain injury (RSI) 12 | 13 | CC0-1.0 14 | GPL-3.0 15 | 16 | 17 | Utility 18 | Accessibility 19 | 20 | 21 | 22 |

23 | Protect your eyes from eye strain using this simple and beautiful, yet extensible break 24 | reminder 25 |

26 |

27 | Features: 28 |

29 |

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

35 |
36 | 37 | io.github.slgobinath.SafeEyes.desktop 38 | 39 | 40 | Safe Eyes short break screen. 41 | https://slgobinath.github.io/SafeEyes/assets/screenshots/safeeyes_1.png 42 | 43 | 44 | Safe Eyes long break screen. 45 | https://slgobinath.github.io/SafeEyes/assets/screenshots/safeeyes_3.png 46 | 47 | 48 | Safe Eyes settings window. 49 | https://slgobinath.github.io/SafeEyes/assets/screenshots/safeeyes_6.png 50 | 51 | 52 | 53 | https://slgobinath.github.io/SafeEyes/ 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | -------------------------------------------------------------------------------- /safeeyes/plugins/audiblealert/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Audible Alert", 4 | "description": "Play audible alert before and after breaks", 5 | "version": "0.0.3" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": ["aplay"], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": ["on_pre_break.wav", "on_stop_break.wav"] 13 | }, 14 | "settings": [{ 15 | "id": "pre_break_alert", 16 | "label": "Play audible alert before breaks", 17 | "type": "BOOL", 18 | "default": true 19 | }, 20 | { 21 | "id": "post_break_alert", 22 | "label": "Play audible alert after breaks", 23 | "type": "BOOL", 24 | "default": true 25 | }], 26 | "break_override_allowed": true 27 | } -------------------------------------------------------------------------------- /safeeyes/plugins/audiblealert/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/audiblealert/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/audiblealert/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2017 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Audible Alert plugin plays a sound after each breaks to notify the user that 20 | the break has end. 21 | """ 22 | 23 | import logging 24 | from safeeyes import utility 25 | 26 | context = None 27 | pre_break_alert = False 28 | post_break_alert = False 29 | 30 | 31 | def play_sound(resource_name): 32 | """Play the audio resource. 33 | 34 | Arguments: 35 | --------- 36 | resource_name {string} -- name of the wav file resource 37 | 38 | """ 39 | logging.info("Playing audible alert %s", resource_name) 40 | try: 41 | # Open the sound file 42 | path = utility.get_resource_path(resource_name) 43 | if path is None: 44 | return 45 | utility.execute_command("aplay", ["-q", path]) 46 | 47 | except BaseException: 48 | logging.error("Failed to play audible alert %s", resource_name) 49 | 50 | 51 | def init(ctx, safeeyes_config, plugin_config): 52 | """Initialize the plugin.""" 53 | global context 54 | global pre_break_alert 55 | global post_break_alert 56 | logging.debug("Initialize Audible Alert plugin") 57 | context = ctx 58 | pre_break_alert = plugin_config["pre_break_alert"] 59 | post_break_alert = plugin_config["post_break_alert"] 60 | 61 | 62 | def on_pre_break(break_obj): 63 | """Play the pre_break sound if the option is enabled. 64 | 65 | Arguments: 66 | --------- 67 | break_obj {safeeyes.model.Break} -- the break object 68 | 69 | """ 70 | if pre_break_alert: 71 | play_sound("on_pre_break.wav") 72 | 73 | 74 | def on_stop_break(): 75 | """After the break, play the alert sound.""" 76 | # Do not play if the break is skipped or postponed 77 | if context["skipped"] or context["postponed"] or not post_break_alert: 78 | return 79 | play_sound("on_stop_break.wav") 80 | -------------------------------------------------------------------------------- /safeeyes/plugins/donotdisturb/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Do Not Disturb", 4 | "description": "Skip break if the active window is in fullscreen mode", 5 | "version": "0.0.2" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [{ 15 | "id": "skip_break_windows", 16 | "label": "Do not interrupt these windows anytime", 17 | "type": "TEXT", 18 | "default": "" 19 | }, 20 | { 21 | "id": "take_break_windows", 22 | "label": "Interrupt these windows regardless of their state", 23 | "type": "TEXT", 24 | "default": "" 25 | }, 26 | { 27 | "id": "unfullscreen", 28 | "label": "Switch the interruptible windows to normal mode", 29 | "type": "BOOL", 30 | "default": true 31 | }, 32 | { 33 | "id": "while_on_battery", 34 | "label": "Do not disturb while on battery", 35 | "type": "BOOL", 36 | "default": false 37 | } 38 | ], 39 | "break_override_allowed": true 40 | } 41 | -------------------------------------------------------------------------------- /safeeyes/plugins/donotdisturb/dependency_checker.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from safeeyes import utility 20 | 21 | 22 | def validate(plugin_config, plugin_settings): 23 | command = None 24 | if utility.IS_WAYLAND: 25 | if utility.DESKTOP_ENVIRONMENT == "gnome": 26 | return None 27 | command = "wlrctl" 28 | else: 29 | command = "xprop" 30 | if not utility.command_exist(command): 31 | return _("Please install the command-line tool '%s'") % command 32 | else: 33 | return None 34 | -------------------------------------------------------------------------------- /safeeyes/plugins/donotdisturb/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/donotdisturb/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/donotdisturb/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2017 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Skip Fullscreen plugin skips the break if the active window is fullscreen.""" 20 | 21 | import os 22 | import logging 23 | import subprocess 24 | 25 | import gi 26 | 27 | gi.require_version("Gio", "2.0") 28 | from gi.repository import Gio 29 | import Xlib 30 | from safeeyes import utility 31 | 32 | context = None 33 | skip_break_window_classes: list[str] = [] 34 | take_break_window_classes: list[str] = [] 35 | unfullscreen_allowed = True 36 | dnd_while_on_battery = False 37 | 38 | 39 | def is_active_window_skipped_wayland(pre_break): 40 | cmdlist = ["wlrctl", "toplevel", "find", "state:fullscreen"] 41 | try: 42 | process = subprocess.Popen(cmdlist, stdout=subprocess.PIPE) 43 | process.communicate()[0] 44 | if process.returncode == 0: 45 | return True 46 | elif process.returncode == 1: 47 | return False 48 | elif process.returncode == 127: 49 | logging.warning( 50 | "Could not find wlrctl needed to detect fullscreen under wayland" 51 | ) 52 | return False 53 | except subprocess.CalledProcessError: 54 | logging.warning("Error in finding full-screen application") 55 | return False 56 | 57 | 58 | def is_active_window_skipped_xorg(pre_break): 59 | """Check for full-screen applications. 60 | 61 | This method must be executed by the main thread. If not, it will 62 | cause random failure. 63 | """ 64 | logging.info("Searching for full-screen application") 65 | 66 | def get_window_property(window, prop, proptype): 67 | result = window.get_full_property(prop, proptype) 68 | if result: 69 | return result.value 70 | 71 | return None 72 | 73 | def get_active_window(x11_display): 74 | """Get active window using EWMH hints. 75 | 76 | Returns None if there is no active window. 77 | This always returns None if the window manager does not use EWMH hints. 78 | However, GTK3 also used this method to get the active window. 79 | """ 80 | root = x11_display.screen().root 81 | NET_ACTIVE_WINDOW = x11_display.intern_atom("_NET_ACTIVE_WINDOW") 82 | 83 | active_windows = get_window_property(root, NET_ACTIVE_WINDOW, Xlib.Xatom.WINDOW) 84 | if active_windows and active_windows[0]: 85 | active_window = active_windows[0] 86 | return x11_display.create_resource_object("window", active_window) 87 | return None 88 | 89 | x11_display = Xlib.display.Display() 90 | 91 | active_window = get_active_window(x11_display) 92 | 93 | if active_window: 94 | NET_WM_STATE = x11_display.intern_atom("_NET_WM_STATE") 95 | NET_WM_STATE_FULLSCREEN = x11_display.intern_atom("_NET_WM_STATE_FULLSCREEN") 96 | 97 | props = get_window_property(active_window, NET_WM_STATE, Xlib.Xatom.ATOM) 98 | is_fullscreen = props and NET_WM_STATE_FULLSCREEN in props.tolist() 99 | 100 | process_names = active_window.get_wm_class() 101 | 102 | if is_fullscreen: 103 | logging.info("fullscreen window found") 104 | 105 | if process_names: 106 | process_name = process_names[1].lower() 107 | if _window_class_matches(process_name, skip_break_window_classes): 108 | logging.info("found uninterruptible window") 109 | return True 110 | elif _window_class_matches(process_name, take_break_window_classes): 111 | logging.info("found interruptible window") 112 | if is_fullscreen and unfullscreen_allowed and not pre_break: 113 | logging.info("interrupting interruptible window") 114 | try: 115 | # To change the fullscreen state, we cannot simply set the 116 | # property - we must send a ClientMessage event 117 | # See https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html#id-1.6.8 118 | root_window = x11_display.screen().root 119 | 120 | cm_event = Xlib.protocol.event.ClientMessage( 121 | window=active_window, 122 | client_type=NET_WM_STATE, 123 | data=( 124 | 32, 125 | [ 126 | 0, # _NET_WM_STATE_REMOVE 127 | NET_WM_STATE_FULLSCREEN, 128 | 0, # other property, must be 0 129 | 1, # source indication 130 | 0, # must be 0 131 | ], 132 | ), 133 | ) 134 | 135 | mask = ( 136 | Xlib.X.SubstructureRedirectMask 137 | | Xlib.X.SubstructureNotifyMask 138 | ) 139 | 140 | root_window.send_event(cm_event, event_mask=mask) 141 | 142 | x11_display.sync() 143 | 144 | except BaseException as e: 145 | logging.error( 146 | "Error in unfullscreen the window " + process_name, 147 | exc_info=e, 148 | ) 149 | return False 150 | 151 | return is_fullscreen 152 | 153 | return False 154 | 155 | 156 | def is_idle_inhibited_gnome(): 157 | """GNOME Shell doesn't work with wlrctl, and there is no way to enumerate 158 | fullscreen windows, but GNOME does expose whether idle actions like 159 | starting a screensaver are inhibited, which is a close approximation if not 160 | a better metric. 161 | """ 162 | dbus_proxy = Gio.DBusProxy.new_for_bus_sync( 163 | bus_type=Gio.BusType.SESSION, 164 | flags=Gio.DBusProxyFlags.NONE, 165 | info=None, 166 | name="org.gnome.SessionManager", 167 | object_path="/org/gnome/SessionManager", 168 | interface_name="org.gnome.SessionManager", 169 | cancellable=None, 170 | ) 171 | result = dbus_proxy.get_cached_property("InhibitedActions").unpack() 172 | 173 | # The result is a bitfield, documented here: 174 | # https://gitlab.gnome.org/GNOME/gnome-session/-/blob/9aa419397b7f6d42bee6e66cc5c5aad12902fba0/gnome-session/org.gnome.SessionManager.xml#L155 175 | # The fourth bit indicates that idle is inhibited. 176 | return bool(result & 0b1000) 177 | 178 | 179 | def _window_class_matches(window_class: str, classes: list) -> bool: 180 | return any(map(lambda w: w in classes, window_class.split())) 181 | 182 | 183 | def is_on_battery(): 184 | """Check if the computer is running on battery.""" 185 | on_battery = False 186 | available_power_sources = os.listdir("/sys/class/power_supply") 187 | logging.info( 188 | "Looking for battery status in available power sources: %s" 189 | % str(available_power_sources) 190 | ) 191 | for power_source in available_power_sources: 192 | if "BAT" in power_source: 193 | # Found battery 194 | battery_status = os.path.join( 195 | "/sys/class/power_supply", power_source, "status" 196 | ) 197 | if os.path.isfile(battery_status): 198 | # Additional check to confirm that the status file exists 199 | try: 200 | with open(battery_status, "r") as status_file: 201 | status = status_file.read() 202 | if status: 203 | on_battery = "discharging" in status.lower() 204 | except BaseException: 205 | logging.error("Failed to read %s" % battery_status) 206 | break 207 | return on_battery 208 | 209 | 210 | def init(ctx, safeeyes_config, plugin_config): 211 | global context 212 | global skip_break_window_classes 213 | global take_break_window_classes 214 | global unfullscreen_allowed 215 | global dnd_while_on_battery 216 | logging.debug("Initialize Skip Fullscreen plugin") 217 | context = ctx 218 | skip_break_window_classes = _normalize_window_classes( 219 | plugin_config["skip_break_windows"] 220 | ) 221 | take_break_window_classes = _normalize_window_classes( 222 | plugin_config["take_break_windows"] 223 | ) 224 | unfullscreen_allowed = plugin_config["unfullscreen"] 225 | dnd_while_on_battery = plugin_config["while_on_battery"] 226 | 227 | 228 | def _normalize_window_classes(classes_as_str: str): 229 | return [w.lower() for w in classes_as_str.split()] 230 | 231 | 232 | def on_pre_break(break_obj): 233 | """Lifecycle method executes before the pre-break period.""" 234 | if utility.IS_WAYLAND: 235 | if utility.DESKTOP_ENVIRONMENT == "gnome": 236 | skip_break = is_idle_inhibited_gnome() 237 | else: 238 | skip_break = is_active_window_skipped_wayland(True) 239 | else: 240 | skip_break = is_active_window_skipped_xorg(True) 241 | if dnd_while_on_battery and not skip_break: 242 | skip_break = is_on_battery() 243 | return skip_break 244 | 245 | 246 | def on_start_break(break_obj): 247 | """Lifecycle method executes just before the break.""" 248 | if utility.IS_WAYLAND: 249 | if utility.DESKTOP_ENVIRONMENT == "gnome": 250 | skip_break = is_idle_inhibited_gnome() 251 | else: 252 | skip_break = is_active_window_skipped_wayland(False) 253 | else: 254 | skip_break = is_active_window_skipped_xorg(False) 255 | if dnd_while_on_battery and not skip_break: 256 | skip_break = is_on_battery() 257 | return skip_break 258 | -------------------------------------------------------------------------------- /safeeyes/plugins/healthstats/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Health Statistics", 4 | "description": "Show statistics based on how you use Safe Eyes", 5 | "version": "0.0.3" 6 | }, 7 | "dependencies": { 8 | "python_modules": ["croniter"], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [{ 15 | "id": "statistics_reset_cron", 16 | "label": "Statistics reset interval (cron expression)", 17 | "type": "TEXT", 18 | "default": "0 0 * * *" 19 | }], 20 | "break_override_allowed": true 21 | } -------------------------------------------------------------------------------- /safeeyes/plugins/healthstats/dependency_checker.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from safeeyes import utility 20 | 21 | 22 | def validate(plugin_config, plugin_settings): 23 | if not utility.module_exist("croniter"): 24 | return _("Please install the Python module '%s'") % "croniter" 25 | -------------------------------------------------------------------------------- /safeeyes/plugins/healthstats/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/healthstats/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/healthstats/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2017 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Show health statistics on the break screen.""" 20 | 21 | import croniter 22 | import datetime 23 | import logging 24 | 25 | context = None 26 | session = None 27 | statistics_reset_cron = None 28 | default_statistics_reset_cron = "0 0 * * *" # Every midnight 29 | next_reset_time = None 30 | start_time = None 31 | 32 | 33 | def init(ctx, safeeyes_config, plugin_config): 34 | """Initialize the plugin.""" 35 | global context 36 | global session 37 | global statistics_reset_cron 38 | 39 | logging.debug("Initialize Health Stats plugin") 40 | context = ctx 41 | statistics_reset_cron = plugin_config.get( 42 | "statistics_reset_cron", default_statistics_reset_cron 43 | ) 44 | 45 | if session is None: 46 | # Read the session 47 | defaults = { 48 | "breaks": 0, 49 | "skipped_breaks": 0, 50 | "screen_time": 0, 51 | "total_breaks": 0, 52 | "total_skipped_breaks": 0, 53 | "total_screen_time": 0, 54 | "total_resets": 0, 55 | } 56 | 57 | session = context["session"]["plugin"].get("healthstats", {}).copy() 58 | session.update(defaults) 59 | if "no_of_breaks" in session: 60 | # Ignore old format session. 61 | session = defaults 62 | context["session"]["plugin"]["healthstats"] = session 63 | 64 | _get_next_reset_time() 65 | 66 | 67 | def on_stop_break(): 68 | # Check if break was skipped. 69 | global session 70 | if context["skipped"]: 71 | session["skipped_breaks"] += 1 72 | 73 | # Screen time is starting again. 74 | on_start() 75 | 76 | 77 | def on_start_break(break_obj): 78 | global session 79 | session["breaks"] += 1 80 | 81 | # Screen time has stopped. 82 | on_stop() 83 | 84 | 85 | def on_stop(): 86 | global start_time 87 | _reset_stats() 88 | if start_time: 89 | screen_time = datetime.datetime.now() - start_time 90 | session["screen_time"] += round(screen_time.total_seconds()) 91 | start_time = None 92 | 93 | 94 | def get_widget_title(break_obj): 95 | """Return the widget title.""" 96 | return _("Health Statistics") 97 | 98 | 99 | def _reset_stats(): 100 | global session 101 | 102 | # Check if the reset time has passed 103 | if next_reset_time and datetime.datetime.now() >= next_reset_time: 104 | logging.info("Resetting the health statistics") 105 | 106 | # Update the next_reset_time 107 | _get_next_reset_time() 108 | 109 | # Reset statistics 110 | session["total_breaks"] += session["breaks"] 111 | session["total_skipped_breaks"] += session["skipped_breaks"] 112 | session["total_screen_time"] += session["screen_time"] 113 | session["total_resets"] += 1 114 | session["breaks"] = 0 115 | session["skipped_breaks"] = 0 116 | session["screen_time"] = 0 117 | 118 | 119 | def get_widget_content(break_obj): 120 | """Return the statistics.""" 121 | global next_reset_time 122 | resets = session["total_resets"] 123 | if ( 124 | session["screen_time"] > 21600 125 | or (session["breaks"] and session["skipped_breaks"] / session["breaks"]) >= 0.2 126 | ): 127 | # Unhealthy behavior -> Red broken heart 128 | heart = "💔️" 129 | else: 130 | # Healthy behavior -> Green heart 131 | heart = "💚" 132 | 133 | content = [ 134 | heart, 135 | f"BREAKS: {session['breaks']}", 136 | f"SKIPPED: {session['skipped_breaks']}", 137 | f"SCREEN TIME: {_format_interval(session['screen_time'])}", 138 | ] 139 | 140 | if resets: 141 | content[1] += f" [{round(session['total_breaks'] / resets, 1)}]" 142 | content[2] += f" [{round(session['total_skipped_breaks'] / resets, 1)}]" 143 | content[3] += f" [{_format_interval(session['total_screen_time'] / resets)}]" 144 | 145 | content = "\t".join(content) 146 | if resets: 147 | content += f"\n\t[] = average of {resets} reset(s)" 148 | if next_reset_time is None: 149 | content += ( 150 | f"\n\tSettings error in statistics reset interval: {statistics_reset_cron}" 151 | ) 152 | return content 153 | 154 | 155 | def on_start(): 156 | """Track the start time.""" 157 | global start_time 158 | _reset_stats() 159 | start_time = datetime.datetime.now() 160 | 161 | 162 | def _get_next_reset_time(): 163 | global next_reset_time 164 | global session 165 | 166 | try: 167 | cron = croniter.croniter(statistics_reset_cron, datetime.datetime.now()) 168 | next_reset_time = cron.get_next(datetime.datetime) 169 | session["next_reset_time"] = next_reset_time.strftime("%Y-%m-%d %H:%M:%S") 170 | logging.debug("Health stats will be reset at " + session["next_reset_time"]) 171 | except: # noqa E722 172 | # TODO: consider catching Exception here instead of bare except 173 | logging.error("Error in statistics reset expression: " + statistics_reset_cron) 174 | next_reset_time = None 175 | 176 | 177 | def _format_interval(seconds): 178 | screen_time = round(seconds / 60) 179 | hours, minutes = divmod(screen_time, 60) 180 | return "{:02d}:{:02d}".format(hours, minutes) 181 | -------------------------------------------------------------------------------- /safeeyes/plugins/limitconsecutiveskipping/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Limit Consecutive Skipping", 4 | "description": "Limit how many breaks can be skipped or postponed in a row", 5 | "version": "0.0.1" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [{ 15 | "id": "number_of_allowed_skips_in_a_row", 16 | "label": "How many skips or postpones are allowed in a row", 17 | "type": "INT", 18 | "default": 2, 19 | "min": 1, 20 | "max": 100 21 | }], 22 | "break_override_allowed": true 23 | } -------------------------------------------------------------------------------- /safeeyes/plugins/limitconsecutiveskipping/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/limitconsecutiveskipping/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/limitconsecutiveskipping/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2024 Leo (@undefiened), based on the code written by Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Limit how many breaks can be skipped or postponed in a row.""" 20 | 21 | import logging 22 | 23 | context = None 24 | no_of_skipped_breaks = 0 25 | session = None 26 | enabled = True 27 | 28 | 29 | def init(ctx, safeeyes_config, plugin_config): 30 | """Initialize the plugin.""" 31 | global enabled 32 | global context 33 | global session 34 | global no_of_skipped_breaks 35 | global no_allowed_skips 36 | 37 | logging.debug("Initialize Limit consecutive skipping plugin") 38 | context = ctx 39 | 40 | no_allowed_skips = plugin_config.get("number_of_allowed_skips_in_a_row", 2) 41 | 42 | if session is None: 43 | # Read the session 44 | session = context["session"]["plugin"].get("limitconsecutiveskipping", None) 45 | if session is None: 46 | session = {"no_of_skipped_breaks": 0} 47 | context["session"]["plugin"]["limitconsecutiveskipping"] = session 48 | no_of_skipped_breaks = session.get("no_of_skipped_breaks", 0) 49 | 50 | 51 | def on_stop_break(): 52 | """After the break, check if it is skipped.""" 53 | # Check if the plugin is enabled 54 | if not enabled: 55 | return 56 | 57 | global no_of_skipped_breaks 58 | if context["skipped"] or context["postponed"]: 59 | no_of_skipped_breaks += 1 60 | session["no_of_skipped_breaks"] = no_of_skipped_breaks 61 | else: 62 | no_of_skipped_breaks = 0 63 | session["no_of_skipped_breaks"] = no_of_skipped_breaks 64 | 65 | 66 | def on_start_break(break_obj): 67 | logging.debug( 68 | "Skipped / allowed = {} / {}".format(no_of_skipped_breaks, no_allowed_skips) 69 | ) 70 | 71 | if no_of_skipped_breaks >= no_allowed_skips: 72 | context["postpone_button_disabled"] = True 73 | context["skip_button_disabled"] = True 74 | 75 | 76 | def get_widget_title(break_obj): 77 | """Return the widget title.""" 78 | # Check if the plugin is enabled 79 | if not enabled: 80 | return "" 81 | 82 | return _("Limit Consecutive Skipping") 83 | 84 | 85 | def get_widget_content(break_obj): 86 | """Return the statistics.""" 87 | # Check if the plugin is enabled 88 | if not enabled: 89 | return "" 90 | 91 | return _("Skipped or postponed %(num)d/%(allowed)d breaks in a row") % { 92 | "num": no_of_skipped_breaks, 93 | "allowed": no_allowed_skips, 94 | } 95 | -------------------------------------------------------------------------------- /safeeyes/plugins/mediacontrol/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Media Control", 4 | "description": "Pause media players from the break screen", 5 | "version": "0.0.1" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [], 15 | "break_override_allowed": true 16 | } -------------------------------------------------------------------------------- /safeeyes/plugins/mediacontrol/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/mediacontrol/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/mediacontrol/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2019 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Media Control plugin lets users to pause currently playing media player from 20 | the break screen. 21 | """ 22 | 23 | import logging 24 | import os 25 | import re 26 | import gi 27 | from safeeyes.model import TrayAction 28 | 29 | gi.require_version("Gio", "2.0") 30 | from gi.repository import Gio 31 | 32 | tray_icon_path = None 33 | 34 | 35 | def __active_players(): 36 | """List of all media players which are playing now.""" 37 | players = [] 38 | 39 | dbus_proxy = Gio.DBusProxy.new_for_bus_sync( 40 | bus_type=Gio.BusType.SESSION, 41 | flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, 42 | info=None, 43 | name="org.freedesktop.DBus", 44 | object_path="/org/freedesktop/DBus", 45 | interface_name="org.freedesktop.DBus", 46 | cancellable=None, 47 | ) 48 | 49 | for service in dbus_proxy.ListNames(): 50 | if re.match("org.mpris.MediaPlayer2.", service): 51 | player = Gio.DBusProxy.new_for_bus_sync( 52 | bus_type=Gio.BusType.SESSION, 53 | flags=Gio.DBusProxyFlags.NONE, 54 | info=None, 55 | name=service, 56 | object_path="/org/mpris/MediaPlayer2", 57 | interface_name="org.mpris.MediaPlayer2.Player", 58 | cancellable=None, 59 | ) 60 | 61 | playbackstatus = player.get_cached_property("PlaybackStatus") 62 | 63 | if playbackstatus is not None: 64 | status = playbackstatus.unpack().lower() 65 | 66 | if status == "playing": 67 | players.append(player) 68 | else: 69 | logging.warning(f"Failed to get PlaybackStatus for {service}") 70 | 71 | return players 72 | 73 | 74 | def __pause_players(players): 75 | """Pause all playing media players using dbus.""" 76 | for player in players: 77 | player.Pause() 78 | 79 | 80 | def init(ctx, safeeyes_config, plugin_config): 81 | """Initialize the screensaver plugin.""" 82 | global tray_icon_path 83 | tray_icon_path = os.path.join(plugin_config["path"], "resource/pause.png") 84 | 85 | 86 | def get_tray_action(break_obj): 87 | """Return TrayAction only if there is a media player currently playing.""" 88 | players = __active_players() 89 | if players: 90 | return TrayAction.build( 91 | "Pause media", 92 | tray_icon_path, 93 | "media-playback-pause", 94 | lambda: __pause_players(players), 95 | ) 96 | -------------------------------------------------------------------------------- /safeeyes/plugins/mediacontrol/resource/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/mediacontrol/resource/pause.png -------------------------------------------------------------------------------- /safeeyes/plugins/notification/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Notification", 4 | "description": "Show a system notification before breaks", 5 | "version": "0.0.1" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [] 15 | } -------------------------------------------------------------------------------- /safeeyes/plugins/notification/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/notification/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/notification/plugin.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | 21 | import gi 22 | from safeeyes.model import BreakType 23 | 24 | gi.require_version("Notify", "0.7") 25 | from gi.repository import Notify 26 | 27 | """ 28 | Safe Eyes Notification plugin 29 | """ 30 | 31 | APPINDICATOR_ID = "safeeyes" 32 | notification = None 33 | context = None 34 | warning_time = 10 35 | 36 | Notify.init(APPINDICATOR_ID) 37 | 38 | 39 | def init(ctx, safeeyes_config, plugin_config): 40 | """Initialize the plugin.""" 41 | global context 42 | global warning_time 43 | logging.debug("Initialize Notification plugin") 44 | context = ctx 45 | warning_time = safeeyes_config.get("pre_break_warning_time") 46 | 47 | 48 | def on_pre_break(break_obj): 49 | """Show the notification.""" 50 | # Construct the message based on the type of the next break 51 | global notification 52 | logging.info("Show the notification") 53 | message = "\n" 54 | if break_obj.type == BreakType.SHORT_BREAK: 55 | message += _("Ready for a short break in %s seconds") % warning_time 56 | else: 57 | message += _("Ready for a long break in %s seconds") % warning_time 58 | 59 | notification = Notify.Notification.new( 60 | "Safe Eyes", message, icon="io.github.slgobinath.SafeEyes-enabled" 61 | ) 62 | try: 63 | notification.show() 64 | except BaseException: 65 | logging.error("Failed to show the notification") 66 | 67 | 68 | def on_start_break(break_obj): 69 | """Close the notification.""" 70 | global notification 71 | logging.info("Close pre-break notification") 72 | if notification: 73 | try: 74 | notification.close() 75 | notification = None 76 | except BaseException: 77 | # Some operating systems automatically close the notification. 78 | pass 79 | 80 | 81 | def on_exit(): 82 | """Uninitialize the registered notification.""" 83 | logging.debug("Stop Notification plugin") 84 | Notify.uninit() 85 | -------------------------------------------------------------------------------- /safeeyes/plugins/screensaver/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Screensaver", 4 | "description": "Lock the screen after long breaks by starting screensaver", 5 | "version": "0.0.2" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [ 15 | { 16 | "id": "command", 17 | "label": "Custom screensaver command", 18 | "type": "TEXT", 19 | "default": "" 20 | }, 21 | { 22 | "id": "min_seconds", 23 | "label": "Minimum seconds to skip without screensaver", 24 | "type": "INT", 25 | "default": 3, 26 | "max": 60, 27 | "min": 0 28 | } 29 | ], 30 | "break_override_allowed": true 31 | } -------------------------------------------------------------------------------- /safeeyes/plugins/screensaver/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/screensaver/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/screensaver/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2017 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """Screensaver plugin locks the desktop using native screensaver application, 20 | after long breaks. 21 | """ 22 | 23 | import logging 24 | import os 25 | 26 | from safeeyes import utility 27 | from safeeyes.model import TrayAction 28 | 29 | context = None 30 | lock_screen = False 31 | user_locked_screen = False 32 | lock_screen_command = None 33 | min_seconds = 0 34 | seconds_passed = 0 35 | tray_icon_path = None 36 | 37 | 38 | def __lock_screen_command(): 39 | """Function tries to detect the screensaver command based on the current 40 | envinroment. 41 | 42 | Possible results: 43 | Gnome, Unity, Budgie: ['gnome-screensaver-command', '--lock'] 44 | Cinnamon: ['cinnamon-screensaver-command', '--lock'] 45 | Pantheon, LXDE: ['light-locker-command', '--lock'] 46 | Mate: ['mate-screensaver-command', '--lock'] 47 | KDE: ['qdbus', 'org.freedesktop.ScreenSaver', 48 | '/ScreenSaver', 'Lock'] 49 | XFCE: ['xflock4'] 50 | Otherwise: None 51 | """ 52 | desktop_session = os.environ.get("DESKTOP_SESSION") 53 | current_desktop = os.environ.get("XDG_CURRENT_DESKTOP") 54 | if desktop_session is not None: 55 | desktop_session = desktop_session.lower() 56 | if ( 57 | "xfce" in desktop_session 58 | or desktop_session.startswith("xubuntu") 59 | or (current_desktop is not None and "xfce" in current_desktop) 60 | ) and utility.command_exist("xflock4"): 61 | return ["xflock4"] 62 | elif desktop_session == "cinnamon" and utility.command_exist( 63 | "cinnamon-screensaver-command" 64 | ): 65 | return ["cinnamon-screensaver-command", "--lock"] 66 | elif ( 67 | desktop_session == "pantheon" or desktop_session.startswith("lubuntu") 68 | ) and utility.command_exist("light-locker-command"): 69 | return ["light-locker-command", "--lock"] 70 | elif desktop_session == "mate" and utility.command_exist( 71 | "mate-screensaver-command" 72 | ): 73 | return ["mate-screensaver-command", "--lock"] 74 | elif ( 75 | desktop_session == "kde" 76 | or "plasma" in desktop_session 77 | or desktop_session.startswith("kubuntu") 78 | or os.environ.get("KDE_FULL_SESSION") == "true" 79 | ): 80 | return ["qdbus", "org.freedesktop.ScreenSaver", "/ScreenSaver", "Lock"] 81 | elif ( 82 | desktop_session in ["gnome", "unity", "budgie-desktop"] 83 | or desktop_session.startswith("ubuntu") 84 | or desktop_session.startswith("gnome") 85 | ): 86 | if utility.command_exist("gnome-screensaver-command"): 87 | return ["gnome-screensaver-command", "--lock"] 88 | # From Gnome 3.8 no gnome-screensaver-command 89 | return [ 90 | "dbus-send", 91 | "--type=method_call", 92 | "--dest=org.gnome.ScreenSaver", 93 | "/org/gnome/ScreenSaver", 94 | "org.gnome.ScreenSaver.Lock", 95 | ] 96 | elif os.environ.get("GNOME_DESKTOP_SESSION_ID"): 97 | if "deprecated" not in os.environ.get( 98 | "GNOME_DESKTOP_SESSION_ID" 99 | ) and utility.command_exist("gnome-screensaver-command"): 100 | # Gnome 2 101 | return ["gnome-screensaver-command", "--lock"] 102 | return None 103 | 104 | 105 | def __lock_screen(): 106 | global user_locked_screen 107 | user_locked_screen = True 108 | 109 | 110 | def init(ctx, safeeyes_config, plugin_config): 111 | """Initialize the screensaver plugin.""" 112 | global context 113 | global lock_screen_command 114 | global min_seconds 115 | global tray_icon_path 116 | logging.debug("Initialize Screensaver plugin") 117 | context = ctx 118 | min_seconds = plugin_config["min_seconds"] 119 | tray_icon_path = os.path.join(plugin_config["path"], "resource/lock.png") 120 | if plugin_config["command"]: 121 | lock_screen_command = plugin_config["command"].split() 122 | else: 123 | lock_screen_command = __lock_screen_command() 124 | 125 | 126 | def on_start_break(break_obj): 127 | """Determine the break type and only if it is a long break, enable the 128 | lock_screen flag. 129 | """ 130 | global lock_screen 131 | global seconds_passed 132 | global user_locked_screen 133 | user_locked_screen = False 134 | seconds_passed = 0 135 | if lock_screen_command: 136 | lock_screen = break_obj.is_long_break() 137 | 138 | 139 | def on_countdown(countdown, seconds): 140 | """Keep track of seconds passed from the beginning of long break.""" 141 | global seconds_passed 142 | seconds_passed = seconds 143 | 144 | 145 | def on_stop_break(): 146 | """Lock the screen after a long break if the user has not skipped within 147 | min_seconds. 148 | """ 149 | if user_locked_screen or (lock_screen and seconds_passed >= min_seconds): 150 | utility.execute_command(lock_screen_command) 151 | 152 | 153 | def get_tray_action(break_obj): 154 | return TrayAction.build( 155 | "Lock screen", tray_icon_path, "dialog-password", __lock_screen 156 | ) 157 | -------------------------------------------------------------------------------- /safeeyes/plugins/screensaver/resource/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/screensaver/resource/lock.png -------------------------------------------------------------------------------- /safeeyes/plugins/smartpause/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Smart Pause", 4 | "description": "Pause Safe Eyes if the system is idle", 5 | "version": "0.0.3" 6 | }, 7 | "dependencies": { 8 | "python_modules": ["pywayland"], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "settings": [ 15 | { 16 | "id": "idle_time", 17 | "label": "Minimum idle time to pause Safe Eyes (in seconds)", 18 | "type": "INT", 19 | "default": 5, 20 | "max": 3600, 21 | "min": 5 22 | }, 23 | { 24 | "id": "postpone_if_active", 25 | "label": "Postpone the next break until the system becomes idle", 26 | "type": "BOOL", 27 | "default": false 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /safeeyes/plugins/smartpause/dependency_checker.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from safeeyes import utility 20 | 21 | 22 | def validate(plugin_config, plugin_settings): 23 | command = None 24 | if utility.DESKTOP_ENVIRONMENT == "gnome" and utility.IS_WAYLAND: 25 | command = "dbus-send" 26 | elif utility.DESKTOP_ENVIRONMENT == "sway": 27 | command = "swayidle" 28 | elif utility.IS_WAYLAND: 29 | if not utility.module_exist("pywayland"): 30 | return _("Please install the Python module '%s'") % "pywayland" 31 | # no command needed with pywayland 32 | return None 33 | else: 34 | command = "xprintidle" 35 | if not utility.command_exist(command): 36 | return _("Please install the command-line tool '%s'") % command 37 | else: 38 | return None 39 | -------------------------------------------------------------------------------- /safeeyes/plugins/smartpause/ext_idle_notify.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # This file is heavily inspired by https://github.com/juienpro/easyland/blob/efc26a0b22d7bdbb0f8436183428f7036da4662a/src/easyland/idle.py 20 | 21 | import threading 22 | import datetime 23 | import os 24 | import select 25 | 26 | from pywayland.client import Display 27 | from pywayland.protocol.wayland.wl_seat import WlSeat 28 | from pywayland.protocol.ext_idle_notify_v1 import ExtIdleNotifierV1 29 | 30 | 31 | class ExtIdleNotify: 32 | _idle_notifier = None 33 | _seat = None 34 | _notification = None 35 | _notifier_set = False 36 | _running = True 37 | _thread = None 38 | _r_channel = None 39 | _w_channel = None 40 | 41 | _idle_since = None 42 | 43 | def __init__(self): 44 | # Note that this creates a new connection to the wayland compositor. 45 | # This is not an issue per se, but does mean that the compositor sees this as 46 | # a new, separate client, that just happens to run in the same process as 47 | # the SafeEyes gtk application. 48 | # (This is not a problem, currently. swayidle does the same, it even runs in a 49 | # separate process.) 50 | # If in the future, a compositor decides to lock down ext-idle-notify-v1 to 51 | # clients somehow, and we need to share the connection to wl_display with gtk 52 | # (which might require some hacks, or FFI code), we might run into other issues 53 | # described in this mailing thread: 54 | # https://lists.freedesktop.org/archives/wayland-devel/2019-March/040344.html 55 | # The best thing would be, of course, for gtk to gain native support for 56 | # ext-idle-notify-v1. 57 | self._display = Display() 58 | self._display.connect() 59 | self._r_channel, self._w_channel = os.pipe() 60 | 61 | def stop(self): 62 | self._running = False 63 | # write anything, just to wake up the channel 64 | os.write(self._w_channel, b"!") 65 | self._notification.destroy() 66 | self._notification = None 67 | self._seat = None 68 | self._thread.join() 69 | os.close(self._r_channel) 70 | os.close(self._w_channel) 71 | 72 | def run(self): 73 | self._thread = threading.Thread( 74 | target=self._run, name="ExtIdleNotify", daemon=False 75 | ) 76 | self._thread.start() 77 | 78 | def _run(self): 79 | reg = self._display.get_registry() 80 | reg.dispatcher["global"] = self._global_handler 81 | 82 | display_fd = self._display.get_fd() 83 | 84 | while self._running: 85 | self._display.flush() 86 | 87 | # this blocks until either there are new events in self._display 88 | # (retrieved using dispatch()) 89 | # or until there are events in self._r_channel - which means that stop() 90 | # was called 91 | # unfortunately, this seems like the best way to make sure that dispatch 92 | # doesn't block potentially forever (up to multiple seconds in my usage) 93 | read, _w, _x = select.select((display_fd, self._r_channel), (), ()) 94 | 95 | if self._r_channel in read: 96 | # the channel was written to, which means stop() was called 97 | # at this point, self._running should be false as well 98 | break 99 | 100 | if display_fd in read: 101 | self._display.dispatch(block=True) 102 | 103 | self._display.disconnect() 104 | 105 | def _global_handler(self, reg, id_num, iface_name, version): 106 | if iface_name == "wl_seat": 107 | self._seat = reg.bind(id_num, WlSeat, version) 108 | if iface_name == "ext_idle_notifier_v1": 109 | self._idle_notifier = reg.bind(id_num, ExtIdleNotifierV1, version) 110 | 111 | if self._idle_notifier and self._seat and not self._notifier_set: 112 | self._notifier_set = True 113 | timeout_sec = 1 114 | self._notification = self._idle_notifier.get_idle_notification( 115 | timeout_sec * 1000, self._seat 116 | ) 117 | self._notification.dispatcher["idled"] = self._idle_notifier_handler 118 | self._notification.dispatcher["resumed"] = ( 119 | self._idle_notifier_resume_handler 120 | ) 121 | 122 | def _idle_notifier_handler(self, notification): 123 | self._idle_since = datetime.datetime.now() 124 | 125 | def _idle_notifier_resume_handler(self, notification): 126 | self._idle_since = None 127 | 128 | def get_idle_time_seconds(self): 129 | if self._idle_since is None: 130 | return 0 131 | 132 | result = datetime.datetime.now() - self._idle_since 133 | return result.total_seconds() 134 | -------------------------------------------------------------------------------- /safeeyes/plugins/smartpause/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/smartpause/icon.png -------------------------------------------------------------------------------- /safeeyes/plugins/smartpause/plugin.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import datetime 20 | import logging 21 | import subprocess 22 | import threading 23 | import re 24 | 25 | from safeeyes import utility 26 | from safeeyes.model import State 27 | 28 | """ 29 | Safe Eyes smart pause plugin 30 | """ 31 | 32 | context = None 33 | idle_condition = threading.Condition() 34 | lock = threading.Lock() 35 | active = False 36 | idle_time = 0 37 | enable_safeeyes = None 38 | disable_safeeyes = None 39 | smart_pause_activated = False 40 | idle_start_time = None 41 | next_break_time = None 42 | next_break_duration = 0 43 | short_break_interval = 0 44 | waiting_time = 2 45 | is_wayland_and_gnome = False 46 | 47 | use_swayidle = False 48 | use_ext_idle_notify = False 49 | swayidle_process = None 50 | swayidle_lock = threading.Lock() 51 | swayidle_idle = 0 52 | swayidle_active = 0 53 | 54 | ext_idle_notify_lock = threading.Lock() 55 | ext_idle_notification_obj = None 56 | 57 | 58 | # swayidle 59 | def __swayidle_running(): 60 | return swayidle_process is not None and swayidle_process.poll() is None 61 | 62 | 63 | def __start_swayidle_monitor(): 64 | global swayidle_process 65 | global swayidle_start 66 | global swayidle_idle 67 | global swayidle_active 68 | logging.debug("Starting swayidle subprocess") 69 | swayidle_process = subprocess.Popen( 70 | ["swayidle", "timeout", "1", "date +S%s", "resume", "date +R%s"], 71 | stdout=subprocess.PIPE, 72 | bufsize=1, 73 | universal_newlines=True, 74 | encoding="utf-8", 75 | ) 76 | for line in swayidle_process.stdout: 77 | with swayidle_lock: 78 | typ = line[0] 79 | timestamp = int(line[1:]) 80 | if typ == "S": 81 | swayidle_idle = timestamp 82 | elif typ == "R": 83 | swayidle_active = timestamp 84 | 85 | 86 | def __stop_swayidle_monitor(): 87 | if __swayidle_running(): 88 | logging.debug("Stopping swayidle subprocess") 89 | swayidle_process.terminate() 90 | 91 | 92 | def __swayidle_idle_time(): 93 | with swayidle_lock: 94 | if not __swayidle_running(): 95 | utility.start_thread(__start_swayidle_monitor) 96 | # Idle more recently than active, meaning idle time isn't stale. 97 | if swayidle_idle > swayidle_active: 98 | idle_time = int(datetime.datetime.now().timestamp()) - swayidle_idle 99 | return idle_time 100 | return 0 101 | 102 | 103 | # ext idle 104 | def __start_ext_idle_monitor(): 105 | global ext_idle_notification_obj 106 | 107 | from .ext_idle_notify import ExtIdleNotify 108 | 109 | ext_idle_notification_obj = ExtIdleNotify() 110 | ext_idle_notification_obj.run() 111 | 112 | 113 | def __stop_ext_idle_monitor(): 114 | global ext_idle_notification_obj 115 | 116 | with ext_idle_notify_lock: 117 | if ext_idle_notification_obj is not None: 118 | ext_idle_notification_obj.stop() 119 | ext_idle_notification_obj = None 120 | 121 | 122 | def __ext_idle_idle_time(): 123 | global ext_idle_notification_obj 124 | with ext_idle_notify_lock: 125 | if ext_idle_notification_obj is None: 126 | __start_ext_idle_monitor() 127 | else: 128 | return ext_idle_notification_obj.get_idle_time_seconds() 129 | return 0 130 | 131 | 132 | # gnome 133 | def __gnome_wayland_idle_time(): 134 | """Determine system idle time in seconds, specifically for gnome with 135 | wayland. 136 | 137 | If there's a failure, return 0. 138 | https://unix.stackexchange.com/a/492328/222290 139 | """ 140 | try: 141 | output = subprocess.check_output( 142 | [ 143 | "dbus-send", 144 | "--print-reply", 145 | "--dest=org.gnome.Mutter.IdleMonitor", 146 | "/org/gnome/Mutter/IdleMonitor/Core", 147 | "org.gnome.Mutter.IdleMonitor.GetIdletime", 148 | ] 149 | ) 150 | return int(re.search(rb"\d+$", output).group(0)) / 1000 151 | except BaseException as e: 152 | logging.warning("Failed to get system idle time for gnome/wayland.") 153 | logging.warning(str(e)) 154 | return 0 155 | 156 | 157 | def __system_idle_time(): 158 | """Get system idle time in minutes. 159 | 160 | Return the idle time if xprintidle is available, otherwise return 0. 161 | """ 162 | try: 163 | if is_wayland_and_gnome: 164 | return __gnome_wayland_idle_time() 165 | elif use_swayidle: 166 | return __swayidle_idle_time() 167 | elif use_ext_idle_notify: 168 | return __ext_idle_idle_time() 169 | # Convert to seconds 170 | return int(subprocess.check_output(["xprintidle"]).decode("utf-8")) / 1000 171 | except BaseException: 172 | return 0 173 | 174 | 175 | def __is_active(): 176 | """Thread safe function to see if this plugin is active or not.""" 177 | is_active = False 178 | with lock: 179 | is_active = active 180 | return is_active 181 | 182 | 183 | def __set_active(is_active): 184 | """Thread safe function to change the state of the plugin.""" 185 | global active 186 | with lock: 187 | active = is_active 188 | 189 | 190 | def init(ctx, safeeyes_config, plugin_config): 191 | """Initialize the plugin.""" 192 | global context 193 | global enable_safeeyes 194 | global disable_safeeyes 195 | global postpone 196 | global idle_time 197 | global short_break_interval 198 | global long_break_duration 199 | global waiting_time 200 | global postpone_if_active 201 | global is_wayland_and_gnome 202 | global use_swayidle 203 | global use_ext_idle_notify 204 | logging.debug("Initialize Smart Pause plugin") 205 | context = ctx 206 | enable_safeeyes = context["api"]["enable_safeeyes"] 207 | disable_safeeyes = context["api"]["disable_safeeyes"] 208 | postpone = context["api"]["postpone"] 209 | idle_time = plugin_config["idle_time"] 210 | postpone_if_active = plugin_config["postpone_if_active"] 211 | short_break_interval = ( 212 | safeeyes_config.get("short_break_interval") * 60 213 | ) # Convert to seconds 214 | long_break_duration = safeeyes_config.get("long_break_duration") 215 | waiting_time = min(2, idle_time) # If idle time is 1 sec, wait only 1 sec 216 | is_wayland_and_gnome = context["desktop"] == "gnome" and context["is_wayland"] 217 | use_swayidle = context["desktop"] == "sway" 218 | use_ext_idle_notify = ( 219 | context["is_wayland"] and not use_swayidle and not is_wayland_and_gnome 220 | ) 221 | 222 | 223 | def __start_idle_monitor(): 224 | """Continuously check the system idle time and pause/resume Safe Eyes based 225 | on it. 226 | """ 227 | global smart_pause_activated 228 | global idle_start_time 229 | 230 | while __is_active(): 231 | # Wait for waiting_time seconds 232 | idle_condition.acquire() 233 | idle_condition.wait(waiting_time) 234 | idle_condition.release() 235 | 236 | if __is_active(): 237 | # Get the system idle time 238 | system_idle_time = __system_idle_time() 239 | if system_idle_time >= idle_time and context["state"] == State.WAITING: 240 | smart_pause_activated = True 241 | idle_start_time = datetime.datetime.now() - datetime.timedelta( 242 | seconds=system_idle_time 243 | ) 244 | logging.info("Pause Safe Eyes due to system idle") 245 | disable_safeeyes(None, True) 246 | elif ( 247 | system_idle_time < idle_time 248 | and context["state"] == State.RESTING 249 | and idle_start_time is not None 250 | ): 251 | logging.info("Resume Safe Eyes due to user activity") 252 | smart_pause_activated = False 253 | idle_period = datetime.datetime.now() - idle_start_time 254 | idle_seconds = idle_period.total_seconds() 255 | context["idle_period"] = idle_seconds 256 | if idle_seconds < short_break_interval: 257 | # Credit back the idle time 258 | if next_break_time is not None: 259 | # This method runs in a thread since the start. 260 | # It may run before next_break is initialized in the 261 | # update_next_break method 262 | next_break = next_break_time + idle_period 263 | enable_safeeyes(next_break.timestamp()) 264 | else: 265 | enable_safeeyes() 266 | else: 267 | # User is idle for more than the time between two breaks 268 | enable_safeeyes() 269 | 270 | 271 | def on_start(): 272 | """Start a thread to continuously call xprintidle.""" 273 | global active 274 | if not __is_active(): 275 | # If SmartPause is already started, do not start it again 276 | logging.debug("Start Smart Pause plugin") 277 | __set_active(True) 278 | utility.start_thread(__start_idle_monitor) 279 | 280 | 281 | def on_stop(): 282 | """Stop the thread from continuously calling xprintidle.""" 283 | global active 284 | global smart_pause_activated 285 | if smart_pause_activated: 286 | # Safe Eyes is stopped due to system idle 287 | smart_pause_activated = False 288 | return 289 | logging.debug("Stop Smart Pause plugin") 290 | if use_swayidle: 291 | __stop_swayidle_monitor() 292 | __set_active(False) 293 | idle_condition.acquire() 294 | idle_condition.notify_all() 295 | idle_condition.release() 296 | 297 | if use_ext_idle_notify: 298 | __stop_ext_idle_monitor() 299 | 300 | 301 | def update_next_break(break_obj, dateTime): 302 | """Update the next break time.""" 303 | global next_break_time 304 | global next_break_duration 305 | next_break_time = dateTime 306 | next_break_duration = break_obj.duration 307 | 308 | 309 | def on_start_break(break_obj): 310 | """Lifecycle method executes just before the break.""" 311 | if postpone_if_active: 312 | # Postpone this break if the user is active 313 | system_idle_time = __system_idle_time() 314 | if system_idle_time < 2: 315 | postpone(2) # Postpone for 2 seconds 316 | 317 | 318 | def disable(): 319 | """SmartPause plugin was active earlier but now user has disabled it.""" 320 | # Remove the idle_period 321 | context.pop("idle_period", None) 322 | -------------------------------------------------------------------------------- /safeeyes/plugins/trayicon/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "Tray Icon", 4 | "description": "Show a tray icon in the notification area", 5 | "version": "0.0.3" 6 | }, 7 | "dependencies": { 8 | "python_modules": [], 9 | "shell_commands": [], 10 | "operating_systems": [], 11 | "desktop_environments": [], 12 | "resources": [] 13 | }, 14 | "required_plugin": true, 15 | "settings": [ 16 | { 17 | "id": "show_time_in_tray", 18 | "label": "Show next break time in tray icon", 19 | "type": "BOOL", 20 | "default": false 21 | }, 22 | { 23 | "id": "show_long_time_in_tray", 24 | "label": "Show only long breaks for tray icon time", 25 | "type": "BOOL", 26 | "default": false 27 | }, 28 | { 29 | "id": "allow_disabling", 30 | "label": "Allow disabling Safe Eyes", 31 | "type": "BOOL", 32 | "default": true 33 | }, 34 | { 35 | "id": "disable_options", 36 | "label": "Disable options", 37 | "type": "HIDDEN", 38 | "default": [ 39 | { 40 | "time": 30, 41 | "unit": "minute" 42 | }, 43 | { 44 | "time": 1, 45 | "unit": "hour" 46 | }, 47 | { 48 | "time": 2, 49 | "unit": "hour" 50 | }, 51 | { 52 | "time": 3, 53 | "unit": "hour" 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /safeeyes/plugins/trayicon/dependency_checker.py: -------------------------------------------------------------------------------- 1 | # Safe Eyes is a utility to remind you to take break frequently 2 | # to protect your eyes from eye strain. 3 | 4 | # Copyright (C) 2017 Gobinath 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from safeeyes import utility 20 | from safeeyes.model import PluginDependency 21 | 22 | import gi 23 | 24 | gi.require_version("Gio", "2.0") 25 | from gi.repository import Gio 26 | 27 | 28 | def validate(plugin_config, plugin_settings): 29 | dbus_proxy = Gio.DBusProxy.new_for_bus_sync( 30 | bus_type=Gio.BusType.SESSION, 31 | flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, 32 | info=None, 33 | name="org.freedesktop.DBus", 34 | object_path="/org/freedesktop/DBus", 35 | interface_name="org.freedesktop.DBus", 36 | cancellable=None, 37 | ) 38 | 39 | if dbus_proxy.NameHasOwner("(s)", "org.kde.StatusNotifierWatcher"): 40 | return None 41 | else: 42 | return PluginDependency( 43 | message=_( 44 | "Please install service providing tray icons for your desktop" 45 | " environment." 46 | ), 47 | link="https://github.com/slgobinath/SafeEyes/wiki/How-to-install-backend-for-Safe-Eyes-tray-icon", 48 | retryable=True, 49 | ) 50 | 51 | command = None 52 | if utility.IS_WAYLAND: 53 | if utility.DESKTOP_ENVIRONMENT == "gnome": 54 | return None 55 | command = "wlrctl" 56 | else: 57 | command = "xprop" 58 | if not utility.command_exist(command): 59 | return _("Please install the command-line tool '%s'") % command 60 | else: 61 | return None 62 | -------------------------------------------------------------------------------- /safeeyes/plugins/trayicon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/plugins/trayicon/icon.png -------------------------------------------------------------------------------- /safeeyes/resource/ic_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/resource/ic_plugin.png -------------------------------------------------------------------------------- /safeeyes/resource/ic_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/resource/ic_warning.png -------------------------------------------------------------------------------- /safeeyes/resource/on_pre_break.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/resource/on_pre_break.wav -------------------------------------------------------------------------------- /safeeyes/resource/on_stop_break.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/resource/on_stop_break.wav -------------------------------------------------------------------------------- /safeeyes/rpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2017 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """RPC server and client implementation.""" 20 | 21 | import logging 22 | from threading import Thread 23 | from xmlrpc.server import SimpleXMLRPCServer 24 | from xmlrpc.client import ServerProxy 25 | 26 | 27 | class RPCServer: 28 | """An asynchronous RPC server.""" 29 | 30 | def __init__(self, port, context): 31 | self.__running = False 32 | logging.info("Setting up an RPC server on port %d", port) 33 | self.__server = SimpleXMLRPCServer( 34 | ("localhost", port), logRequests=False, allow_none=True 35 | ) 36 | self.__server.register_function( 37 | context["api"]["show_settings"], "show_settings" 38 | ) 39 | self.__server.register_function(context["api"]["show_about"], "show_about") 40 | self.__server.register_function( 41 | context["api"]["enable_safeeyes"], "enable_safeeyes" 42 | ) 43 | self.__server.register_function( 44 | context["api"]["disable_safeeyes"], "disable_safeeyes" 45 | ) 46 | self.__server.register_function(context["api"]["take_break"], "take_break") 47 | self.__server.register_function(context["api"]["status"], "status") 48 | self.__server.register_function(context["api"]["quit"], "quit") 49 | 50 | def start(self): 51 | """Start the RPC server.""" 52 | if not self.__running: 53 | self.__running = True 54 | logging.info("Start the RPC server") 55 | server_thread = Thread(target=self.__server.serve_forever) 56 | server_thread.start() 57 | 58 | def stop(self): 59 | """Stop the server.""" 60 | if self.__running: 61 | logging.info("Stop the RPC server") 62 | self.__running = False 63 | self.__server.shutdown() 64 | 65 | 66 | class RPCClient: 67 | """An RPC client to communicate with the RPC server.""" 68 | 69 | def __init__(self, port): 70 | self.port = port 71 | self.proxy = ServerProxy("http://localhost:%d/" % self.port, allow_none=True) 72 | 73 | def show_settings(self): 74 | """Show the settings dialog.""" 75 | self.proxy.show_settings() 76 | 77 | def show_about(self): 78 | """Show the about dialog.""" 79 | self.proxy.show_about() 80 | 81 | def enable_safeeyes(self): 82 | """Enable Safe Eyes.""" 83 | self.proxy.enable_safeeyes() 84 | 85 | def disable_safeeyes(self): 86 | """Disable Safe Eyes.""" 87 | self.proxy.disable_safeeyes(None) 88 | 89 | def take_break(self): 90 | """Take a break now.""" 91 | self.proxy.take_break() 92 | 93 | def status(self): 94 | """Return the status of Safe Eyes.""" 95 | return self.proxy.status() 96 | 97 | def quit(self): 98 | """Quit Safe Eyes.""" 99 | self.proxy.quit() 100 | -------------------------------------------------------------------------------- /safeeyes/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slgobinath/SafeEyes/de504daeaa164c4e0253ae27073fecd37e082e96/safeeyes/ui/__init__.py -------------------------------------------------------------------------------- /safeeyes/ui/about_dialog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2016 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """This module creates the AboutDialog which shows the version and license.""" 20 | 21 | import os 22 | 23 | from safeeyes import utility 24 | 25 | ABOUT_DIALOG_GLADE = os.path.join(utility.BIN_DIRECTORY, "glade/about_dialog.glade") 26 | 27 | 28 | class AboutDialog: 29 | """AboutDialog reads the about_dialog.glade and build the user interface 30 | using that file. 31 | 32 | It shows the application name with version, a small description, 33 | license and the GitHub url. 34 | """ 35 | 36 | def __init__(self, application, version): 37 | builder = utility.create_gtk_builder(ABOUT_DIALOG_GLADE) 38 | self.window = builder.get_object("window_about") 39 | self.window.set_application(application) 40 | 41 | self.window.connect("close-request", self.on_window_delete) 42 | builder.get_object("btn_close").connect("clicked", self.on_close_clicked) 43 | 44 | builder.get_object("lbl_decription").set_label( 45 | _( 46 | "Safe Eyes protects your eyes from eye strain (asthenopia) by reminding" 47 | " you to take breaks while you're working long hours at the computer" 48 | ) 49 | ) 50 | builder.get_object("lbl_license").set_label(_("License") + ":") 51 | 52 | # Set the version at the runtime 53 | builder.get_object("lbl_app_name").set_label("Safe Eyes " + version) 54 | 55 | def show(self): 56 | """Show the About dialog.""" 57 | self.window.present() 58 | 59 | def on_window_delete(self, *args): 60 | """Window close event handler.""" 61 | self.window.destroy() 62 | 63 | def on_close_clicked(self, *args): 64 | """Close button click event handler.""" 65 | self.window.destroy() 66 | -------------------------------------------------------------------------------- /safeeyes/ui/required_plugin_dialog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2016 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | """This module creates the RequiredPluginDialog which shows the error for a 20 | required plugin. 21 | """ 22 | 23 | import os 24 | 25 | from safeeyes import utility 26 | from safeeyes.model import PluginDependency 27 | 28 | REQUIRED_PLUGIN_DIALOG_GLADE = os.path.join( 29 | utility.BIN_DIRECTORY, "glade/required_plugin_dialog.glade" 30 | ) 31 | 32 | 33 | class RequiredPluginDialog: 34 | """RequiredPluginDialog shows an error when a plugin has required 35 | dependencies. 36 | """ 37 | 38 | def __init__(self, plugin_id, plugin_name, message, on_quit, on_disable_plugin): 39 | self.on_quit = on_quit 40 | self.on_disable_plugin = on_disable_plugin 41 | 42 | builder = utility.create_gtk_builder(REQUIRED_PLUGIN_DIALOG_GLADE) 43 | self.window = builder.get_object("window_required_plugin") 44 | 45 | self.window.connect("close-request", self.on_window_delete) 46 | builder.get_object("btn_close").connect("clicked", self.on_close_clicked) 47 | builder.get_object("btn_disable_plugin").connect( 48 | "clicked", self.on_disable_plugin_clicked 49 | ) 50 | 51 | builder.get_object("lbl_header").set_label( 52 | _("The required plugin '%s' is missing dependencies!") % _(plugin_name) 53 | ) 54 | 55 | builder.get_object("lbl_main").set_label( 56 | _( 57 | "Please install the dependencies or disable the plugin. To hide this" 58 | " message, you can also deactivate the plugin in the settings." 59 | ) 60 | ) 61 | 62 | builder.get_object("btn_close").set_label(_("Quit")) 63 | builder.get_object("btn_disable_plugin").set_label( 64 | _("Disable plugin temporarily") 65 | ) 66 | 67 | if isinstance(message, PluginDependency): 68 | builder.get_object("lbl_message").set_label(_(message.message)) 69 | btn_extra_link = builder.get_object("btn_extra_link") 70 | btn_extra_link.set_label(_("Click here for more information")) 71 | btn_extra_link.set_uri(message.link) 72 | btn_extra_link.set_visible(True) 73 | else: 74 | builder.get_object("lbl_message").set_label(_(message)) 75 | 76 | def show(self): 77 | """Show the dialog.""" 78 | self.window.present() 79 | 80 | def on_window_delete(self, *args): 81 | """Window close event handler.""" 82 | self.window.destroy() 83 | self.on_quit() 84 | 85 | def on_close_clicked(self, *args): 86 | self.window.destroy() 87 | self.on_quit() 88 | 89 | def on_disable_plugin_clicked(self, *args): 90 | self.window.destroy() 91 | self.on_disable_plugin() 92 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | 4 | from pathlib import Path 5 | from setuptools import Command, setup 6 | from setuptools.command.build import build as OriginalBuildCommand 7 | 8 | 9 | class BuildCommand(OriginalBuildCommand): 10 | sub_commands = [("build_mo", None), *OriginalBuildCommand.sub_commands] 11 | 12 | 13 | class BuildMoSubCommand(Command): 14 | description = "Compile .po files into .mo files" 15 | 16 | files = None 17 | 18 | def initialize_options(self): 19 | self.files = None 20 | self.editable_mode = False 21 | self.build_lib = None 22 | 23 | def finalize_options(self): 24 | self.set_undefined_options("build_py", ("build_lib", "build_lib")) 25 | 26 | def run(self): 27 | files = self._get_files() 28 | 29 | for build_file, source_file in files.items(): 30 | if not self.editable_mode: 31 | # Parent directory required for msgfmt to work correctly 32 | Path(build_file).parent.mkdir(parents=True, exist_ok=True) 33 | self.spawn(["msgfmt", source_file, "-o", build_file]) 34 | 35 | def _get_files(self): 36 | if self.files is not None: 37 | return self.files 38 | 39 | files = {} 40 | 41 | localedir = Path("safeeyes/config/locale") 42 | po_dirs = [d.joinpath("LC_MESSAGES") for d in localedir.iterdir() if d.is_dir()] 43 | for po_dir in po_dirs: 44 | po_files = [ 45 | f for f in po_dir.iterdir() if f.is_file() and f.suffix == ".po" 46 | ] 47 | for po_file in po_files: 48 | mo_file = po_file.with_suffix(".mo") 49 | 50 | source_file = po_file 51 | build_file = mo_file 52 | 53 | if not self.editable_mode: 54 | build_file = Path(self.build_lib).joinpath(build_file) 55 | 56 | files[str(build_file)] = str(source_file) 57 | 58 | self.files = files 59 | return files 60 | 61 | def get_output_mapping(self): 62 | return self._get_files() 63 | 64 | def get_outputs(self): 65 | return self._get_files().keys() 66 | 67 | def get_source_files(self): 68 | return self._get_files().values() 69 | 70 | 71 | setup(cmdclass={"build": BuildCommand, "build_mo": BuildMoSubCommand}) 72 | -------------------------------------------------------------------------------- /update-po.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for filename in safeeyes/config/locale/*/LC_MESSAGES/safeeyes.po; do 3 | echo "$filename" 4 | msgmerge -U -N "$filename" safeeyes/config/locale/safeeyes.pot 5 | if [ -f "$filename~" ] ; then 6 | rm "$filename~" 7 | fi 8 | done -------------------------------------------------------------------------------- /validate_po.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Safe Eyes is a utility to remind you to take break frequently 3 | # to protect your eyes from eye strain. 4 | 5 | # Copyright (C) 2021 Gobinath 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import argparse 21 | from collections import defaultdict 22 | import glob 23 | import os 24 | import polib 25 | import re 26 | import sys 27 | import subprocess 28 | 29 | 30 | def xgettext() -> str: 31 | def _xgettext(files: list, lang: str) -> str: 32 | return subprocess.check_output( 33 | [ 34 | "xgettext", 35 | "--language", 36 | lang, 37 | "--sort-by-file", 38 | "--no-wrap", 39 | "-d", 40 | "safeeyes", 41 | "--no-location", 42 | "--omit-header", 43 | "-o", 44 | "-", 45 | "--", 46 | *files, 47 | ] 48 | ).decode() 49 | 50 | files_py = glob.glob("safeeyes/**/*.py", recursive=True) 51 | files_glade = glob.glob("safeeyes/**/*.glade", recursive=True) 52 | 53 | output = _xgettext(files_py, "Python") 54 | output = output + _xgettext(files_glade, "Glade") 55 | 56 | return output 57 | 58 | 59 | def validate_placeholders(message: str) -> bool: 60 | pos = 0 61 | 62 | success = True 63 | 64 | count_placeholders = 0 65 | count_unnamed = 0 66 | 67 | while True: 68 | index = message.find("%", pos) 69 | if index == -1: 70 | break 71 | 72 | pos = index + 1 73 | 74 | nextchar = message[pos : pos + 1] 75 | 76 | name = None 77 | 78 | if nextchar == "(": 79 | index = message.find(")", pos) 80 | if index == -1: 81 | success = False 82 | print(f"Unclosed parenthetical in '{message}'") 83 | break 84 | name = message[pos + 1 : index] 85 | 86 | pos = index + 1 87 | 88 | nextchar = message[pos : pos + 1] 89 | if nextchar not in ["%", "s", "d", "i", "f", "F"]: 90 | success = False 91 | print(f"Invalid format modifier in '{message}'") 92 | break 93 | 94 | if nextchar != "%": 95 | count_placeholders += 1 96 | if name is None: 97 | count_unnamed += 1 98 | 99 | pos += 1 100 | continue 101 | 102 | if count_unnamed > 1: 103 | success = False 104 | print(f"Multiple unnamed placeholders in '{message}'") 105 | 106 | if count_unnamed > 0 and count_placeholders > count_unnamed: 107 | success = False 108 | print(f"Mixing named and unnamed placeholders in '{message}'") 109 | 110 | return success 111 | 112 | 113 | def get_placeholders(message: str) -> tuple: 114 | percents = re.finditer(r"%(?P\(\w+\))?(?P[a-z])", message) 115 | 116 | unnamed = defaultdict(int) 117 | named = set() 118 | for percent in percents: 119 | if percent.group("name"): 120 | named.add(f"%({percent.group('name')}){percent.group('format')}") 121 | else: 122 | match = f"%{percent.group('format')}" 123 | unnamed[match] += 1 124 | return (unnamed, named) 125 | 126 | 127 | def ensure_named_placeholders(message: str) -> bool: 128 | (unnamed, named) = get_placeholders(message) 129 | return len(unnamed) == 0 130 | 131 | 132 | def validate_pot() -> bool: 133 | success = True 134 | 135 | new_pot_contents = xgettext() 136 | new_pot = polib.pofile(new_pot_contents) 137 | old_pot = polib.pofile("safeeyes/config/locale/safeeyes.pot") 138 | 139 | for new_entry in new_pot: 140 | if old_pot.find(new_entry.msgid) is None: 141 | print(f"missing entry in pot: '{new_entry.msgid}'") 142 | success = False 143 | if not validate_placeholders(new_entry.msgid): 144 | success = False 145 | if new_entry.msgid_plural: 146 | if not ( 147 | ensure_named_placeholders(new_entry.msgid) 148 | and ensure_named_placeholders(new_entry.msgid_plural) 149 | ): 150 | print( 151 | f"Plural message must use named placeholders: '{new_entry.msgid}'" 152 | ) 153 | success = False 154 | 155 | return success 156 | 157 | 158 | def has_equal_placeholders(left: str, right: str) -> bool: 159 | (left_unnamed, left_named) = get_placeholders(left) 160 | (right_unnamed, right_named) = get_placeholders(right) 161 | 162 | # count unnamed cases (eg. %s, %d) 163 | for match, count in left_unnamed.items(): 164 | if right_unnamed.get(match, 0) != count: 165 | return False 166 | 167 | # named cases are optional - but ensure that translation does not add new ones 168 | if not right_named.issubset(left_named): 169 | return False 170 | 171 | return True 172 | 173 | 174 | def validate_po(locale: str, path: str) -> bool: 175 | success = True 176 | po = polib.pofile(path) 177 | for entry in po: 178 | if entry.msgstr: 179 | if not validate_placeholders(entry.msgstr): 180 | success = False 181 | if not has_equal_placeholders(entry.msgid, entry.msgstr): 182 | print("Number of variables mismatched in " + locale) 183 | print(entry.msgid + " -> " + entry.msgstr) 184 | print() 185 | success = False 186 | for plural in entry.msgstr_plural.values(): 187 | if plural: 188 | if not validate_placeholders(plural): 189 | success = False 190 | if not has_equal_placeholders(entry.msgid, plural): 191 | print("Number of variables mismatched in " + locale) 192 | print(entry.msgid + " -> " + plural) 193 | print() 194 | success = False 195 | return success 196 | 197 | 198 | def validate_po_files() -> bool: 199 | success = True 200 | 201 | locales = os.listdir("safeeyes/config/locale") 202 | for locale in sorted(locales): 203 | path = os.path.join("safeeyes/config/locale", locale, "LC_MESSAGES/safeeyes.po") 204 | if os.path.isfile(path): 205 | print("Validating translation %s..." % path) 206 | success = validate_po(locale, path) and success 207 | 208 | return success 209 | 210 | 211 | def validate(): 212 | success = True 213 | success = validate_pot() and success 214 | success = validate_po_files() and success 215 | sys.exit(0 if success else 1) 216 | 217 | 218 | def extract(): 219 | success = True 220 | new_pot_contents = xgettext() 221 | new_pot = polib.pofile(new_pot_contents) 222 | pot_on_disk = polib.pofile("safeeyes/config/locale/safeeyes.pot", wrapwidth=0) 223 | 224 | for new_entry in new_pot: 225 | if not validate_placeholders(new_entry.msgid): 226 | success = False 227 | if pot_on_disk.find(new_entry.msgid) is None: 228 | pot_on_disk.append(new_entry) 229 | 230 | if success: 231 | pot_on_disk.save() 232 | 233 | sys.exit(0 if success else 1) 234 | 235 | 236 | def main(): 237 | parser = argparse.ArgumentParser(prog="validate_po") 238 | group = parser.add_mutually_exclusive_group() 239 | group.add_argument( 240 | "--validate", action="store_const", dest="mode", const="validate" 241 | ) 242 | group.add_argument( 243 | "--extract", 244 | help="Extract strings to pot file", 245 | action="store_const", 246 | dest="mode", 247 | const="extract", 248 | ) 249 | parser.set_defaults(mode="validate") 250 | args = parser.parse_args() 251 | 252 | if args.mode == "validate": 253 | validate() 254 | elif args.mode == "extract": 255 | extract() 256 | 257 | 258 | if __name__ == "__main__": 259 | main() 260 | --------------------------------------------------------------------------------