Skip to content

Commit a96b8b3

Browse files
committed
Initial implementation of the wpm meter
0 parents  commit a96b8b3

9 files changed

+330
-0
lines changed

.gitignore

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
2+
# Created by https://www.gitignore.io/api/python
3+
4+
### Python ###
5+
# Byte-compiled / optimized / DLL files
6+
__pycache__/
7+
*.py[cod]
8+
*$py.class
9+
10+
# C extensions
11+
*.so
12+
13+
# Distribution / packaging
14+
.Python
15+
env/
16+
build/
17+
develop-eggs/
18+
dist/
19+
downloads/
20+
eggs/
21+
.eggs/
22+
lib/
23+
lib64/
24+
parts/
25+
sdist/
26+
var/
27+
wheels/
28+
*.egg-info/
29+
.installed.cfg
30+
*.egg
31+
32+
# PyInstaller
33+
# Usually these files are written by a python script from a template
34+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
35+
*.manifest
36+
*.spec
37+
38+
# Installer logs
39+
pip-log.txt
40+
pip-delete-this-directory.txt
41+
42+
# Unit test / coverage reports
43+
htmlcov/
44+
.tox/
45+
.coverage
46+
.coverage.*
47+
.cache
48+
nosetests.xml
49+
coverage.xml
50+
*,cover
51+
.hypothesis/
52+
53+
# Translations
54+
*.mo
55+
*.pot
56+
57+
# Django stuff:
58+
*.log
59+
local_settings.py
60+
61+
# Flask stuff:
62+
instance/
63+
.webassets-cache
64+
65+
# Scrapy stuff:
66+
.scrapy
67+
68+
# Sphinx documentation
69+
docs/_build/
70+
71+
# PyBuilder
72+
target/
73+
74+
# Jupyter Notebook
75+
.ipynb_checkpoints
76+
77+
# pyenv
78+
.python-version
79+
80+
# celery beat schedule file
81+
celerybeat-schedule
82+
83+
# dotenv
84+
.env
85+
86+
# virtualenv
87+
.venv
88+
venv/
89+
ENV/
90+
91+
# Spyder project settings
92+
.spyderproject
93+
94+
# Rope project settings
95+
.ropeproject
96+
97+
# End of https://www.gitignore.io/api/python

plover_wpm_meter/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*_ui.py

plover_wpm_meter/__init__.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import time
2+
3+
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
4+
5+
from plover.gui_qt.tool import Tool
6+
from textstat.textstat import textstat
7+
8+
from plover_wpm_meter.wpm_meter_ui import Ui_WpmMeter
9+
10+
_NUM_SYLLABLES_PER_WORD = 1.44
11+
12+
13+
class PloverWpmMeter(Tool, Ui_WpmMeter):
14+
TITLE = "WPM Meter"
15+
16+
_TIMEOUTS = {
17+
"wpm1": 10,
18+
"wpm2": 60,
19+
}
20+
21+
def __init__(self, engine):
22+
super().__init__(engine)
23+
self.setupUi(self)
24+
25+
self._timer = QTimer()
26+
self._timer.setInterval(100)
27+
self._timer.setTimerType(Qt.PreciseTimer)
28+
self._timer.timeout.connect(self._update_wpms)
29+
self._timer.start()
30+
31+
self._chars = []
32+
engine.signal_connect("translated", self._on_translation)
33+
34+
def _on_translation(self, old, new):
35+
for action in old:
36+
remove = len(action.text)
37+
if remove > 0:
38+
self._chars = self._chars[:-remove]
39+
self._chars += _timestamp_chars(action.replace)
40+
41+
for action in new:
42+
remove = len(action.replace)
43+
if remove > 0:
44+
self._chars = self._chars[:-remove]
45+
self._chars += _timestamp_chars(action.text)
46+
47+
self._update_wpms()
48+
49+
@pyqtSlot()
50+
def _update_wpms(self):
51+
max_timeout = max(self._TIMEOUTS.values())
52+
self._chars = _filter_old_chars(self._chars, max_timeout)
53+
for name, timeout in self._TIMEOUTS.items():
54+
chars = _filter_old_chars(self._chars, timeout)
55+
wpm = _wpm_of_chars(chars, timeout)
56+
getattr(self, name).display(str(wpm))
57+
58+
59+
def _timestamp_chars(chars):
60+
current_time = time.time()
61+
return [(c, current_time) for c in chars]
62+
63+
64+
def _filter_old_chars(chars, timeout):
65+
current_time = time.time()
66+
return [(c, t) for c, t in chars
67+
if (current_time - t) <= timeout]
68+
69+
70+
def _wpm_of_chars(chars, timeout):
71+
text = "".join(c for c, _ in chars)
72+
num_words = textstat.syllable_count(text) / _NUM_SYLLABLES_PER_WORD
73+
num_minutes = timeout / 60
74+
num_words_per_minute = num_words / num_minutes
75+
return int(round(num_words_per_minute))

