Skip to content

Commit d7931eb

Browse files
authored
Merge pull request #48 from ActivityWatch/dev/pynput
2 parents 9fc0e78 + f39dfa1 commit d7931eb

File tree

6 files changed

+247
-268
lines changed

6 files changed

+247
-268
lines changed

.github/workflows/build.yml

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fail-fast: false
1515
matrix:
1616
os: [ubuntu-18.04, windows-latest, macOS-latest]
17-
python_version: [3.7, 3.8]
17+
python_version: [3.7, 3.9]
1818
steps:
1919
- uses: actions/checkout@v2
2020
with:
@@ -26,8 +26,7 @@ jobs:
2626
- name: Create virtualenv
2727
shell: bash
2828
run: |
29-
pip install virtualenv
30-
python -m virtualenv venv
29+
python -m venv venv
3130
- name: Install dependencies
3231
shell: bash
3332
run: |
@@ -43,11 +42,14 @@ jobs:
4342
shell: bash
4443
run: |
4544
source venv/bin/activate || source venv/Scripts/activate
46-
pip install pyinstaller==4.1
45+
pip install pyinstaller==4.3
4746
make package
48-
#- name: Upload packages
49-
# uses: actions/upload-artifact@v2-preview
50-
# with:
51-
# name: builds-${{ runner.os }}
52-
# path: dist/activitywatch-*.*
53-
47+
- name: Test package
48+
shell: bash
49+
run: |
50+
dist/aw-watcher-afk/aw-watcher-afk --help
51+
- name: Upload package
52+
uses: actions/upload-artifact@v2
53+
with:
54+
name: aw-watcher-afk-${{ runner.os }}-py${{ matrix.python_version }}
55+
path: dist/aw-watcher-afk

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ build:
44
poetry install
55

66
test:
7-
aw-watcher-afk --help # Ensures that it at least starts
7+
poetry run aw-watcher-afk --help # Ensures that it at least starts
88
make typecheck
99

1010
typecheck:
11-
python -m mypy aw_watcher_afk --ignore-missing-imports
11+
poetry run mypy aw_watcher_afk --ignore-missing-imports
1212

1313
package:
1414
pyinstaller aw-watcher-afk.spec --clean --noconfirm

aw_watcher_afk/listeners.py

Lines changed: 74 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,109 @@
1+
"""
2+
Listeners for aggregated keyboard and mouse events.
3+
4+
This is used for AFK detection on Linux, as well as used in aw-watcher-input to track input activity in general.
5+
6+
NOTE: Logging usage should be commented out before committed, for performance reasons.
7+
"""
8+
19
import logging
2-
from datetime import datetime, timedelta
310
import threading
4-
from time import sleep
5-
6-
from pykeyboard import PyKeyboardEvent
7-
from pymouse import PyMouseEvent
11+
from abc import ABCMeta, abstractmethod
12+
from collections import defaultdict
13+
from typing import Dict, Any
814

915
logger = logging.getLogger(__name__)
16+
# logger.setLevel(logging.DEBUG)
17+
1018

19+
class EventFactory(metaclass=ABCMeta):
20+
def __init__(self) -> None:
21+
self.new_event = threading.Event()
22+
self._reset_data()
23+
24+
@abstractmethod
25+
def _reset_data(self) -> None:
26+
self.event_data: Dict[str, Any] = {}
1127

12-
class EventFactory:
13-
def next_event(self):
28+
def next_event(self) -> dict:
1429
"""Returns an event and prepares the internal state so that it can start to build a new event"""
15-
raise NotImplementedError
30+
self.new_event.clear()
31+
data = self.event_data
32+
# self.logger.debug(f"Event: {data}")
33+
self._reset_data()
34+
return data
1635

17-
def has_new_event(self):
18-
raise NotImplementedError
36+
def has_new_event(self) -> bool:
37+
return self.new_event.is_set()
1938

2039

21-
class KeyboardListener(PyKeyboardEvent, EventFactory):
40+
class KeyboardListener(EventFactory):
2241
def __init__(self):
23-
PyKeyboardEvent.__init__(self)
42+
EventFactory.__init__(self)
2443
self.logger = logger.getChild("keyboard")
25-
# self.logger.setLevel(logging.DEBUG)
26-
self.new_event = threading.Event()
27-
self._reset_data()
44+
45+
def start(self):
46+
from pynput import keyboard
47+
48+
listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
49+
listener.start()
2850

