Skip to content

Commit

Permalink
sleep - wire up. Tests TBD
Browse files Browse the repository at this point in the history
  • Loading branch information
IGalat committed Nov 23, 2024
1 parent ec9297f commit 85bead2
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 43 deletions.
11 changes: 10 additions & 1 deletion src/tapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from tapper.controller.send_processor import (
SendCommandProcessor as _SendCommandProcessor,
)
from tapper.controller.sleep_processor import (
SleepCommandProcessor as _SleepCommandProcessor,
)
from tapper.controller.window.window_api import WindowController as _WindowController
from tapper.model import tap_tree as _tap_tree
from tapper.signal.base_listener import SignalListener as _SignalListener
Expand All @@ -15,6 +18,7 @@
_listeners: list[_SignalListener]

_send_processor = _SendCommandProcessor.from_none()
_sleep_processor = _SleepCommandProcessor.from_none()
_initialized = False
_blocking = True

Expand Down Expand Up @@ -44,6 +48,9 @@
send = _send_processor.send
"""A versatile command, allows sending many instructions in one str."""

sleep = _sleep_processor.sleep
"""Use this instead of time.sleep to be able to pause and kill running actions."""

if _kbc := _datastructs.get_first_in(_KeyboardController, config.controllers):
"""Keyboard controller. Mainly useful for getting the state of keys, send is recommended for typing."""
kb: _KeyboardController = _kbc
Expand All @@ -62,7 +69,9 @@ def init() -> None:
global _listeners
global _initialized
_initializer.set_default_controls_if_empty(control_group)
_listeners = _initializer.init(root, control_group, _send_processor, send)
_listeners = _initializer.init(
root, control_group, _send_processor, _sleep_processor
)
_initialized = True


Expand Down
12 changes: 7 additions & 5 deletions src/tapper/action/wrapper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import threading
from dataclasses import dataclass
from uuid import UUID

from tapper.controller import flow_control
from tapper.controller.flow_control import config_thread_local_storage
from tapper.model import types_
from tapper.model.tap_tree_shadow import STap

Expand All @@ -11,17 +13,17 @@ class ActionConfig:

send_interval: float
send_press_duration: float
kill_id: UUID

@classmethod
def from_tap(cls, tap: STap) -> "ActionConfig":
return ActionConfig(
send_interval=tap.send_interval, send_press_duration=tap.send_press_duration
send_interval=tap.send_interval,
send_press_duration=tap.send_press_duration,
kill_id=flow_control.kill_id,
)


config_thread_local_storage = threading.local()


def wrapped_action(tap: STap) -> types_.Action:
"""
:param tap: source.
Expand Down
13 changes: 9 additions & 4 deletions src/tapper/boot/initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from tapper.action.runner import ActionRunnerImpl
from tapper.boot import tray_icon
from tapper.boot.tree_transformer import TreeTransformer
from tapper.controller import flow_control
from tapper.controller.keyboard.kb_api import KeyboardController
from tapper.controller.mouse.mouse_api import MouseController
from tapper.controller.send_processor import SendCommandProcessor
from tapper.controller.sleep_processor import SleepCommandProcessor
from tapper.controller.window.window_api import WindowController
from tapper.helper import controls
from tapper.model import constants
Expand All @@ -18,7 +20,6 @@
from tapper.model.send import SleepInstruction
from tapper.model.send import WheelInstruction
from tapper.model.tap_tree import Group
from tapper.model.types_ import SendFn
from tapper.parser.send_parser import SendParser
from tapper.parser.trigger_parser import TriggerParser
from tapper.signal.base_listener import SignalListener
Expand Down Expand Up @@ -84,14 +85,14 @@ def init(
iroot: Group,
icontrol: Group,
send_processor: SendCommandProcessor,
send: SendFn,
sleep_processor: SleepCommandProcessor,
) -> list[SignalListener]:
"""Initialize components with config values."""
global keeper_pressed
os = config.os

transformer = TreeTransformer(
send, default_trigger_parser(os), config.kw_trigger_conditions
send_processor.send, default_trigger_parser(os), config.kw_trigger_conditions
)
verify_settings_exist(iroot)
root = transformer.transform(iroot)
Expand Down Expand Up @@ -126,11 +127,15 @@ def init(
wc._only_visible_windows = config.only_visible_windows
[c._init() for c in controllers]

sleep_processor.check_interval = config.sleep_check_interval
sleep_processor.kill_check_fn = flow_control.should_be_killed
sleep_processor.pause_check_fn = flow_control.should_be_paused

send_processor.os = os
send_processor.parser = default_send_parser(os)
send_processor.kb_controller = kbc # type: ignore
send_processor.mouse_controller = mc # type: ignore
send_processor.default_interval = lambda: config.default_send_interval # type: ignore
send_processor.sleep_fn = sleep_processor.sleep

return listeners

Expand Down
3 changes: 3 additions & 0 deletions src/tapper/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
"""Limit windows to visible - ones that are open on the taskbar.
Reduces WindowController lag, and junk windows caught into filters."""

sleep_check_interval = 0.1
"""How often tapper.sleep checks for pause/kill."""

"""
------------------------------------
SECTION 3: Extending the functionality.
Expand Down
33 changes: 33 additions & 0 deletions src/tapper/controller/flow_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import threading
import uuid


