Skip to content

Commit

Permalink
Merge pull request #48 from ActivityWatch/dev/pynput
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed May 23, 2021
2 parents 9fc0e78 + f39dfa1 commit d7931eb
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 268 deletions.
22 changes: 12 additions & 10 deletions .github/workflows/build.yml
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-18.04, windows-latest, macOS-latest]
python_version: [3.7, 3.8]
python_version: [3.7, 3.9]
steps:
- uses: actions/checkout@v2
with:
Expand All @@ -26,8 +26,7 @@ jobs:
- name: Create virtualenv
shell: bash
run: |
pip install virtualenv
python -m virtualenv venv
python -m venv venv
- name: Install dependencies
shell: bash
run: |
Expand All @@ -43,11 +42,14 @@ jobs:
shell: bash
run: |
source venv/bin/activate || source venv/Scripts/activate
pip install pyinstaller==4.1
pip install pyinstaller==4.3
make package
#- name: Upload packages
# uses: actions/upload-artifact@v2-preview
# with:
# name: builds-${{ runner.os }}
# path: dist/activitywatch-*.*

- name: Test package
shell: bash
run: |
dist/aw-watcher-afk/aw-watcher-afk --help
- name: Upload package
uses: actions/upload-artifact@v2
with:
name: aw-watcher-afk-${{ runner.os }}-py${{ matrix.python_version }}
path: dist/aw-watcher-afk
4 changes: 2 additions & 2 deletions Makefile
Expand Up @@ -4,11 +4,11 @@ build:
poetry install

test:
aw-watcher-afk --help # Ensures that it at least starts
poetry run aw-watcher-afk --help # Ensures that it at least starts
make typecheck

typecheck:
python -m mypy aw_watcher_afk --ignore-missing-imports
poetry run mypy aw_watcher_afk --ignore-missing-imports

package:
pyinstaller aw-watcher-afk.spec --clean --noconfirm
Expand Down
139 changes: 74 additions & 65 deletions aw_watcher_afk/listeners.py
@@ -1,100 +1,109 @@
"""
Listeners for aggregated keyboard and mouse events.
This is used for AFK detection on Linux, as well as used in aw-watcher-input to track input activity in general.
NOTE: Logging usage should be commented out before committed, for performance reasons.
"""

import logging
from datetime import datetime, timedelta
import threading
from time import sleep

from pykeyboard import PyKeyboardEvent
from pymouse import PyMouseEvent
from abc import ABCMeta, abstractmethod
from collections import defaultdict
from typing import Dict, Any

logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)


class EventFactory(metaclass=ABCMeta):
def __init__(self) -> None:
self.new_event = threading.Event()
self._reset_data()

@abstractmethod
def _reset_data(self) -> None:
self.event_data: Dict[str, Any] = {}

class EventFactory:
def next_event(self):
def next_event(self) -> dict:
"""Returns an event and prepares the internal state so that it can start to build a new event"""
raise NotImplementedError
self.new_event.clear()
data = self.event_data
# self.logger.debug(f"Event: {data}")
self._reset_data()
return data

def has_new_event(self):
raise NotImplementedError
def has_new_event(self) -> bool:
return self.new_event.is_set()


class KeyboardListener(PyKeyboardEvent, EventFactory):
class KeyboardListener(EventFactory):
def __init__(self):
PyKeyboardEvent.__init__(self)
EventFactory.__init__(self)
self.logger = logger.getChild("keyboard")
# self.logger.setLevel(logging.DEBUG)
self.new_event = threading.Event()
self._reset_data()

def start(self):
from pynput import keyboard

listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
listener.start()

def _reset_data(self):
self.event_data = {
"presses": 0
}
self.event_data = {"presses": 0}

def tap(self, keycode, character, press):
# logging.debug("Clicked keycode: {}".format(keycode))
self.logger.debug("Input received")
def on_press(self, key):
# self.logger.debug(f"Press: {key}")
self.event_data["presses"] += 1
self.new_event.set()

def escape(self, event):
# Always returns False so that listening is never stopped
return False

def next_event(self):
"""Returns an event and prepares the internal state so that it can start to build a new event"""
self.new_event.clear()
data = self.event_data
self._reset_data()
return data

def has_new_event(self):
return self.new_event.is_set()
def on_release(self, key):
# Don't count releases, only clicks
# self.logger.debug(f"Release: {key}")
pass


class MouseListener(PyMouseEvent, EventFactory):
class MouseListener(EventFactory):
def __init__(self):
PyMouseEvent.__init__(self)
EventFactory.__init__(self)
self.logger = logger.getChild("mouse")
self.logger.setLevel(logging.INFO)
self.new_event = threading.Event()
self.pos = None
self._reset_data()

def _reset_data(self):
self.event_data = {
"clicks": 0,
"deltaX": 0,
"deltaY": 0
}

def click(self, x, y, button, press):
# TODO: Differentiate between leftclick and rightclick?
if press:
self.logger.debug("Clicked mousebutton")
self.event_data["clicks"] += 1
self.new_event.set()
self.event_data = defaultdict(int)
self.event_data.update(
{"clicks": 0, "deltaX": 0, "deltaY": 0, "scrollX": 0, "scrollY": 0}
)

def move(self, x, y):
def start(self):
from pynput import mouse

listener = mouse.Listener(
on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll
)
listener.start()

def on_move(self, x, y):
newpos = (x, y)
#self.logger.debug("Moved mouse to: {},{}".format(x, y))
# self.logger.debug("Moved mouse to: {},{}".format(x, y))
if not self.pos:
self.pos = newpos

delta = tuple(abs(self.pos[i] - newpos[i]) for i in range(2))
self.event_data["deltaX"] += delta[0]
self.event_data["deltaY"] += delta[1]
delta = tuple(self.pos[i] - newpos[i] for i in range(2))
self.event_data["deltaX"] += abs(delta[0])
self.event_data["deltaY"] += abs(delta[1])

self.pos = newpos
self.new_event.set()

def has_new_event(self):
answer = self.new_event.is_set()
self.new_event.clear()
return answer
def on_click(self, x, y, button, down):
# self.logger.debug(f"Click: {button} at {(x, y)}")
# Only count presses, not releases
if down:
self.event_data["clicks"] += 1
self.new_event.set()

def next_event(self):
self.new_event.clear()
data = self.event_data
self._reset_data()
return data
def on_scroll(self, x, y, scrollx, scrolly):
# self.logger.debug(f"Scroll: {scrollx}, {scrolly} at {(x, y)}")
self.event_data["scrollX"] += abs(scrollx)
self.event_data["scrollY"] += abs(scrolly)
self.new_event.set()
8 changes: 3 additions & 5 deletions aw_watcher_afk/unix.py
@@ -1,12 +1,8 @@
import logging
from datetime import datetime, timedelta, timezone
from datetime import datetime

from .listeners import KeyboardListener, MouseListener

# Silences annoying "Unable to determine character".
# See: https://github.com/ActivityWatch/activitywatch/issues/87
logging.getLogger("pykeyboard.x11").setLevel(logging.WARN)


class LastInputUnix:
def __init__(self):
Expand All @@ -33,6 +29,7 @@ def seconds_since_last_input(self) -> float:
keyboard_event = self.keyboardListener.next_event()
return (now - self.last_activity).total_seconds()


_last_input_unix = None


Expand All @@ -47,6 +44,7 @@ def seconds_since_last_input():

if __name__ == "__main__":
from time import sleep

while True:
sleep(1)
print(seconds_since_last_input())

0 comments on commit d7931eb

Please sign in to comment.