Full Code of eliostvs/tomate-gtk for AI

master d982876b3eee cached
94 files
211.0 KB
51.9k tokens
545 symbols
1 requests
Download .txt
Showing preview only (232K chars total). Download the full file or copy to clipboard to get everything.
Repository: eliostvs/tomate-gtk
Branch: master
Commit: d982876b3eee
Files: 94
Total size: 211.0 KB

Directory structure:
gitextract_2xxpwi1o/

├── .bumpversion.cfg
├── .envfile
├── .github/
│   └── workflows/
│       ├── lint.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── CHANGELOG.md
├── COPYING
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── data/
│   ├── applications/
│   │   └── tomate-gtk.desktop
│   ├── media/
│   │   ├── alarm.ogg
│   │   └── clock.ogg
│   └── plugins/
│       ├── alarm.plugin
│       ├── alarm.py
│       ├── autopause.plugin
│       ├── autopause.py
│       ├── breakscreen.plugin
│       ├── breakscreen.py
│       ├── notify.plugin
│       ├── notify.py
│       ├── script.plugin
│       ├── script.py
│       ├── ticking.plugin
│       └── ticking.py
├── pyproject.toml
├── setup.cfg
├── setup.py
├── tests/
│   ├── conftest.py
│   ├── data/
│   │   ├── icons/
│   │   │   └── hicolor/
│   │   │       └── index.theme
│   │   ├── mime/
│   │   │   └── packages/
│   │   │       └── freedesktop.org.xml
│   │   ├── pulse/
│   │   │   └── cookie
│   │   └── tomate/
│   │       ├── media/
│   │       │   ├── alarm.ogg
│   │       │   └── clock.ogg
│   │       ├── plugins/
│   │       │   ├── .gitkeep
│   │       │   ├── plugin_a.plugin
│   │       │   ├── plugin_a.py
│   │       │   ├── plugin_b.plugin
│   │       │   ├── plugin_b.py
│   │       │   └── plugin_b_old.plugin
│   │       └── tomate.conf
│   ├── plugins/
│   │   ├── test_alarm.py
│   │   ├── test_autopause.py
│   │   ├── test_breakscreen.py
│   │   ├── test_notify.py
│   │   ├── test_script.py
│   │   └── test_ticking.py
│   ├── pomodoro/
│   │   ├── test_app.py
│   │   ├── test_config.py
│   │   ├── test_event.py
│   │   ├── test_graph.py
│   │   ├── test_plugin.py
│   │   ├── test_session.py
│   │   └── test_timer.py
│   └── ui/
│       ├── dialogs/
│       │   ├── test_about.py
│       │   └── test_preference.py
│       ├── test_shortcut.py
│       ├── test_systray.py
│       ├── test_window.py
│       └── widgets/
│           ├── test_countdown.py
│           ├── test_headerbar.py
│           └── test_session_button.py
└── tomate/
    ├── __init__.py
    ├── __main__.py
    ├── audio/
    │   ├── __init__.py
    │   └── player.py
    ├── main.py
    ├── pomodoro/
    │   ├── __init__.py
    │   ├── app.py
    │   ├── config.py
    │   ├── event.py
    │   ├── fsm.py
    │   ├── graph.py
    │   ├── plugin.py
    │   ├── session.py
    │   └── timer.py
    └── ui/
        ├── __init__.py
        ├── dialogs/
        │   ├── __init__.py
        │   ├── about.py
        │   └── preference.py
        ├── shortcut.py
        ├── systray.py
        ├── testing.py
        ├── widgets/
        │   ├── __init__.py
        │   ├── countdown.py
        │   ├── headerbar.py
        │   ├── mode_button.py
        │   └── session_button.py
        └── window.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .bumpversion.cfg
================================================
[bumpversion]
current_version = 0.25.2

[bumpversion:file:setup.py]

[bumpversion:file:tomate/__init__.py]

[bumpversion:file:tomate/ui/dialogs/about.py]

[bumpversion:file:CHANGELOG.md]
search = [Unreleased]
replace = {new_version}


================================================
FILE: .envfile
================================================
XDG_CONFIG_HOME=$PROJECT_DIR$/tests/data
XDG_DATA_HOME=$PROJECT_DIR$/tests/data
XDG_DATA_DIRS=$PROJECT_DIR$/tests/data
PYTHONPATH=$PROJECT_DIR$:$PROJECT_DIR$/data/plugins


================================================
FILE: .github/workflows/lint.yml
================================================
name: lint

on:
  push:
    branches:
      - main
      - develop
  pull_request:
  workflow_dispatch: 

jobs:
  lint:
    name: Run linter
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        
      - uses: chartboost/ruff-action@v1

================================================
FILE: .github/workflows/release.yml
================================================
name: release

on:
  push:
    branches:
      - master
    paths:
      - 'data/**'
      - 'tomate/**'
      - 'setup.py'
      - '.bumpversion.cfg'

concurrency:
  group: ${{ github.workflow }}

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Trigger build
        env:
          TOKEN: ${{ secrets.OBS_TOKEN }}
        run: make trigger-build

================================================
FILE: .github/workflows/test.yml
================================================
name: test

on:
  push:
    branches:
      - main
      - develop
  pull_request: 
  workflow_dispatch: 

jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest
    container:
      image: eliostvs/tomate
      volumes:
        - ${{ github.workspace }}:/code
    
    steps:
      - uses: actions/checkout@v3
        
      - name: Create mime database
        run: |
          make mime
        
      - name: Test
        run: |
          make test

================================================
FILE: .gitignore
================================================
*.egg
*.egg-info
*.log
*.py[cod]
*.pyc
.coverage
venv/

.idea/
*.deb
.cache
.envrc
.pytest_cache
tests/data/mime/mime.cache


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/psf/black
    rev: 23.3.0
    hooks:
      - id: black
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
        types: [python]
      - id: check-added-large-files
      - id: check-ast
      - id: check-merge-conflict
      - id: debug-statements
      - id: detect-private-key
  - repo: local
    hooks:
      - id: test
        name: Running tests
        always_run: true
        language: system
        require_serial: true
        stages: [push]
        entry: make test

================================================
FILE: AUTHORS
================================================
Copyright (C) 2012 <Elio Esteves Duarte> <elio.esteves.duarte@gmail.com>

================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.25.2

### Fixed

- Autopause plugin freezing the app.

## 0.25.1

### Removed

- Dependency on python-tomate package.

## 0.25.0

### Changed

- Rename template variable $type to $session

## 0.24.0

### Changed

- Move player auto pause plugin to the tomate-gtk package

## 0.23.1

### Fixed

- The script plugin name in the metadata file

## 0.23.0

### Changed

- Move exec plugin to the tomate-gtk package
- Rename exec plugin to script plugin
- Move break screen plugin to the tomate-gtk package

## 0.22.0

### Changed

- Move notify plugin to the tomate-gtk package

## 0.21.0

### Changed

- Move alarm plugin to the tomate-gtk package

## 0.20.0

### Added

- Start playing the ticking sound when the plugin is activated during a Pomodoro session

## 0.19.1

### Fixed

- Missing core plugin and sound files

## 0.19.0

### Added

- Ticking core plugin (without settings interface) 
- Audio player with volume control
- `get_float` method to Config class

## 0.18.1

### Changed

- Workaround setuptools auto discovery behaviour

## 0.18.0

### Fixed

- Change STOCK_ICON labels to freedesktop names

## 0.17.0

### Changed

- Remove the SessionEndPayload and start sending SessionPayload with the finished session data
- Send SessionPayload instead of the SessionEndPayload in the Events.SESSION\_END event

### Added

- Session triggers an Events.SESSION\_CHANGE after the Events.SESSION\_END

## 0.16.0

### Changed

- Remove event type, sender, from the blinker receiver callback

## 0.15.0

### Added

- Send Events.SESSION\_READY when the main window is created to initialize widget components

### Changed

- Session.change receive a position argument instead of a keyword one

## 0.14.0

### Added

- Read boolean values from config with the Config.get\_bool method

### Fixed

- Save and send event after remove option

## 0.13.0

### Added

- Countdown field to the timer and session payloads

## 0.12.0

### Fixed

- Ratio rounding in Timer payload

### Added

- Shortcuts to change session to pomodoro (Ctrl+1), short break (Ctrl+2) and long break (Ctrl+3)
- Shortcut to open settings
- EndSessionPayload have the same fields of a SessionPayload plus the previous field that is the last SessionPayload

### Changed

- Session actions (start, stop, reset) keys in shortcuts section in the config
- Join python-tomate and tomate-gtk projects
- Timer, Session and App have their own state values instead of a shared Enum
- Redesign the event API
- Redesign the plugin API

### Removed

- Remove Session, Timer, Config, and View blinker.Signal objects
- Remove State and Session enums

## 0.11.0

### Added

- Session keyboard shortcuts

## 0.10.0

### Changed

- The timer, session and settings now emit a payload object instead of a dictionary

## 0.9.2

### Fixed

- Timer countdown blinking

## 0.9.1

### Changed

- Arch linux installation instructions

## 0.9.0

### Changed

- Change UI to use a headerbar widget instead of a toolbar
- The Task enum was renamed to Sessions

### Removed

- Show notifications in then main widget (**show\_message view interface**)

## 0.8.0

## Added

- Show notifications in the main widget (**show\_message** view interface)

### Fixed

- Reopen from command line

### Changed

- Arch installation instructions

## 0.7.0

### Added

- Using wiring.scanning
- Add plugin settings window
  
### Changed

- Python 3 only

## 0.6.0

### Added

- Add menu widget

## 0.5.0

### Fixed

- Fix Gtk warnings

## 0.4.0

### Added

- Using the new event system
  
### Removed

- Remove appindicator3 dependency


================================================
FILE: COPYING
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.


================================================
FILE: Dockerfile
================================================
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update -qq && apt-get install -y --no-install-recommends \
    dbus-x11 \
    gir1.2-appindicator3-0.1 \
    gir1.2-gdkpixbuf-2.0 \
    gir1.2-glib-2.0 \
    gir1.2-gstreamer-1.0 \
    gir1.2-gtk-3.0 \
    gir1.2-gtk-3.0 \
    gir1.2-notify-0.7 \
    gir1.2-playerctl-2.0 \
    gir1.2-unity-5.0 \
    git \
    gstreamer1.0-plugins-base \
    make \
    notification-daemon \
    python3-blinker \
    python3-dbus \
    python3-dbusmock \
    python3-gi \
    python3-pip \
    python3-pytest \
    python3-pytest-cov \
    python3-pytest-flake8 \
    python3-pytest-mock \
    python3-venusian \
    python3-wrapt \
    python3-xdg \
    python3-yapsy \
    xvfb \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

RUN pip3 install --no-cache-dir \
    black \
    isort \
    ruff \
    git+https://git@github.com/eliostvs/wiring.git@master

WORKDIR /code

ENTRYPOINT ["make"]

CMD ["test"]


================================================
FILE: LICENSE
================================================
License
-------

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.

================================================
FILE: MANIFEST.in
================================================
include AUTHORS COPYING CHANGELOG.md README.md
recursive-include data/applications *.desktop
recursive-include data/icons *.png *.svg

================================================
FILE: Makefile
================================================
ifeq ($(origin .RECIPEPREFIX), undefined)
	$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)
endif

.DEFAULT_GOAL = help
.DELETE_ON_ERROR:
.ONESHELL:
.SHELLFLAGS   := -eu -o pipefail -c
.SILENT:
MAKEFLAGS     += --no-builtin-rules
MAKEFLAGS     += --warn-undefined-variables
SHELL         = bash

DATAPATH     = $(CURDIR)/tests/data
DOCKER_IMAGE = eliostvs/$(PACKAGE)
OBS_API_URL  = https://api.opensuse.org/trigger/runservice
PACKAGE      = tomate
PYTHON       ?= python3
PLUGINPATH   = $(CURDIR)/data/plugins
PYTEST       ?= pytest-3
PYTHONPATH   = PYTHONPATH=$(CURDIR):$(PLUGINPATH)
TESTARGS     ?=
VERSION      = `cat .bumpversion.cfg | grep current_version | awk '{print $$3}'`
WORKDIR      = /code
XDGPATH      = XDG_CONFIG_HOME=$(DATAPATH) XDG_DATA_HOME=$(DATAPATH) XDG_DATA_DIRS=$(DATAPATH)

ifeq ($(shell which xvfb-run 1> /dev/null && echo yes),yes)
	ARGS = xvfb-run -a
else
	ARGS ?=
endif

## help: print this help message
help:
	echo 'Usage:'
	sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' | sort
.PHONY: help

## clean: clean development files
clean:
	find . \( -iname "__pycache__" \) -print0 | xargs -0 rm -rf
	rm -rf .eggs *.egg-info/ .coverage build/ .cache .pytest_cache tests/data/mime/mime.cache venv
.PHONY: clean

## venv: create develop environment
venv:
	python3 -m venv --system-site-package --upgrade-deps --prompt "." venv
	source venv/bin/activate
	pip install \
		black \
		isort \
		pytest \
		pytest-cov \
		pytest-flake8 \
		pytest-mock \
		python-dbusmock \
		ruff 

## mime: generate test mine index
mime: clean
	$(XDGPATH) update-mime-database tests/data/mime
	rm -rf tests/data/mime/{image,aliases,generic-icons,globs,globs2,icons,magic,subclasses,treemagic,types,version,XMLnamespaces}
.PHONY: mime

## format: run code formatter
format:
	black $(PACKAGE) tests/ $(PLUGINPATH)
	isort $(PACKAGE) tests/ $(PLUGINPATH)
.PHONY: format

## lint: run lint
lint:
	ruff check $(ARGS) $(PACKAGE) tests/ $(PLUGINPATH)
.PHONY: lint

## test: run tests
test:
	$(XDGPATH) $(PYTHONPATH) $(ARGS) $(PYTEST) $(TESTARGS) -v --cov=$(PACKAGE)
.PHONY: test

## run: run app locally
run:
	$(XDGPATH) $(PYTHONPATH) TOMATE_DEBUG=true $(PYTHON) -m $(PACKAGE) -v
.PHONY: run

## release-[major|minor|patch]: create release
release-%:
	git flow init -d
	@grep -q '\[Unreleased\]' CHANGELOG.md || (echo 'Create the [Unreleased] section in the changelog first!' && exit)
	bumpversion --verbose --commit $*
	git flow release start $(VERSION)
	GIT_MERGE_AUTOEDIT=no git flow release finish -m "Merge branch release/$(VERSION)" -T $(VERSION) $(VERSION) -p

## trigger-build: trigger obs build
trigger-build:
	curl -X POST -H "Authorization: Token $(TOKEN)" $(OBS_API_URL)
.PHONY: trigger-build

## docker: create docker image
docker:
	docker built -t $(DOCKER_IMAGE) .

## docker: push image to docker hub
docker-push:
	docker image push $(DOCKER_IMAGE):latest

## docker-run: run app inside
docker-run:
	docker run --rm -it -e DISPLAY --net=host \
	-v $(CURDIR):/$(WORKDIR) \
	-v $(HOME)/.Xauthority:/root/.Xauthority \
	$(DOCKER_IMAGE) run
.PHONY: docker-run

## docker-test: run tests inside docker
docker-test:
	docker run --rm -it -v $(CURDIR):$(WORKDIR) --workdir $(WORKDIR) $(DOCKER_IMAGE) mime test
.PHONY: docker-test

## docker-enter: open terminal inside docker environment
docker-enter:
	docker run --rm -v $(CURDIR):$(WORKDIR) -it --workdir $(WORKDIR) --entrypoint="bash" $(DOCKER_IMAGE)
.PHONY: docker-enter


================================================
FILE: README.md
================================================
# Tomate

A Pomodoro timer written in Gtk3 and Python for Linux desktops.

## About the technique