config_thread_local_storage = threading.local()
"""Used to store config for running actions."""


class StopTapperActionException(Exception):
"""Normal way to interrupt tapper's action. Will not cause error logs etc."""


paused = False
"""When this is True, running actions will wait unpause when they call tapper.sleep."""


kill_id = uuid.uuid4()
"""When this is changed, all running actions will be killed
with StopTapperActionException when they call tapper.sleep."""


def should_be_paused() -> bool:
return paused


def should_be_killed() -> bool:
config = config_thread_local_storage.action_config
if config is None:
raise ValueError(
"No config for action. "
"Did you initialize tapper and are you running this inside an action?"
)
return config.kill_id != kill_id
10 changes: 5 additions & 5 deletions src/tapper/controller/send_processor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import time
from typing import Callable

from tapper.action.wrapper import config_thread_local_storage
from tapper.controller.flow_control import config_thread_local_storage
from tapper.controller.keyboard.kb_api import KeyboardController
from tapper.controller.mouse.mouse_api import MouseController
from tapper.model import constants
Expand All @@ -23,25 +22,26 @@ class SendCommandProcessor:
parser: SendParser
kb_controller: KeyboardController
mouse_controller: MouseController
sleep_fn: Callable[[float], None] = time.sleep
default_interval: Callable[[], float]
sleep_fn: Callable[[float], None]

def __init__(
self,
os: str,
parser: SendParser,
kb_controller: KeyboardController,
mouse_controller: MouseController,
sleep_fn: Callable[[float], None],
) -> None:
self.os = os
self.parser = parser
self.kb_controller = kb_controller
self.mouse_controller = mouse_controller
self.sleep_fn = sleep_fn

@classmethod
def from_none(cls) -> "SendCommandProcessor":
"""To be filled during init."""
return SendCommandProcessor(None, None, None, None) # type: ignore
return SendCommandProcessor(None, None, None, None, None) # type: ignore