2951
def _reset_data(self):
30-
self.event_data = {
31-
"presses": 0
32-
}
52+
self.event_data = {"presses": 0}
3353

34-
def tap(self, keycode, character, press):
35-
# logging.debug("Clicked keycode: {}".format(keycode))
36-
self.logger.debug("Input received")
54+
def on_press(self, key):
55+
# self.logger.debug(f"Press: {key}")
3756
self.event_data["presses"] += 1
3857
self.new_event.set()
3958

40-
def escape(self, event):
41-
# Always returns False so that listening is never stopped
42-
return False
43-
44-
def next_event(self):
45-
"""Returns an event and prepares the internal state so that it can start to build a new event"""
46-
self.new_event.clear()
47-
data = self.event_data
48-
self._reset_data()
49-
return data
50-
51-
def has_new_event(self):
52-
return self.new_event.is_set()
59+
def on_release(self, key):
60+
# Don't count releases, only clicks
61+
# self.logger.debug(f"Release: {key}")
62+
pass
5363

5464

55-
class MouseListener(PyMouseEvent, EventFactory):
65+
class MouseListener(EventFactory):
5666
def __init__(self):
57-
PyMouseEvent.__init__(self)
67+
EventFactory.__init__(self)
5868
self.logger = logger.getChild("mouse")
59-
self.logger.setLevel(logging.INFO)
60-
self.new_event = threading.Event()
6169
self.pos = None
62-
self._reset_data()
6370

6471
def _reset_data(self):
65-
self.event_data = {
66-
"clicks": 0,
67-
"deltaX": 0,
68-
"deltaY": 0
69-
}
70-
71-
def click(self, x, y, button, press):
72-
# TODO: Differentiate between leftclick and rightclick?
73-
if press:
74-
self.logger.debug("Clicked mousebutton")
75-
self.event_data["clicks"] += 1
76-
self.new_event.set()
72+
self.event_data = defaultdict(int)
73+
self.event_data.update(
74+
{"clicks": 0, "deltaX": 0, "deltaY": 0, "scrollX": 0, "scrollY": 0}
75+
)
7776

78-
def move(self, x, y):
77+
def start(self):
78+
from pynput import mouse
79+
80+
listener = mouse.Listener(
81+
on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll
82+
)
83+
listener.start()
84+
85+
def on_move(self, x, y):
7986
newpos = (x, y)
80-
#self.logger.debug("Moved mouse to: {},{}".format(x, y))
87+
# self.logger.debug("Moved mouse to: {},{}".format(x, y))
8188
if not self.pos:
8289
self.pos = newpos
8390

84-
delta = tuple(abs(self.pos[i] - newpos[i]) for i in range(2))
85-
self.event_data["deltaX"] += delta[0]
86-
self.event_data["deltaY"] += delta[1]
91+
delta = tuple(self.pos[i] - newpos[i] for i in range(2))
92+
self.event_data["deltaX"] += abs(delta[0])
93+
self.event_data["deltaY"] += abs(delta[1])
8794

8895
self.pos = newpos
8996
self.new_event.set()
9097

91-
def has_new_event(self):
92-
answer = self.new_event.is_set()
93-
self.new_event.clear()
94-
return answer
98+
def on_click(self, x, y, button, down):
99+
# self.logger.debug(f"Click: {button} at {(x, y)}")
100+
# Only count presses, not releases
101+
if down:
102+
self.event_data["clicks"] += 1
103+
self.new_event.set()
95104

96-
def next_event(self):
97-
self.new_event.clear()
98-
data = self.event_data
99-
self._reset_data()
100-
return data
105+
def on_scroll(self, x, y, scrollx, scrolly):
106+
# self.logger.debug(f"Scroll: {scrollx}, {scrolly} at {(x, y)}")
107+
self.event_data["scrollX"] += abs(scrollx)
108+
self.event_data["scrollY"] += abs(scrolly)
109+
self.new_event.set()

aw_watcher_afk/unix.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import logging
2-
from datetime import datetime, timedelta, timezone
2+
from datetime import datetime
33

44
from .listeners import KeyboardListener, MouseListener
55

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

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

32+
3633
_last_input_unix = None
3734

3835

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

4845
if __name__ == "__main__":
4946
from time import sleep
47+
5048
while True:
5149
sleep(1)
5250
print(seconds_since_last_input())

0 commit comments

Comments
 (0)