Events

With the event-driven API, the user’s code can respond to events on the system. For example, when a window appears, code to resize it can run automatically.

Listening

Each user function requires registration with a function from the listen_to module to listen to a range of events called WinEvents . We call the user’s function with a single argument providing an EventData dataclass containing information about the event and the window. The function can then do stuff with the data we provide it.

from systa.events.store import callback_store
from systa.events.decorators import listen_to
from systa.events.types import EventData


@listen_to.any_event # often not a good idea, see next example
def user_function(data: EventData) -> None:
    if data.window and "Notepad" in data.window.title:
        print(f"It's a Notepad event! ({data.event_info.event_name})")

callback_store.run(.5)

Ideally, you won’t listen to every event. There are a lot of events fired on a modern Windows system, and it consumes OS resources to call your functions. Luckily, we can listen to only the events we care about.

from systa.events.store import callback_store
from systa.events.decorators import listen_to
from systa.events.types import EventData
from systa.events.constants import win_events

# Only listens to location-changed events
@listen_to.location_change
def user_function(data: EventData) -> None:
    name = data.event_info.event
    if data.window and "Notepad" in data.window.title:
        print(f"Notepad moved!")

callback_store.run(.5)

Note

There are many more event decorators you can use in the listen_to module.

Other events

If you know what you’re doing you can use the the specified_events() decorator to specify the exact events you want to listen to.

import requests
from systa.events.constants import win_events
from systa.events.decorators import listen_to
from systa.events.store import callback_store


@listen_to.specified_events(
    (win_events.EVENT_OBJECT_CONTENTSCROLLED, win_events.EVENT_OBJECT_FOCUS)
)
def the_user_func(event_data):
    """POST the window title  every time content is scrolled or an object receives focus."""
    requests.post("http://myservice/events", data={"window": event_data.window.title})


callback_store.run()

Warning

listen_to decorators should always be specified before filter_by decorators.

Filtering

Listening to specific events will probably still give us too many events. For example, you might just be interested in running your code when Notepad is moved to a new location. However, Windows will call your code whenever any window is moved.

One option to handle this is branching in your function as in the above examples wherein we check if the window title has the word “Notepad”.

Or, you can get fancy and use some decorators from filter_by:

Ignore events that aren’t for a specific window

from systa.events.store import callback_store
from systa.events.decorators import filter_by, listen_to
from systa.events.types import EventData

@filter_by.require_title("Untitled - Notepad")
@listen_to.location_change
def notepad_moved(data: EventData) -> None:
  print("Notepad moved!")

callback_store.run(.6)

Note

The above is equivalent to the code above where we check if Notepad moved.

Combine as many filters as you want

from systa.events.store import callback_store
from systa.events.decorators import filter_by, listen_to
from systa.events.types import EventData
from systa.types import Point, Rect

origin = Point(100, 100)
end = Point(500, 500)

@filter_by.require_origin_within(Rect(origin, end))
@filter_by.require_title("Untitled - Notepad")
@listen_to.location_change
def notepad_moved(data: EventData) -> None:
  print(f"Notepad moved to {data.window.position}!")

callback_store.run(.6)

Warning

If your filters aren’t behaving as you expect, remember that decorators are evaluated from the bottom up and the first one that doesn’t pass prevents the rest of them from running. In other words, all filters must pass for your code to be called. You can use the any_filter decorator to change this behavior.

Combine filters with any_filter

Combine filters with the any_filter() decorator to make it so that any single filter passing will run your function.

from systa.events.decorators import filter_by, listen_to
from systa.events.store import callback_store
from systa.events.types import EventData

@filter_by.any_filter(
    filter_by.require_title("*Notepad"),
    filter_by.require_size_is_less_than(200, 200),
)
@listen_to.move_or_sizing_ended
def some_func(event_data: EventData):
    print('Notepad resized or small window moved.')

callback_store.run(1.6)

My god, it’s full of decorators

If you have a lot of filtering or events to capture, your code can get pretty ugly and hard to reason about as the decorators stack up. Decorators only aid readability to a point, then they can begin to hurt readability.

Some potential solutions follow.

When you have just a few events to listen to

When you have a lot of filtering, but just one or a few events you can move the filtering into your own code.

from systa.events.decorators import listen_to
from systa.events.types import EventData

@listen_to.capture_mouse
@listen_to.location_change
def my_func(data: EventData):
  if not data.window:
    return

  if "Chrome" in data.window.title:
    # do stuff in here
    pass
  elif data.window.active and data.window.classname == "MozillaWindowClass":
    # do something else here
    pass
  # do whatever you want here

Combining filter_by decorators

Combine multiple filters into one with all_filters() or any_filter() and use the resulting decorator in multiple places.

from systa.events.decorators import filter_by, listen_to
from systa.events.types import EventData

import requests

small_editor_on_right_monitor = filter_by.all_filters(
    # If Notepad _or_ Word...
    filter_by.any_filter(
        filter_by.require_title("*Notepad"), filter_by.require_title("*Word")
    ),
    # are less than 200x200
    filter_by.require_size_is_less_than(200, 200),

    # _and_ are on monitor 3
    filter_by.touches_monitors(3, exclusive=True),
)


@small_editor_on_right_monitor
@listen_to.location_change
def make_tall_editor(event_data: EventData):
    event_data.window.height = 1000


@small_editor_on_right_monitor
@listen_to.location_change
def log_small_editor(event_data: EventData):
    requests.post("https://MY_LOGGING_SERVICE/a_small_editor")

Combining multiple listen_to decorators

You can also combine multiple listen_to decorators with systa.utils.composed().

from systa.events.decorators import filter_by, listen_to
from systa.events.store import callback_store
from systa.events.types import EventData
from systa.utils import composed

our_listener = composed(listen_to.location_change, listen_to.restore)

@filter_by.require_title("Untitled - Notepad")
@our_listener
def notepad_moved(data: EventData) -> None:
  print("Notepad moved!")

callback_store.run(.6)