def send(
self,
Expand Down
14 changes: 8 additions & 6 deletions src/tapper/controller/sleep_processor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import time
from dataclasses import dataclass
from typing import Callable

from attr import dataclass
from tapper.controller.flow_control import StopTapperActionException
from tapper.parser import common


class StopTapperActionException(Exception):
"""Normal way to interrupt tapper's action. Will not cause error logs etc."""


def parse_sleep_time(length_of_time: float | str) -> float:
if isinstance(length_of_time, str):
sleep_time = common.parse_sleep_time(length_of_time)
Expand Down Expand Up @@ -38,8 +35,13 @@ class SleepCommandProcessor:
actual_sleep_fn: Callable[[float], None] = time.sleep
get_time_fn: Callable[[], float] = time.perf_counter

@classmethod
def from_none(cls) -> "SleepCommandProcessor":
"""To be filled during init."""
return SleepCommandProcessor(None, None, None) # type: ignore

def __post_init__(self) -> None:
if self.check_interval <= 0:
if self.check_interval is not None and self.check_interval <= 0:
raise ValueError(
"SleepCommandProcessor check_interval must be greater than 0."
)
Expand Down
4 changes: 3 additions & 1 deletion src/tapper/helper/_util/repeat_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import tapper
from tapper.action.wrapper import ActionConfig
from tapper.action.wrapper import config_thread_local_storage
from tapper.boot import initializer
from tapper.controller import flow_control
from tapper.controller.flow_control import config_thread_local_storage
from tapper.helper.model import Repeatable

TIME_SPLIT = 0.1
Expand Down Expand Up @@ -34,6 +35,7 @@ def _run_task(repeatable: Repeatable) -> None:
config_thread_local_storage.action_config = ActionConfig(
send_interval=tapper.root.send_interval, # type: ignore
send_press_duration=tapper.root.send_press_duration, # type: ignore
kill_id=flow_control.kill_id,
)
for i in range(repeatable.max_repeats or 99999999999999):
if is_end_run(repeatable):
Expand Down
27 changes: 27 additions & 0 deletions src/tapper/helper/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import signal
import subprocess
import sys
import uuid

from tapper.controller import flow_control as _flow_control


def restart() -> None:
Expand All @@ -23,3 +26,27 @@ def terminate() -> None:
def _terminate() -> None:
atexit._run_exitfuncs() # run all @atexit functions
os.kill(os.getpid(), signal.SIGINT)


def pause_actions_on() -> None:
"""Puts actions (current and future) on pause, until turned off.
Note: will only pause when `tapper.sleep` is used inside the action,
directly or via other tapper functions."""
_flow_control.paused = True


def pause_actions_off() -> None:
"""Turns off pause. Does nothing if pause is already off."""
_flow_control.paused = False


def pause_actions_toggle() -> None:
"""Turns on pause if not paused, turns pause off if it's on."""
_flow_control.paused = not _flow_control.paused


def kill_running_actions() -> None:
"""Kills currently running actions.
Note: will only kill when `tapper.sleep` is used inside the action,
directly or via other tapper functions."""
_flow_control.kill_id = uuid.uuid4()
28 changes: 14 additions & 14 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def f(dummy: Dummy, is_debug: bool, make_group: Callable) -> Fixture:
tapper.root = make_group("root")
tapper.control_group = make_group("control_group")
tapper._initialized = False
tapper.action.wrapper.config_thread_local_storage.action_config = (
wrapper.ActionConfig(0, 0)
tapper.controller.flow_control.config_thread_local_storage.action_config = (
wrapper.ActionConfig(0, 0, tapper.controller.flow_control.kill_id)
)

listener = dummy.Listener.get_for_os("")
Expand All @@ -92,25 +92,25 @@ def f(dummy: Dummy, is_debug: bool, make_group: Callable) -> Fixture:
wtc = dummy.WinTC([])
tapper.window._tracker, tapper.window._commander = wtc, wtc

def sleep_logged(time_s: float, signals: list[Signal]) -> None:
signals.append(sleep_signal(time_s))
time.sleep(time_s)
def start() -> None:
tapper.init() # this will init sender

sender = tapper._send_processor
sender.sleep_fn = partial(sleep_logged, signals=fixture.emul_signals)
def sleep_logged(time_s: float, signals: list[Signal]) -> None:
signals.append(sleep_signal(time_s))
time.sleep(time_s)

def send_and_sleep(send: SendFn, command: str) -> None:
send(command)
time.sleep(0.01 * debug_sleep_mult) # so actions can run their course
def send_and_sleep(send: SendFn, command: str) -> None:
send(command)
time.sleep(0.01 * debug_sleep_mult) # so actions can run their course

real_sender = SendCommandProcessor.from_none()
fixture.send_real = partial(send_and_sleep, real_sender.send)
sender = tapper._send_processor
sender.sleep_fn = partial(sleep_logged, signals=fixture.emul_signals)

def start() -> None:
tapper.init() # this will init sender
real_sender = SendCommandProcessor.from_none()
real_sender.os = sender.os
real_sender.parser = sender.parser
real_sender.sleep_fn = partial(sleep_logged, signals=fixture.real_signals)
fixture.send_real = partial(send_and_sleep, real_sender.send)

emul_keeper = keeper.Emul()

Expand Down
Loading

0 comments on commit 85bead2

Please sign in to comment.