plover_wpm_meter/wpm_meter.ui

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>WpmMeter</class>
4+
<widget class="QDialog" name="WpmMeter">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>182</width>
10+
<height>120</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>WPM Meter</string>
15+
</property>
16+
<widget class="QWidget" name="centralWidget" native="true">
17+
<property name="geometry">
18+
<rect>
19+
<x>30</x>
20+
<y>0</y>
21+
<width>100</width>
22+
<height>30</height>
23+
</rect>
24+
</property>
25+
</widget>
26+
<widget class="QLCDNumber" name="wpm1">
27+
<property name="geometry">
28+
<rect>
29+
<x>0</x>
30+
<y>0</y>
31+
<width>121</width>
32+
<height>61</height>
33+
</rect>
34+
</property>
35+
<property name="segmentStyle">
36+
<enum>QLCDNumber::Filled</enum>
37+
</property>
38+
<property name="value" stdset="0">
39+
<double>0.000000000000000</double>
40+
</property>
41+
</widget>
42+
<widget class="QLCDNumber" name="wpm2">
43+
<property name="geometry">
44+
<rect>
45+
<x>0</x>
46+
<y>60</y>
47+
<width>121</width>
48+
<height>61</height>
49+
</rect>
50+
</property>
51+
<property name="segmentStyle">
52+
<enum>QLCDNumber::Filled</enum>
53+
</property>
54+
<property name="value" stdset="0">
55+
<double>0.000000000000000</double>
56+
</property>
57+
</widget>
58+
<widget class="QLabel" name="label">
59+
<property name="geometry">
60+
<rect>
61+
<x>130</x>
62+
<y>20</y>
63+
<width>51</width>
64+
<height>16</height>
65+
</rect>
66+
</property>
67+
<property name="text">
68+
<string>last 10s</string>
69+
</property>
70+
</widget>
71+
<widget class="QLabel" name="label_2">
72+
<property name="geometry">
73+
<rect>
74+
<x>130</x>
75+
<y>80</y>
76+
<width>51</width>
77+
<height>16</height>
78+
</rect>
79+
</property>
80+
<property name="text">
81+
<string>last 1m</string>
82+
</property>
83+
</widget>
84+
</widget>
85+
<layoutdefault spacing="6" margin="11"/>
86+
<resources/>
87+
<connections/>
88+
</ui>

pyuic.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"files": [
3+
["plover_wpm_meter/*.ui", "plover_wpm_meter"]
4+
],
5+
"hooks": [],
6+
"pyrcc": "pyrcc5",
7+
"pyrcc_options": "",
8+
"pyuic": "pyuic5",
9+
"pyuic_options": "--from-import"
10+
}

requirements-dev.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pytest==3.0.6
2+
pytest-pythonpath==0.7.1
3+
mock==2.0.0

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool:pytest]
2+
python_paths = .

setup.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from setuptools import find_packages, setup
2+
3+
try:
4+
from pyqt_distutils.build_ui import build_ui
5+
cmdclass = {"build_ui": build_ui}
6+
except ImportError:
7+
cmdclass = {}
8+
9+
setup(
10+
name="Plover: WPM meter",
11+
version="0.1",
12+
description="A meter to show your typing speed in Plover.",
13+
author="Waleed Khan",
14+
author_email="[email protected]",
15+
license="GPLv3",
16+
install_requires=[
17+
"PyQt5",
18+
"plover>=4.0.0.dev0",
19+
"textstat>=0.3.1",
20+
],
21+
packages=find_packages(),
22+
entry_points="""
23+
[plover.gui.qt.tool]
24+
wpm_meter = plover_wpm_meter:PloverWpmMeter
25+
""",
26+
cmdclass=cmdclass,
27+
)

test/test_plover_wpm_meter.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import mock
2+
3+
from plover_wpm_meter import (
4+
_NUM_SYLLABLES_PER_WORD,
5+
_timestamp_chars,
6+
_filter_old_chars,
7+
_wpm_of_chars,
8+
)
9+
10+
11+
@mock.patch("time.time")
12+
def test_timestamp_chars(time):
13+
time.return_value = 1
14+
assert _timestamp_chars("foo") == [("f", 1), ("o", 1), ("o", 1)]
15+
16+
17+
@mock.patch("time.time")
18+
def test_filter_old_chars(time):
19+
time.return_value = 20
20+
chars = [("a", 0), ("b", 10), ("c", 20)]
21+
assert _filter_old_chars(chars, 10) == [("b", 10), ("c", 20)]
22+
23+
24+
@mock.patch("plover_wpm_meter.textstat")
25+
def test_wpm_of_chars(textstat):
26+
textstat.syllable_count.return_value = _NUM_SYLLABLES_PER_WORD * 10
27+
assert _wpm_of_chars(_timestamp_chars("foo"), timeout=10) == 60

0 commit comments

Comments
 (0)