The Pomodoro Technique® is a management technique developed by Francesco Cirillo that helps you keep focused.
Read more about it at the [official website](http://pomodorotechnique.com/).

Pomodoro Technique® and Pomodoro™ are registered and filed trademarks owned by Francesco Cirillo.
Tomate is not affiliated by, associated with nor endorsed by Francesco Cirillo.

## Screenshots

![main screen](docs/img/main-screen.png)

![preference duration](docs/img/preference-duration.png)

![preference extension](docs/img/preference-extension.png)

## Installation

### Ubuntu 20.04+

If you have installed the program using the **old ppa repository** uninstall the old version first.
If you use an Ubuntu-based distro, such as Mint, manually set the **RELEASE** variable to the Ubuntu version number, such as 16.04, rather than running the sed script bellow.

```bash
RELEASE=`sed -n 's/VERSION_ID="\(.*\)"/\1/p' /etc/os-release`
curl -fsSL "http://download.opensuse.org/repositories/home:/eliostvs:/tomate/xUbuntu_$RELEASE/Release.key" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tomate.gpg > /dev/null
echo "deb http://download.opensuse.org/repositories/home:/eliostvs:/tomate/xUbuntu_$RELEASE/ ./" | sudo tee /etc/apt/sources.list.d/tomate.list
sudo apt-get update && sudo apt-get install tomate-gtk
```

### Debian 10+

```bash
RELEASE=`sed -n 's/VERSION_ID="\(.*\)"/\1/p' /etc/os-release`
curl -fsSL "http://download.opensuse.org/repositories/home:/eliostvs:/tomate/Debian_$RELEASE/Release.key" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tomate.gpg > /dev/null
echo "deb http://download.opensuse.org/repositories/home:/eliostvs:/tomate/Debian_$RELEASE/ ./" | sudo tee /etc/apt/sources.list.d/tomate.list
sudo apt-get update && sudo apt-get install tomate-gtk
```

### Opensuse Tumbleweed

```bash
sudo zypper ar -f http://download.opensuse.org/repositories/home:/eliostvs:/tomate/openSUSE_Tumbleweed/home:eliostvs:tomate.repo
sudo zypper install tomate-gtk
```

### Fedora 36+

```bash
RELEASE=`cat /etc/fedora-release | grep -o '[0-9][0-9]*'`
sudo dnf config-manager --add-repo http://download.opensuse.org/repositories/home:/eliostvs:/tomate/Fedora_$RELEASE/home:eliostvs:tomate.repo
sudo dnf install tomate-gtk
```

### Arch

The packages are available in [aur repository](https://aur.archlinux.org/packages/tomate-gtk/)

## Plugins

### Pre-installed

- [Alarm][alarm-plugin] Play a alarm when the timer end
- [Ticking][ticking-plugin] Play a ticking sound during a work session
- [Notify][notify-plugin] Display notification end the timer start, stop and end
- [Script][script-plugin] Run scripts when the timer start, stop or end
- [Break Screen][breakscreen-plugin] Block all screens during break tim
- [Auto Pause][autopause-plugin] Pause all the playing media when a work session ends

### External

- [Indicator][indicator-plugin] Displays a countdown icon in the systray (uses libappindicator)
- [StatusIcon][statusicon-plugin] Displays a countdown icon in the systray (old method for creating a systray with GNOME)
- [StatusNotifierItem][statusnotifieritem-plugin] Displays a countdown icon in the systray (freedesktop standard for creating a systray)
- [Launcher][launcher-plugin] Shows the timer countdown and the total of sessions in the launcher (ubuntu only)

---

[alarm-plugin]: ./data/plugins/alarm.plugin
[ticking-plugin]: ./data/plugins/ticking.plugin
[notify-plugin]: ./data/plugins/notify.plugin
[script-plugin]: ./data/plugins/script.plugin
[breakscreen-plugin]: ./data/plugins/breakscreen.plugin
[autopause-plugin]: ./data/plugins/autopause.plugin
[indicator-plugin]: https://github.com/eliostvs/tomate-indicator-plugin
[statusicon-plugin]: https://github.com/eliostvs/tomate-statusicon-plugin
[launcher-plugin]: https://github.com/eliostvs/tomate-launcher-plugin
[statusnotifieritem-plugin]: https://github.com/eliostvs/tomate-statusnotifieritem-plugin


================================================
FILE: data/applications/tomate-gtk.desktop
================================================
[Desktop Entry]
Name=Tomate
GenericName=Clock
Comment=Gtk version from Tomate pomodoro timer.
Categories=Utility;Clock;
Exec=tomate-gtk
Icon=tomate
Terminal=false
Type=Application


================================================
FILE: data/plugins/alarm.plugin
================================================
[Core]
Name = Alarm
Module = alarm

[Documentation]
Author = Elio Esteves Duarte
Version = 0.12.0
Website = https://github.com/eliostvs/tomate-gtk
Description = Plays alarm at session end

================================================
FILE: data/plugins/alarm.py
================================================
import logging
from locale import gettext as _
from os import path
from urllib.parse import urlparse

import gi
from wiring import Graph

gi.require_version("Gtk", "3.0")

from gi.repository import Gtk

import tomate.pomodoro.plugin as plugin
from tomate.audio import GStreamerPlayer
from tomate.pomodoro import Bus, Config, Events, on, suppress_errors

logger = logging.getLogger(__name__)

SECTION_NAME = "alarm_plugin"
OPTION_NAME = "uri"


class AlarmPlugin(plugin.Plugin):
    has_settings = True

    @suppress_errors
    def __init__(self):
        super().__init__()
        self.config = None
        self.player = None

    def configure(self, bus: Bus, graph: Graph) -> None:
        super().configure(bus, graph)
        self.config = graph.get("tomate.config")

    @suppress_errors
    def activate(self) -> None:
        super().activate()
        logger.debug("action=activate uri=%s volume=%f", self.audio_path, self.volume)
        self.player = GStreamerPlayer(repeat=False)
        self.player.file = self.audio_path
        self.player.volume = self.volume

    @suppress_errors
    def deactivate(self) -> None:
        super().deactivate()
        logger.debug("action=deactivate")

        if self.player:
            self.player.stop()

    @suppress_errors
    @on(Events.SESSION_END)
    def on_end(self, **__) -> None:
        logger.debug("action=on_end")
        if self.player:
            self.player.play()

    @property
    def audio_path(self) -> str:
        return self.config.get(SECTION_NAME, OPTION_NAME, fallback=self.config.media_uri("alarm.ogg"))

    @property
    def volume(self) -> float:
        return self.config.get_float(SECTION_NAME, "volume", fallback=1.0)

    def settings_window(self, toplevel: Gtk.Dialog) -> "SettingsDialog":
        return SettingsDialog(self.config, toplevel)


class SettingsDialog:
    def __init__(self, config: Config, toplevel: Gtk.Dialog):
        self.config = config
        self.widget = self.create_dialog(toplevel)

    def create_dialog(self, toplevel: Gtk.Dialog) -> Gtk.Dialog:
        dialog = Gtk.Dialog(
            border_width=12,
            modal=True,
            resizable=False,
            title=_("Preferences"),
            transient_for=toplevel,
            window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
        )
        dialog.add_button(_("Close"), Gtk.ResponseType.CLOSE)
        dialog.connect("response", lambda widget, _: widget.destroy())
        dialog.set_size_request(350, -1)
        dialog.get_content_area().add(self.create_options())
        return dialog

    def create_options(self) -> Gtk.Grid:
        custom_audio = self.config.get(SECTION_NAME, OPTION_NAME, fallback="")

        grid = Gtk.Grid(column_spacing=12, row_spacing=12, margin_bottom=12, margin_top=12)
        label = Gtk.Label(label=_("Custom:"), hexpand=True, halign=Gtk.Align.END)
        grid.attach(label, 0, 0, 1, 1)

        entry = self.create_custom_alarm_input(custom_audio)
        grid.attach(entry, 0, 1, 4, 1)

        switch = self.create_custom_alarm_switch(custom_audio, entry)
        grid.attach_next_to(switch, label, Gtk.PositionType.RIGHT, 1, 1)

        return grid

    def create_custom_alarm_input(self, custom_audio) -> Gtk.Entry:
        entry = Gtk.Entry(
            editable=False,
            hexpand=True,
            name="custom_entry",
            secondary_icon_activatable=True,
            secondary_icon_name=Gtk.STOCK_FILE,
            sensitive=bool(custom_audio),
            text=custom_audio,
        )
        entry.connect("icon-press", self.select_custom_alarm)
        entry.connect("notify::text", self.custom_alarm_changed)
        return entry

    def create_custom_alarm_switch(self, custom_audio, entry) -> Gtk.Switch:
        switch = Gtk.Switch(
            hexpand=True,
            halign=Gtk.Align.START,
            active=bool(custom_audio),
            name="custom_switch",
        )
        switch.connect("notify::active", self.custom_alarm_toggle, entry)
        return switch

    def select_custom_alarm(self, entry: Gtk.Entry, *_) -> None:
        dialog = self.create_file_chooser(self.dirname(entry.props.text))
        response = dialog.run()

        if response == Gtk.ResponseType.OK:
            entry.set_text(dialog.get_uri())

        dialog.destroy()

    def dirname(self, audio_path: str) -> str:
        return path.dirname(urlparse(audio_path).path) if audio_path else path.expanduser("~")

    def create_file_chooser(self, current_folder: str) -> Gtk.FileChooserDialog:
        dialog = Gtk.FileChooserDialog(
            _("Please choose a file"),
            self.widget,
            Gtk.FileChooserAction.OPEN,
            (
                Gtk.STOCK_CANCEL,
                Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OPEN,
                Gtk.ResponseType.OK,
            ),
        )
        dialog.add_filter(self.create_filter("audio/mp3", "audio/mpeg"))
        dialog.add_filter(self.create_filter("audio/ogg", "audio/ogg"))
        dialog.set_current_folder(current_folder)
        return dialog

    @staticmethod
    def create_filter(name: str, mime_type: str) -> Gtk.FileFilter:
        mime_type_filter = Gtk.FileFilter()
        mime_type_filter.set_name(name)
        mime_type_filter.add_mime_type(mime_type)
        return mime_type_filter

    def custom_alarm_changed(self, entry: Gtk.Entry, _) -> None:
        custom_alarm = entry.props.text

        if custom_alarm:
            logger.debug("action=set_option section=%s option=%s value", SECTION_NAME, OPTION_NAME)
            self.config.set(SECTION_NAME, OPTION_NAME, custom_alarm)
        else:
            logger.debug("action=remove_option section=%s option=%s", SECTION_NAME, OPTION_NAME)
            self.config.remove(SECTION_NAME, OPTION_NAME)

    @staticmethod
    def custom_alarm_toggle(switch: Gtk.Switch, _, entry: Gtk.Entry) -> None:
        if switch.props.active:
            entry.set_properties(sensitive=True)
        else:
            entry.set_properties(text="", sensitive=False)

    def run(self) -> Gtk.Dialog:
        self.widget.show_all()
        return self.widget


================================================
FILE: data/plugins/autopause.plugin
================================================
[Core]
Name = Auto Pause
Module = autopause

[Documentation]
Author = Elio Esteves Duarte
Version = 0.2.0
Website =  https://github.com/eliostvs/tomate-gtk
Description = Pauses all running media players when the session ends


================================================
FILE: data/plugins/autopause.py
================================================
import logging

import gi

import tomate.pomodoro.plugin as plugin
from tomate.pomodoro import Events, on, suppress_errors

gi.require_version("Playerctl", "2.0")

from gi.repository import GLib, Playerctl

logger = logging.getLogger(__name__)


class AutoPausePlugin(plugin.Plugin):
    @suppress_errors
    @on(Events.SESSION_END)
    def on_session_end(self, **_):
        self.pause()

    def pause(self) -> None:
        try:
            for player in Playerctl.list_players():
                instance = Playerctl.Player.new_for_source(player.instance, player.source)
                logger.debug(
                    "action=check player=%s status=%s",
                    player.name,
                    instance.props.playback_status,
                )

                # pause is not an idempotent operation, it can start a paused player :(
                # so we need to check if the player is running first
                if instance.props.playback_status == Playerctl.PlaybackStatus.PLAYING:
                    instance.pause()
                    logger.debug("action=paused player=%s", player.name)

        except GLib.Error as err:
            logger.error("action=failed error='%s'", err)


================================================
FILE: data/plugins/breakscreen.plugin
================================================
[Core]
Name = Break Screen
Module = breakscreen

[Documentation]
Author = Elio Esteves Duarte
Version = 0.7.0
Website = https://github.com/eliostva/tomate-gtk
Description = Shows a full screen window which prevents users from using the computer during a break


================================================
FILE: data/plugins/breakscreen.py
================================================
import logging
from collections import namedtuple
from locale import gettext as _
from typing import Dict

import gi

gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")

from gi.repository import Gdk, GLib, Gtk

import tomate.pomodoro.plugin as plugin
from tomate.pomodoro import (
    Config,
    ConfigPayload,
    Events,
    Session,
    SessionPayload,
    SessionType,
    Subscriber,
    Timer,
    TimerPayload,
    on,
    suppress_errors,
)

logger = logging.getLogger(__name__)

SECTION_NAME = "break_screen"
SKIP_BREAK_OPTION = "skip_break"
AUTO_START_OPTION = "auto_start"


class Monitor(namedtuple("Monitor", "number geometry")):
    @property
    def x(self) -> int:
        return self.geometry.x

    @property
    def y(self) -> int:
        return self.geometry.y

    @property
    def width(self) -> int:
        return self.geometry.width

    @property
    def height(self) -> int:
        return self.geometry.height


class BreakScreen(Subscriber):
    def __init__(self, monitor: Monitor, session: Session, config: Config):
        logger.debug("action=init_screen monitor=%s", monitor)

        self.monitor = monitor
        self.session = session
        self.options = self.create_options(config)
        self.countdown = Gtk.Label(label="00:00", name="countdown")
        self.skip_button = self.create_button()
        content = self.create_content_area(self.countdown, self.skip_button)
        self.widget = self.create_window(self.monitor, content)

    def create_options(self, config) -> Dict[str, bool]:
        return {
            SKIP_BREAK_OPTION: config.get_bool(SECTION_NAME, SKIP_BREAK_OPTION, fallback=False),
            AUTO_START_OPTION: config.get_bool(SECTION_NAME, AUTO_START_OPTION, fallback=False),
        }

    def create_button(self) -> Gtk.Button:
        logger.debug("action=create_skip_button visibile=%s", self.options[SKIP_BREAK_OPTION])
        button = Gtk.Button(label=_("Skip"), name="skip", visible=self.options[SKIP_BREAK_OPTION], no_show_all=True)
        button.connect("clicked", self.skip_break)
        button.grab_focus()
        return button

    def skip_break(self, _) -> None:
        logger.debug("action=skip_break")
        self.session.stop()
        self.session.change(SessionType.POMODORO)

    def create_content_area(self, countdown: Gtk.Label, skip_button: Gtk.Button) -> Gtk.Box:
        content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        content.pack_start(countdown, False, False, 0)
        content.pack_start(skip_button, False, False, 0)

        space = Gtk.Box(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
        space.pack_start(content, True, True, 0)
        return space

    def create_window(self, monitor: Monitor, box: Gtk.Box) -> Gtk.Window:
        window = Gtk.Window(
            can_focus=False,
            decorated=False,
            deletable=False,
            focus_on_map=False,
            gravity=Gdk.Gravity.CENTER,
            name="breakscreen",
            skip_taskbar_hint=True,
            urgency_hint=True,
        )
        window.set_visual(window.get_screen().get_rgba_visual())
        window.stick()
        window.set_keep_above(True)
        window.fullscreen()
        window.move(monitor.x, monitor.y)
        window.resize(monitor.width, monitor.height)
        window.add(box)
        return window

    @on(Events.SESSION_START)
    def on_session_start(self, payload=SessionPayload) -> None:
        logger.debug("action=session_start monitor=%d session=%s", self.monitor.number, payload.type)

        if payload.type != SessionType.POMODORO:
            self.countdown.set_text(payload.countdown)
            self.widget.show_all()

    @on(Events.SESSION_INTERRUPT)
    def on_session_interrupt(self, **__) -> None:
        logger.debug("action=session_start monitor=%d", self.monitor.number)
        self.widget.hide()

    @on(Events.SESSION_END)
    def on_session_end(self, payload: SessionPayload) -> None:
        logger.debug(
            "action=session_end monitor=%d auto_start=%s session_type=%s",
            self.monitor.number,
            self.auto_start,
            payload.type,
        )

        if payload.type == SessionType.POMODORO and self.auto_start:
            GLib.timeout_add_seconds(Timer.ONE_SECOND, self._start_session)
        else:
            self.widget.hide()

    def _start_session(self) -> bool:
        self.session.start()
        return False

    @property
    def auto_start(self) -> bool:
        return self.options[AUTO_START_OPTION]

    @on(Events.TIMER_UPDATE)
    def on_timer_update(self, payload: TimerPayload) -> None:
        logger.debug("action=update_countdown monitor=%s countdown=%s", payload.countdown, self.monitor.number)
        self.countdown.set_text(payload.countdown)

    @on(Events.CONFIG_CHANGE)
    def on_settings_change(self, payload: ConfigPayload) -> None:
        if payload.section != SECTION_NAME:
            return

        logger.debug(
            "action=change_option monitor=%d config=%s option=%s",
            self.monitor.number,
            payload.action,
            payload.option,
        )
        self.options[payload.option] = payload.action == "set"
        self.skip_button.props.visible = self.options[SKIP_BREAK_OPTION]


class SettingsDialog:
    def __init__(self, config: Config, toplevel):
        self.options = {SKIP_BREAK_OPTION: False, AUTO_START_OPTION: False}
        self.config = config
        self.widget = self.create_dialog(toplevel)

    def create_dialog(self, toplevel) -> Gtk.Dialog:
        dialog = Gtk.Dialog(
            border_width=12,
            modal=True,
            resizable=False,
            title=_("Preferences"),
            transient_for=toplevel,
            window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
        )
        dialog.add_button(_("Close"), Gtk.ResponseType.CLOSE)
        dialog.connect("response", lambda widget, _: widget.destroy())
        dialog.set_size_request(350, -1)
        dialog.get_content_area().add(self.create_options())
        return dialog

    def create_options(self):
        grid = Gtk.Grid(column_spacing=12, row_spacing=12, margin_bottom=12, margin_top=12)
        self.create_option(grid, 0, _("Auto start:"), AUTO_START_OPTION)
        self.create_option(grid, 1, _("Skip break:"), SKIP_BREAK_OPTION)
        return grid

    def run(self):
        self.widget.show_all()
        return self.widget

    def create_option(self, grid: Gtk.Grid, row: int, label: str, option):
        active = self.config.get_bool(SECTION_NAME, option, fallback=False)
        self.options[option] = active

        label = Gtk.Label(label=_(label), hexpand=True, halign=Gtk.Align.END)
        grid.attach(label, 0, row, 1, 1)

        switch = Gtk.Switch(hexpand=True, halign=Gtk.Align.START, name=option, active=active)
        switch.connect("notify::active", self.on_option_change, option)
        grid.attach_next_to(switch, label, Gtk.PositionType.RIGHT, 1, 1)

    def on_option_change(self, switch: Gtk.Switch, _, option: str):
        self.options[option] = switch.props.active

        if switch.props.active:
            self.config.set(SECTION_NAME, option, "true")
        else:
            logger.debug("action=remove_option name=%s", option)
            self.config.remove(SECTION_NAME, option)


class BreakScreenPlugin(plugin.Plugin):
    has_settings = True

    @suppress_errors
    def __init__(self, display=Gdk.Display.get_default()):
        super().__init__()
        self.display = display
        self.screens = []
        self.configure_style()

    @staticmethod
    def configure_style():
        style = b"""
        #breakscreen {
            background-color: #1c1c1c;
        }

        #skip {
            color: white;
            font-size: 1.5em;
            font-weight: 900;
            background: transparent;
            border-color: white;
            border-image: none;
            border-radius: 25px;
            border-width: 2px;
            padding-bottom: 10px;
            padding-left: 25px;
            padding-right: 25px;
            padding-top: 10px;
        }

        #skip:hover, #skip:active {
            background: white;
            color: black;
        }

        #countdown {
            font-size: 10em;
        }
        """
        style_provider = Gtk.CssProvider()
        style_provider.load_from_data(style)
        Gtk.StyleContext.add_provider_for_screen(
            Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

    @suppress_errors
    def activate(self):
        super().activate()

        for monitor in range(self.display.get_n_monitors()):
            geometry = self.display.get_monitor(monitor).get_geometry()
            screen = BreakScreen(
                Monitor(monitor, geometry), self.graph.get("tomate.session"), self.graph.get("tomate.config")
            )
            screen.connect(self.bus)
            self.screens.append(screen)

    @suppress_errors
    def deactivate(self):
        super().deactivate()

        for screen in self.screens:
            screen.disconnect(self.bus)
            screen.widget.destroy()

        del self.screens[:]

    def settings_window(self, toplevel) -> SettingsDialog:
        return SettingsDialog(self.graph.get("tomate.config"), toplevel)


================================================
FILE: data/plugins/notify.plugin
================================================
[Core]
Name = Notify
Module = notify

[Documentation]
Author = Elio Esteves Duarte
Version = 0.15.0
Website = https://github.com/eliostvs/tomate-gtk
Description = Shows screen notifications

================================================
FILE: data/plugins/notify.py
================================================
import logging
from locale import gettext as _
from typing import Tuple

import gi
from wiring import Graph

import tomate.pomodoro.plugin as plugin
from tomate.pomodoro import (
    Bus,
    Events,
    SessionPayload,
    SessionType,
    on,
    suppress_errors,
)

gi.require_version("Notify", "0.7")

from gi.repository import Notify

logger = logging.getLogger(__name__)


class NotifyPlugin(plugin.Plugin):
    messages = {
        SessionType.POMODORO: {"title": _("Pomodoro"), "content": _("Get back to work!")},
        SessionType.SHORT_BREAK: {"title": _("Short Break"), "content": _("It's coffee time!")},
        SessionType.LONG_BREAK: {"title": _("Long Break"), "content": _("Step away from the machine!")},
    }

    @suppress_errors
    def __init__(self):
        super().__init__()
        self.config = None
        self.notification = Notify.Notification.new("tomate-notify-plugin")

    def configure(self, bus: Bus, graph: Graph) -> None:
        super().configure(bus, graph)
        self.config = graph.get("tomate.config")

    @suppress_errors
    def activate(self):
        super().activate()
        Notify.init("tomate-notify-plugin")

    @suppress_errors
    def deactivate(self):
        super().deactivate()
        Notify.uninit()

    @on(Events.SESSION_START)
    def on_session_started(self, payload: SessionPayload):
        self.show_notification(*self.get_message(payload.type))

    @on(Events.SESSION_END)
    def on_session_finished(self, **__):
        self.show_notification(title="The time is up!")

    @on(Events.SESSION_INTERRUPT)
    def on_session_stopped(self, **__):
        self.show_notification(title="Session stopped manually")

    def get_message(self, session: SessionType) -> Tuple[str, str]:
        return (
            self.messages[session]["title"],
            self.messages[session]["content"],
        )

    @suppress_errors
    def show_notification(self, title, message=""):
        self.notification.update(title, message, self.icon_path)
        result = self.notification.show()

        logger.debug(
            'action=show title="%s" message="%s" success=%r icon=%s',
            title,
            message,
            result,
            self.icon_path,
        )

    @property
    def icon_path(self):
        return self.config.icon_path("tomate", 32)


================================================
FILE: data/plugins/script.plugin
================================================
[Core]
Name = Script
Module = script

[Documentation]
Author = Elio Esteves Duarte
Version = 0.2.0
Website = https://github.com/eliostvs/tomate-gtk
Description = Run scripts when a session starts, stops or finishes


================================================
FILE: data/plugins/script.py
================================================
import locale
import logging
import subprocess
from locale import gettext as _
from string import Template
from typing import Dict, Optional

import gi
from wiring import Graph

gi.require_version("Gtk", "3.0")

from gi.repository import Gtk

import tomate.pomodoro.plugin as plugin
from tomate.pomodoro import Bus, Config, Events, SessionPayload, on, suppress_errors

locale.textdomain("tomate")
logger = logging.getLogger(__name__)

SECTION_NAME = "script_plugin"
START_OPTION = "start_command"
STOP_OPTION = "stop_command"
FINISH_OPTION = "finish_command"


def strip_space(command: Optional[str]) -> Optional[str]:
    if command is not None:
        return command.strip()


class ScriptPlugin(plugin.Plugin):
    has_settings = True

    @suppress_errors
    def __init__(self):
        super().__init__()
        self.config = None

    def configure(self, bus: Bus, graph: Graph) -> None:
        super().configure(bus, graph)
        self.config = graph.get("tomate.config")

    @suppress_errors
    @on(Events.SESSION_START)
    def on_session_started(self, payload: SessionPayload):
        return self.call_command(START_OPTION, Events.SESSION_START, payload)

    @suppress_errors
    @on(Events.SESSION_INTERRUPT)
    def on_session_interrupted(self, payload: SessionPayload):
        return self.call_command(STOP_OPTION, Events.SESSION_INTERRUPT, payload)

    @suppress_errors
    @on(Events.SESSION_END)
    def on_session_end(self, payload: SessionPayload):
        return self.call_command(FINISH_OPTION, Events.SESSION_END, payload)

    def call_command(self, section, event: Events, payload: SessionPayload):
        command = self.read_command(section, {"event": event.name, "session": payload.type.name})
        if command:
            try:
                logger.debug("action=call-command cmd=%s", command)
                subprocess.run(command, shell=True, check=True)
                return True
            except subprocess.CalledProcessError as error:
                logger.debug(
                    "action=run-command-failed event=%s cmd=%s output=%s return-code=%s",
                    event,
                    error.cmd,
                    error.output,
                    error.returncode,
                )
        return False

    def read_command(self, section: str, repl: Dict[str, str]) -> Optional[str]:
        template = strip_space(self.config.get(SECTION_NAME, section))
        return self._interpolate(template, repl) if template else None

    @staticmethod
    def _interpolate(template: str, replacements: Dict[str, str]) -> str:
        return Template(template).substitute(**replacements)

    def settings_window(self, toplevel):
        return SettingsDialog(self.config, toplevel)


class SettingsDialog:
    def __init__(self, config: Config, toplevel):
        self.config = config
        self.widget = self.create_dialog(toplevel)

    def create_dialog(self, toplevel) -> Gtk.Dialog:
        dialog = Gtk.Dialog(
            border_width=12,
            modal=True,
            resizable=False,
            title=_("Preferences"),
            transient_for=toplevel,
            window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
        )
        dialog.add_button(_("Close"), Gtk.ResponseType.CLOSE)
        dialog.connect("response", lambda widget, _: widget.destroy())
        dialog.set_size_request(350, -1)
        dialog.get_content_area().add(self.create_options())
        return dialog

    def create_options(self):
        grid = Gtk.Grid(column_spacing=12, row_spacing=12, margin_bottom=12)
        self.create_section(grid)
        self.create_option(grid, 3, _("On start:"), START_OPTION)
        self.create_option(grid, 5, _("On stop:"), STOP_OPTION)
        self.create_option(grid, 7, _("On finish:"), FINISH_OPTION)
        return grid

    @staticmethod
    def create_section(grid: Gtk.Grid) -> None:
        title = Gtk.Label(
            label="<b>{0}</b>".format(_("Scripts")),
            halign=Gtk.Align.START,
            hexpand=True,
            use_markup=True,
        )
        grid.attach(title, 0, 0, 1, 1)

        help_text = Gtk.Label(
            label=_(
                "You can use the session and event names in your script using the"
                " <i>$session</i> and <i>$event</i> template variables."
            ),
            halign=Gtk.Align.CENTER,
            margin_bottom=24,
            hexpand=False,
            wrap=True,
            max_width_chars=40,
            use_markup=True,
        )
        grid.attach(help_text, 0, 1, 4, 1)

    def run(self) -> None:
        self.widget.show_all()
        return self.widget

    def create_option(self, grid: Gtk.Grid, row: int, label: str, option: str) -> None:
        command = self.config.get(SECTION_NAME, option, fallback="")

        label = Gtk.Label(label=_(label), hexpand=True, halign=Gtk.Align.END)
        grid.attach(label, 0, row, 1, 1)

        entry = Gtk.Entry(editable=True, sensitive=bool(command), text=command, name=option + "_entry")
        entry.connect("notify::text", self.on_command_change, option)
        grid.attach(entry, 0, row + 1, 4, 1)

        switch = Gtk.Switch(hexpand=True, halign=Gtk.Align.START, active=bool(command), name=option + "_switch")
        switch.connect("notify::active", self.on_option_change, entry, option)
        grid.attach_next_to(switch, label, Gtk.PositionType.RIGHT, 1, 1)

    def on_command_change(self, entry: Gtk.Entry, _, option: str) -> None:
        command = strip_space(entry.props.text)
        if command:
            logger.debug("action=set_option option=%s command=%s", option, command)
            self.config.set(SECTION_NAME, option, command)

    def on_option_change(self, switch: Gtk.Switch, _, entry: Gtk.Entry, option: str) -> None:
        if switch.props.active:
            entry.props.sensitive = True
        else:
            self.remove_option(entry, option)

    def remove_option(self, entry: Gtk.Entry, option: str) -> None:
        logger.debug("action=remove_option option=%s", option)
        self.config.remove(SECTION_NAME, option)
        entry.set_properties(text="", sensitive=False)


================================================
FILE: data/plugins/ticking.plugin
================================================
[Core]
Name = Ticking
Module = ticking

[Documentation]
Author = Elio Esteves Duarte
Version = 0.0.2
Website = https://github.com/eliostvs/tomate-gtk
Description = Ticking sound during a pomodoro session

================================================
FILE: data/plugins/ticking.py
================================================
import logging

import gi
from wiring import Graph

gi.require_version("Gst", "1.0")

import tomate.pomodoro.plugin as plugin
from tomate.audio import GStreamerPlayer
from tomate.pomodoro import (
    Bus,
    Events,
    SessionPayload,
    SessionType,
    on,
    suppress_errors,
)

logger = logging.getLogger(__name__)


class TickingPlugin(plugin.Plugin):
    has_settings = False
    SECTION_NAME = "ticking_plugin"

    @suppress_errors
    def __init__(self):
        super().__init__()
        self.config = None
        self.player = None
        self.session = None

    def configure(self, bus: Bus, graph: Graph) -> None:
        super().configure(bus, graph)
        self.config = graph.get("tomate.config")
        self.session = graph.get("tomate.session")

    @suppress_errors
    def activate(self) -> None:
        super().activate()
        logger.debug("action=activate uri=%s volume=%f", self.audio_path, self.volume)
        self.player = GStreamerPlayer(repeat=True)
        self.player.file = self.audio_path
        self.player.volume = self.volume

        if self.session.is_running() and self.session.current == SessionType.POMODORO:
            self.player.play()

    @suppress_errors
    def deactivate(self) -> None:
        super().deactivate()
        logger.debug("action=deactivate")

        if self.player:
            self.player.stop()

    @suppress_errors
    @on(Events.SESSION_START)
    def on_start(self, payload: SessionPayload) -> None:
        logger.debug(f"action=play session_type={payload.type}")
        if self.player and payload.type == SessionType.POMODORO:
            self.player.play()

    @suppress_errors
    @on(Events.SESSION_INTERRUPT, Events.SESSION_END)
    def on_end(self, **_) -> None:
        logger.debug("action=stop")
        if self.player:
            self.player.stop()

    @property
    def audio_path(self) -> str:
        return self.config.get(self.SECTION_NAME, "uri", fallback=self.config.media_uri("clock.ogg"))

    @property
    def volume(self) -> float:
        return self.config.get_float(self.SECTION_NAME, "volume", fallback=1.0)


================================================
FILE: pyproject.toml
================================================
[tool.black]
line-length = 120
include = '\.pyi?$'
exclude = '''
/(
    \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
)/
'''

[tool.ruff]
line-length = 120
ignore = ["E402", "S101"]
target-version = "py39"

[tool.isort]
profile = "black"


================================================
FILE: setup.cfg
================================================
[global]
verbose=1

[wheel]
universal = 1

[tool:pytest]
addopts = --verbose --cov-report term-missing

[coverage:run]
include =
    tomate


================================================
FILE: setup.py
================================================
#!/bin/env python
import os

from setuptools import find_packages, setup


def find_xdg_data_files(from_dir, to_dir, package_name, data_files):
    for root, _, files in os.walk(from_dir):
        if files:
            to_dir = to_dir.format(pkgname=package_name)

            sub_path = root.split(from_dir)[1]
            if sub_path.startswith("/"):
                sub_path = sub_path[1:]

            files = [os.path.join(root, file) for file in files]
            data_files.append((os.path.join(to_dir, sub_path), files))


def find_data_files(data_map, package_name):
    data_files = []

    for from_dir, to_dir in data_map:
        find_xdg_data_files(from_dir, to_dir, package_name, data_files)

    return data_files


DATA_FILES = [
    ("data/icons", "share/icons"),
    ("data/applications", "share/applications"),
    ("data/plugins", "share/{pkgname}/plugins"),
    ("data/media", "share/{pkgname}/media"),
]

setup(
    author="Elio Esteves Duarte",
    author_email="elio.esteves.duarte@gmail.com",
    description="A pomodoro timer",
    include_package_data=True,
    keywords="pomodoro,tomate",
    license="GPL-3",
    long_description=open("README.md", "r", encoding="utf-8").read(),
    name="tomate-gtk",
    packages=find_packages(exclude=["tests"]),
    py_modules=[],
    data_files=find_data_files(DATA_FILES, "tomate"),
    url="https://github.com/eliostvs/tomate-gtk",
    version="0.25.2",
    zip_safe=False,
    entry_points={"console_scripts": ["tomate-gtk=tomate.__main__:main"]},
)


================================================
FILE: tests/conftest.py
================================================
import os

import gi
import pytest
from wiring import Graph

gi.require_version("Gtk", "3.0")

from gi.repository import Gtk

from tomate.pomodoro import Bus, Config, PluginEngine, Session
from tomate.ui import ShortcutEngine, Window

TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")


@pytest.fixture
def session(mocker):
    return mocker.Mock(spec=Session)


@pytest.fixture
def bus() -> Bus:
    return Bus()


@pytest.fixture
def graph() -> Graph:
    g = Graph()
    g.register_instance(Graph, g)
    return g


@pytest.fixture
def window(mocker):
    return mocker.Mock(spec=Window, widget=Gtk.Window())


@pytest.fixture
def config(bus, tmpdir) -> Config:
    cfg = Config(bus)
    tmp_path = tmpdir.mkdir("tomate").join("tomate.config")
    cfg.config_path = lambda: tmp_path.strpath
    return cfg


@pytest.fixture
def shortcut_engine(config: Config) -> ShortcutEngine:
    return ShortcutEngine(config)


@pytest.fixture
def plugin_engine(bus: Bus, graph: Graph, config: Config) -> PluginEngine:
    return PluginEngine(bus, config, graph)


================================================
FILE: tests/data/icons/hicolor/index.theme
================================================
[Icon Theme]
Name=Hicolor
Comment=Fallback icon theme
Hidden=true
Directories=16x16/apps,24x24/apps,scalable/apps,scalable/actions,scalable/categories,scalable/status

[16x16/apps]
Size=16
Context=Applications

[24x24/apps]
Size=24
Context=Applications

[scalable/apps]
Size=16
MaxSize=48
Type=Scalable
Context=Applications

[scalable/apps]
Size=16
MaxSize=48
Type=Scalable
Context=Applications

[scalable/actions]
Size=16
MaxSize=48
Type=Scalable
Context=Actions

[scalable/categories]
Size=16
MaxSize=48
Type=Scalable
Context=Categories

[scalable/status]
Size=16
MaxSize=48
Type=Scalable
Context=Status

================================================
FILE: tests/data/mime/packages/freedesktop.org.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
The freedesktop.org shared MIME database (this file) was created by merging
several existing MIME databases (all released under the GPL).

It comes with ABSOLUTELY NO WARRANTY, to the extent permitted by law. You may
redistribute copies of update-mime-database under the terms of the GNU General
Public License. For more information about these matters, see the file named
COPYING.

The latest version is available from:

	http://www.freedesktop.org/wiki/Software/shared-mime-info/

To extend this database, users and applications should create additional
XML files in the 'packages' directory and run the update-mime-database
command to generate the output files.
-->
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
  <mime-type type="image/png">
    <comment>PNG image</comment>
    <magic priority="50">
      <match type="string" value="\x89PNG" offset="0"/>
    </magic>
    <glob pattern="*.png"/>
  </mime-type>
  <mime-type type="image/svg+xml">
    <comment>SVG image</comment>
    <acronym>SVG</acronym>
    <expanded-acronym>Scalable Vector Graphics</expanded-acronym>
    <sub-class-of type="application/xml"/>
    <magic priority="80">
      <match type="string" value="&lt;!DOCTYPE svg" offset="0:256"/>
    </magic>
    <magic priority="80">
      <match type="string" value="&lt;!-- Created with Inkscape" offset="0"/>
      <match type="string" value="&lt;svg" offset="0"/>
    </magic>
    <magic priority="45">
      <match type="string" value="&lt;svg" offset="1:256"/>
    </magic>
    <glob pattern="*.svg"/>
    <root-XML namespaceURI="http://www.w3.org/2000/svg" localName="svg"/>
  </mime-type>
</mime-info>


================================================
FILE: tests/data/pulse/cookie
================================================
?Q/"$"cܗRdkr}JMDiqf_~V9asj7pGm"kF)CޭޝgGBkH0
O	oʢmDV>N{iq>X䟤ߺB]uքe1Hۅ
cX=)h&]
ٍamm^K?I3rA1hn%J4He}d/q|[`ir

================================================
FILE: tests/data/tomate/plugins/.gitkeep
================================================


================================================
FILE: tests/data/tomate/plugins/plugin_a.plugin
================================================
[Core]
Name = PluginA
Module = plugin_a

[Documentation]
Author = Elio Esteves Duarte
Version = 1.0.0
Website = https://github.com/eliostvs/tomate-plugin-a
Description = Description A

================================================
FILE: tests/data/tomate/plugins/plugin_a.py
================================================
from gi.repository import Gtk

import tomate.pomodoro.plugin as plugin
from tomate.pomodoro import Events, on


class PluginA(plugin.Plugin):
    has_settings = True

    def __init__(self):
        super().__init__()
        self.parent = None

    @on(Events.WINDOW_SHOW)
    def listener(self, **__) -> str:
        return "plugin_a"

    def settings_window(self, parent: Gtk.Widget) -> Gtk.Dialog:
        self.parent = parent
        dialog = Gtk.MessageDialog(
            message_type=Gtk.MessageType.INFO,
            transient_for=parent,
            buttons=Gtk.ButtonsType.OK,
            text="Plugin A Settings",
        )
        dialog.connect("response", lambda widget, _: widget.destroy())
        return dialog


================================================
FILE: tests/data/tomate/plugins/plugin_b.plugin
================================================
[Core]
Name = PluginB
Module = plugin_b

[Documentation]
Author = Elio Esteves Duarte
Version = 2.0.0
Website = https://github.com/eliostvs/tomate-plugin-b
Description = Description B

================================================
FILE: tests/data/tomate/plugins/plugin_b.py
================================================
import tomate.pomodoro.plugin as plugin
from tomate.pomodoro import Events, on


class PluginB(plugin.Plugin):
    has_settings = False

    @on(Events.WINDOW_SHOW)
    def listener(self, **__) -> str:
        return "plugin_b"


================================================
FILE: tests/data/tomate/plugins/plugin_b_old.plugin
================================================
[Core]
Name = PluginB
Module = plugin_b

[Documentation]
Author = Elio Esteves Duarte
Version = 1.5.0
Website = https://github.com/eliostvs/tomate-plugin-b
Description = Should not loaded because has an older version

================================================
FILE: tests/data/tomate/tomate.conf
================================================
[DEFAULT]
pomodoro_duration = 25
shortbreak_duration = 5
longbreak_duration = 15
long_break_interval = 4

[timer]
pomodoro_duration = 25
shortbreak_duration = 5
longbreak_duration = 15

[shortcuts]
test = <control>S

[Plugin Management]
default_plugins_to_load = PluginB

[ticking_plugin]
volume=0.5

[alarm_plugin]
volume=0.8

[script_plugin]
start_command = echo start
stop_command = echo stop
finish_command = echo finish


================================================
FILE: tests/plugins/test_alarm.py
================================================
from os.path import dirname, join
from unittest.mock import patch

import gi
import pytest

gi.require_version("Gtk", "3.0")
gi.require_version("Gst", "1.0")

from gi.repository import Gtk

from tomate.pomodoro import Events
from tomate.ui.testing import Q

CUSTOM_ALARM = f'file://{join(dirname(dirname(__file__)), "data", "tomate", "media", "custom.ogg")}'
DEFAULT_ALARM = f'file://{join(dirname(dirname(__file__)), "data", "tomate", "media", "alarm.ogg")}'
SECTION_NAME = "alarm_plugin"
URI_OPTION_NAME = "uri"


@pytest.fixture
def plugin(bus, config, graph):
    graph.providers.clear()
    graph.register_instance("tomate.config", config)
    graph.register_instance("tomate.bus", bus)

    from alarm import AlarmPlugin

    instance = AlarmPlugin()
    instance.configure(bus, graph)
    return instance


class TestPlugin:
    def test_loads_configuration_when_is_activated(self, bus, config, plugin):
        plugin.activate()

        assert pytest.approx(plugin.player.volume, rel=1e-3) == 0.8
        assert plugin.player.file == DEFAULT_ALARM
        assert not plugin.player.repeat

    @patch("alarm.GStreamerPlayer")
    def test_plays_alarm_when_session_ends(self, player, bus, config, plugin):
        plugin.activate()

        bus.send(Events.SESSION_END)

        assert player.return_value.play.called


class TestSettingsWindow:
    def test_without_custom_alarm(self, config, plugin):
        config.remove(SECTION_NAME, URI_OPTION_NAME)
        dialog = plugin.settings_window(Gtk.Window())

        entry = Q.select(dialog.widget, Q.props("name", "custom_entry"))
        assert entry.props.text == ""
        assert entry.props.sensitive is False

        switch = Q.select(dialog.widget, Q.props("name", "custom_switch"))
        assert switch.props.active is False

    def test_with_custom_alarm(self, plugin, config):
        config.set(SECTION_NAME, URI_OPTION_NAME, CUSTOM_ALARM)

        dialog = plugin.settings_window(Gtk.Window())
        dialog.run()

        entry = Q.select(dialog.widget, Q.props("name", "custom_entry"))
        assert entry.props.text == CUSTOM_ALARM
        assert entry.props.sensitive is True

        switch = Q.select(dialog.widget, Q.props("name", "custom_switch"))
        assert switch.props.active is True

    def test_configures_custom_alarm(self, config, plugin):
        dialog = plugin.settings_window(Gtk.Window())

        switch = Q.select(dialog.widget, Q.props("name", "custom_switch"))
        switch.props.active = True

        entry = Q.select(dialog.widget, Q.props("name", "custom_entry"))
        assert entry.props.sensitive is True
        entry.set_text(CUSTOM_ALARM)

        dialog.widget.emit("response", 0)
        assert dialog.widget.props.window is None

        assert config.get(SECTION_NAME, URI_OPTION_NAME) == CUSTOM_ALARM

    def test_disables_custom_alarm(self, config, plugin):
        config.set(SECTION_NAME, URI_OPTION_NAME, CUSTOM_ALARM)

        dialog = plugin.settings_window(Gtk.Window())

        switch = Q.select(dialog.widget, Q.props("name", "custom_switch"))
        switch.props.active = False

        entry = Q.select(dialog.widget, Q.props("name", "custom_entry"))
        assert entry.props.text == ""
        assert entry.props.sensitive is False

        dialog.widget.emit("response", 0)
        assert dialog.widget.props.window is None

        assert config.has_option(SECTION_NAME, URI_OPTION_NAME) is False


================================================
FILE: tests/plugins/test_autopause.py
================================================
import gi
import pytest

gi.require_version("Playerctl", "2.0")
gi.require_version("Gtk", "3.0")

from tomate.pomodoro import Events
from tomate.ui.testing import create_session_payload


@pytest.fixture
def plugin(bus, graph):
    from autopause import AutoPausePlugin

    instance = AutoPausePlugin()
    instance.configure(bus, graph)
    return instance


def test_stop_all_running_players(bus, plugin, mocker):
    from gi.repository import Playerctl

    playing = mocker.Mock(props=mocker.Mock(playback_status=Playerctl.PlaybackStatus.PLAYING))
    paused = mocker.Mock(props=mocker.Mock(playback_status=Playerctl.PlaybackStatus.PAUSED))
    players = {
        "playing-playing": playing,
        "paused-paused": paused,
    }

    def side_effect(instance, source):
        return players.get(f"{instance}-{source}")

    mocker.patch(
        "autopause.Playerctl.list_players",
        return_value=[
            mocker.Mock(instance="playing", source="playing"),
            mocker.Mock(instance="paused", source="paused"),
        ],
    )
    mocker.patch("autopause.Playerctl.Player.new_for_source", side_effect)

    plugin.activate()

    bus.send(Events.SESSION_END, payload=create_session_payload())

    paused.pause.assert_not_called()
    playing.pause.assert_called_once()


================================================
FILE: tests/plugins/test_breakscreen.py
================================================
import random
from typing import Iterator

import gi
import pytest

gi.require_version("Gtk", "3.0")

from gi.repository import Gtk

from tomate.pomodoro import ConfigPayload, Events, SessionType, TimerPayload
from tomate.ui.testing import Q, create_session_payload, run_loop_for

SECTION_NAME = "break_screen"
SKIP_BREAK_OPTION = "skip_break"
AUTO_START_OPTION = "auto_start"


@pytest.fixture
def plugin(bus, config, graph, session):
    graph.providers.clear()
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.config", config)
    graph.register_instance("tomate.session", session)

    from breakscreen import BreakScreenPlugin

    instance = BreakScreenPlugin()
    instance.configure(bus, graph)
    return instance


def none(values: Iterator) -> bool:
    return all([value is False for value in values])


def label_text(countdown: str, plugin) -> bool:
    return len(plugin.screens) > 0 and all(
        [Q.select(screen.widget, Q.props("name", "countdown")).get_text() == countdown for screen in plugin.screens]
    )


class TestPlugin:
    @pytest.mark.parametrize("session_type", [SessionType.SHORT_BREAK, SessionType.LONG_BREAK])
    def test_shows_when_pause_begins(self, session_type, bus, plugin):
        plugin.activate()

        payload = create_session_payload(type=session_type)
        bus.send(Events.SESSION_START, payload=payload)

        assert all([screen.widget.props.visible for screen in plugin.screens])
        assert label_text(payload.countdown, plugin)

    def test_not_show_when_pomodoro_begins(self, bus, plugin):
        plugin.activate()

        payload = create_session_payload(type=SessionType.POMODORO)
        bus.send(Events.SESSION_START, payload=payload)

        assert none([screen.widget.props.visible for screen in plugin.screens])

    def test_hides_when_plugin_is_deactivated(self, bus, plugin):
        plugin.activate()

        payload = create_session_payload(type=SessionType.SHORT_BREAK)
        bus.send(Events.SESSION_START, payload=payload)

        plugin.deactivate()

        assert none([screen.widget.props.visible for screen in plugin.screens])

    def test_starts_break_when_auto_start_option_is_enabled(self, bus, config, plugin, session):
        config.set(SECTION_NAME, AUTO_START_OPTION, "true")

        plugin.activate()

        payload = create_session_payload(type=SessionType.POMODORO)
        bus.send(Events.SESSION_END, payload=payload)

        run_loop_for(1)

        session.start.assert_called_once()

    def test_not_start_break_when_auto_start_is_disabled(self, bus, config, plugin, session):
        config.set(SECTION_NAME, AUTO_START_OPTION, "false")

        plugin.activate()

        payload = create_session_payload(type=SessionType.POMODORO)
        bus.send(Events.SESSION_END, payload=payload)

        session.start.assert_not_called()

        assert none([screen.widget.props.visible for screen in plugin.screens])

    def test_hides_when_session_is_interrupted(self, bus, plugin):
        plugin.activate()

        bus.send(Events.SESSION_START, payload=create_session_payload(type=SessionType.SHORT_BREAK))
        bus.send(Events.SESSION_INTERRUPT, payload=create_session_payload())

        assert none([screen.widget.props.visible for screen in plugin.screens])

    @pytest.mark.parametrize("session_type", [SessionType.SHORT_BREAK, SessionType.LONG_BREAK])
    def test_not_start_break_when_is_not_a_pomodoro(self, session_type, bus, config, plugin, session):
        config.set(SECTION_NAME, AUTO_START_OPTION, "True")
        plugin.activate()

        for screen in plugin.screens:
            screen.widget.show()

        payload = create_session_payload(type=session_type)
        bus.send(Events.SESSION_END, payload=payload)

        session.start.assert_not_called()

        assert none([screen.widget.props.visible for screen in plugin.screens])

    def test_updates_countdown(self, bus, plugin):
        plugin.activate()

        time_left = random.randint(1, 100)

        payload = TimerPayload(time_left=time_left, duration=150)
        bus.send(Events.TIMER_UPDATE, payload=payload)

        assert label_text(payload.countdown, plugin)

    @pytest.mark.parametrize(
        "action,option,initial,value,want",
        [
            ("set", AUTO_START_OPTION, "false", "true", True),
            ("remove", AUTO_START_OPTION, "true", "", False),
            ("set", SKIP_BREAK_OPTION, "false", "true", True),
            ("remove", SKIP_BREAK_OPTION, "true", "", False),
        ],
    )
    def test_updates_when_config_changes(self, action, option, initial, value, want, bus, config, plugin):
        config.set(SECTION_NAME, option, initial)
        plugin.activate()

        payload = ConfigPayload(action, SECTION_NAME, option, value)
        bus.send(Events.CONFIG_CHANGE, payload=payload)

        assert all([screen.options[option] == want for screen in plugin.screens])

    @pytest.mark.parametrize(
        "action, want",
        [
            ("set", True),
            ("remove", False),
        ],
    )
    def test_hide_skip_button_when_config_changes(self, action, want, bus, plugin):
        plugin.activate()

        payload = ConfigPayload(action, SECTION_NAME, SKIP_BREAK_OPTION, "")
        bus.send(Events.CONFIG_CHANGE, payload=payload)

        assert all([screen.skip_button.props.visible == want for screen in plugin.screens])


class TestSettingsWindow:
    def test_options_labels(self, plugin):
        dialog = plugin.settings_window(Gtk.Window())

        assert Q.select(dialog.widget, Q.props("label", "Auto start:")) is not None
        assert Q.select(dialog.widget, Q.props("label", "Skip break:")) is not None

    def test_with_all_options_enabled(self, config, plugin):
        config.set(SECTION_NAME, AUTO_START_OPTION, "true")
        config.set(SECTION_NAME, SKIP_BREAK_OPTION, "true")

        dialog = plugin.settings_window(Gtk.Window())

        assert Q.select(dialog.widget, Q.props("name", AUTO_START_OPTION)).props.active is True
        assert Q.select(dialog.widget, Q.props("name", SKIP_BREAK_OPTION)).props.active is True

    def test_with_all_options_disabled(self, config, plugin):
        config.remove(SECTION_NAME, AUTO_START_OPTION)
        config.remove(SECTION_NAME, SKIP_BREAK_OPTION)

        dialog = plugin.settings_window(Gtk.Window())

        assert Q.select(dialog.widget, Q.props("name", AUTO_START_OPTION)).props.active is False
        assert Q.select(dialog.widget, Q.props("name", SKIP_BREAK_OPTION)).props.active is False

    def test_change_options(self, config, plugin):
        config.remove(SECTION_NAME, AUTO_START_OPTION)
        config.remove(SECTION_NAME, SKIP_BREAK_OPTION)

        dialog = plugin.settings_window(Gtk.Window())

        Q.select(dialog.widget, Q.props("name", AUTO_START_OPTION)).props.active = True
        Q.select(dialog.widget, Q.props("name", SKIP_BREAK_OPTION)).props.active = True

        config.load()

        assert config.get_bool(SECTION_NAME, AUTO_START_OPTION) is True
        assert config.get_bool(SECTION_NAME, SKIP_BREAK_OPTION) is True


================================================
FILE: tests/plugins/test_notify.py
================================================
from os.path import dirname, join
from unittest.mock import patch

import gi
import pytest

from tomate.pomodoro import Config, Events, SessionType
from tomate.ui.testing import create_session_payload

gi.require_version("Notify", "0.7")

IconPath = join(dirname(dirname(__file__)), "data", "icons", "hicolor", "24x24", "apps", "tomate.png")


@pytest.fixture
@patch("gi.repository.Notify.Notification.new")
def plugin(_, graph, bus):
    graph.providers.clear()
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.config", Config(bus))

    from notify import NotifyPlugin

    instance = NotifyPlugin()
    instance.configure(bus, graph)
    return instance


@patch("gi.repository.Notify.init")
def test_enable_notify_when_plugin_active(init, plugin):
    plugin.activate()

    init.assert_called_with("tomate-notify-plugin")


@patch("gi.repository.Notify.uninit")
def test_disable_notify_when_plugin_deactivate(uninit, plugin):
    plugin.deactivate()

    uninit.assert_called_with()


@pytest.mark.parametrize(
    "event,session,title,message",
    [
        (Events.SESSION_START, SessionType.POMODORO, "Pomodoro", "Get back to work!"),
        (Events.SESSION_START, SessionType.SHORT_BREAK, "Short Break", "It's coffee time!"),
        (Events.SESSION_START, SessionType.LONG_BREAK, "Long Break", "Step away from the machine!"),
        (Events.SESSION_INTERRUPT, SessionType.POMODORO, "Session stopped manually", ""),
        (Events.SESSION_END, SessionType.POMODORO, "The time is up!", ""),
    ],
)
def test_show_notification_when_session_starts(event, session, title, message, bus, plugin):
    plugin.activate()

    payload = create_session_payload(type=session)
    bus.send(event, payload=payload)

    plugin.notification.update.assert_called_once_with(title, message, IconPath)
    plugin.notification.show.assert_called_once()


================================================
FILE: tests/plugins/test_script.py
================================================
import subprocess

import gi
import pytest

gi.require_version("Gtk", "3.0")

from gi.repository import Gtk

from tomate.pomodoro import Events, SessionType
from tomate.ui.testing import Q, create_session_payload

SECTION_NAME = "script_plugin"


@pytest.fixture
def subprocess_run(mocker, monkeypatch):
    import script

    mock = mocker.Mock(spec=subprocess.run)
    monkeypatch.setattr(script.subprocess, "run", mock)
    return mock


@pytest.fixture
def plugin(bus, config, graph):
    graph.providers.clear()
    graph.register_instance("tomate.config", config)
    graph.register_instance("tomate.bus", bus)

    from script import ScriptPlugin

    instance = ScriptPlugin()
    instance.configure(bus, graph)
    return instance


@pytest.mark.parametrize(
    "event,option",
    [
        (Events.SESSION_START, "start_command"),
        (Events.SESSION_INTERRUPT, "stop_command"),
        (Events.SESSION_END, "finish_command"),
    ],
)
def test_execute_command_when_event_is_trigger(event, option, bus, subprocess_run, config, plugin):
    command = config.get(SECTION_NAME, option)
    plugin.activate()

    bus.send(event, create_session_payload())

    subprocess_run.assert_called_once_with(command, shell=True, check=True)


@pytest.mark.parametrize(
    "event,section,session_type",
    [
        (Events.SESSION_START, "start_command", SessionType.POMODORO),
        (Events.SESSION_INTERRUPT, "stop_command", SessionType.LONG_BREAK),
        (Events.SESSION_END, "finish_command", SessionType.SHORT_BREAK),
    ],
)
def test_command_variables(event, section, session_type, bus, subprocess_run, config, plugin):
    config.set(SECTION_NAME, section, "$event $session")
    plugin.activate()

    bus.send(event, create_session_payload(type=session_type))

    subprocess_run.assert_called_once_with(f"{event.name} {session_type.name}", shell=True, check=True)


@pytest.mark.parametrize(
    "event, option",
    [
        (Events.SESSION_START, "start_command"),
        (Events.SESSION_INTERRUPT, "stop_command"),
        (Events.SESSION_END, "finish_command"),
    ],
)
def test_does_not_execute_commands_when_they_are_not_configured(event, option, bus, subprocess_run, config, plugin):
    config.remove(SECTION_NAME, option)
    plugin.activate()

    assert bus.send(event, create_session_payload()) == [False]

    subprocess_run.assert_not_called()


def test_execute_command_fail(bus, config, plugin):
    config.set(SECTION_NAME, "start_command", "fail")

    plugin.activate()

    assert bus.send(Events.SESSION_START, create_session_payload()) == [False]


class TestSettingsWindow:
    @pytest.mark.parametrize(
        "option,command",
        [
            ("start_command", "echo start"),
            ("stop_command", "echo stop"),
            ("finish_command", "echo finish"),
        ],
    )
    def test_with_custom_commands(self, option, command, plugin):
        dialog = plugin.settings_window(Gtk.Window())

        switch = Q.select(dialog.widget, Q.props("name", f"{option}_switch"))
        assert switch.props.active is True

        entry = Q.select(dialog.widget, Q.props("name", f"{option}_entry"))
        assert entry.props.text == command

    @pytest.mark.parametrize("option", ["start_command", "stop_command", "finish_command"])
    def test_without_custom_commands(self, option, config, plugin):
        config.remove_section(SECTION_NAME)
        config.save()

        assert config.has_section(SECTION_NAME) is False

        dialog = plugin.settings_window(Gtk.Window())

        switch = Q.select(dialog.widget, Q.props("name", f"{option}_switch"))
        assert switch.props.active is False

        entry = Q.select(dialog.widget, Q.props("name", f"{option}_entry"))
        assert entry.props.text == ""

    @pytest.mark.parametrize("option", ["start_command", "stop_command", "finish_command"])
    def test_disable_command(self, option, config, plugin):
        dialog = plugin.settings_window(Gtk.Window())

        switch = Q.select(dialog.widget, Q.props("name", f"{option}_switch"))
        switch.props.active = False

        entry = Q.select(dialog.widget, Q.props("name", f"{option}_entry"))
        assert entry.props.sensitive is False
        assert entry.props.text == ""

        dialog.widget.emit("response", 0)
        assert dialog.widget.props.window is None

        config.load()
        assert config.has_option(SECTION_NAME, option) is False

    @pytest.mark.parametrize("option", ["start_command", "stop_command", "finish_command"])
    def test_configure_command(self, option, config, plugin):
        config.remove(SECTION_NAME, option)

        dialog = plugin.settings_window(Gtk.Window())

        switch = Q.select(dialog.widget, Q.props("name", f"{option}_switch"))
        switch.props.active = True

        entry = Q.select(dialog.widget, Q.props("name", f"{option}_entry"))
        assert entry.props.sensitive is True
        entry.props.text = "echo changed"

        dialog.widget.emit("response", 0)
        assert dialog.widget.props.window is None

        config.load()
        assert config.get(SECTION_NAME, option) == "echo changed"

    def test_text(self, config, plugin):
        dialog = plugin.settings_window(Gtk.Window())

        expected = (
            "You can use the session and event names in your script using the"
            " <i>$session</i> and <i>$event</i> template variables."
        )
        assert Q.select(dialog.widget, Q.props("label", expected))

        assert Q.select(dialog.widget, Q.props("label", "<b>Scripts</b>"))


================================================
FILE: tests/plugins/test_ticking.py
================================================
from os.path import dirname, join
from unittest.mock import patch

import gi
import pytest

from tomate.ui.testing import create_session_payload

gi.require_version("Gtk", "3.0")
gi.require_version("Gst", "1.0")

from tomate.pomodoro import Events, SessionType

DEFAULT_ALARM = f'file://{join(dirname(dirname(__file__)), "data", "tomate", "media", "clock.ogg")}'


@pytest.fixture
def plugin(bus, config, graph, session):
    graph.providers.clear()
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.config", config)
    graph.register_instance("tomate.session", session)

    from ticking import TickingPlugin

    instance = TickingPlugin()
    instance.configure(bus, graph)
    return instance


class TestPlugin:
    def test_loads_configuration_when_is_activated(self, bus, config, plugin):
        plugin.activate()

        assert plugin.player.volume == 0.5
        assert plugin.player.file == DEFAULT_ALARM
        assert plugin.player.repeat

    @patch("ticking.GStreamerPlayer")
    @pytest.mark.parametrize(
        "is_running,session_type,want",
        [
            (True, SessionType.POMODORO, True),
            (True, SessionType.SHORT_BREAK, False),
            (False, SessionType.POMODORO, False),
        ],
    )
    def test_starts_player_when_is_activated(
        self, player, is_running, session_type, want, bus, config, session, plugin
    ):
        session.is_running.return_value = is_running
        session.current = session_type

        plugin.activate()

        assert player.return_value.play.called == want

    @patch("ticking.GStreamerPlayer")
    def test_starts_player_when_session_start(self, player, bus, config, plugin):
        plugin.activate()

        bus.send(Events.SESSION_START, payload=create_session_payload())

        player.return_value.play.assert_called_once()

    @patch("ticking.GStreamerPlayer")
    @pytest.mark.parametrize("event", [Events.SESSION_END, Events.SESSION_INTERRUPT])
    def test_stops_player_when_session_(self, player, event, bus, config, plugin):
        plugin.activate()

        bus.send(Events.SESSION_START, payload=create_session_payload())
        bus.send(event)

        player.return_value.stop.assert_called_once()

    @patch("ticking.GStreamerPlayer")
    def test_stops_player_when_is_deactivate(self, player, bus, config, plugin):
        plugin.activate()

        plugin.deactivate()

        player.return_value.stop.assert_called_once()


================================================
FILE: tests/pomodoro/test_app.py
================================================
import dbus
import pytest
from dbus.mainloop.glib import DBusGMainLoop
from dbusmock import DBusTestCase
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Application
from tomate.pomodoro.app import State

DBusGMainLoop(set_as_default=True)


@pytest.fixture
def app(graph, window, plugin_engine, mocker) -> Application:
    graph.register_instance("tomate.ui.view", window)
    graph.register_instance("tomate.plugin", plugin_engine)
    graph.register_instance("dbus.session", mocker.Mock())

    scan_to_graph(["tomate.pomodoro.app"], graph)

    return graph.get("tomate.app")


def test_module(graph, app):
    instance = graph.get("tomate.app")

    assert isinstance(instance, Application)
    assert instance is app


def test_collects_plugins_on_start(app, plugin_engine):
    assert plugin_engine.has_plugins() is True


class TestRun:
    def test_start_window_when_app_is_not_running(self, app, window):
        app.state = State.STOPPED

        app.Run()

        window.run.assert_called_once_with()

    def test_shows_window_when_app_is_running(self, app, window):
        app.state = State.STARTED

        app.Run()

        window.show.assert_called_once_with()


class TestFromGraph:
    def setup_method(self):
        DBusTestCase.start_session_bus()

    def teardown_method(self):
        DBusTestCase.tearDownClass()

    def test_create_app_instance_when_it_is_not_registered_in_dbus(self, graph, window, plugin_engine):
        graph.register_instance("tomate.ui.view", window)
        graph.register_instance("tomate.plugin", plugin_engine)
        scan_to_graph(["tomate.pomodoro.app"], graph)

        instance = Application.from_graph(graph, DBusTestCase.get_dbus())

        assert isinstance(instance, Application)

    @pytest.fixture()
    def mock_dbus(self):
        mock = DBusTestCase.spawn_server(Application.BUS_NAME, Application.BUS_PATH, Application.BUS_INTERFACE)
        yield mock
        mock.terminate()
        mock.wait()

    def test_get_dbus_interface_when_is_registered_in_dbus(self, graph, mock_dbus):
        instance = Application.from_graph(graph, DBusTestCase.get_dbus())

        assert isinstance(instance, dbus.Interface)
        assert instance.dbus_interface == Application.BUS_INTERFACE
        assert instance.object_path == Application.BUS_PATH
        assert instance.requested_bus_name == Application.BUS_NAME


================================================
FILE: tests/pomodoro/test_config.py
================================================
import os

import pytest
from wiring.scanning import scan_to_graph

from tests.conftest import TEST_DATA_DIR
from tomate.pomodoro import Config, ConfigPayload, Events


@pytest.fixture
def config(graph, bus):
    graph.register_instance("tomate.bus", bus)
    scan_to_graph(["tomate.pomodoro.config"], graph)
    return graph.get("tomate.config")


def test_module(graph, config):
    instance = graph.get("tomate.config")

    assert isinstance(instance, Config)
    assert instance is config


def test_get_plugin_paths(config):
    expected = os.path.join(TEST_DATA_DIR, "tomate", "plugins")

    assert expected in config.plugin_paths()


def test_get_config_path(config):
    assert config.config_path() == os.path.join(TEST_DATA_DIR, "tomate", "tomate.conf")


def test_get_media_uri_raises_error_when_media_is_not_found(config):
    with pytest.raises(OSError) as excinfo:
        config.media_uri("tomate.jpg")

    assert str(excinfo.value) == "Resource 'tomate.jpg' not found!"


def test_get_media_uri(config):
    expected = "file://" + os.path.join(TEST_DATA_DIR, "tomate", "media", "tomate.png")

    assert config.media_uri("tomate.png") == expected


def test_get_icon_path_raises_when_icon_not_found(config):
    with pytest.raises(OSError) as excinfo:
        assert config.icon_path("foo", 48, "foobar")

    assert str(excinfo.value) == "Icon 'foo' not found!"


def test_get_icon_path(config):
    expected = os.path.join(TEST_DATA_DIR, "icons", "hicolor", "24x24", "apps", "tomate.png")
    assert config.icon_path("tomate", 48, "hicolor") == expected


def test_icon_paths(config):
    assert os.path.join(TEST_DATA_DIR, "icons") in config.icon_paths()


def test_get_option_as_int(config):
    assert config.get_int("Timer", "pomodoro_duration") == 25


def test_get_option_as_float(config):
    assert config.get_float("ticking_plugin", "volume") == 0.50


def test_get_option(config):
    assert config.get("Timer", "pomodoro_duration") == "25"


def test_get_option_with_fallback(config):
    assert config.get("Timer", "pomodoro_pass", fallback="23") == "23"


def test_get_defaults_option(config):
    assert config.get("Timer", "shortbreak_duration") == "5"


def test_set_option(bus, config, mocker, tmpdir):
    config_path = tmpdir.mkdir("tmp").join("tomate.config").strpath
    config.config_path = lambda: config_path

    subscriber = mocker.Mock()
    bus.connect(Events.CONFIG_CHANGE, subscriber, weak=False)

    config.set("section", "option", "value")

    config.load()
    assert config.get("section", "option") == "value"

    payload = ConfigPayload("set", "section", "option", "value")
    subscriber.assert_called_once_with(Events.CONFIG_CHANGE, payload=payload)


def test_remove_option(bus, config, mocker, tmpdir):
    tmp_path = tmpdir.mkdir("tmp").join("tomate.config")
    config.config_path = lambda: tmp_path.strpath

    config.set("section", "option", "value")
    subscriber = mocker.Mock()
    bus.connect(Events.CONFIG_CHANGE, subscriber, weak=False)
    config.remove("section", "option")

    config.load()
    assert config.parser.has_option("section", "option") is False

    payload = ConfigPayload("remove", "section", "option", "")
    subscriber.assert_called_once_with(Events.CONFIG_CHANGE, payload=payload)


================================================
FILE: tests/pomodoro/test_event.py
================================================
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Bus, Events, Subscriber, on


class TestBus:
    def test_connect_receiver(self, bus, mocker):
        def side_effect(*_, payload):
            return payload

        receiver = mocker.Mock(side_effect=side_effect)
        bus.connect(Events.SESSION_START, receiver, weak=False)

        assert bus.send(Events.SESSION_START, payload="payload") == ["payload"]

    def test_disconnect_receiver(self, bus, mocker):
        receiver = mocker.Mock()
        bus.connect(Events.SESSION_START, receiver, weak=False)
        bus.disconnect(Events.SESSION_START, receiver)

        assert bus.send(Events.SESSION_START, payload="payload") == []


def test_subscriber(bus):
    class Subject(Subscriber):
        @on(Events.TIMER_START, Events.SESSION_START)
        def bar(self, **__) -> bool:
            return True

    subject = Subject()
    subject.connect(bus)

    result = bus.send(Events.TIMER_START, "timer_start")
    assert len(result) == 1 and result[0] is True

    result = bus.send(Events.SESSION_START)
    assert len(result) == 1 and result[0] is True

    assert bus.send(Events.WINDOW_SHOW) == []

    subject.disconnect(bus)

    assert bus.send(Events.TIMER_START) == []
    assert bus.send(Events.SESSION_START) == []


def test_module(graph):
    scan_to_graph(["tomate.pomodoro.event"], graph)
    instance = graph.get("tomate.bus")

    assert isinstance(instance, Bus)
    assert graph.get("tomate.bus") is instance


================================================
FILE: tests/pomodoro/test_graph.py
================================================
from wiring import Graph
from wiring.scanning import scan_to_graph


def test_module():
    graph = Graph()

    scan_to_graph(["tomate.pomodoro.graph"], graph)

    assert isinstance(graph.get(Graph), Graph)


================================================
FILE: tests/pomodoro/test_plugin.py
================================================
import os
from distutils.version import StrictVersion

import pytest
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events, PluginEngine, suppress_errors


@pytest.fixture
def plugin_engine(bus, graph, config) -> PluginEngine:
    return PluginEngine(bus, config, graph)


def test_module(bus, config, graph):
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.config", config)
    scan_to_graph(["tomate.pomodoro.plugin"], graph)

    instance = graph.get("tomate.plugin")

    assert isinstance(instance, PluginEngine)
    assert instance == graph.get("tomate.plugin")


class TestPluginEngine:
    def test_collect(self, bus, graph, plugin_engine):
        assert plugin_engine.has_plugins() is False

        plugin_engine.collect()

        assert plugin_engine.has_plugins() is True

        for plugin in plugin_engine.all():
            assert plugin.plugin_object.bus is bus
            assert plugin.plugin_object.graph is graph

    def test_activate(self, bus, plugin_engine):
        plugin_engine.collect()
        plugin_a = plugin_engine.lookup("PluginA")

        assert plugin_a.is_activated is False
        assert bus.is_connect(Events.WINDOW_SHOW, plugin_a.plugin_object.listener) is False

        plugin_engine.activate("PluginA")
        assert plugin_a.is_activated is True
        assert bus.is_connect(Events.WINDOW_SHOW, plugin_a.plugin_object.listener) is True

    def test_deactivate(self, bus, plugin_engine):
        plugin_engine.collect()
        plugin_b = plugin_engine.lookup("PluginB")

        assert plugin_b.is_activated is True
        assert bus.is_connect(Events.WINDOW_SHOW, plugin_b.plugin_object.listener) is True

        plugin_engine.deactivate("PluginB")
        assert plugin_b.is_activated is False
        assert bus.is_connect(Events.WINDOW_SHOW, plugin_b.plugin_object.listener) is False

    def test_all(self, plugin_engine):
        plugin_engine.collect()

        got = [(p.name, p.version, p.is_activated, p.plugin_object.has_settings) for p in plugin_engine.all()]

        assert got == [
            ("PluginA", StrictVersion("1.0.0"), False, True),
            ("PluginB", StrictVersion("2.0.0"), True, False),
        ]

    def test_lookup(self, plugin_engine):
        plugin_engine.collect()

        assert plugin_engine.lookup("Not Exist") is None

        plugin = plugin_engine.lookup("PluginA")

        assert plugin is not None


class TestRaiseException:
    def test_does_not_raise_exception_when_debug_is_disabled(self):
        os.unsetenv("TOMATE_DEBUG")

        @suppress_errors
        def raise_exception():
            raise Exception()

        assert not raise_exception()

    def test_raises_exception_when_debug_enable(self):
        os.environ.setdefault("TOMATE_DEBUG", "1")

        @suppress_errors
        def raise_exception():
            raise Exception()

        with pytest.raises(Exception):
            raise_exception()


================================================
FILE: tests/pomodoro/test_session.py
================================================
import pytest
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events, Session, SessionPayload, SessionType
from tomate.pomodoro.config import Config
from tomate.pomodoro.session import State
from tomate.ui.testing import create_session_payload, run_loop_for


@pytest.fixture()
def session(graph, config, bus, mocker):
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.config", config)
    scan_to_graph(["tomate.pomodoro.timer", "tomate.pomodoro.session"], graph)
    return graph.get("tomate.session")


def test_module(graph, session):
    instance = graph.get("tomate.session")

    assert isinstance(instance, Session)
    assert instance is session


def test_sends_ready_event(bus, mocker, session):
    subscriber = mocker.Mock()
    bus.connect(Events.SESSION_READY, subscriber, weak=False)

    session.ready()

    payload = create_session_payload()
    subscriber.assert_called_once_with(Events.SESSION_READY, payload=payload)


class TestSessionStart:
    def test_not_start_when_session_is_already_running(self, session):
        session.state = State.STARTED

        assert not session.start()

    @pytest.mark.parametrize("state", [State.ENDED, State.STOPPED])
    def test_starts_when_session_is_not_running(self, state, session, bus, mocker):
        session.state = state

        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_START, subscriber, weak=False)

        result = session.start()

        assert result is True
        payload = SessionPayload(
            type=SessionType.POMODORO,
            pomodoros=0,
            duration=25 * 60,
        )
        subscriber.assert_called_once_with(Events.SESSION_START, payload=payload)


class TestSessionStop:
    @pytest.mark.parametrize("state", [State.INITIAL, State.ENDED, State.STOPPED, State.STARTED])
    def test_not_stop_when_session_is_not_running(self, state, session):
        session.state = state

        assert not session.stop()

    def test_stops_when_session_is_running(self, session, bus, mocker):
        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_INTERRUPT, subscriber, False)

        session.ready()
        session.start()
        result = session.stop()

        assert result is True
        payload = SessionPayload(
            type=SessionType.POMODORO,
            duration=25 * 60,
            pomodoros=0,
        )
        subscriber.assert_called_once_with(Events.SESSION_INTERRUPT, payload=payload)


class TestSessionReset:
    @pytest.mark.parametrize("state", [State.INITIAL, State.STARTED])
    def test_not_reset_when_session_is_running(self, state, session):
        session.state = state

        assert not session.reset()

    @pytest.mark.parametrize(
        "state,session_type,duration",
        [
            (State.ENDED, SessionType.SHORT_BREAK, 5 * 60),
            (State.STOPPED, SessionType.POMODORO, 25 * 60),
        ],
    )
    def test_resets_when_session_is_not_running(self, state, session_type, duration, session, bus, mocker):
        session.state = state
        session.current = session_type
        session.pomodoros = 1

        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_RESET, subscriber, False)

        result = session.reset()

        assert result is True
        payload = SessionPayload(
            type=session_type,
            pomodoros=0,
            duration=duration,
        )
        subscriber.assert_called_once_with(Events.SESSION_RESET, payload=payload)


class TestSessionEnd:
    @pytest.mark.parametrize("state", [State.INITIAL, State.ENDED, State.STOPPED])
    def test_ends_when_session_is_not_running(self, state, session):
        session.state = state

        assert not session._end(None, None)

    def test_not_end_when_session_start_but_time_still_running(self, session):
        session.start()

        assert not session._end(None, None)

    @pytest.mark.parametrize(
        "old_session,old_pomodoros,new_session,new_pomodoros",
        [
            (SessionType.POMODORO, 0, SessionType.SHORT_BREAK, 1),
            (SessionType.LONG_BREAK, 0, SessionType.POMODORO, 0),
            (SessionType.SHORT_BREAK, 0, SessionType.POMODORO, 0),
            (SessionType.POMODORO, 3, SessionType.LONG_BREAK, 4),
        ],
    )
    def test_ends_when_session_is_running(
        self,
        old_session,
        old_pomodoros,
        new_session,
        new_pomodoros,
        config,
        bus,
        mocker,
        session,
    ):
        session.current = old_session
        session.pomodoros = old_pomodoros

        config.set(config.DURATION_SECTION, config.DURATION_POMODORO, 0.02)
        config.set(config.DURATION_SECTION, config.DURATION_LONG_BREAK, 0.02)
        config.set(config.DURATION_SECTION, config.DURATION_SHORT_BREAK, 0.02)
        config.parser.getint = config.parser.getfloat

        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_END, subscriber, False)

        session.ready()
        session.start()
        run_loop_for(2)

        payload = create_session_payload(
            type=old_session,
            pomodoros=new_pomodoros,
            duration=1,
        )
        subscriber.assert_called_once_with(Events.SESSION_END, payload=payload)
        assert session.current is new_session

    def test_changes_session_type(self, bus, config, mocker, session):
        config.set(config.DURATION_SECTION, config.DURATION_POMODORO, 0.02)
        config.parser.getint = config.parser.getfloat

        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_CHANGE, subscriber, False)

        session.ready()
        session.start()
        run_loop_for(2)

        payload = SessionPayload(
            type=SessionType.SHORT_BREAK,
            duration=5 * 60,
            pomodoros=1,
        )
        subscriber.assert_called_once_with(Events.SESSION_CHANGE, payload=payload)


class TestSessionChange:
    @pytest.mark.parametrize("state", [State.INITIAL, State.STARTED])
    def test_not_change_when_session_is_running(self, state, session):
        session.state = state

        assert session.change(session=SessionType.LONG_BREAK) is False
        assert session.current is SessionType.POMODORO

    @pytest.mark.parametrize(
        "state, session_type",
        [
            (State.STOPPED, SessionType.SHORT_BREAK),
            (State.ENDED, SessionType.LONG_BREAK),
        ],
    )
    def test_changes_when_session_is_not_running(self, state, session_type, bus, config, mocker, session):
        session.state = state
        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_CHANGE, subscriber, False)

        assert session.change(session=session_type) is True
        assert session.current is session_type

        payload = create_session_payload(
            type=session_type, duration=config.get_int(config.DURATION_SECTION, session_type.option) * 60
        )
        subscriber.assert_called_once_with(Events.SESSION_CHANGE, payload=payload)

    @pytest.mark.parametrize("state", [State.STOPPED, State.ENDED])
    def test_changes_when_config_change_and_session_is_not_running(self, state, bus, config, mocker, session):
        session.state = state
        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_CHANGE, subscriber, False)

        config.set(config.DURATION_SECTION, SessionType.POMODORO.option, 20)

        payload = create_session_payload(duration=20 * 60)
        subscriber.assert_called_once_with(Events.SESSION_CHANGE, payload=payload)

    def test_not_change_when_config_section_is_not_timer(self, bus, config, mocker, session):
        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_CHANGE, subscriber, False)

        config.set("section", "option", "value")

        subscriber.assert_not_called()

    def test_not_change_when_config_timer_changes_and_session_is_running(self, bus, config, mocker, session):
        session.state = State.STARTED
        subscriber = mocker.Mock()
        bus.connect(Events.SESSION_CHANGE, subscriber, False)

        config.set(config.DURATION_SECTION, SessionType.POMODORO.option, 24)

        subscriber.assert_not_called()


@pytest.mark.parametrize(
    "number,session_type",
    [
        (0, SessionType.POMODORO),
        (1, SessionType.SHORT_BREAK),
        (2, SessionType.LONG_BREAK),
    ],
)
def test_type_of(number, session_type):
    assert SessionType.of(number) == session_type


def test_type_of_unknown():
    with pytest.raises(Exception):
        assert SessionType.of(999)


@pytest.mark.parametrize(
    "session_type, option",
    [
        (SessionType.POMODORO, Config.DURATION_POMODORO),
        (SessionType.SHORT_BREAK, Config.DURATION_SHORT_BREAK),
        (SessionType.LONG_BREAK, Config.DURATION_LONG_BREAK),
    ],
)
def test_type_option(session_type, option):
    assert session_type.option == option


================================================
FILE: tests/pomodoro/test_timer.py
================================================
import pytest
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events, Timer, TimerPayload
from tomate.pomodoro.timer import State
from tomate.ui.testing import run_loop_for


def test_module(bus, graph):
    graph.register_instance("tomate.bus", bus)
    scan_to_graph(["tomate.pomodoro.timer"], graph)

    instance = graph.get("tomate.timer")

    assert isinstance(instance, Timer)


class TestTimerStart:
    def test_not_start_when_timer_is_already_running(self, bus):
        timer = Timer(bus)
        timer.state = State.STARTED

        assert not timer.start(60)

    @pytest.mark.parametrize("state", [State.ENDED, State.STOPPED])
    def test_starts_when_timer_not_started_yet(self, bus, mocker, state):
        timer = Timer(bus)
        timer.state = state

        subscriber = mocker.Mock()
        bus.connect(Events.TIMER_START, subscriber, weak=False)

        result = timer.start(60)

        assert result is True
        subscriber.assert_called_once_with(Events.TIMER_START, payload=TimerPayload(time_left=60, duration=60))


class TestTimerStop:
    @pytest.mark.parametrize("state", [State.ENDED, State.STOPPED])
    def test_not_stop_when_timer_is_not_running(self, bus, state):
        timer = Timer(bus)
        timer.state = state

        assert not timer.stop()

    def test_stops_when_timer_is_running(self, bus, mocker):
        timer = Timer(bus)
        subscriber = mocker.Mock()
        bus.connect(Events.TIMER_STOP, subscriber, weak=False)

        timer.start(60)
        result = timer.stop()

        assert result is True
        assert timer.is_running() is False
        subscriber.assert_called_once_with(Events.TIMER_STOP, payload=TimerPayload(time_left=0, duration=0))


class TestTimerEnd:
    @pytest.mark.parametrize("state", [State.ENDED, State.STOPPED])
    def test_not_end_when_timer_is_not_running(self, bus, state):
        timer = Timer(bus)
        timer.state = state

        assert not timer.end()

    def test_ends_when_time_is_up(self, bus, mocker):
        timer = Timer(bus)
        changed = mocker.Mock()
        timer._bus.connect(Events.TIMER_UPDATE, changed, weak=False)

        finished = mocker.Mock()
        bus.connect(Events.TIMER_END, finished, weak=False)

        timer.start(1)
        run_loop_for(2)

        assert timer.is_running() is False
        changed.assert_called_once_with(Events.TIMER_UPDATE, payload=TimerPayload(time_left=0, duration=1))
        finished.assert_called_once_with(Events.TIMER_END, payload=TimerPayload(time_left=0, duration=1))


class TestTimerPayload:
    @pytest.mark.parametrize(
        "duration,time_left,ratio",
        [(100, 99, 0.99), (100, 90, 0.9), (100, 50, 0.5), (100, 0, 0.0)],
    )
    def test_remaining_ratio(self, duration, time_left, ratio):
        payload = TimerPayload(duration=duration, time_left=time_left)

        assert payload.remaining_ratio == ratio

    @pytest.mark.parametrize(
        "duration,time_left,ratio",
        [
            (100, 100, 0.0),
            (100, 99, 0.01),
            (100, 98, 0.02),
            (100, 97, 0.03),
            (100, 96, 0.04),
            (100, 95, 0.05),
            (100, 94, 0.06),
            (100, 93, 0.07),
            (100, 92, 0.08),
            (100, 91, 0.09),
            (100, 90, 0.1),
            (100, 89, 0.11),
            (100, 50, 0.5),
            (100, 15, 0.85),
            (100, 10, 0.9),
            (100, 5, 0.95),
            (100, 0, 1.0),
        ],
    )
    def test_elapsed_ratio(self, duration, time_left, ratio):
        payload = TimerPayload(duration=duration, time_left=time_left)

        assert payload.elapsed_ratio == ratio

    @pytest.mark.parametrize(
        "duration,time_left,percent",
        [
            (100, 100, 0.0),
            (100, 99, 0.0),
            (100, 98, 0.0),
            (100, 97, 0.0),
            (100, 96, 0.0),
            (100, 95, 5.0),
            (100, 94, 5.0),
            (100, 93, 5.0),
            (100, 92, 5.0),
            (100, 91, 5.0),
            (100, 90, 10.0),
            (100, 89, 10.0),
            (100, 6, 90.0),
            (100, 5, 95.0),
            (100, 4, 95.0),
            (100, 3, 95.0),
            (100, 2, 95.0),
            (100, 1, 95.0),
            (100, 0, 100.0),
        ],
    )
    def test_elapsed_percent(self, duration, time_left, percent):
        payload = TimerPayload(duration=duration, time_left=time_left)

        assert payload.elapsed_percent == percent

    @pytest.mark.parametrize(
        "seconds,formatted",
        [
            (25 * 60, "25:00"),
            (15 * 60, "15:00"),
            (5 * 60, "05:00"),
        ],
    )
    def test_payload_markup(self, seconds, formatted):
        payload = TimerPayload(time_left=seconds, duration=0)

        assert payload.countdown == formatted


================================================
FILE: tests/ui/dialogs/test_about.py
================================================
import pytest
from gi.repository import Gtk
from wiring.scanning import scan_to_graph

from tomate import __version__
from tomate.ui.testing import refresh_gui


@pytest.fixture
def about(graph, config):
    graph.register_instance("tomate.config", config)
    scan_to_graph(["tomate.ui.dialogs.about"], graph)

    return graph.get("tomate.ui.about")


def test_module(graph, about):
    assert graph.get("tomate.ui.about") is about


def test_dialog_info(about):
    assert about.get_comments() == "Tomate Pomodoro Timer (GTK+ Interface)"
    assert about.get_copyright() == "2014, Elio Esteves Duarte"
    assert about.get_version() == __version__
    assert about.get_website() == "https://github.com/eliostvs/tomate-gtk"
    assert about.get_website_label() == "Tomate GTK on Github"
    assert about.get_license_type() == Gtk.License.GPL_3_0
    assert about.get_logo() is not None


def test_close_dialog(about, mocker):
    about.hide = mocker.Mock()

    about.emit("response", 0)
    refresh_gui()

    about.hide.assert_called_once()


================================================
FILE: tests/ui/dialogs/test_preference.py
================================================
import pytest
from gi.repository import Gtk
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Config, Events
from tomate.ui.dialogs import ExtensionTab, PluginGrid, PreferenceDialog, TimerTab
from tomate.ui.testing import TV, Q


@pytest.fixture
def preference(bus, plugin_engine, config, mocker) -> PreferenceDialog:
    mocker.patch("tomate.ui.dialogs.preference.Gtk.Dialog.run")
    return PreferenceDialog(TimerTab(config), ExtensionTab(bus, config, plugin_engine))


def test_preference_module(graph, bus, config, plugin_engine):
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.plugin", plugin_engine)
    graph.register_instance("tomate.config", config)
    scan_to_graph(["tomate.ui.dialogs.preference"], graph)
    instance = graph.get("tomate.ui.preference")

    assert isinstance(instance, PreferenceDialog)
    assert graph.get("tomate.ui.preference") is instance


def test_refresh_reload_plugins(preference, plugin_engine):
    plugin_engine.collect()
    preference.run()

    plugin_list = Q.select(preference.widget, Q.props("name", "plugin.list"))
    assert Q.map(plugin_list, TV.model, len) == 2

    for plugin in plugin_engine.all():
        plugin_engine.remove(plugin)

    preference.run()

    assert Q.map(plugin_list, TV.model, len) == 0


@pytest.mark.parametrize(
    "plugin,row,columns",
    [
        (
            "PluginA",
            0,
            ["PluginA", False, "<b>PluginA</b> (1.0)\n<small>Description A</small>"],
        ),
        (
            "PluginB",
            1,
            ["PluginB", True, "<b>PluginB</b> (2.0)\n<small>Description B</small>"],
        ),
    ],
)
def test_initial_plugin_list(plugin, row, columns, preference, plugin_engine):
    plugin_engine.collect()
    preference.run()

    def get_columns(tree_store: Gtk.TreeStore):
        return [tree_store[row][column] for column in (PluginGrid.NAME, PluginGrid.ACTIVE, PluginGrid.DETAIL)]

    assert Q.map(Q.select(preference.widget, Q.props("name", "plugin.list")), TV.model, get_columns) == columns
    assert Q.select(preference.widget, Q.props("name", "plugin.settings")).props.sensitive is False


def test_open_plugin_settings(preference, plugin_engine):
    plugin_engine.collect()
    preference.run()

    Q.map(
        Q.select(preference.widget, Q.props("name", "plugin.list")),
        TV.column(Q.props("title", "Active")),
        TV.cell_renderer(0),
        Q.emit("toggled", 0),
    )

    settings_button = Q.select(preference.widget, Q.props("name", "plugin.settings"))
    assert settings_button.props.sensitive is True

    settings_button.emit("clicked")
    plugin_a = plugin_engine.lookup("PluginA")
    assert plugin_a.plugin_object.parent == preference.widget


def test_connect_and_disconnect_plugins(bus, plugin_engine, preference):
    plugin_engine.collect()
    preference.run()

    result = bus.send(Events.WINDOW_SHOW)
    assert len(result) == 1 and result[0] == "plugin_b"

    def toggle_plugin(row: int):
        Q.map(
            Q.select(preference.widget, Q.props("name", "plugin.list")),
            TV.column(Q.props("title", "Active")),
            TV.cell_renderer(0),
            Q.emit("toggled", row),
        )

    toggle_plugin(0)
    toggle_plugin(1)

    result = bus.send(Events.WINDOW_SHOW)
    assert len(result) == 1 and result[0] == "plugin_a"


@pytest.mark.parametrize(
    "duration_name,option,value",
    [
        ("duration.pomodoro", Config.DURATION_POMODORO, 24),
        ("duration.shortbreak", Config.DURATION_SHORT_BREAK, 4),
        ("duration.longbreak", Config.DURATION_LONG_BREAK, 14),
    ],
)
def test_save_config_when_task_duration_change(duration_name, option, value, config, preference):
    spin_button = Q.select(preference.widget, Q.props("name", duration_name))
    assert spin_button is not None

    spin_button.props.value = value
    spin_button.emit("value-changed")

    assert config.get_int(Config.DURATION_SECTION, option) == value


================================================
FILE: tests/ui/test_shortcut.py
================================================
import pytest
from gi.repository import Gtk
from wiring.scanning import scan_to_graph

from tomate.ui import Shortcut, ShortcutEngine
from tomate.ui.testing import active_shortcut


@pytest.fixture
def shortcut_engine(bus, config, graph) -> ShortcutEngine:
    graph.register_instance("tomate.config", config)
    scan_to_graph(["tomate.ui.shortcut"], graph)
    return graph.get("tomate.ui.shortcut")


def test_module(graph, shortcut_engine):
    instance = graph.get("tomate.ui.shortcut")

    assert isinstance(instance, ShortcutEngine)
    assert instance is shortcut_engine


def test_label(shortcut_engine):
    label = shortcut_engine.label(Shortcut("test", ""))

    assert label == "Ctrl+S"


def test_label_with_fallback(shortcut_engine):
    label = shortcut_engine.label(Shortcut("", "<control>p"))

    assert label == "Ctrl+P"


def test_connect(shortcut_engine, mocker):
    callback = mocker.Mock(return_value=True)
    shortcut = Shortcut("start", "<control>s")

    shortcut_engine.connect(shortcut, callback)
    assert active_shortcut(shortcut_engine, shortcut) is True

    key, mod = Gtk.accelerator_parse(shortcut.value)
    callback.assert_called_once_with(shortcut_engine.accel_group, mocker.ANY, key, mod)


def test_disconnect(shortcut_engine, mocker):
    shortcut = Shortcut("start", "<control>s")
    shortcut_engine.connect(shortcut, mocker.Mock())

    shortcut_engine.disconnect(shortcut)

    assert active_shortcut(shortcut_engine, shortcut) is False


def test_change(shortcut_engine, mocker):
    callback = mocker.Mock(return_value=True)
    old_shortcut = Shortcut("start", "<control>a")
    new_shortcut = Shortcut("start", "<control>b")

    shortcut_engine.connect(old_shortcut, callback)
    shortcut_engine.change(new_shortcut)

    assert active_shortcut(shortcut_engine, old_shortcut) is False
    assert active_shortcut(shortcut_engine, new_shortcut) is True

    key, mod = Gtk.accelerator_parse(new_shortcut.value)
    callback.assert_called_once_with(shortcut_engine.accel_group, mocker.ANY, key, mod)


================================================
FILE: tests/ui/test_systray.py
================================================
import pytest
from gi.repository import Gtk
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events
from tomate.ui import SystrayMenu
from tomate.ui.testing import refresh_gui


@pytest.fixture
def window():
    return Gtk.Label()


@pytest.fixture
def subject(graph, bus, window):
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.ui.view", window)
    scan_to_graph(["tomate.ui.systray"], graph)
    return graph.get("tomate.ui.systray.menu")


def test_module(graph, subject):
    instance = graph.get("tomate.ui.systray.menu")

    assert isinstance(instance, SystrayMenu)
    assert instance is subject


def test_hide_view_when_hide_menu_is_clicked(window, subject):
    window.props.visible = False

    subject.hide_item.emit("activate")

    refresh_gui()

    assert window.props.visible is False


def test_show_window_when_hide_item_is_clicked(window, subject):
    window.props.visible = False

    subject.show_item.emit("activate")

    refresh_gui()

    assert window.props.visible is True


@pytest.mark.parametrize("event,hide,show", [(Events.WINDOW_HIDE, False, True), (Events.WINDOW_SHOW, True, False)])
def test_change_items_visibility(event, hide, show, bus, subject):
    bus.send(event)

    assert subject.hide_item.props.visible is hide
    assert subject.show_item.props.visible is show


================================================
FILE: tests/ui/test_window.py
================================================
import pytest
from gi.repository import Gdk, Gtk
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events
from tomate.ui import Systray, Window
from tomate.ui.testing import Q, active_shortcut, create_session_payload


@pytest.fixture
def window(bus, config, graph, session) -> Window:
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.config", config)
    graph.register_instance("tomate.session", session)

    namespaces = [
        "tomate.ui",
        "tomate.pomodoro.plugin",
    ]
    scan_to_graph(namespaces, graph)
    return graph.get("tomate.ui.view")


def test_module(graph, window):
    instance = graph.get("tomate.ui.view")

    assert isinstance(instance, Window)
    assert instance is window


def test_init(session, window):
    session.ready.assert_called_once_with()

    # session button
    assert Q.select(window.widget, Q.props("name", "session.pomodoro"))
    assert Q.select(window.widget, Q.props("name", "session.short_break"))
    assert Q.select(window.widget, Q.props("name", "session.long_break"))

    # headerbar
    assert Q.select(window.widget, Q.props("name", "session.start"))
    assert Q.select(window.widget, Q.props("name", "session.reset"))
    assert Q.select(window.widget, Q.props("name", "session.stop"))

    # countdown
    assert Q.select(window.widget, Q.props("label", "00:00"))


def test_shortcuts(shortcut_engine, window):
    from tomate.ui.widgets import HeaderBar

    assert active_shortcut(shortcut_engine, HeaderBar.START_SHORTCUT, window=window.widget) is True


def test_run(mocker, window):
    gtk_main = mocker.patch("tomate.ui.window.Gtk.main")
    show_all = mocker.patch("tomate.ui.window.Gtk.Window.show_all")

    window.run()

    gtk_main.assert_called_once_with()
    show_all.assert_called_once_with()


class TestWindowHide:
    def test_iconify_when_tray_icon_plugin_is_not_registered(self, window, bus, mocker):
        subscriber = mocker.Mock()
        bus.connect(Events.WINDOW_HIDE, subscriber, weak=False)

        result = window.hide()

        assert result is Gtk.true
        subscriber.assert_called_once_with(Events.WINDOW_HIDE, payload=None)

    def test_deletes_when_tray_icon_plugin_is_registered(self, bus, graph, mocker, window):
        graph.register_factory(Systray, mocker.Mock)
        subscriber = mocker.Mock()
        bus.connect(Events.WINDOW_HIDE, subscriber, weak=False)
        window.widget.set_visible(True)

        result = window.hide()

        assert result
        assert window.widget.get_visible() is False
        subscriber.assert_called_once_with(Events.WINDOW_HIDE, payload=None)


class TestWindowQuit:
    def test_quits_when_timer_is_not_running(self, mocker, session, window):
        main_quit = mocker.patch("tomate.ui.window.Gtk.main_quit")
        session.is_running.return_value = False

        window.widget.emit("delete-event", Gdk.Event.new(Gdk.EventType.DELETE))

        main_quit.assert_called_once_with()

    def test_hides_when_timer_is_running(self, bus, mocker, session, window):
        session.is_running.return_value = True
        subscriber = mocker.Mock()
        bus.connect(Events.WINDOW_HIDE, subscriber, weak=False)

        window.widget.emit("delete-event", Gdk.Event.new(Gdk.EventType.DELETE))

        subscriber.assert_called_once_with(Events.WINDOW_HIDE, payload=None)


def test_shows_window_when_session_end(bus, mocker, window):
    window.widget.props.visible = False
    subscriber = mocker.Mock()
    bus.connect(Events.WINDOW_SHOW, subscriber, weak=False)

    payload = create_session_payload()
    bus.send(Events.SESSION_END, payload=payload)

    assert window.widget.props.visible is True
    subscriber.assert_called_once_with(Events.WINDOW_SHOW, payload=None)


================================================
FILE: tests/ui/widgets/test_countdown.py
================================================
import random

import pytest
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events, TimerPayload
from tomate.ui.testing import create_session_payload
from tomate.ui.widgets import Countdown


@pytest.fixture
def countdown(bus, graph) -> Countdown:
    graph.register_instance("tomate.bus", bus)
    scan_to_graph(["tomate.ui.widgets.countdown"], graph)
    return graph.get("tomate.ui.countdown")


def test_module(countdown, graph):
    instance = graph.get("tomate.ui.countdown")

    assert isinstance(instance, Countdown)
    assert instance is countdown


@pytest.mark.parametrize(
    "event, payload",
    [
        (Events.SESSION_READY, create_session_payload(duration=random.randint(1, 100))),
        (Events.SESSION_INTERRUPT, create_session_payload(duration=random.randint(1, 100))),
        (Events.SESSION_CHANGE, create_session_payload(duration=random.randint(1, 100))),
        (Events.TIMER_UPDATE, TimerPayload(time_left=random.randint(1, 100), duration=0)),
    ],
)
def test_updates_countdown_when_session_state_changes(event, payload, bus, countdown):
    bus.send(event, payload=payload)

    assert payload.countdown in countdown.widget.get_text()


================================================
FILE: tests/ui/widgets/test_headerbar.py
================================================
import pytest
from gi.repository import Gtk
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events
from tomate.ui.testing import Q, active_shortcut, create_session_payload, refresh_gui
from tomate.ui.widgets import HeaderBar, HeaderBarMenu


class TestHeaderBar:
    @pytest.fixture
    def menu(self, mocker):
        return mocker.Mock(spec=HeaderBarMenu, widget=Gtk.Menu())

    @pytest.fixture
    def headerbar(self, graph, menu, shortcut_engine, session, bus, mocker) -> HeaderBar:
        graph.register_instance("tomate.bus", bus)
        graph.register_instance("tomate.session", session)
        graph.register_factory("tomate.ui.about", mocker.Mock)
        graph.register_factory("tomate.ui.preference", mocker.Mock)
        graph.register_instance("tomate.ui.menu", menu)
        graph.register_instance("tomate.ui.shortcut", shortcut_engine)

        # gtk shortcuts binds leave beyond the scope
        shortcut_engine.disconnect(HeaderBar.START_SHORTCUT)
        shortcut_engine.disconnect(HeaderBar.STOP_SHORTCUT)
        shortcut_engine.disconnect(HeaderBar.RESET_SHORTCUT)

        namespaces = ["tomate.ui.widgets.headerbar"]

        scan_to_graph(namespaces, graph)

        return graph.get("tomate.ui.headerbar")

    def test_module(self, graph, headerbar):
        instance = graph.get("tomate.ui.headerbar")

        assert isinstance(instance, HeaderBar)
        assert instance is headerbar

    @pytest.mark.parametrize(
        "shortcut,action",
        [
            (HeaderBar.START_SHORTCUT, "start"),
            (HeaderBar.STOP_SHORTCUT, "stop"),
            (HeaderBar.RESET_SHORTCUT, "reset"),
        ],
    )
    def test_shortcuts(self, shortcut, action, headerbar, menu, session, shortcut_engine):
        assert active_shortcut(shortcut_engine, shortcut)

        getattr(session, action).assert_called_once_with()

    @pytest.mark.parametrize(
        "button_name,action",
        [
            (HeaderBar.START_SHORTCUT.name, "start"),
            (HeaderBar.STOP_SHORTCUT.name, "stop"),
            (HeaderBar.RESET_SHORTCUT.name, "reset"),
        ],
    )
    def test_change_session(self, button_name, action, session, headerbar):
        button = Q.select(headerbar.widget, Q.props("name", button_name))

        button.emit("clicked")
        refresh_gui()

        getattr(session, action).assert_called_once_with()

    @pytest.mark.parametrize(
        "button_name,tooltip",
        [
            (HeaderBar.START_SHORTCUT.name, "Starts the session (Ctrl+S)"),
            (HeaderBar.STOP_SHORTCUT.name, "Stops the session (Ctrl+P)"),
            (HeaderBar.RESET_SHORTCUT.name, "Clear session count (Ctrl+R)"),
            (HeaderBarMenu.PREFERENCE_SHORTCUT.name, "Open preferences (Ctrl+,)"),
        ],
    )
    def test_buttons_tooltip(self, button_name, tooltip, headerbar):
        button = Q.select(headerbar.widget, Q.props("name", button_name))
        assert tooltip == button.props.tooltip_text

    def test_enable_only_the_stop_button_when_session_starts(self, bus, headerbar):
        bus.send(Events.SESSION_START)

        assert Q.select(headerbar.widget, Q.props("name", HeaderBar.START_SHORTCUT.name)).props.visible is False
        assert Q.select(headerbar.widget, Q.props("name", HeaderBar.STOP_SHORTCUT.name)).props.visible is True
        assert Q.select(headerbar.widget, Q.props("name", HeaderBar.RESET_SHORTCUT.name)).props.sensitive is False

    def test_disables_reset_button_when_session_is_reset(self, headerbar, bus, session):
        reset_button = Q.select(headerbar.widget, Q.props("name", headerbar.RESET_SHORTCUT.name))
        reset_button.props.sensitive = True

        bus.send(Events.SESSION_RESET)

        assert reset_button.props.sensitive is False
        assert headerbar.widget.props.title == "No session yet"

    @pytest.mark.parametrize(
        "event,reset,title,payload",
        [
            (Events.SESSION_INTERRUPT, False, "No session yet", create_session_payload()),
            (Events.SESSION_END, True, "Session 1", create_session_payload(pomodoros=1)),
        ],
    )
    def test_buttons_visibility_and_title_in_the_first_session(self, event, title, reset, headerbar, payload, bus):
        bus.send(event, payload=payload)

        assert Q.select(headerbar.widget, Q.props("name", headerbar.START_SHORTCUT.name)).props.visible is True
        assert Q.select(headerbar.widget, Q.props("name", headerbar.STOP_SHORTCUT.name)).props.visible is False
        assert Q.select(headerbar.widget, Q.props("name", headerbar.RESET_SHORTCUT.name)).props.sensitive is reset
        assert headerbar.widget.props.title == title


class TestHeaderBarMenu:
    @pytest.fixture
    def preference(self, mocker):
        return mocker.Mock(widget=mocker.Mock(spec=Gtk.Dialog))

    @pytest.fixture()
    def about(self, mocker):
        return mocker.Mock(widget=mocker.Mock(spec=Gtk.Dialog))

    @pytest.fixture
    def menu(self, bus, about, preference, shortcut_engine) -> HeaderBarMenu:
        shortcut_engine.disconnect(HeaderBarMenu.PREFERENCE_SHORTCUT)

        return HeaderBarMenu(bus, about, preference, shortcut_engine)

    def test_module(self, about, bus, preference, graph, shortcut_engine):
        graph.register_instance("tomate.bus", bus)
        graph.register_instance("tomate.ui.about", about)
        graph.register_instance("tomate.ui.preference", preference)
        graph.register_instance("tomate.ui.shortcut", shortcut_engine)

        namespaces = ["tomate.ui.widgets.headerbar"]
        scan_to_graph(namespaces, graph)

        instance = graph.get("tomate.ui.headerbar.menu")

        assert isinstance(instance, HeaderBarMenu)
        assert instance is graph.get("tomate.ui.headerbar.menu")

    @pytest.mark.parametrize(
        "widget,label,mock_name",
        [
            ("header.menu.preference", "Preferences", "preference"),
            ("header.menu.about", "About", "about"),
        ],
    )
    def test_menu_items(self, widget, label, mock_name, menu, about, preference):
        menu_item = Q.select(menu.widget, Q.props("name", widget))
        assert menu_item.props.label == label

        menu_item.emit("activate")
        refresh_gui()

        dialog = locals()[mock_name].widget
        dialog.run.assert_called_once_with()

    def test_shortcut(self, menu, shortcut_engine, preference):
        assert active_shortcut(shortcut_engine, HeaderBarMenu.PREFERENCE_SHORTCUT) is True

        preference.widget.run.assert_called_once_with()


================================================
FILE: tests/ui/widgets/test_session_button.py
================================================
import pytest
from wiring.scanning import scan_to_graph

from tomate.pomodoro import Events, SessionType
from tomate.ui.testing import Q, active_shortcut, create_session_payload, refresh_gui
from tomate.ui.widgets import SessionButton


@pytest.fixture
def session_button(bus, graph, session, shortcut_engine) -> SessionButton:
    graph.register_instance("tomate.bus", bus)
    graph.register_instance("tomate.session", session)
    graph.register_instance("tomate.ui.shortcut", shortcut_engine)
    scan_to_graph(["tomate.ui.widgets.session_button"], graph)

    # clean key binds between tests
    shortcut_engine.disconnect(SessionButton.POMODORO_SHORTCUT)
    shortcut_engine.disconnect(SessionButton.SHORT_BREAK_SHORTCUT)
    shortcut_engine.disconnect(SessionButton.LONG_BREAK_SHORTCUT)

    return graph.get("tomate.ui.taskbutton")


def test_module(graph, session_button):
    instance = graph.get("tomate.ui.taskbutton")

    assert isinstance(instance, SessionButton)
    assert instance is session_button


@pytest.mark.parametrize(
    "button_name,label,tooltip_text",
    [
        (SessionButton.POMODORO_SHORTCUT.name, "Pomodoro", "Pomodoro (Ctrl+1)"),
        (SessionButton.SHORT_BREAK_SHORTCUT.name, "Short Break", "Short Break (Ctrl+2)"),
        (SessionButton.LONG_BREAK_SHORTCUT.name, "Long Break", "Long Break (Ctrl+3)"),
    ],
)
def test_buttons_content(button_name, label, tooltip_text, session_button):
    button = Q.select(session_button.widget, Q.props("name", button_name))

    assert button.props.tooltip_text == tooltip_text

    assert Q.select(button, Q.props("label", label))


def test_disables_buttons_when_session_starts(bus, session_button):
    bus.send(Events.SESSION_START)

    assert session_button.widget.props.sensitive is False


@pytest.mark.parametrize(
    "event,payload,session_type",
    [
        (Events.SESSION_INTERRUPT, create_session_payload(type=SessionType.LONG_BREAK), SessionType.LONG_BREAK),
        (Events.SESSION_READY, create_session_payload(), SessionType.POMODORO),
    ],
)
def test_selects_button_when_session_stops_and_begins(event, payload, session_type, bus, session_button, session):
    session_button.widget.props.sensitive = False

    bus.send(event, payload=payload)

    assert session_button.widget.props.sensitive is True
    assert session_button.widget.get_selected() is session_type.value


@pytest.mark.parametrize("session_type", [SessionType.POMODORO, SessionType.SHORT_BREAK, SessionType.LONG_BREAK])
def test_changes_session_when_button_is_clicked(session_type, session_button, session):
    session_button.widget.emit("mode_changed", session_type.value)

    refresh_gui()

    session.change.assert_called_once_with(session_type)


@pytest.mark.parametrize(
    "shortcut,session_type",
    [
        (SessionButton.POMODORO_SHORTCUT, SessionType.POMODORO),
        (SessionButton.SHORT_BREAK_SHORTCUT, SessionType.SHORT_BREAK),
        (SessionButton.LONG_BREAK_SHORTCUT, SessionType.LONG_BREAK),
    ],
)
def test_shortcuts(shortcut, session_type, session_button, shortcut_engine, session):
    assert active_shortcut(shortcut_engine, shortcut) is True

    session.change.assert_called_once_with(session_type)


def test_selects_button_when_session_changes(bus, session_button, session):
    bus.send(Events.SESSION_CHANGE, payload=create_session_payload(type=SessionType.SHORT_BREAK))

    assert session_button.widget.get_selected() is SessionType.SHORT_BREAK.value


================================================
FILE: tomate/__init__.py
================================================
__version__ = "0.25.2"


================================================
FILE: tomate/__main__.py
================================================
from .main import main

if __name__ == "__main__":
    main()


================================================
FILE: tomate/audio/__init__.py
================================================
from .player import GStreamerPlayer

__all__ = ["GStreamerPlayer"]


================================================
FILE: tomate/audio/player.py
================================================
import logging

import gi

gi.require_version("Gst", "1.0")
gi.require_version("Gtk", "3.0")

from gi.repository import Gst

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


class GStreamerPlayer:
    def __init__(self, repeat=False):
        Gst.init(None)
        self.repeat = repeat
        self._file = None
        self._is_about_to_finished = False

        self._playbin = Gst.ElementFactory.make("playbin", "player")
        self._volume_filter = Gst.ElementFactory.make("volume", "volume")
        self._playbin.props.audio_filter = self._volume_filter
        self._playbin.bus.add_signal_watch()
        self._playbin.bus.connect("message", self._on_bus_callback)
        self._playbin.connect("about-to-finish", self._on_about_to_finish)
        self._playbin.volume = 1.0
        self._volume_filter.volume = 0

    @property
    def file(self) -> str:
        return self._file

    @file.setter
    def file(self, filepath: str) -> None:
        _, state, pending_state = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        self._file = filepath

        logger.debug(f"action=set_file filepath={filepath} state={state} pending_state={pending_state}")

        if not self._file:
            self.stop()
            return

        if pending_state != Gst.State.VOID_PENDING:
            state = pending_state

        if state == Gst.State.PLAYING or state == Gst.State.PAUSED:
            self._is_about_to_finished = False
            self._playbin.set_state(Gst.State.READY)
            self._playbin.props.uri = self._file
            self._playbin.set_state(state)

    @property
    def volume(self) -> float:
        return self._volume_filter.props.volume

    @volume.setter
    def volume(self, volume: float) -> None:
        logger.debug(f"action=set_volume volume={volume}")
        self._volume_filter.props.volume = max(0.0, min(1.0, volume))

    def play(self) -> None:
        logger.debug("action=play")
        self._playbin.props.uri = self._file
        self._playbin.set_state(Gst.State.PLAYING)

    def stop(self) -> None:
        logger.debug("action=stop")
        self._playbin.set_state(Gst.State.NULL)

    def _on_bus_callback(self, _, message):
        if message.type == Gst.MessageType.EOS:
            if self._is_about_to_finished:
                self._is_about_to_finished = False
            else:
                self._finished()

        elif message.type == Gst.MessageType.ERROR:
            logger.error("action=audio_failed message='%s-%s'", *message.parse_error())
            self.stop()

    def _on_about_to_finish(self, _) -> None:
        logger.debug("action=about_to_finish")
        self._is_about_to_finished = True
        self._finished()

    def _finished(self) -> None:
        current_uri = self._playbin.props.current_uri

        logger.debug(f"action=finished repeat={self.repeat} current_uri={current_uri}")

        if current_uri and self.repeat:
            self._playbin.props.uri = current_uri


================================================
FILE: tomate/main.py
================================================
import argparse
import locale
import logging
from locale import gettext as _

import gi

gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")

from gi.repository import Gdk
from wiring.scanning import scan_to_graph

from tomate.pomodoro.app import Application
from tomate.pomodoro.graph import graph

locale.textdomain("tomate")
logger = logging.getLogger(__name__)


def main():
    try:
        options = parse_options()
        setup_logging(options)

        scan_to_graph(["tomate"], graph)
        app = Application.from_graph(graph)

        app.Run()
        if app.IsRunning():
            Gdk.notify_startup_complete()

    except Exception as ex:
        logger.error(ex, exc_info=True)
        raise ex


def setup_logging(options):
    level = logging.DEBUG if options.verbose else logging.INFO
    fmt = "%(levelname)s:%(asctime)s:%(name)s:%(message)s"
    logging.basicConfig(level=level, format=fmt)


def parse_options():
    parser = argparse.ArgumentParser(prog="tomate-gtk")

    parser.add_argument(
        "-v",
        "--verbose",
        default=False,
        action="store_true",
        help=_("Show debug messages"),
    )

    return parser.parse_args()


================================================
FILE: tomate/pomodoro/__init__.py
================================================
from .app import Application
from .config import Config
from .config import Payload as ConfigPayload
from .event import Bus, Events, Subscriber, on
from .graph import graph
from .plugin import Plugin, PluginEngine, suppress_errors
from .session import Payload as SessionPayload
from .session import Session
from .session import Type as SessionType
from .timer import Payload as TimerPayload
from .timer import Timer, format_seconds

__all__ = [
    "Application",
    "Bus",
    "Config",
    "ConfigPayload",
    "Events",
    "Plugin",
    "PluginEngine",
    "Session",
    "SessionPayload",
    "SessionType",
    "Subscriber",
    "Timer",
    "TimerPayload",
    "format_seconds",
    "graph",
    "on",
    "suppress_errors",
]


================================================
FILE: tomate/pomodoro/app.py
================================================
import enum

import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
from wiring import SingletonScope, inject
from wiring.scanning import register

from .plugin import PluginEngine


class State(enum.Enum):
    STOPPED = 1
    STARTED = 2


@register.factory("tomate.app", scope=SingletonScope)
class Application(dbus.service.Object):
    BUS_NAME = "com.github.Tomate"
    BUS_PATH = "/com/github/Tomate"
    BUS_INTERFACE = "com.github.Tomate"
    SPEC = "tomate.app"

    @inject(bus="dbus.session", window="tomate.ui.view", plugins="tomate.plugin")
    def __init__(self, bus, window, plugins: PluginEngine):
        dbus.service.Object.__init__(self, bus, self.BUS_PATH)
        self.state = State.STOPPED
        self._window = window
        plugins.collect()

    @dbus.service.method(BUS_INTERFACE, out_signature="b")
    def IsRunning(self):
        return self.state == State.STARTED

    @dbus.service.method(BUS_INTERFACE, out_signature="b")
    def Run(self):
        if self.IsRunning():
            self._window.show()
        else:
            self.state = State.STARTED
            self._window.run()

        return True

    @classmethod
    def from_graph(cls, graph, bus=dbus.SessionBus(mainloop=DBusGMainLoop())):
        request = bus.request_name(cls.BUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE)

        if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS:
            graph.register_instance("dbus.session", bus)
            instance = graph.get(cls.SPEC)
        else:
            bus_object = bus.get_object(cls.BUS_NAME, cls.BUS_PATH)
            instance = dbus.Interface(bus_object, cls.BUS_INTERFACE)

        return instance


================================================
FILE: tomate/pomodoro/config.py
================================================
import logging
import os
from collections import namedtuple
from configparser import RawConfigParser
from typing import List, Union

from wiring import SingletonScope, inject
from wiring.scanning import register
from xdg import BaseDirectory, IconTheme

from .event import Bus, Events

logger = logging.getLogger(__name__)

Payload = namedtuple("ConfigPayload", "action section option value")


@register.factory("tomate.config", scope=SingletonScope)
class Config:
    APP_NAME = "tomate"
    SHORTCUT_SECTION = "shortcuts"
    DURATION_SECTION = "timer"
    DURATION_LONG_BREAK = "longbreak_duration"
    DURATION_POMODORO = "pomodoro_duration"
    DURATION_SHORT_BREAK = "shortbreak_duration"
    DEFAULTS = {
        DURATION_POMODORO: "25",
        DURATION_SHORT_BREAK: "5",
        DURATION_LONG_BREAK: "15",
        "long_break_interval": "4",
    }

    @inject(bus="tomate.bus")
    def __init__(self, bus: Bus, parser=RawConfigParser(defaults=DEFAULTS, strict=True)):
        self.parser = parser
        self._bus = bus
        self.load()

    def __getattr__(self, attr):
        return getattr(self.parser, attr)

    def load(self) -> None:
        logger.debug("action=load uri=%s", self.config_path())

        self.parser.read(self.config_path())

    def save(self) -> None:
        logger.debug("action=write uri=%s", self.config_path())

        with open(self.config_path(), "w") as f:
            self.parser.write(f)

    def config_path(self) -> str:
        BaseDirectory.save_config_path(self.APP_NAME)
        return os.path.join(BaseDirectory.xdg_config_home, self.APP_NAME, self.APP_NAME + ".conf")

    def media_uri(self, *resources: str) -> str:
        return "file://" + self._resource_path(self.APP_NAME, "media", *resources)

    def plugin_paths(self) -> List[str]:
        return remove_duplicates(self._load_data_paths(self.APP_NAME, "plugins"))

    def icon_paths(self) -> List[str]:
        return remove_duplicates(self._load_data_paths("icons"))

    def _resource_path(self, *resources) -> str:
        for resource in self._load_data_paths(*resources):
            if os.path.exists(resource):
                return resource

        raise EnvironmentError("Resource '%s' not found!" % resources[-1])

    def _load_data_paths(self, *resources) -> List[str]:
        return [path for path in BaseDirectory.load_data_paths(*resources)]

    def icon_path(self, iconname, size=None, theme=None) -> str:
        icon_path = IconTheme.getIconPath(iconname, size, theme, extensions=["png", "svg", "xpm"])

        if icon_path is not None:
            return icon_path

        raise EnvironmentError("Icon '%s' not found!" % iconname)

    def get_int(self, section: str, option: str, fallback=None) -> int:
        return self.get(section, option, fallback, method="getint")

    def get_bool(self, section: str, option: str, fallback=None) -> bool:
        return self.get(section, option, fallback, method="getboolean")

    def get_float(self, section: str, option: str, fallback=None) -> int:
        return self.get(section, option, fallback, method="getfloat")

    def get(self, section: str, option: str, fallback=None, method="get") -> Union[str, int, bool]:
        section = self.normalize(section)
        option = self.normalize(option)
        if not self.parser.has_section(section):
            self.parser.add_section(section)

        logger.debug("action=get section=%s option=%s", section, option)

        return getattr(self.parser, method)(section, option, fallback=fallback)

    def set(self, section: str, option: str, value) -> None:
        logger.debug("action=set section=%s option=%s value=%s", section, option, value)

        section = self.normalize(section)
        option = self.normalize(option)
        if not self.parser.has_section(section):
            self.parser.add_section(section)
        self.parser.set(section, option, value)
        self.save()

        payload = Payload(action="set", section=section, option=option, value=value)
        self._bus.send(Events.CONFIG_CHANGE, payload=payload)

    def remove(self, section, option) -> None:
        logger.debug("action=remove section=%s option=%s", section, option)

        section = self.normalize(section)
        option = self.normalize(option)
        self.parser.remove_option(section, option)
        self.save()

        payload = Payload(action="remove", section=section, option=option, value="")
        self._bus.send(Events.CONFIG_CHANGE, payload=payload)

    @staticmethod
    def normalize(name: str) -> str:
        return name.replace(" ", "_").lower()


def remove_duplicates(original: List[str]) -> List[str]:
    return list(set(original))


================================================
FILE: tomate/pomodoro/event.py
================================================
import enum
import functools
import logging
from typing import Any, Callable, List, Tuple

import blinker
from wiring import SingletonScope
from wiring.scanning import register

logger = logging.getLogger(__name__)


@enum.unique
class Events(enum.Enum):
    TIMER_START = 0
    TIMER_UPDATE = 1
    TIMER_STOP = 2
    TIMER_END = 3

    SESSION_READY = 4
    SESSION_START = 5
    SESSION_INTERRUPT = 6
    SESSION_CHANGE = 7
    SESSION_END = 8
    SESSION_RESET = 9

    WINDOW_SHOW = 10
    WINDOW_HIDE = 11

    CONFIG_CHANGE = 12


Receiver = Callable[[Events, Any], Any]


@register.factory("tomate.bus", scope=SingletonScope)
class Bus:
    def __init__(self):
        self._bus = blinker.NamedSignal("tomate")

    def connect(self, event: Events, receiver: Receiver, weak: bool = True):
        self._bus.connect(receiver, sender=event, weak=weak)

    def is_connect(self, event: Events, receiver: Receiver) -> bool:
        return receiver in self._bus.receivers_for(event)

    def send(self, event: Events, payload: Any = None) -> List[Any]:
        # ignore the receiver in the result
        return [result[1] for result in self._bus.send(event, payload=payload)]

    def disconnect(self, event: Events, receiver: Receiver):
        self._bus.disconnect(receiver, sender=event)


def on(*events: Events):
    def wrapper(method):
        method._events = events

        @functools.wraps(method)
        def wrapped(*args, **kwargs):
            # ignore the event type, Events, in the receiver callback
            return method(*(arg for arg in args if not isinstance(arg, Events)), **kwargs)

        return wrapped

    return wrapper


class Subscriber:
    def connect(self, bus: Bus) -> None:
        for method, events in self.__methods_with_events():
            for event in events:
                logger.debug(
                    "action=connect event=%s method=%s.%s",
                    event,
                    self.__class__.__name__,
                    method.__name__,
                )
                bus.connect(event, method)

    def disconnect(self, bus: Bus):
        for method, events in self.__methods_with_events():
            for event in events:
                logger.debug(
                    "action=disconnect event=%s method=%s.%s",
                    event,
                    self.__class__.__name__,
                    method.__name__,
                )
                bus.disconnect(event, method)

    def __methods_with_events(self) -> List[Tuple[Any, List[Events]]]:
        return [
            (getattr(self, attr), getattr(getattr(self, attr), "_events"))
            for attr in dir(self)
            if hasattr(getattr(self, attr), "_events")
        ]


================================================
FILE: tomate/pomodoro/fsm.py
================================================
import logging

import wrapt

logger = logging.getLogger(__name__)


class fsm:
    def __init__(self, target, **kwargs):
        self.target = target
        self.source = kwargs.pop("source", "*")
        self.attr = kwargs.pop("attr", "state")
        self.condition = kwargs.pop("condition", None)
        self.exit_action = kwargs.pop("exit", None)

    def is_valid_transition(self, instance) -> bool:
        return self.source == "*" or getattr(instance, self.attr) in self.source

    def is_valid_condition(self, instance) -> bool:
        if not self.condition:
            return True

        return self.condition(instance)

    def change_state(self, instance) -> None:
        current_target = getattr(instance, self.attr, None)

        logger.debug(
            "action=change attribute=%s.%s from=%s to=%s",
            instance.__class__.__name__,
            self.attr,
            current_target,
            self.target,
        )

        if self.target != "self" and current_target != self.target:
            setattr(instance, self.attr, self.target)

    def call_exit_action(self, instance) -> None:
        if self.exit_action is not None:
            self.exit_action(instance)

    @wrapt.decorator
    def __call__(self, wrapped, instance, args, kwargs):
        # when there is a decorator over the @fsm the instance will be None so we need to get it from the first param
        if instance is None:
            instance = args[0]

        logger.debug(
            "action=before method=%s.%s",
            instance.__class__.__name__,
            wrapped.__name__,
        )

        if self.is_valid_transition(instance) and self.is_valid_condition(instance):
            result = wrapped(*args, **kwargs)
            self.change_state(instance)
            self.call_exit_action(instance)

            logger.debug(
                "action=after method=%s.%s called=true",
                instance.__class__.__name__,
                wrapped.__name__,
            )

            return result

        logger.debug(
            "action=after method=%s.%s called=false transition=%s",
            instance.__class__.__name__,
            wrapped.__name__,
            self.is_valid_transition(instance),
        )

        return False


================================================
FILE: tomate/pomodoro/graph.py
================================================
from wiring import Graph
from wiring.scanning import register

graph = Graph()
register.instance(Graph)(graph)


================================================
FILE: tomate/pomodoro/plugin.py
================================================
import logging
import os
from typing import List, Optional, Union

import wrapt
from gi.repository import Gtk
from wiring import Graph, SingletonScope, inject
from wiring.scanning import register
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
from yapsy.IPlugin import IPlugin
from yapsy.PluginInfo import PluginInfo
from yapsy.VersionedPluginManager import VersionedPluginManager

from .config import Config
from .event import Bus, Subscriber

logger = logging.getLogger(__name__)


class Plugin(IPlugin, Subscriber):
    has_settings = False

    def __init__(self):
        super().__init__()
        self.bus = None
        self.graph = None

    def configure(self, bus: Bus, graph: Graph) -> None:
        self.bus = bus
        self.graph = graph

    def activate(self) -> None:
        super().activate()
        self.connect(self.bus)

    def deactivate(self) -> None:
        self.disconnect(self.bus)
        super().deactivate()

    def settings_window(self, parent) -> Union[Gtk.Dialog, None]:
        return None


@register.factory("tomate.plugin", scope=SingletonScope)
class PluginEngine:
    @inject(bus="tomate.bus", config="tomate.config", graph=Graph)
    def __init__(self, bus: Bus, config: Config, graph: Graph):
        self._bus = bus
        self._graph = graph

        logger.debug("action=init paths=%s", config.plugin_paths())
        self._plugin_manager = ConfigurablePluginManager(decorated_manager=VersionedPluginManager())
        self._plugin_manager.setPluginPlaces(config.plugin_paths())
        self._plugin_manager.setPluginInfoExtension("plugin")
        self._plugin_manager.setConfigParser(config.parser, config.save)

    def collect(self) -> None:
        logger.debug("action=collect")
        self._plugin_manager.locatePlugins()
        self._plugin_manager.loadPlugins(callback_after=self._configure_plugin)

    def _configure_plugin(self, plugin: PluginInfo) -> None:
        if plugin.error is None:
            plugin.plugin_object.configure(self._bus, self._graph)

    def deactivate(self, name: str) -> None:
        self._plugin_manager.deactivatePluginByName(name)

    def activate(self, name: str) -> None:
        self._plugin_manager.activatePluginByName(name)

    def all(self) -> List[PluginInfo]:
        logger.debug("action=all")
        return sorted(self._plugin_manager.getAllPlugins(), key=lambda info: info.name)

    def lookup(self, name: str, category="Default") -> Optional[PluginInfo]:
        logger.debug("action=lookup name=%s category=%s", name, category)
        return self._plugin_manager.getPluginByName(name, category)

    def has_plugins(self) -> bool:
        has = len(self.all()) > 0
        logger.debug("action=has_plugin has=%s", has)
        return has

    def remove(self, plugin: object, category="Default") -> None:
        self._plugin_manager.removePluginFromCategory(plugin, category)


@wrapt.decorator
def suppress_errors(wrapped, _, args, kwargs):
    try:
        return wrapped(*args, **kwargs)
    except Exception as ex:
        if in_debug_mode():
            raise ex

        log = logging.getLogger(__name__)
        log.error(ex, exc_info=True)

    return None


def in_debug_mode():
    return "TOMATE_DEBUG" in os.environ.keys()


================================================
FILE: tomate/pomodoro/session.py
================================================
from __future__ import annotations

import enum
import logging
from collections import namedtuple

from wiring import SingletonScope, inject
from wiring.scanning import register

from .config import Config
from .config import Payload as ConfigPayload
from .event import Bus, Events, Subscriber, on
from .fsm import fsm
from .timer import SECONDS_IN_A_MINUTE
from .timer import Payload as TimerPayload
from .timer import Timer, format_seconds

logger = logging.getLogger(__name__)


class Payload(namedtuple("SessionPayload", ["type", "pomodoros", "duration"])):
    @property
    def countdown(self) -> str:
        return format_seconds(self.duration)


class Type(enum.Enum):
    POMODORO = 0
    SHORT_BREAK = 1
    LONG_BREAK = 2

    @classmethod
    def of(cls, index: int) -> Type:
        for number, attr in enumerate(cls):
            if number == index:
                return attr

        raise Exception(f"invalid index: {index}")

    @property
    def option(self) -> str:
        return "{}_duration".format(self.name.replace("_", "").lower())


@enum.unique
class State(enum.Enum):
    INITIAL = 0
    STOPPED = 1
    STARTED = 2
    ENDED = 3


@register.factory("tomate.session", scope=SingletonScope)
class Session(Subscriber):
    @inject(
        bus="tomate.bus",
        config="tomate.config",
        timer="tomate.timer",
    )
    def __init__(self, bus: Bus, config: Config, timer: Timer):
        self._config = config
        self._timer = timer
        self._bus = bus
        self.state = State.INITIAL
        self.current = Type.POMODORO
        self.pomodoros = 0
        self.connect(bus)

    @fsm(source=[State.INITIAL], target=State.STOPPED, exit=lambda self: self._trigger(Events.SESSION_READY))
    def ready(self) -> None:
        pass

    @fsm(
        source=[State.STOPPED, State.ENDED], target=State.STARTED, exit=lambda self: self._trigger(Events.SESSION_START)
    )
    def start(self) -> bool:
        logger.debug("action=start")
        self._timer.start(self.duration)
        return True

    def is_running(self) -> bool:
        return self._timer.is_running()

    @fsm(
        source=[State.STARTED],
        target=State.STOPPED,
        condition=is_running,
        exit=lambda self: self._trigger(Events.SESSION_INTERRUPT),
    )
    def stop(self) -> bool:
        logger.debug("action=stop")
        self._timer.stop()
        return True

    @fsm(source=[State.STOPPED, State.ENDED], target="self", exit=lambda self: self._trigger(Events.SESSION_RESET))
    def reset(self) -> bool:
        logger.debug("action=reset")
        self.pomodoros = 0
        return True

    @on(Events.CONFIG_CHANGE)
    def _on_config_change(self, payload: ConfigPayload) -> bool:
        if payload.section != "timer":
            return False

        return self.change(self.current)

    @fsm(source=[State.STOPPED, State.ENDED], target="self", exit=lambda self: self._trigger(Events.SESSION_CHANGE))
    def change(self, session: Type) -> bool:
        logger.debug("action=change current=%s next=%s", self.current, session)
        self.current = session
        return True

    @property
    def duration(self) -> int:
        minutes = self._config.get_int(self._config.DURATION_SECTION, self.current.option)
        return int(minutes * SECONDS_IN_A_MINUTE)

    def timer_is_up(self) -> bool:
        return not self._timer.is_running()

    @on(Events.TIMER_END)
    @fsm(
        source=[State.STARTED],
        target=State.ENDED,
        condition=timer_is_up,
        exit=lambda self: self._trigger(Events.SESSION_CHANGE),
    )
    def _end(self, payload: TimerPayload) -> bool:
        payload = self._create_payload(duration=payload.duration)

        if self.current == Type.POMODORO:
            self.pomodoros += 1
            self.current = self._choose_break()
        else:
            self.current = Type.POMODORO

        logger.debug("action=end previous=%s current=%s", payload.type, self.current)

        self.state = State.ENDED
        self._bus.send(Events.SESSION_END, payload=payload._replace(pomodoros=self.pomodoros))

        return True

    def _choose_break(self):
        return Type.LONG_BREAK if self._is_long_break() else Type.SHORT_BREAK

    def _is_long_break(self) -> bool:
        long_break_interval = self._config.get_int(self._config.DURATION_SECTION, "long_break_interval")
        return not self.pomodoros % long_break_interval

    def _trigger(self, event: Events) -> None:
        self._bus.send(event, payload=self._create_payload())

    def _create_payload(self, **kwargs) -> Payload:
        defaults = {
            "duration": self.duration,
            "pomodoros": self.pomodoros,
            "type": self.current,
        }
        defaults.update(kwargs)
        return Payload(**defaults)


================================================
FILE: tomate/pomodoro/timer.py
================================================
import enum
import logging
from collections import namedtuple

from gi.repository import GLib
from wiring import SingletonScope, inject
from wiring.scanning import register

from .event import Bus, Events
from .fsm import fsm

logger = logging.getLogger(__name__)
SECONDS_IN_A_MINUTE = 60


def format_seconds(seconds: int) -> str:
    minutes, seconds = divmod(seconds, SECONDS_IN_A_MINUTE)
    return "{0:0>2}:{1:0>2}".format(minutes, seconds)


class Payload(namedtuple("TimerPayload", ["time_left", "duration"])):
    @property
    def remaining_ratio(self) -> float:
        try:
            return self.time_left / self.duration
        except ZeroDivisionError:
            return 0.0

    @property
    def elapsed_ratio(self) -> float:
        return round(1.0 - self.remaining_ratio, 2)

    @property
    def elapsed_percent(self):
        """
        Returns the percentage in 5% steps
        """
        percent = self.elapsed_ratio * 100
        return percent - percent % 5

    @property
    def countdown(self) -> str:
        return format_seconds(self.time_left)


# Based on Tomatoro create by Pierre Quillery.
# https://github.com/dandelionmood/Tomatoro
# Thanks Pierre!


class State(enum.Enum):
    STOPPED = 1
    STARTED = 2
    ENDED = 3


@register.factory("tomate.timer", scope=SingletonScope)
class Timer:
    ONE_SECOND = 1

    @inject(bus="tomate.bus")
    def __init__(self, bus: Bus):
        self.duration = self.time_left = 0
        self.state = State.STOPPED
        self._bus = bus

    @fsm(target=State.STARTED, source=[State.ENDED, State.STOPPED], exit=lambda self: self._trigger(Events.TIMER_START))
    def start(self, seconds: int) -> bool:
        logger.debug("action=start")
        self.duration = self.time_left = seconds
        GLib.timeout_add_seconds(Timer.ONE_SECOND, self._update, priority=GLib.PRIORITY_HIGH)
        return True

    @fsm(target=State.STOPPED, source=[State.STARTED], exit=lambda self: self._trigger(Events.TIMER_STOP))
    def stop(self) -> bool:
        logger.debug("action=reset")
        self._reset()
        return True

    def _is_up(self) -> bool:
        return self.time_left <= 0

    def is_running(self) -> bool:
        return self.state == State.STARTED

    @fsm(
        target=State.ENDED, source=[State.STARTED], condition=_is_up, exit=lambda self: self._trigger(Events.TIMER_END)
    )
    def end(self) -> bool:
        logger.debug("action=end")
        return True

    def _update(self) -> bool:
        logger.debug("action=update time_left=%d duration=%d", self.time_left, self.duration)

        if self.state != State.STARTED:
            return False

        if self._is_up():
            return self.end()

        self.time_left -= 1
        self._trigger(Events.TIMER_UPDATE)
        return True

    def _reset(self) -> None:
        self.duration = self.time_left = 0

    def _trigger(self, event) -> None:
        self._bus.send(event, payload=Payload(time_left=self.time_left, duration=self.duration))


================================================
FILE: tomate/ui/__init__.py
================================================
from .shortcut import Shortcut, ShortcutEngine
from .systray import Menu as SystrayMenu
from .systray import Systray
from .window import Window

__all__ = ["Shortcut", "ShortcutEngine", "Systray", "SystrayMenu", "Window"]


================================================
FILE: tomate/ui/dialogs/__init__.py
================================================
from .about import AboutDialog
from .preference import ExtensionTab, PluginGrid, PreferenceDialog, TimerTab

__all__ = ["AboutDialog", "ExtensionTab", "PreferenceDialog", "TimerTab", "PluginGrid"]


================================================
FILE: tomate/ui/dialogs/about.py
================================================
from gi.repository import GdkPixbuf, Gtk
from wiring import SingletonScope, inject
from wiring.scanning import register


@register.factory("tomate.ui.about", scope=SingletonScope)
class AboutDialog(Gtk.AboutDialog):
    @inject(config="tomate.config")
    def __init__(self, config):
        Gtk.AboutDialog.__init__(
            self,
            comments="Tomate Pomodoro Timer (GTK+ Interface)",
            copyright="2014, Elio Esteves Duarte",
            license="GPL-3",
            license_type=Gtk.License.GPL_3_0,
            logo=GdkPixbuf.Pixbuf.new_from_file(config.icon_path("tomate", 48)),
            modal=True,
            program_name="Tomate Gtk",
            title="Tomate Gtk",
            version="0.25.2",
            website="https://github.com/eliostvs/tomate-gtk",
            website_label="Tomate GTK on Github",
        )

        self.props.authors = ["Elio Esteves Duarte"]
        self.connect("response", lambda widget, _: widget.hide())

    @property
    def widget(self):
        return self


================================================
FILE: tomate/ui/dialogs/preference.py
================================================
import locale
import logging
from locale import gettext as _

from gi.repository import GdkPixbuf, Gtk, Pango
from wiring import SingletonScope, inject
from wiring.scanning import register

from tomate.pomodoro import Bus, Config, PluginEngine

locale.textdomain("tomate")
logger = logging.getLogger(__name__)


@register.factory("tomate.ui.preference", scope=SingletonScope)
class PreferenceDialog(Gtk.Dialog):
    @inject(
        timer_tab="tomate.ui.preference.timer",
        extension_tab="tomate.ui.preference.extension",
    )
    def __init__(self, timer_tab, extension_tab):
        stack = Gtk.Stack()
        stack.add_titled(timer_tab.widget, "timer", _("Timer"))
        stack.add_titled(extension_tab.widget, "extension", _("Extensions"))

        switcher = Gtk.StackSwitcher(halign=Gtk.Align.CENTER)
        switcher.props.stack = stack

        content_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_bottom=12)
        content_area.pack_start(switcher, False, False, 0)
        content_area.pack_start(stack, False, False, 0)
        content_area.show_all()

        super().__init__(
            title=_("Preferences"),
            border_width=12,
            resizable=False,
            window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
            flags=Gtk.DialogFlags.MODAL,
        )
        self.add_button(_("Close"), Gtk.ResponseType.CLOSE)
        self.connect("response", lambda widget, response: widget.hide())
        self.set_size_request(350, -1)
        self.get_content_area().add(content_area)

        stack.set_visible_child_name("timer")

        self._extension_tab = extension_tab
        self._extension_tab.set_toplevel(self)

    @property
    def widget(self):
        return self

    def run(self):
        logger.debug("action=run")
        self._extension_tab.refresh()
        return super().run()


@register.factory("tomate.ui.preference.timer", scope=SingletonScope)
class TimerTab:
    @inject(config="tomate.config")
    def __init__(self, config: Config):
        self._config = config

        section = self._create_section(_("Duration"))
        self.widget = Gtk.Grid(column_spacing=12, row_spacing=6)
        self.widget.attach(section, 0, 0, 1, 1)

        # Pomodoro Duration
        label, button = self._create_option("duration.pomodoro", _("Pomodoro"), Config.DURATION_POMODORO)
        self.widget.attach(label, 0, 1, 1, 1)
        self.widget.attach_next_to(button, label, Gtk.PositionType.RIGHT, 3, 1)
        self.pomodoro = button

        # Short Break Duration
        label, button = self._create_option("duration.shortbreak", _("Short break"), Config.DURATION_SHORT_BREAK)
        self.widget.attach(label, 0, 2, 1, 1)
        self.widget.attach_next_to(button, label, Gtk.PositionType.RIGHT, 3, 1)
        self.shortbreak = button

        # Long Break Duration
        label, button = self._create_option("duration.longbreak", _("Long Break"), Config.DURATION_LONG_BREAK)
        self.widget.attach(label, 0, 3, 1, 1)
        self.widget.attach_next_to(button, label, Gtk.PositionType.RIGHT, 3, 1)
        self.longbreak = button

    def _create_section(self, name):
        section = Gtk.Label.new()
        section.set_markup("<b>{0}</b>".format(name))
        section.props.halign = Gtk.Align.START
        return section

    def _create_option(self, name: str, label: str, option: str):
        label = Gtk.Label.new(label + ":")
        label.set_properties(margin_left=12, hexpand=True, halign=Gtk.Align.END)

        button = Gtk.SpinButton.new_with_range(1, 99, 1)
        button.set_properties(
            hexpand=True, halign=Gtk.Align.START, name=name, value=self._config.get_int("Timer", option)
        )
        button.connect("value-changed", self._on_change, option)

        return label, button

    def _on_change(self, widget, option):
        value = str(widget.get_value_as_int())
        self._config.set("Timer", option, value)


@register.factory("tomate.ui.preference.extension", scop
Download .txt
gitextract_2xxpwi1o/

├── .bumpversion.cfg
├── .envfile
├── .github/
│   └── workflows/
│       ├── lint.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── CHANGELOG.md
├── COPYING
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── data/
│   ├── applications/
│   │   └── tomate-gtk.desktop
│   ├── media/
│   │   ├── alarm.ogg
│   │   └── clock.ogg
│   └── plugins/
│       ├── alarm.plugin
│       ├── alarm.py
│       ├── autopause.plugin
│       ├── autopause.py
│       ├── breakscreen.plugin
│       ├── breakscreen.py
│       ├── notify.plugin
│       ├── notify.py
│       ├── script.plugin
│       ├── script.py
│       ├── ticking.plugin
│       └── ticking.py
├── pyproject.toml
├── setup.cfg
├── setup.py
├── tests/
│   ├── conftest.py
│   ├── data/
│   │   ├── icons/
│   │   │   └── hicolor/
│   │   │       └── index.theme
│   │   ├── mime/
│   │   │   └── packages/
│   │   │       └── freedesktop.org.xml
│   │   ├── pulse/
│   │   │   └── cookie
│   │   └── tomate/
│   │       ├── media/
│   │       │   ├── alarm.ogg
│   │       │   └── clock.ogg
│   │       ├── plugins/
│   │       │   ├── .gitkeep
│   │       │   ├── plugin_a.plugin
│   │       │   ├── plugin_a.py
│   │       │   ├── plugin_b.plugin
│   │       │   ├── plugin_b.py
│   │       │   └── plugin_b_old.plugin
│   │       └── tomate.conf
│   ├── plugins/
│   │   ├── test_alarm.py
│   │   ├── test_autopause.py
│   │   ├── test_breakscreen.py
│   │   ├── test_notify.py
│   │   ├── test_script.py
│   │   └── test_ticking.py
│   ├── pomodoro/
│   │   ├── test_app.py
│   │   ├── test_config.py
│   │   ├── test_event.py
│   │   ├── test_graph.py
│   │   ├── test_plugin.py
│   │   ├── test_session.py
│   │   └── test_timer.py
│   └── ui/
│       ├── dialogs/
│       │   ├── test_about.py
│       │   └── test_preference.py
│       ├── test_shortcut.py
│       ├── test_systray.py
│       ├── test_window.py
│       └── widgets/
│           ├── test_countdown.py
│           ├── test_headerbar.py
│           └── test_session_button.py
└── tomate/
    ├── __init__.py
    ├── __main__.py
    ├── audio/
    │   ├── __init__.py
    │   └── player.py
    ├── main.py
    ├── pomodoro/
    │   ├── __init__.py
    │   ├── app.py
    │   ├── config.py
    │   ├── event.py
    │   ├── fsm.py
    │   ├── graph.py
    │   ├── plugin.py
    │   ├── session.py
    │   └── timer.py
    └── ui/
        ├── __init__.py
        ├── dialogs/
        │   ├── __init__.py
        │   ├── about.py
        │   └── preference.py
        ├── shortcut.py
        ├── systray.py
        ├── testing.py
        ├── widgets/
        │   ├── __init__.py
        │   ├── countdown.py
        │   ├── headerbar.py
        │   ├── mode_button.py
        │   └── session_button.py
        └── window.py
Download .txt
SYMBOL INDEX (545 symbols across 50 files)

FILE: data/plugins/alarm.py
  class AlarmPlugin (line 23) | class AlarmPlugin(plugin.Plugin):
    method __init__ (line 27) | def __init__(self):
    method configure (line 32) | def configure(self, bus: Bus, graph: Graph) -> None:
    method activate (line 37) | def activate(self) -> None:
    method deactivate (line 45) | def deactivate(self) -> None:
    method on_end (line 54) | def on_end(self, **__) -> None:
    method audio_path (line 60) | def audio_path(self) -> str:
    method volume (line 64) | def volume(self) -> float:
    method settings_window (line 67) | def settings_window(self, toplevel: Gtk.Dialog) -> "SettingsDialog":
  class SettingsDialog (line 71) | class SettingsDialog:
    method __init__ (line 72) | def __init__(self, config: Config, toplevel: Gtk.Dialog):
    method create_dialog (line 76) | def create_dialog(self, toplevel: Gtk.Dialog) -> Gtk.Dialog:
    method create_options (line 91) | def create_options(self) -> Gtk.Grid:
    method create_custom_alarm_input (line 106) | def create_custom_alarm_input(self, custom_audio) -> Gtk.Entry:
    method create_custom_alarm_switch (line 120) | def create_custom_alarm_switch(self, custom_audio, entry) -> Gtk.Switch:
    method select_custom_alarm (line 130) | def select_custom_alarm(self, entry: Gtk.Entry, *_) -> None:
    method dirname (line 139) | def dirname(self, audio_path: str) -> str:
    method create_file_chooser (line 142) | def create_file_chooser(self, current_folder: str) -> Gtk.FileChooserD...
    method create_filter (line 160) | def create_filter(name: str, mime_type: str) -> Gtk.FileFilter:
    method custom_alarm_changed (line 166) | def custom_alarm_changed(self, entry: Gtk.Entry, _) -> None:
    method custom_alarm_toggle (line 177) | def custom_alarm_toggle(switch: Gtk.Switch, _, entry: Gtk.Entry) -> None:
    method run (line 183) | def run(self) -> Gtk.Dialog:

FILE: data/plugins/autopause.py
  class AutoPausePlugin (line 15) | class AutoPausePlugin(plugin.Plugin):
    method on_session_end (line 18) | def on_session_end(self, **_):
    method pause (line 21) | def pause(self) -> None:

FILE: data/plugins/breakscreen.py
  class Monitor (line 35) | class Monitor(namedtuple("Monitor", "number geometry")):
    method x (line 37) | def x(self) -> int:
    method y (line 41) | def y(self) -> int:
    method width (line 45) | def width(self) -> int:
    method height (line 49) | def height(self) -> int:
  class BreakScreen (line 53) | class BreakScreen(Subscriber):
    method __init__ (line 54) | def __init__(self, monitor: Monitor, session: Session, config: Config):
    method create_options (line 65) | def create_options(self, config) -> Dict[str, bool]:
    method create_button (line 71) | def create_button(self) -> Gtk.Button:
    method skip_break (line 78) | def skip_break(self, _) -> None:
    method create_content_area (line 83) | def create_content_area(self, countdown: Gtk.Label, skip_button: Gtk.B...
    method create_window (line 92) | def create_window(self, monitor: Monitor, box: Gtk.Box) -> Gtk.Window:
    method on_session_start (line 113) | def on_session_start(self, payload=SessionPayload) -> None:
    method on_session_interrupt (line 121) | def on_session_interrupt(self, **__) -> None:
    method on_session_end (line 126) | def on_session_end(self, payload: SessionPayload) -> None:
    method _start_session (line 139) | def _start_session(self) -> bool:
    method auto_start (line 144) | def auto_start(self) -> bool:
    method on_timer_update (line 148) | def on_timer_update(self, payload: TimerPayload) -> None:
    method on_settings_change (line 153) | def on_settings_change(self, payload: ConfigPayload) -> None:
  class SettingsDialog (line 167) | class SettingsDialog:
    method __init__ (line 168) | def __init__(self, config: Config, toplevel):
    method create_dialog (line 173) | def create_dialog(self, toplevel) -> Gtk.Dialog:
    method create_options (line 188) | def create_options(self):
    method run (line 194) | def run(self):
    method create_option (line 198) | def create_option(self, grid: Gtk.Grid, row: int, label: str, option):
    method on_option_change (line 209) | def on_option_change(self, switch: Gtk.Switch, _, option: str):
  class BreakScreenPlugin (line 219) | class BreakScreenPlugin(plugin.Plugin):
    method __init__ (line 223) | def __init__(self, display=Gdk.Display.get_default()):
    method configure_style (line 230) | def configure_style():
    method activate (line 267) | def activate(self):
    method deactivate (line 279) | def deactivate(self):
    method settings_window (line 288) | def settings_window(self, toplevel) -> SettingsDialog:

FILE: data/plugins/notify.py
  class NotifyPlugin (line 25) | class NotifyPlugin(plugin.Plugin):
    method __init__ (line 33) | def __init__(self):
    method configure (line 38) | def configure(self, bus: Bus, graph: Graph) -> None:
    method activate (line 43) | def activate(self):
    method deactivate (line 48) | def deactivate(self):
    method on_session_started (line 53) | def on_session_started(self, payload: SessionPayload):
    method on_session_finished (line 57) | def on_session_finished(self, **__):
    method on_session_stopped (line 61) | def on_session_stopped(self, **__):
    method get_message (line 64) | def get_message(self, session: SessionType) -> Tuple[str, str]:
    method show_notification (line 71) | def show_notification(self, title, message=""):
    method icon_path (line 84) | def icon_path(self):

FILE: data/plugins/script.py
  function strip_space (line 27) | def strip_space(command: Optional[str]) -> Optional[str]:
  class ScriptPlugin (line 32) | class ScriptPlugin(plugin.Plugin):
    method __init__ (line 36) | def __init__(self):
    method configure (line 40) | def configure(self, bus: Bus, graph: Graph) -> None:
    method on_session_started (line 46) | def on_session_started(self, payload: SessionPayload):
    method on_session_interrupted (line 51) | def on_session_interrupted(self, payload: SessionPayload):
    method on_session_end (line 56) | def on_session_end(self, payload: SessionPayload):
    method call_command (line 59) | def call_command(self, section, event: Events, payload: SessionPayload):
    method read_command (line 76) | def read_command(self, section: str, repl: Dict[str, str]) -> Optional...
    method _interpolate (line 81) | def _interpolate(template: str, replacements: Dict[str, str]) -> str:
    method settings_window (line 84) | def settings_window(self, toplevel):
  class SettingsDialog (line 88) | class SettingsDialog:
    method __init__ (line 89) | def __init__(self, config: Config, toplevel):
    method create_dialog (line 93) | def create_dialog(self, toplevel) -> Gtk.Dialog:
    method create_options (line 108) | def create_options(self):
    method create_section (line 117) | def create_section(grid: Gtk.Grid) -> None:
    method run (line 140) | def run(self) -> None:
    method create_option (line 144) | def create_option(self, grid: Gtk.Grid, row: int, label: str, option: ...
    method on_command_change (line 158) | def on_command_change(self, entry: Gtk.Entry, _, option: str) -> None:
    method on_option_change (line 164) | def on_option_change(self, switch: Gtk.Switch, _, entry: Gtk.Entry, op...
    method remove_option (line 170) | def remove_option(self, entry: Gtk.Entry, option: str) -> None:

FILE: data/plugins/ticking.py
  class TickingPlugin (line 22) | class TickingPlugin(plugin.Plugin):
    method __init__ (line 27) | def __init__(self):
    method configure (line 33) | def configure(self, bus: Bus, graph: Graph) -> None:
    method activate (line 39) | def activate(self) -> None:
    method deactivate (line 50) | def deactivate(self) -> None:
    method on_start (line 59) | def on_start(self, payload: SessionPayload) -> None:
    method on_end (line 66) | def on_end(self, **_) -> None:
    method audio_path (line 72) | def audio_path(self) -> str:
    method volume (line 76) | def volume(self) -> float:

FILE: setup.py
  function find_xdg_data_files (line 7) | def find_xdg_data_files(from_dir, to_dir, package_name, data_files):
  function find_data_files (line 20) | def find_data_files(data_map, package_name):

FILE: tests/conftest.py
  function session (line 18) | def session(mocker):
  function bus (line 23) | def bus() -> Bus:
  function graph (line 28) | def graph() -> Graph:
  function window (line 35) | def window(mocker):
  function config (line 40) | def config(bus, tmpdir) -> Config:
  function shortcut_engine (line 48) | def shortcut_engine(config: Config) -> ShortcutEngine:
  function plugin_engine (line 53) | def plugin_engine(bus: Bus, graph: Graph, config: Config) -> PluginEngine:

FILE: tests/data/tomate/plugins/plugin_a.py
  class PluginA (line 7) | class PluginA(plugin.Plugin):
    method __init__ (line 10) | def __init__(self):
    method listener (line 15) | def listener(self, **__) -> str:
    method settings_window (line 18) | def settings_window(self, parent: Gtk.Widget) -> Gtk.Dialog:

FILE: tests/data/tomate/plugins/plugin_b.py
  class PluginB (line 5) | class PluginB(plugin.Plugin):
    method listener (line 9) | def listener(self, **__) -> str:

FILE: tests/plugins/test_alarm.py
  function plugin (line 22) | def plugin(bus, config, graph):
  class TestPlugin (line 34) | class TestPlugin:
    method test_loads_configuration_when_is_activated (line 35) | def test_loads_configuration_when_is_activated(self, bus, config, plug...
    method test_plays_alarm_when_session_ends (line 43) | def test_plays_alarm_when_session_ends(self, player, bus, config, plug...
  class TestSettingsWindow (line 51) | class TestSettingsWindow:
    method test_without_custom_alarm (line 52) | def test_without_custom_alarm(self, config, plugin):
    method test_with_custom_alarm (line 63) | def test_with_custom_alarm(self, plugin, config):
    method test_configures_custom_alarm (line 76) | def test_configures_custom_alarm(self, config, plugin):
    method test_disables_custom_alarm (line 91) | def test_disables_custom_alarm(self, config, plugin):

FILE: tests/plugins/test_autopause.py
  function plugin (line 12) | def plugin(bus, graph):
  function test_stop_all_running_players (line 20) | def test_stop_all_running_players(bus, plugin, mocker):

FILE: tests/plugins/test_breakscreen.py
  function plugin (line 20) | def plugin(bus, config, graph, session):
  function none (line 33) | def none(values: Iterator) -> bool:
  function label_text (line 37) | def label_text(countdown: str, plugin) -> bool:
  class TestPlugin (line 43) | class TestPlugin:
    method test_shows_when_pause_begins (line 45) | def test_shows_when_pause_begins(self, session_type, bus, plugin):
    method test_not_show_when_pomodoro_begins (line 54) | def test_not_show_when_pomodoro_begins(self, bus, plugin):
    method test_hides_when_plugin_is_deactivated (line 62) | def test_hides_when_plugin_is_deactivated(self, bus, plugin):
    method test_starts_break_when_auto_start_option_is_enabled (line 72) | def test_starts_break_when_auto_start_option_is_enabled(self, bus, con...
    method test_not_start_break_when_auto_start_is_disabled (line 84) | def test_not_start_break_when_auto_start_is_disabled(self, bus, config...
    method test_hides_when_session_is_interrupted (line 96) | def test_hides_when_session_is_interrupted(self, bus, plugin):
    method test_not_start_break_when_is_not_a_pomodoro (line 105) | def test_not_start_break_when_is_not_a_pomodoro(self, session_type, bu...
    method test_updates_countdown (line 119) | def test_updates_countdown(self, bus, plugin):
    method test_updates_when_config_changes (line 138) | def test_updates_when_config_changes(self, action, option, initial, va...
    method test_hide_skip_button_when_config_changes (line 154) | def test_hide_skip_button_when_config_changes(self, action, want, bus,...
  class TestSettingsWindow (line 163) | class TestSettingsWindow:
    method test_options_labels (line 164) | def test_options_labels(self, plugin):
    method test_with_all_options_enabled (line 170) | def test_with_all_options_enabled(self, config, plugin):
    method test_with_all_options_disabled (line 179) | def test_with_all_options_disabled(self, config, plugin):
    method test_change_options (line 188) | def test_change_options(self, config, plugin):

FILE: tests/plugins/test_notify.py
  function plugin (line 17) | def plugin(_, graph, bus):
  function test_enable_notify_when_plugin_active (line 30) | def test_enable_notify_when_plugin_active(init, plugin):
  function test_disable_notify_when_plugin_deactivate (line 37) | def test_disable_notify_when_plugin_deactivate(uninit, plugin):
  function test_show_notification_when_session_starts (line 53) | def test_show_notification_when_session_starts(event, session, title, me...

FILE: tests/plugins/test_script.py
  function subprocess_run (line 17) | def subprocess_run(mocker, monkeypatch):
  function plugin (line 26) | def plugin(bus, config, graph):
  function test_execute_command_when_event_is_trigger (line 46) | def test_execute_command_when_event_is_trigger(event, option, bus, subpr...
  function test_command_variables (line 63) | def test_command_variables(event, section, session_type, bus, subprocess...
  function test_does_not_execute_commands_when_they_are_not_configured (line 80) | def test_does_not_execute_commands_when_they_are_not_configured(event, o...
  function test_execute_command_fail (line 89) | def test_execute_command_fail(bus, config, plugin):
  class TestSettingsWindow (line 97) | class TestSettingsWindow:
    method test_with_custom_commands (line 106) | def test_with_custom_commands(self, option, command, plugin):
    method test_without_custom_commands (line 116) | def test_without_custom_commands(self, option, config, plugin):
    method test_disable_command (line 131) | def test_disable_command(self, option, config, plugin):
    method test_configure_command (line 148) | def test_configure_command(self, option, config, plugin):
    method test_text (line 166) | def test_text(self, config, plugin):

FILE: tests/plugins/test_ticking.py
  function plugin (line 18) | def plugin(bus, config, graph, session):
  class TestPlugin (line 31) | class TestPlugin:
    method test_loads_configuration_when_is_activated (line 32) | def test_loads_configuration_when_is_activated(self, bus, config, plug...
    method test_starts_player_when_is_activated (line 48) | def test_starts_player_when_is_activated(
    method test_starts_player_when_session_start (line 59) | def test_starts_player_when_session_start(self, player, bus, config, p...
    method test_stops_player_when_session_ (line 68) | def test_stops_player_when_session_(self, player, event, bus, config, ...
    method test_stops_player_when_is_deactivate (line 77) | def test_stops_player_when_is_deactivate(self, player, bus, config, pl...

FILE: tests/pomodoro/test_app.py
  function app (line 14) | def app(graph, window, plugin_engine, mocker) -> Application:
  function test_module (line 24) | def test_module(graph, app):
  function test_collects_plugins_on_start (line 31) | def test_collects_plugins_on_start(app, plugin_engine):
  class TestRun (line 35) | class TestRun:
    method test_start_window_when_app_is_not_running (line 36) | def test_start_window_when_app_is_not_running(self, app, window):
    method test_shows_window_when_app_is_running (line 43) | def test_shows_window_when_app_is_running(self, app, window):
  class TestFromGraph (line 51) | class TestFromGraph:
    method setup_method (line 52) | def setup_method(self):
    method teardown_method (line 55) | def teardown_method(self):
    method test_create_app_instance_when_it_is_not_registered_in_dbus (line 58) | def test_create_app_instance_when_it_is_not_registered_in_dbus(self, g...
    method mock_dbus (line 68) | def mock_dbus(self):
    method test_get_dbus_interface_when_is_registered_in_dbus (line 74) | def test_get_dbus_interface_when_is_registered_in_dbus(self, graph, mo...

FILE: tests/pomodoro/test_config.py
  function config (line 11) | def config(graph, bus):
  function test_module (line 17) | def test_module(graph, config):
  function test_get_plugin_paths (line 24) | def test_get_plugin_paths(config):
  function test_get_config_path (line 30) | def test_get_config_path(config):
  function test_get_media_uri_raises_error_when_media_is_not_found (line 34) | def test_get_media_uri_raises_error_when_media_is_not_found(config):
  function test_get_media_uri (line 41) | def test_get_media_uri(config):
  function test_get_icon_path_raises_when_icon_not_found (line 47) | def test_get_icon_path_raises_when_icon_not_found(config):
  function test_get_icon_path (line 54) | def test_get_icon_path(config):
  function test_icon_paths (line 59) | def test_icon_paths(config):
  function test_get_option_as_int (line 63) | def test_get_option_as_int(config):
  function test_get_option_as_float (line 67) | def test_get_option_as_float(config):
  function test_get_option (line 71) | def test_get_option(config):
  function test_get_option_with_fallback (line 75) | def test_get_option_with_fallback(config):
  function test_get_defaults_option (line 79) | def test_get_defaults_option(config):
  function test_set_option (line 83) | def test_set_option(bus, config, mocker, tmpdir):
  function test_remove_option (line 99) | def test_remove_option(bus, config, mocker, tmpdir):

FILE: tests/pomodoro/test_event.py
  class TestBus (line 6) | class TestBus:
    method test_connect_receiver (line 7) | def test_connect_receiver(self, bus, mocker):
    method test_disconnect_receiver (line 16) | def test_disconnect_receiver(self, bus, mocker):
  function test_subscriber (line 24) | def test_subscriber(bus):
  function test_module (line 47) | def test_module(graph):

FILE: tests/pomodoro/test_graph.py
  function test_module (line 5) | def test_module():

FILE: tests/pomodoro/test_plugin.py
  function plugin_engine (line 11) | def plugin_engine(bus, graph, config) -> PluginEngine:
  function test_module (line 15) | def test_module(bus, config, graph):
  class TestPluginEngine (line 26) | class TestPluginEngine:
    method test_collect (line 27) | def test_collect(self, bus, graph, plugin_engine):
    method test_activate (line 38) | def test_activate(self, bus, plugin_engine):
    method test_deactivate (line 49) | def test_deactivate(self, bus, plugin_engine):
    method test_all (line 60) | def test_all(self, plugin_engine):
    method test_lookup (line 70) | def test_lookup(self, plugin_engine):
  class TestRaiseException (line 80) | class TestRaiseException:
    method test_does_not_raise_exception_when_debug_is_disabled (line 81) | def test_does_not_raise_exception_when_debug_is_disabled(self):
    method test_raises_exception_when_debug_enable (line 90) | def test_raises_exception_when_debug_enable(self):

FILE: tests/pomodoro/test_session.py
  function session (line 11) | def session(graph, config, bus, mocker):
  function test_module (line 18) | def test_module(graph, session):
  function test_sends_ready_event (line 25) | def test_sends_ready_event(bus, mocker, session):
  class TestSessionStart (line 35) | class TestSessionStart:
    method test_not_start_when_session_is_already_running (line 36) | def test_not_start_when_session_is_already_running(self, session):
    method test_starts_when_session_is_not_running (line 42) | def test_starts_when_session_is_not_running(self, state, session, bus,...
  class TestSessionStop (line 59) | class TestSessionStop:
    method test_not_stop_when_session_is_not_running (line 61) | def test_not_stop_when_session_is_not_running(self, state, session):
    method test_stops_when_session_is_running (line 66) | def test_stops_when_session_is_running(self, session, bus, mocker):
  class TestSessionReset (line 83) | class TestSessionReset:
    method test_not_reset_when_session_is_running (line 85) | def test_not_reset_when_session_is_running(self, state, session):
    method test_resets_when_session_is_not_running (line 97) | def test_resets_when_session_is_not_running(self, state, session_type,...
  class TestSessionEnd (line 116) | class TestSessionEnd:
    method test_ends_when_session_is_not_running (line 118) | def test_ends_when_session_is_not_running(self, state, session):
    method test_not_end_when_session_start_but_time_still_running (line 123) | def test_not_end_when_session_start_but_time_still_running(self, sessi...
    method test_ends_when_session_is_running (line 137) | def test_ends_when_session_is_running(
    method test_changes_session_type (line 171) | def test_changes_session_type(self, bus, config, mocker, session):
  class TestSessionChange (line 190) | class TestSessionChange:
    method test_not_change_when_session_is_running (line 192) | def test_not_change_when_session_is_running(self, state, session):
    method test_changes_when_session_is_not_running (line 205) | def test_changes_when_session_is_not_running(self, state, session_type...
    method test_changes_when_config_change_and_session_is_not_running (line 219) | def test_changes_when_config_change_and_session_is_not_running(self, s...
    method test_not_change_when_config_section_is_not_timer (line 229) | def test_not_change_when_config_section_is_not_timer(self, bus, config...
    method test_not_change_when_config_timer_changes_and_session_is_running (line 237) | def test_not_change_when_config_timer_changes_and_session_is_running(s...
  function test_type_of (line 255) | def test_type_of(number, session_type):
  function test_type_of_unknown (line 259) | def test_type_of_unknown():
  function test_type_option (line 272) | def test_type_option(session_type, option):

FILE: tests/pomodoro/test_timer.py
  function test_module (line 9) | def test_module(bus, graph):
  class TestTimerStart (line 18) | class TestTimerStart:
    method test_not_start_when_timer_is_already_running (line 19) | def test_not_start_when_timer_is_already_running(self, bus):
    method test_starts_when_timer_not_started_yet (line 26) | def test_starts_when_timer_not_started_yet(self, bus, mocker, state):
  class TestTimerStop (line 39) | class TestTimerStop:
    method test_not_stop_when_timer_is_not_running (line 41) | def test_not_stop_when_timer_is_not_running(self, bus, state):
    method test_stops_when_timer_is_running (line 47) | def test_stops_when_timer_is_running(self, bus, mocker):
  class TestTimerEnd (line 60) | class TestTimerEnd:
    method test_not_end_when_timer_is_not_running (line 62) | def test_not_end_when_timer_is_not_running(self, bus, state):
    method test_ends_when_time_is_up (line 68) | def test_ends_when_time_is_up(self, bus, mocker):
  class TestTimerPayload (line 84) | class TestTimerPayload:
    method test_remaining_ratio (line 89) | def test_remaining_ratio(self, duration, time_left, ratio):
    method test_elapsed_ratio (line 116) | def test_elapsed_ratio(self, duration, time_left, ratio):
    method test_elapsed_percent (line 145) | def test_elapsed_percent(self, duration, time_left, percent):
    method test_payload_markup (line 158) | def test_payload_markup(self, seconds, formatted):

FILE: tests/ui/dialogs/test_about.py
  function about (line 10) | def about(graph, config):
  function test_module (line 17) | def test_module(graph, about):
  function test_dialog_info (line 21) | def test_dialog_info(about):
  function test_close_dialog (line 31) | def test_close_dialog(about, mocker):

FILE: tests/ui/dialogs/test_preference.py
  function preference (line 11) | def preference(bus, plugin_engine, config, mocker) -> PreferenceDialog:
  function test_preference_module (line 16) | def test_preference_module(graph, bus, config, plugin_engine):
  function test_refresh_reload_plugins (line 27) | def test_refresh_reload_plugins(preference, plugin_engine):
  function test_initial_plugin_list (line 57) | def test_initial_plugin_list(plugin, row, columns, preference, plugin_en...
  function test_open_plugin_settings (line 68) | def test_open_plugin_settings(preference, plugin_engine):
  function test_connect_and_disconnect_plugins (line 87) | def test_connect_and_disconnect_plugins(bus, plugin_engine, preference):
  function test_save_config_when_task_duration_change (line 117) | def test_save_config_when_task_duration_change(duration_name, option, va...

FILE: tests/ui/test_shortcut.py
  function shortcut_engine (line 10) | def shortcut_engine(bus, config, graph) -> ShortcutEngine:
  function test_module (line 16) | def test_module(graph, shortcut_engine):
  function test_label (line 23) | def test_label(shortcut_engine):
  function test_label_with_fallback (line 29) | def test_label_with_fallback(shortcut_engine):
  function test_connect (line 35) | def test_connect(shortcut_engine, mocker):
  function test_disconnect (line 46) | def test_disconnect(shortcut_engine, mocker):
  function test_change (line 55) | def test_change(shortcut_engine, mocker):

FILE: tests/ui/test_systray.py
  function window (line 11) | def window():
  function subject (line 16) | def subject(graph, bus, window):
  function test_module (line 23) | def test_module(graph, subject):
  function test_hide_view_when_hide_menu_is_clicked (line 30) | def test_hide_view_when_hide_menu_is_clicked(window, subject):
  function test_show_window_when_hide_item_is_clicked (line 40) | def test_show_window_when_hide_item_is_clicked(window, subject):
  function test_change_items_visibility (line 51) | def test_change_items_visibility(event, hide, show, bus, subject):

FILE: tests/ui/test_window.py
  function window (line 11) | def window(bus, config, graph, session) -> Window:
  function test_module (line 24) | def test_module(graph, window):
  function test_init (line 31) | def test_init(session, window):
  function test_shortcuts (line 48) | def test_shortcuts(shortcut_engine, window):
  function test_run (line 54) | def test_run(mocker, window):
  class TestWindowHide (line 64) | class TestWindowHide:
    method test_iconify_when_tray_icon_plugin_is_not_registered (line 65) | def test_iconify_when_tray_icon_plugin_is_not_registered(self, window,...
    method test_deletes_when_tray_icon_plugin_is_registered (line 74) | def test_deletes_when_tray_icon_plugin_is_registered(self, bus, graph,...
  class TestWindowQuit (line 87) | class TestWindowQuit:
    method test_quits_when_timer_is_not_running (line 88) | def test_quits_when_timer_is_not_running(self, mocker, session, window):
    method test_hides_when_timer_is_running (line 96) | def test_hides_when_timer_is_running(self, bus, mocker, session, window):
  function test_shows_window_when_session_end (line 106) | def test_shows_window_when_session_end(bus, mocker, window):

FILE: tests/ui/widgets/test_countdown.py
  function countdown (line 12) | def countdown(bus, graph) -> Countdown:
  function test_module (line 18) | def test_module(countdown, graph):
  function test_updates_countdown_when_session_state_changes (line 34) | def test_updates_countdown_when_session_state_changes(event, payload, bu...

FILE: tests/ui/widgets/test_headerbar.py
  class TestHeaderBar (line 10) | class TestHeaderBar:
    method menu (line 12) | def menu(self, mocker):
    method headerbar (line 16) | def headerbar(self, graph, menu, shortcut_engine, session, bus, mocker...
    method test_module (line 35) | def test_module(self, graph, headerbar):
    method test_shortcuts (line 49) | def test_shortcuts(self, shortcut, action, headerbar, menu, session, s...
    method test_change_session (line 62) | def test_change_session(self, button_name, action, session, headerbar):
    method test_buttons_tooltip (line 79) | def test_buttons_tooltip(self, button_name, tooltip, headerbar):
    method test_enable_only_the_stop_button_when_session_starts (line 83) | def test_enable_only_the_stop_button_when_session_starts(self, bus, he...
    method test_disables_reset_button_when_session_is_reset (line 90) | def test_disables_reset_button_when_session_is_reset(self, headerbar, ...
    method test_buttons_visibility_and_title_in_the_first_session (line 106) | def test_buttons_visibility_and_title_in_the_first_session(self, event...
  class TestHeaderBarMenu (line 115) | class TestHeaderBarMenu:
    method preference (line 117) | def preference(self, mocker):
    method about (line 121) | def about(self, mocker):
    method menu (line 125) | def menu(self, bus, about, preference, shortcut_engine) -> HeaderBarMenu:
    method test_module (line 130) | def test_module(self, about, bus, preference, graph, shortcut_engine):
    method test_menu_items (line 151) | def test_menu_items(self, widget, label, mock_name, menu, about, prefe...
    method test_shortcut (line 161) | def test_shortcut(self, menu, shortcut_engine, preference):

FILE: tests/ui/widgets/test_session_button.py
  function session_button (line 10) | def session_button(bus, graph, session, shortcut_engine) -> SessionButton:
  function test_module (line 24) | def test_module(graph, session_button):
  function test_buttons_content (line 39) | def test_buttons_content(button_name, label, tooltip_text, session_button):
  function test_disables_buttons_when_session_starts (line 47) | def test_disables_buttons_when_session_starts(bus, session_button):
  function test_selects_button_when_session_stops_and_begins (line 60) | def test_selects_button_when_session_stops_and_begins(event, payload, se...
  function test_changes_session_when_button_is_clicked (line 70) | def test_changes_session_when_button_is_clicked(session_type, session_bu...
  function test_shortcuts (line 86) | def test_shortcuts(shortcut, session_type, session_button, shortcut_engi...
  function test_selects_button_when_session_changes (line 92) | def test_selects_button_when_session_changes(bus, session_button, session):

FILE: tomate/audio/player.py
  class GStreamerPlayer (line 14) | class GStreamerPlayer:
    method __init__ (line 15) | def __init__(self, repeat=False):
    method file (line 31) | def file(self) -> str:
    method file (line 35) | def file(self, filepath: str) -> None:
    method volume (line 55) | def volume(self) -> float:
    method volume (line 59) | def volume(self, volume: float) -> None:
    method play (line 63) | def play(self) -> None:
    method stop (line 68) | def stop(self) -> None:
    method _on_bus_callback (line 72) | def _on_bus_callback(self, _, message):
    method _on_about_to_finish (line 83) | def _on_about_to_finish(self, _) -> None:
    method _finished (line 88) | def _finished(self) -> None:

FILE: tomate/main.py
  function main (line 21) | def main():
  function setup_logging (line 38) | def setup_logging(options):
  function parse_options (line 44) | def parse_options():

FILE: tomate/pomodoro/app.py
  class State (line 11) | class State(enum.Enum):
  class Application (line 17) | class Application(dbus.service.Object):
    method __init__ (line 24) | def __init__(self, bus, window, plugins: PluginEngine):
    method IsRunning (line 31) | def IsRunning(self):
    method Run (line 35) | def Run(self):
    method from_graph (line 45) | def from_graph(cls, graph, bus=dbus.SessionBus(mainloop=DBusGMainLoop(...

FILE: tomate/pomodoro/config.py
  class Config (line 19) | class Config:
    method __init__ (line 34) | def __init__(self, bus: Bus, parser=RawConfigParser(defaults=DEFAULTS,...
    method __getattr__ (line 39) | def __getattr__(self, attr):
    method load (line 42) | def load(self) -> None:
    method save (line 47) | def save(self) -> None:
    method config_path (line 53) | def config_path(self) -> str:
    method media_uri (line 57) | def media_uri(self, *resources: str) -> str:
    method plugin_paths (line 60) | def plugin_paths(self) -> List[str]:
    method icon_paths (line 63) | def icon_paths(self) -> List[str]:
    method _resource_path (line 66) | def _resource_path(self, *resources) -> str:
    method _load_data_paths (line 73) | def _load_data_paths(self, *resources) -> List[str]:
    method icon_path (line 76) | def icon_path(self, iconname, size=None, theme=None) -> str:
    method get_int (line 84) | def get_int(self, section: str, option: str, fallback=None) -> int:
    method get_bool (line 87) | def get_bool(self, section: str, option: str, fallback=None) -> bool:
    method get_float (line 90) | def get_float(self, section: str, option: str, fallback=None) -> int:
    method get (line 93) | def get(self, section: str, option: str, fallback=None, method="get") ...
    method set (line 103) | def set(self, section: str, option: str, value) -> None:
    method remove (line 116) | def remove(self, section, option) -> None:
    method normalize (line 128) | def normalize(name: str) -> str:
  function remove_duplicates (line 132) | def remove_duplicates(original: List[str]) -> List[str]:

FILE: tomate/pomodoro/event.py
  class Events (line 14) | class Events(enum.Enum):
  class Bus (line 37) | class Bus:
    method __init__ (line 38) | def __init__(self):
    method connect (line 41) | def connect(self, event: Events, receiver: Receiver, weak: bool = True):
    method is_connect (line 44) | def is_connect(self, event: Events, receiver: Receiver) -> bool:
    method send (line 47) | def send(self, event: Events, payload: Any = None) -> List[Any]:
    method disconnect (line 51) | def disconnect(self, event: Events, receiver: Receiver):
  function on (line 55) | def on(*events: Events):
  class Subscriber (line 69) | class Subscriber:
    method connect (line 70) | def connect(self, bus: Bus) -> None:
    method disconnect (line 81) | def disconnect(self, bus: Bus):
    method __methods_with_events (line 92) | def __methods_with_events(self) -> List[Tuple[Any, List[Events]]]:

FILE: tomate/pomodoro/fsm.py
  class fsm (line 8) | class fsm:
    method __init__ (line 9) | def __init__(self, target, **kwargs):
    method is_valid_transition (line 16) | def is_valid_transition(self, instance) -> bool:
    method is_valid_condition (line 19) | def is_valid_condition(self, instance) -> bool:
    method change_state (line 25) | def change_state(self, instance) -> None:
    method call_exit_action (line 39) | def call_exit_action(self, instance) -> None:
    method __call__ (line 44) | def __call__(self, wrapped, instance, args, kwargs):

FILE: tomate/pomodoro/plugin.py
  class Plugin (line 20) | class Plugin(IPlugin, Subscriber):
    method __init__ (line 23) | def __init__(self):
    method configure (line 28) | def configure(self, bus: Bus, graph: Graph) -> None:
    method activate (line 32) | def activate(self) -> None:
    method deactivate (line 36) | def deactivate(self) -> None:
    method settings_window (line 40) | def settings_window(self, parent) -> Union[Gtk.Dialog, None]:
  class PluginEngine (line 45) | class PluginEngine:
    method __init__ (line 47) | def __init__(self, bus: Bus, config: Config, graph: Graph):
    method collect (line 57) | def collect(self) -> None:
    method _configure_plugin (line 62) | def _configure_plugin(self, plugin: PluginInfo) -> None:
    method deactivate (line 66) | def deactivate(self, name: str) -> None:
    method activate (line 69) | def activate(self, name: str) -> None:
    method all (line 72) | def all(self) -> List[PluginInfo]:
    method lookup (line 76) | def lookup(self, name: str, category="Default") -> Optional[PluginInfo]:
    method has_plugins (line 80) | def has_plugins(self) -> bool:
    method remove (line 85) | def remove(self, plugin: object, category="Default") -> None:
  function suppress_errors (line 90) | def suppress_errors(wrapped, _, args, kwargs):
  function in_debug_mode (line 103) | def in_debug_mode():

FILE: tomate/pomodoro/session.py
  class Payload (line 21) | class Payload(namedtuple("SessionPayload", ["type", "pomodoros", "durati...
    method countdown (line 23) | def countdown(self) -> str:
  class Type (line 27) | class Type(enum.Enum):
    method of (line 33) | def of(cls, index: int) -> Type:
    method option (line 41) | def option(self) -> str:
  class State (line 46) | class State(enum.Enum):
  class Session (line 54) | class Session(Subscriber):
    method __init__ (line 60) | def __init__(self, bus: Bus, config: Config, timer: Timer):
    method ready (line 70) | def ready(self) -> None:
    method start (line 76) | def start(self) -> bool:
    method is_running (line 81) | def is_running(self) -> bool:
    method stop (line 90) | def stop(self) -> bool:
    method reset (line 96) | def reset(self) -> bool:
    method _on_config_change (line 102) | def _on_config_change(self, payload: ConfigPayload) -> bool:
    method change (line 109) | def change(self, session: Type) -> bool:
    method duration (line 115) | def duration(self) -> int:
    method timer_is_up (line 119) | def timer_is_up(self) -> bool:
    method _end (line 129) | def _end(self, payload: TimerPayload) -> bool:
    method _choose_break (line 145) | def _choose_break(self):
    method _is_long_break (line 148) | def _is_long_break(self) -> bool:
    method _trigger (line 152) | def _trigger(self, event: Events) -> None:
    method _create_payload (line 155) | def _create_payload(self, **kwargs) -> Payload:

FILE: tomate/pomodoro/timer.py
  function format_seconds (line 16) | def format_seconds(seconds: int) -> str:
  class Payload (line 21) | class Payload(namedtuple("TimerPayload", ["time_left", "duration"])):
    method remaining_ratio (line 23) | def remaining_ratio(self) -> float:
    method elapsed_ratio (line 30) | def elapsed_ratio(self) -> float:
    method elapsed_percent (line 34) | def elapsed_percent(self):
    method countdown (line 42) | def countdown(self) -> str:
  class State (line 51) | class State(enum.Enum):
  class Timer (line 58) | class Timer:
    method __init__ (line 62) | def __init__(self, bus: Bus):
    method start (line 68) | def start(self, seconds: int) -> bool:
    method stop (line 75) | def stop(self) -> bool:
    method _is_up (line 80) | def _is_up(self) -> bool:
    method is_running (line 83) | def is_running(self) -> bool:
    method end (line 89) | def end(self) -> bool:
    method _update (line 93) | def _update(self) -> bool:
    method _reset (line 106) | def _reset(self) -> None:
    method _trigger (line 109) | def _trigger(self, event) -> None:

FILE: tomate/ui/dialogs/about.py
  class AboutDialog (line 7) | class AboutDialog(Gtk.AboutDialog):
    method __init__ (line 9) | def __init__(self, config):
    method widget (line 29) | def widget(self):

FILE: tomate/ui/dialogs/preference.py
  class PreferenceDialog (line 16) | class PreferenceDialog(Gtk.Dialog):
    method __init__ (line 21) | def __init__(self, timer_tab, extension_tab):
    method widget (line 52) | def widget(self):
    method run (line 55) | def run(self):
  class TimerTab (line 62) | class TimerTab:
    method __init__ (line 64) | def __init__(self, config: Config):
    method _create_section (line 89) | def _create_section(self, name):
    method _create_option (line 95) | def _create_option(self, name: str, label: str, option: str):
    method _on_change (line 107) | def _on_change(self, widget, option):
  class ExtensionTab (line 113) | class ExtensionTab:
    method __init__ (line 115) | def __init__(self, bus: Bus, config: Config, plugin_engine: PluginEngi...
    method _on_plugin_changed (line 156) | def _on_plugin_changed(self, selection):
    method _on_plugin_toggle (line 163) | def _on_plugin_toggle(self, _, path):
    method _on_plugin_settings_clicked (line 174) | def _on_plugin_settings_clicked(self, _):
    method _activate (line 180) | def _activate(self, plugin):
    method _deactivate (line 184) | def _deactivate(self, plugin):
    method set_toplevel (line 188) | def set_toplevel(self, widget: Gtk.Widget) -> None:
    method refresh (line 191) | def refresh(self):
    method _add (line 202) | def _add(self, plugin):
    method _select_first (line 206) | def _select_first(self):
    method _clear (line 209) | def _clear(self):
  class PluginGrid (line 214) | class PluginGrid(object):
    method __init__ (line 228) | def __init__(self, row):
    method name (line 232) | def name(self):
    method is_enable (line 236) | def is_enable(self):
    method toggle (line 239) | def toggle(self):
    method instance (line 243) | def instance(self):
    method has_settings (line 247) | def has_settings(self):
    method open_settings (line 250) | def open_settings(self, toplevel):
    method create_row (line 254) | def create_row(plugin, config):
    method pixbuf (line 264) | def pixbuf(plugin, config):
    method description (line 270) | def description(plugin):
    method from_iter (line 276) | def from_iter(cls, tree_store, tree_iter):
    method from_path (line 280) | def from_path(cls, tree_store, tree_path):

FILE: tomate/ui/shortcut.py
  class Shortcut (line 12) | class Shortcut(namedtuple("Shortcut", ["name", "value"])):
    method __str__ (line 13) | def __str__(self) -> str:
    method accel_path (line 17) | def accel_path(self) -> str:
  class ShortcutEngine (line 22) | class ShortcutEngine:
    method __init__ (line 24) | def __init__(self, config, accel_group=Gtk.AccelGroup()):
    method init (line 28) | def init(self, window: Gtk.Window) -> None:
    method change (line 32) | def change(self, shortcut: Shortcut) -> None:
    method connect (line 36) | def connect(self, shortcut: Shortcut, callback: Callable[[], Any]) -> ...
    method disconnect (line 41) | def disconnect(self, shortcut: Shortcut) -> None:
    method label (line 45) | def label(self, shortcut: Shortcut) -> str:
    method _parse (line 48) | def _parse(self, shortcut: Shortcut) -> Tuple[int, Gdk.ModifierType]:

FILE: tomate/ui/systray.py
  class Systray (line 11) | class Systray:
    method show (line 12) | def show(*args, **kwargs):
    method hide (line 15) | def hide(*args, **kwargs):
  class Menu (line 20) | class Menu(Subscriber):
    method __init__ (line 22) | def __init__(self, bus: Bus, window):
    method _create_menu (line 26) | def _create_menu(self, window) -> Tuple[Gtk.Menu, Gtk.MenuItem, Gtk.Me...
    method _create_menu_item (line 33) | def _create_menu_item(self, label: str, activate: Callable[[], None], ...
    method _on_window_show (line 40) | def _on_window_show(self, **__):
    method _on_window_hide (line 45) | def _on_window_hide(self, **__):

FILE: tomate/ui/testing.py
  function active_shortcut (line 12) | def active_shortcut(shortcut_engine: ShortcutEngine, shortcut: Shortcut,...
  function create_session_payload (line 21) | def create_session_payload(**kwargs) -> SessionPayload:
  function run_loop_for (line 31) | def run_loop_for(seconds: int = 1) -> None:
  function refresh_gui (line 36) | def refresh_gui(delay: int = 0) -> None:
  class GtkWidgetNotFound (line 42) | class GtkWidgetNotFound(Exception):
  class Q (line 49) | class Q:
    method props (line 51) | def props(name: str, value: Any) -> Filter:
    method combine (line 60) | def combine(*fns: Filter) -> Filter:
    method select (line 70) | def select(root: Gtk.Widget, *fns: Filter) -> Gtk.Widget:
    method emit (line 85) | def emit(method: str, *args) -> Callable[[Gtk.Widget], None]:
    method map (line 92) | def map(widget: Gtk.Widget, *fns: Callable[[Any], Any]):
  class TV (line 96) | class TV:
    method model (line 98) | def model(tree_view: Gtk.TreeView) -> Gtk.TreeStore:
    method column (line 102) | def column(fn: Filter) -> Callable[[Gtk.TreeView], List[Gtk.TreeViewCo...
    method cell_renderer (line 109) | def cell_renderer(position: int) -> Callable[[Gtk.TreeViewColumn], Gtk...

FILE: tomate/ui/widgets/countdown.py
  class Countdown (line 14) | class Countdown(Subscriber):
    method __init__ (line 16) | def __init__(self, bus: Bus):
    method _update_countdown (line 21) | def _update_countdown(self, payload: Union[SessionPayload, TimerPayloa...
    method timer_markup (line 26) | def timer_markup(time_left: str) -> str:

FILE: tomate/ui/widgets/headerbar.py
  class Menu (line 17) | class Menu(Subscriber):
    method __init__ (line 26) | def __init__(self, bus: Bus, about, preference, shortcuts: ShortcutEng...
    method _create_menu_item (line 36) | def _create_menu_item(self, name: str, label: str, dialog: Gtk.Dialog)...
  class HeaderBar (line 44) | class HeaderBar(Subscriber):
    method __init__ (line 55) | def __init__(self, bus: Bus, menu: Menu, session: Session, shortcuts: ...
    method _create_headerbar (line 86) | def _create_headerbar(self):
    method _add_button (line 93) | def _add_button(self, icon: str, tooltip_text: str, shortcut: Shortcut...
    method _add_preference_button (line 108) | def _add_preference_button(self, menu, shortcuts) -> None:
    method _on_session_start (line 119) | def _on_session_start(self, **__):
    method _on_session_stop (line 126) | def _on_session_stop(self, payload: SessionPayload) -> None:
    method _on_session_reset (line 134) | def _on_session_reset(self, **__):
    method _update_title (line 139) | def _update_title(self, pomodoros: int) -> None:

FILE: tomate/ui/widgets/mode_button.py
  class ModeButtonItem (line 6) | class ModeButtonItem(Gtk.ToggleButton):
    method __init__ (line 7) | def __init__(self, index: int, **props: Dict[str, Any]):
  class ModeButton (line 12) | class ModeButton(Gtk.Box):
    method __init__ (line 15) | def __init__(self, **kwargs):
    method get_selected (line 25) | def get_selected(self):
    method append_text (line 28) | def append_text(self, text: str, **props: Dict[str, Any]):
    method on_button_press_event (line 38) | def on_button_press_event(self, widget, event=None):
    method set_selected (line 41) | def set_selected(self, index):

FILE: tomate/ui/widgets/session_button.py
  class SessionButton (line 27) | class SessionButton(Subscriber):
    method __init__ (line 37) | def __init__(self, bus: Bus, session: Session, shortcuts: ShortcutEngi...
    method _create_mode_button (line 48) | def _create_mode_button(self) -> ModeButton:
    method _add_button (line 58) | def _add_button(self, shortcut: Shortcut, label: str, session_type: Se...
    method _select (line 66) | def _select(self, session_type: SessionType) -> Callable[[], bool]:
    method _clicked (line 74) | def _clicked(self, _, number):
    method _change (line 80) | def _change(self, payload=SessionPayload) -> None:
    method _disable (line 86) | def _disable(self, **__):
    method _enable (line 91) | def _enable(self, payload: SessionPayload):

FILE: tomate/ui/window.py
  class Window (line 18) | class Window(Subscriber):
    method __init__ (line 29) | def __init__(
    method _create_window (line 51) | def _create_window(self, config: Config, headerbar: HeaderBar, box: Gt...
    method _create_content (line 64) | def _create_content(self, countdown: Countdown, session_button: Sessio...
    method run (line 70) | def run(self) -> None:
    method quit (line 75) | def quit(self, *_) -> None:
    method hide (line 82) | def hide(self):
    method show (line 94) | def show(self, **__) -> None:
Condensed preview — 94 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (230K chars).
[
  {
    "path": ".bumpversion.cfg",
    "chars": 233,
    "preview": "[bumpversion]\ncurrent_version = 0.25.2\n\n[bumpversion:file:setup.py]\n\n[bumpversion:file:tomate/__init__.py]\n\n[bumpversion"
  },
  {
    "path": ".envfile",
    "chars": 171,
    "preview": "XDG_CONFIG_HOME=$PROJECT_DIR$/tests/data\nXDG_DATA_HOME=$PROJECT_DIR$/tests/data\nXDG_DATA_DIRS=$PROJECT_DIR$/tests/data\nP"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 262,
    "preview": "name: lint\n\non:\n  push:\n    branches:\n      - main\n      - develop\n  pull_request:\n  workflow_dispatch: \n\njobs:\n  lint:\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 401,
    "preview": "name: release\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - 'data/**'\n      - 'tomate/**'\n      - 'setup."
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 458,
    "preview": "name: test\n\non:\n  push:\n    branches:\n      - main\n      - develop\n  pull_request: \n  workflow_dispatch: \n\njobs:\n  test:"
  },
  {
    "path": ".gitignore",
    "chars": 124,
    "preview": "*.egg\n*.egg-info\n*.log\n*.py[cod]\n*.pyc\n.coverage\nvenv/\n\n.idea/\n*.deb\n.cache\n.envrc\n.pytest_cache\ntests/data/mime/mime.ca"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 580,
    "preview": "repos:\n  - repo: https://github.com/psf/black\n    rev: 23.3.0\n    hooks:\n      - id: black\n  - repo: https://github.com/"
  },
  {
    "path": "AUTHORS",
    "chars": 72,
    "preview": "Copyright (C) 2012 <Elio Esteves Duarte> <elio.esteves.duarte@gmail.com>"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3817,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
  },
  {
    "path": "COPYING",
    "chars": 35147,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "Dockerfile",
    "chars": 964,
    "preview": "FROM ubuntu:22.04\n\nENV DEBIAN_FRONTEND noninteractive\n\nRUN apt-get update -qq && apt-get install -y --no-install-recomme"
  },
  {
    "path": "LICENSE",
    "chars": 592,
    "preview": "License\n-------\n\nThis program is free software: you can redistribute it and/or modify it\nunder the terms of the GNU Gene"
  },
  {
    "path": "MANIFEST.in",
    "chars": 133,
    "preview": "include AUTHORS COPYING CHANGELOG.md README.md\nrecursive-include data/applications *.desktop\nrecursive-include data/icon"
  },
  {
    "path": "Makefile",
    "chars": 3487,
    "preview": "ifeq ($(origin .RECIPEPREFIX), undefined)\n\t$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or "
  },
  {
    "path": "README.md",
    "chars": 3992,
    "preview": "# Tomate\n\nA Pomodoro timer written in Gtk3 and Python for Linux desktops.\n\n## About the technique\n\nThe Pomodoro Techniqu"
  },
  {
    "path": "data/applications/tomate-gtk.desktop",
    "chars": 180,
    "preview": "[Desktop Entry]\nName=Tomate\nGenericName=Clock\nComment=Gtk version from Tomate pomodoro timer.\nCategories=Utility;Clock;\n"
  },
  {
    "path": "data/plugins/alarm.plugin",
    "chars": 187,
    "preview": "[Core]\nName = Alarm\nModule = alarm\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 0.12.0\nWebsite = https://gith"
  },
  {
    "path": "data/plugins/alarm.py",
    "chars": 6166,
    "preview": "import logging\nfrom locale import gettext as _\nfrom os import path\nfrom urllib.parse import urlparse\n\nimport gi\nfrom wir"
  },
  {
    "path": "data/plugins/autopause.plugin",
    "chars": 225,
    "preview": "[Core]\nName = Auto Pause\nModule = autopause\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 0.2.0\nWebsite =  htt"
  },
  {
    "path": "data/plugins/autopause.py",
    "chars": 1212,
    "preview": "import logging\n\nimport gi\n\nimport tomate.pomodoro.plugin as plugin\nfrom tomate.pomodoro import Events, on, suppress_erro"
  },
  {
    "path": "data/plugins/breakscreen.plugin",
    "chars": 260,
    "preview": "[Core]\nName = Break Screen\nModule = breakscreen\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 0.7.0\nWebsite = "
  },
  {
    "path": "data/plugins/breakscreen.py",
    "chars": 9397,
    "preview": "import logging\nfrom collections import namedtuple\nfrom locale import gettext as _\nfrom typing import Dict\n\nimport gi\n\ngi"
  },
  {
    "path": "data/plugins/notify.plugin",
    "chars": 189,
    "preview": "[Core]\nName = Notify\nModule = notify\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 0.15.0\nWebsite = https://gi"
  },
  {
    "path": "data/plugins/notify.py",
    "chars": 2340,
    "preview": "import logging\nfrom locale import gettext as _\nfrom typing import Tuple\n\nimport gi\nfrom wiring import Graph\n\nimport toma"
  },
  {
    "path": "data/plugins/script.plugin",
    "chars": 215,
    "preview": "[Core]\nName = Script\nModule = script\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 0.2.0\nWebsite = https://git"
  },
  {
    "path": "data/plugins/script.py",
    "chars": 6175,
    "preview": "import locale\nimport logging\nimport subprocess\nfrom locale import gettext as _\nfrom string import Template\nfrom typing i"
  },
  {
    "path": "data/plugins/ticking.plugin",
    "chars": 203,
    "preview": "[Core]\nName = Ticking\nModule = ticking\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 0.0.2\nWebsite = https://g"
  },
  {
    "path": "data/plugins/ticking.py",
    "chars": 2127,
    "preview": "import logging\n\nimport gi\nfrom wiring import Graph\n\ngi.require_version(\"Gst\", \"1.0\")\n\nimport tomate.pomodoro.plugin as p"
  },
  {
    "path": "pyproject.toml",
    "chars": 288,
    "preview": "[tool.black]\nline-length = 120\ninclude = '\\.pyi?$'\nexclude = '''\n/(\n    \\.git\n  | \\.hg\n  | \\.mypy_cache\n  | \\.tox\n  | \\."
  },
  {
    "path": "setup.cfg",
    "chars": 140,
    "preview": "[global]\nverbose=1\n\n[wheel]\nuniversal = 1\n\n[tool:pytest]\naddopts = --verbose --cov-report term-missing\n\n[coverage:run]\ni"
  },
  {
    "path": "setup.py",
    "chars": 1522,
    "preview": "#!/bin/env python\nimport os\n\nfrom setuptools import find_packages, setup\n\n\ndef find_xdg_data_files(from_dir, to_dir, pac"
  },
  {
    "path": "tests/conftest.py",
    "chars": 1067,
    "preview": "import os\n\nimport gi\nimport pytest\nfrom wiring import Graph\n\ngi.require_version(\"Gtk\", \"3.0\")\n\nfrom gi.repository import"
  },
  {
    "path": "tests/data/icons/hicolor/index.theme",
    "chars": 605,
    "preview": "[Icon Theme]\nName=Hicolor\nComment=Fallback icon theme\nHidden=true\nDirectories=16x16/apps,24x24/apps,scalable/apps,scalab"
  },
  {
    "path": "tests/data/mime/packages/freedesktop.org.xml",
    "chars": 1703,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nThe freedesktop.org shared MIME database (this file) was created by merging\n"
  },
  {
    "path": "tests/data/pulse/cookie",
    "chars": 139,
    "preview": "\u0016\u0011\u0006?Q/\"$\"cܗR\u0007dk\b\u0013r}\u000fJ\u001aM\u001cDiqf_~V9asj\u000b7pGm\"\u0005\u0018kF)C\u0014ޭޝ\u001d\u001egGBkH0\nO\t\u0014o\u0019\u0017ʢm\fD\u0018V>N\u001d{i\bq>\u0002X\u0002䟤ߺB]uքe1Hۅ\ncX=)h\u0016\u0013&]\rٍamm^K?\u0011\u0017\u0018I3rA\u0013\u000f"
  },
  {
    "path": "tests/data/tomate/plugins/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/data/tomate/plugins/plugin_a.plugin",
    "chars": 183,
    "preview": "[Core]\nName = PluginA\nModule = plugin_a\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 1.0.0\nWebsite = https://"
  },
  {
    "path": "tests/data/tomate/plugins/plugin_a.py",
    "chars": 730,
    "preview": "from gi.repository import Gtk\n\nimport tomate.pomodoro.plugin as plugin\nfrom tomate.pomodoro import Events, on\n\n\nclass Pl"
  },
  {
    "path": "tests/data/tomate/plugins/plugin_b.plugin",
    "chars": 183,
    "preview": "[Core]\nName = PluginB\nModule = plugin_b\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 2.0.0\nWebsite = https://"
  },
  {
    "path": "tests/data/tomate/plugins/plugin_b.py",
    "chars": 228,
    "preview": "import tomate.pomodoro.plugin as plugin\nfrom tomate.pomodoro import Events, on\n\n\nclass PluginB(plugin.Plugin):\n    has_s"
  },
  {
    "path": "tests/data/tomate/plugins/plugin_b_old.plugin",
    "chars": 216,
    "preview": "[Core]\nName = PluginB\nModule = plugin_b\n\n[Documentation]\nAuthor = Elio Esteves Duarte\nVersion = 1.5.0\nWebsite = https://"
  },
  {
    "path": "tests/data/tomate/tomate.conf",
    "chars": 425,
    "preview": "[DEFAULT]\npomodoro_duration = 25\nshortbreak_duration = 5\nlongbreak_duration = 15\nlong_break_interval = 4\n\n[timer]\npomodo"
  },
  {
    "path": "tests/plugins/test_alarm.py",
    "chars": 3440,
    "preview": "from os.path import dirname, join\nfrom unittest.mock import patch\n\nimport gi\nimport pytest\n\ngi.require_version(\"Gtk\", \"3"
  },
  {
    "path": "tests/plugins/test_autopause.py",
    "chars": 1298,
    "preview": "import gi\nimport pytest\n\ngi.require_version(\"Playerctl\", \"2.0\")\ngi.require_version(\"Gtk\", \"3.0\")\n\nfrom tomate.pomodoro i"
  },
  {
    "path": "tests/plugins/test_breakscreen.py",
    "chars": 7141,
    "preview": "import random\nfrom typing import Iterator\n\nimport gi\nimport pytest\n\ngi.require_version(\"Gtk\", \"3.0\")\n\nfrom gi.repository"
  },
  {
    "path": "tests/plugins/test_notify.py",
    "chars": 1882,
    "preview": "from os.path import dirname, join\nfrom unittest.mock import patch\n\nimport gi\nimport pytest\n\nfrom tomate.pomodoro import "
  },
  {
    "path": "tests/plugins/test_script.py",
    "chars": 5575,
    "preview": "import subprocess\n\nimport gi\nimport pytest\n\ngi.require_version(\"Gtk\", \"3.0\")\n\nfrom gi.repository import Gtk\n\nfrom tomate"
  },
  {
    "path": "tests/plugins/test_ticking.py",
    "chars": 2477,
    "preview": "from os.path import dirname, join\nfrom unittest.mock import patch\n\nimport gi\nimport pytest\n\nfrom tomate.ui.testing impor"
  },
  {
    "path": "tests/pomodoro/test_app.py",
    "chars": 2401,
    "preview": "import dbus\nimport pytest\nfrom dbus.mainloop.glib import DBusGMainLoop\nfrom dbusmock import DBusTestCase\nfrom wiring.sca"
  },
  {
    "path": "tests/pomodoro/test_config.py",
    "chars": 3277,
    "preview": "import os\n\nimport pytest\nfrom wiring.scanning import scan_to_graph\n\nfrom tests.conftest import TEST_DATA_DIR\nfrom tomate"
  },
  {
    "path": "tests/pomodoro/test_event.py",
    "chars": 1511,
    "preview": "from wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Bus, Events, Subscriber, on\n\n\nclass TestBus:\n    "
  },
  {
    "path": "tests/pomodoro/test_graph.py",
    "chars": 209,
    "preview": "from wiring import Graph\nfrom wiring.scanning import scan_to_graph\n\n\ndef test_module():\n    graph = Graph()\n\n    scan_to"
  },
  {
    "path": "tests/pomodoro/test_plugin.py",
    "chars": 2983,
    "preview": "import os\nfrom distutils.version import StrictVersion\n\nimport pytest\nfrom wiring.scanning import scan_to_graph\n\nfrom tom"
  },
  {
    "path": "tests/pomodoro/test_session.py",
    "chars": 8928,
    "preview": "import pytest\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Events, Session, SessionPayload, Se"
  },
  {
    "path": "tests/pomodoro/test_timer.py",
    "chars": 4851,
    "preview": "import pytest\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Events, Timer, TimerPayload\nfrom to"
  },
  {
    "path": "tests/ui/dialogs/test_about.py",
    "chars": 1045,
    "preview": "import pytest\nfrom gi.repository import Gtk\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate import __version__\nfr"
  },
  {
    "path": "tests/ui/dialogs/test_preference.py",
    "chars": 4006,
    "preview": "import pytest\nfrom gi.repository import Gtk\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Confi"
  },
  {
    "path": "tests/ui/test_shortcut.py",
    "chars": 2053,
    "preview": "import pytest\nfrom gi.repository import Gtk\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.ui import Shortcut, S"
  },
  {
    "path": "tests/ui/test_systray.py",
    "chars": 1370,
    "preview": "import pytest\nfrom gi.repository import Gtk\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Event"
  },
  {
    "path": "tests/ui/test_window.py",
    "chars": 3792,
    "preview": "import pytest\nfrom gi.repository import Gdk, Gtk\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import "
  },
  {
    "path": "tests/ui/widgets/test_countdown.py",
    "chars": 1196,
    "preview": "import random\n\nimport pytest\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Events, TimerPayload"
  },
  {
    "path": "tests/ui/widgets/test_headerbar.py",
    "chars": 6533,
    "preview": "import pytest\nfrom gi.repository import Gtk\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Event"
  },
  {
    "path": "tests/ui/widgets/test_session_button.py",
    "chars": 3469,
    "preview": "import pytest\nfrom wiring.scanning import scan_to_graph\n\nfrom tomate.pomodoro import Events, SessionType\nfrom tomate.ui."
  },
  {
    "path": "tomate/__init__.py",
    "chars": 23,
    "preview": "__version__ = \"0.25.2\"\n"
  },
  {
    "path": "tomate/__main__.py",
    "chars": 62,
    "preview": "from .main import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tomate/audio/__init__.py",
    "chars": 67,
    "preview": "from .player import GStreamerPlayer\n\n__all__ = [\"GStreamerPlayer\"]\n"
  },
  {
    "path": "tomate/audio/player.py",
    "chars": 3013,
    "preview": "import logging\n\nimport gi\n\ngi.require_version(\"Gst\", \"1.0\")\ngi.require_version(\"Gtk\", \"3.0\")\n\nfrom gi.repository import "
  },
  {
    "path": "tomate/main.py",
    "chars": 1198,
    "preview": "import argparse\nimport locale\nimport logging\nfrom locale import gettext as _\n\nimport gi\n\ngi.require_version(\"Gdk\", \"3.0\""
  },
  {
    "path": "tomate/pomodoro/__init__.py",
    "chars": 735,
    "preview": "from .app import Application\nfrom .config import Config\nfrom .config import Payload as ConfigPayload\nfrom .event import "
  },
  {
    "path": "tomate/pomodoro/app.py",
    "chars": 1664,
    "preview": "import enum\n\nimport dbus.service\nfrom dbus.mainloop.glib import DBusGMainLoop\nfrom wiring import SingletonScope, inject\n"
  },
  {
    "path": "tomate/pomodoro/config.py",
    "chars": 4707,
    "preview": "import logging\nimport os\nfrom collections import namedtuple\nfrom configparser import RawConfigParser\nfrom typing import "
  },
  {
    "path": "tomate/pomodoro/event.py",
    "chars": 2729,
    "preview": "import enum\nimport functools\nimport logging\nfrom typing import Any, Callable, List, Tuple\n\nimport blinker\nfrom wiring im"
  },
  {
    "path": "tomate/pomodoro/fsm.py",
    "chars": 2273,
    "preview": "import logging\n\nimport wrapt\n\nlogger = logging.getLogger(__name__)\n\n\nclass fsm:\n    def __init__(self, target, **kwargs)"
  },
  {
    "path": "tomate/pomodoro/graph.py",
    "chars": 111,
    "preview": "from wiring import Graph\nfrom wiring.scanning import register\n\ngraph = Graph()\nregister.instance(Graph)(graph)\n"
  },
  {
    "path": "tomate/pomodoro/plugin.py",
    "chars": 3274,
    "preview": "import logging\nimport os\nfrom typing import List, Optional, Union\n\nimport wrapt\nfrom gi.repository import Gtk\nfrom wirin"
  },
  {
    "path": "tomate/pomodoro/session.py",
    "chars": 4813,
    "preview": "from __future__ import annotations\n\nimport enum\nimport logging\nfrom collections import namedtuple\n\nfrom wiring import Si"
  },
  {
    "path": "tomate/pomodoro/timer.py",
    "chars": 3017,
    "preview": "import enum\nimport logging\nfrom collections import namedtuple\n\nfrom gi.repository import GLib\nfrom wiring import Singlet"
  },
  {
    "path": "tomate/ui/__init__.py",
    "chars": 222,
    "preview": "from .shortcut import Shortcut, ShortcutEngine\nfrom .systray import Menu as SystrayMenu\nfrom .systray import Systray\nfro"
  },
  {
    "path": "tomate/ui/dialogs/__init__.py",
    "chars": 197,
    "preview": "from .about import AboutDialog\nfrom .preference import ExtensionTab, PluginGrid, PreferenceDialog, TimerTab\n\n__all__ = ["
  },
  {
    "path": "tomate/ui/dialogs/about.py",
    "chars": 1031,
    "preview": "from gi.repository import GdkPixbuf, Gtk\nfrom wiring import SingletonScope, inject\nfrom wiring.scanning import register\n"
  },
  {
    "path": "tomate/ui/dialogs/preference.py",
    "chars": 9932,
    "preview": "import locale\nimport logging\nfrom locale import gettext as _\n\nfrom gi.repository import GdkPixbuf, Gtk, Pango\nfrom wirin"
  },
  {
    "path": "tomate/ui/shortcut.py",
    "chars": 1884,
    "preview": "import logging\nfrom collections import namedtuple\nfrom typing import Any, Callable, Tuple\n\nfrom gi.repository import Gdk"
  },
  {
    "path": "tomate/ui/systray.py",
    "chars": 1613,
    "preview": "from locale import gettext as _\nfrom typing import Callable, Tuple\n\nfrom gi.repository import Gtk\nfrom wiring import Sin"
  },
  {
    "path": "tomate/ui/testing.py",
    "chars": 3069,
    "preview": "import time\nfrom collections import deque\nfrom functools import reduce\nfrom typing import Any, Callable, List, Optional\n"
  },
  {
    "path": "tomate/ui/widgets/__init__.py",
    "chars": 298,
    "preview": "from .countdown import Countdown\nfrom .headerbar import HeaderBar\nfrom .headerbar import Menu as HeaderBarMenu\nfrom .mod"
  },
  {
    "path": "tomate/ui/widgets/countdown.py",
    "chars": 1042,
    "preview": "import logging\nfrom typing import Union\n\nfrom gi.repository import Gtk\nfrom wiring import SingletonScope, inject\nfrom wi"
  },
  {
    "path": "tomate/ui/widgets/headerbar.py",
    "chars": 5042,
    "preview": "import locale\nimport logging\nfrom locale import gettext as _\n\nfrom gi.repository import Gtk\nfrom wiring import Singleton"
  },
  {
    "path": "tomate/ui/widgets/mode_button.py",
    "chars": 1642,
    "preview": "from typing import Any, Dict\n\nfrom gi.repository import GObject, Gtk\n\n\nclass ModeButtonItem(Gtk.ToggleButton):\n    def _"
  },
  {
    "path": "tomate/ui/widgets/session_button.py",
    "chars": 3228,
    "preview": "import locale\nimport logging\nfrom locale import gettext as _\nfrom typing import Callable\n\nfrom wiring import SingletonSc"
  },
  {
    "path": "tomate/ui/window.py",
    "chars": 3006,
    "preview": "import logging\nimport time\n\nfrom gi.repository import GdkPixbuf, Gtk\nfrom wiring import Graph, SingletonScope, inject\nfr"
  }
]

// ... and 4 more files (download for full content)

About this extraction

This page contains the full source code of the eliostvs/tomate-gtk GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 94 files (211.0 KB), approximately 51.9k tokens, and a symbol index with 545 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!