Compare commits
1 Commits
master
...
feature-bo
Author | SHA1 | Date | |
---|---|---|---|
|
20bcfcdbb9 |
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] Title"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
* Setup and configuration of the affected parts of the firmware (avoid copy-pasting the entire configuration if possible)
|
||||
* Setup and configuration of peripherals
|
||||
* Input: keys pressed, ...
|
||||
(Choose which are applicable.)
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Debug output**
|
||||
If applicable, add debug output from the serial console to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Enhancement] Title"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
11
README.md
11
README.md
@ -9,15 +9,10 @@ KMK is a feature-rich and beginner-friendly firmware for computer keyboards
|
||||
written and configured in
|
||||
[CircuitPython](https://github.com/adafruit/circuitpython).
|
||||
|
||||
## Support
|
||||
|
||||
For asynchronous support and chatter about KMK, [join our Zulip
|
||||
community](https://kmkfw.zulipchat.com)!
|
||||
|
||||
If you ask for help in chat or open a bug report, if possible
|
||||
make sure your copy of KMK is up-to-date.
|
||||
In particular, swing by the Zulip chat *before* opening a GitHub Issue about
|
||||
configuration, documentation, etc. concerns.
|
||||
community](https://kmkfw.zulipchat.com)! In particular, swing by the Zulip chat
|
||||
*before* opening a GitHub Issue about configuration, documentation, etc.
|
||||
concerns.
|
||||
|
||||
> The former Matrix and Discord rooms once linked to in this README are no
|
||||
> longer officially supported, please do not use them!
|
||||
|
@ -20,6 +20,8 @@ encoder_handler.pins = ((board.D1, board.D2, board.D0),)
|
||||
encoder_handler.map = (((KC.VOLD, KC.VOLU, KC.MUTE),),)
|
||||
knob.modules.append(encoder_handler)
|
||||
|
||||
print('ANAVI Knob 1')
|
||||
|
||||
rgb_ext = RGB(
|
||||
pixel_pin=board.NEOPIXEL,
|
||||
num_pixels=1,
|
||||
|
@ -1,98 +0,0 @@
|
||||
## Combo Layers
|
||||
|
||||
Combo Layers is when you hold down 2 or more KC.MO() or KC.LM() keys at a time, and it goes to a defined layer.
|
||||
|
||||
By default combo layers is not activated. You can activate combo layers by adding this to your `main.py` file.
|
||||
The combolayers NEEDS to be above the `keyboard.modules.append(Layers(combolayers))`
|
||||
|
||||
```python
|
||||
combo_layers = {
|
||||
(1, 2): 3,
|
||||
}
|
||||
keyboard.modules.append(Layers(combo_layers))
|
||||
```
|
||||
|
||||
In the above code, when layer 1 and 2 are held, layer 3 will activate. If you release 1 or 2 it will go to whatever key is still being held, if both are released it goes to the default (0) layer.
|
||||
You should also notice that if you already have the layers Module activated, you can just add combolayers into `(Layers())`
|
||||
|
||||
You can add more, and even add more than 2 layers at a time.
|
||||
|
||||
```python
|
||||
combo_layers = {
|
||||
(1, 2): 3,
|
||||
(1, 2, 3): 4,
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
There can only be one combo layer active at a time and for overlapping matches
|
||||
the first matching combo in `combo_layers` takes precedence.
|
||||
Example:
|
||||
```python
|
||||
layers = Layers()
|
||||
layers.combo_layers = {
|
||||
(1, 2, 3): 8,
|
||||
(1, 2): 9,
|
||||
}
|
||||
keyboard.modules.append(Layers(combo_layers))
|
||||
```
|
||||
* If you activate layers 1 then 2, your active layer will be layer number 9.
|
||||
* If you activate layers 1 then 2, then 3, your active layer will be layer
|
||||
number 3 (because the layer combo `(1,2)` has been activated, but layer 3
|
||||
stacks on top).
|
||||
* deactivate 1: you're on layer 3
|
||||
* deactivate 2: you're on layer 3
|
||||
* deactivate 3: you're on layer 8
|
||||
* If you activate layers 3 then 1, then 2, your active layer will be layer
|
||||
number 8. Deativate layer
|
||||
* deactivate any of 1/2/3: you're on layer 0
|
||||
|
||||
|
||||
## Fully Working Example code
|
||||
|
||||
Below is an example of a fully working keypad that uses combo layers.
|
||||
|
||||
```python
|
||||
print("Starting")
|
||||
|
||||
import board
|
||||
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.keys import KC
|
||||
|
||||
combo_layers = {
|
||||
(1, 2): 3,
|
||||
keyboard.modules.append(Layers(combo_layers))
|
||||
|
||||
|
||||
keyboard = KMKKeyboard()
|
||||
|
||||
|
||||
keyboard.keymap = [
|
||||
[ #Default
|
||||
KC.A, KC.B KC.C KC.D,
|
||||
KC.E, KC.F KC.G KC.H,
|
||||
KC.MO(1), KC.J, KC.K, KC.MO(2),
|
||||
],
|
||||
[ #Layer 1
|
||||
KC.N1, KC.N2, KC.N3, KC.N4,
|
||||
KC.N5, KC.N6, KC.N7, KC.8,
|
||||
KC.MO(1), KC.N9, KC.N0, KC.MO(2),
|
||||
],
|
||||
[ #Layer 2
|
||||
KC.EXLM, KC.AT, KC.HASH, KC.DLR,
|
||||
KC.PERC, KC.CIRC, KC.AMPR, KC.ASTR,
|
||||
KC.MO(1), KC.LPRN, KC.RPRN, KC.MO(2),
|
||||
],
|
||||
[ #Layer 3
|
||||
KC.F1, KC.F2, KC.F3, KC.F4,
|
||||
KC.F5, KC.F6, KC.F7, KC.F8,
|
||||
KC.MO(1) KC.F9, KC.F10, KC.MO(2)
|
||||
]
|
||||
|
||||
]
|
||||
|
||||
if __name__ == '__main__':
|
||||
keyboard.go()
|
||||
```
|
@ -188,7 +188,6 @@
|
||||
| `KC.RESET` | Restarts the keyboard |
|
||||
| `KC.RELOAD`, `KC.RLD` | Reloads the keyboard software, preserving any serial connections |
|
||||
| `KC.DEBUG` | Toggle `debug_enabled`, which enables log spew to serial console |
|
||||
| `KC.ANY` | Any key between `A and `/` |
|
||||
| `KC.GESC` | Escape when tapped, <code>`</code> when pressed with Shift or GUI |
|
||||
| `KC.BKDL` | Backspace when tapped, Delete when pressed with GUI |
|
||||
| `KC.UC_MODE_NOOP` | Sets UnicodeMode to NOOP |
|
||||
|
@ -33,11 +33,6 @@ Some helpful guidelines to keep in mind as you design your layers:
|
||||
- Only reference higher-numbered layers from a given layer
|
||||
- Leave keys as `KC.TRNS` in higher layers when they would overlap with a layer-switch
|
||||
|
||||
## Using Combo Layers
|
||||
Combo Layers allow you to activate a corresponding layer based on the activation of 2 or more other layers.
|
||||
The advantage of using Combo layers is that when you release one of the layer keys, it stays on whatever layer is still being held.
|
||||
See [combo layers documentation](combolayers.md) for more information on it's function and to see examples.
|
||||
|
||||
### Using Multiple Base Layers
|
||||
In some cases, you may want to have more than one base layer (for instance you want to use
|
||||
both QWERTY and Dvorak layouts, or you have a custom gamepad that can switch between
|
||||
@ -45,7 +40,6 @@ different games). In this case, best practice is to have these layers be the low
|
||||
defined first in your keymap. These layers are mutually-exclusive, so treat changing default
|
||||
layers with `KC.DF()` the same way that you would treat using `KC.TO()`
|
||||
|
||||
|
||||
## Example Code
|
||||
For our example, let's take a simple 3x3 macropad with two layers as follows:
|
||||
|
||||
|
@ -7,6 +7,3 @@ If you ask for help in chat or open a bug report, if possible
|
||||
make sure your copy of KMK is up-to-date.
|
||||
In particular, swing by the Zulip chat *before* opening a GitHub Issue about
|
||||
configuration, documentation, etc. concerns.
|
||||
|
||||
> The former Matrix and Discord rooms once linked to in this README are no
|
||||
> longer officially supported, please do not use them!
|
||||
|
60
kmk/bootcfg.py
Normal file
60
kmk/bootcfg.py
Normal file
@ -0,0 +1,60 @@
|
||||
import digitalio
|
||||
import microcontroller
|
||||
|
||||
|
||||
def bootcfg(
|
||||
sense,
|
||||
source=None,
|
||||
no_cdc=True,
|
||||
no_hid=False,
|
||||
no_midi=True,
|
||||
no_storage=True,
|
||||
usb_id=None,
|
||||
):
|
||||
|
||||
if isinstance(sense, microcontroller.Pin):
|
||||
sense = digitalio.DigitalInOut(sense)
|
||||
sense.direction = digitalio.Direction.INPUT
|
||||
sense.pull = digitalio.Pull.UP
|
||||
|
||||
if isinstance(source, microcontroller.Pin):
|
||||
source = digitalio.DigitalInOut(source)
|
||||
source.direction = digitalio.Direction.OUTPUT
|
||||
source.value = False
|
||||
else:
|
||||
return False
|
||||
|
||||
# sense pulled low -> skip boot configuration
|
||||
if not sense.value:
|
||||
return False
|
||||
|
||||
if no_cdc:
|
||||
import usb_cdc
|
||||
|
||||
usb_cdc.disable()
|
||||
|
||||
if no_hid:
|
||||
import usb_hid
|
||||
|
||||
usb_hid.disable()
|
||||
|
||||
if no_midi:
|
||||
import usb_midi
|
||||
|
||||
usb_midi.disable()
|
||||
|
||||
if isinstance(usb_id, tuple):
|
||||
import supervisor
|
||||
|
||||
if hasattr(supervisor, 'set_usb_identification'):
|
||||
supervisor.set_usb_identification(*usb_id)
|
||||
|
||||
# The no_storage entry is intentionally evaluated last to ensure the drive
|
||||
# is mountable and rescueable, in case any of the previous code throws an
|
||||
# exception.
|
||||
if no_storage:
|
||||
import storage
|
||||
|
||||
storage.disable_usb_drive()
|
||||
|
||||
return True
|
@ -49,6 +49,3 @@ class Extension:
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def deinit(self, keyboard):
|
||||
pass
|
||||
|
@ -1,272 +0,0 @@
|
||||
import busio
|
||||
from supervisor import ticks_ms
|
||||
|
||||
import adafruit_displayio_ssd1306
|
||||
import displayio
|
||||
import terminalio
|
||||
from adafruit_display_text import label
|
||||
|
||||
from kmk.extensions import Extension
|
||||
from kmk.handlers.stock import passthrough as handler_passthrough
|
||||
from kmk.keys import make_key
|
||||
from kmk.kmktime import PeriodicTimer, ticks_diff
|
||||
from kmk.modules.split import Split, SplitSide
|
||||
from kmk.utils import clamp
|
||||
|
||||
displayio.release_displays()
|
||||
|
||||
|
||||
class TextEntry:
|
||||
def __init__(
|
||||
self,
|
||||
text='',
|
||||
x=0,
|
||||
y=0,
|
||||
x_anchor='L',
|
||||
y_anchor='T',
|
||||
direction='LTR',
|
||||
line_spacing=0.75,
|
||||
inverted=False,
|
||||
layer=None,
|
||||
side=None,
|
||||
):
|
||||
self.text = text
|
||||
self.direction = direction
|
||||
self.line_spacing = line_spacing
|
||||
self.inverted = inverted
|
||||
self.layer = layer
|
||||
self.color = 0xFFFFFF
|
||||
self.background_color = 0x000000
|
||||
self.x_anchor = 0.0
|
||||
self.y_anchor = 0.0
|
||||
if x_anchor == 'L':
|
||||
self.x_anchor = 0.0
|
||||
x = x + 1
|
||||
if x_anchor == 'M':
|
||||
self.x_anchor = 0.5
|
||||
if x_anchor == 'R':
|
||||
self.x_anchor = 1.0
|
||||
if y_anchor == 'T':
|
||||
self.y_anchor = 0.0
|
||||
if y_anchor == 'M':
|
||||
self.y_anchor = 0.5
|
||||
if y_anchor == 'B':
|
||||
self.y_anchor = 1.0
|
||||
self.anchor_point = (self.x_anchor, self.y_anchor)
|
||||
self.anchored_position = (x, y)
|
||||
if inverted:
|
||||
self.color = 0x000000
|
||||
self.background_color = 0xFFFFFF
|
||||
self.side = side
|
||||
if side == 'L':
|
||||
self.side = SplitSide.LEFT
|
||||
if side == 'R':
|
||||
self.side = SplitSide.RIGHT
|
||||
|
||||
|
||||
class ImageEntry:
|
||||
def __init__(self, x=0, y=0, image='', layer=None, side=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.image = displayio.OnDiskBitmap(image)
|
||||
self.layer = layer
|
||||
self.side = side
|
||||
if side == 'L':
|
||||
self.side = SplitSide.LEFT
|
||||
if side == 'R':
|
||||
self.side = SplitSide.RIGHT
|
||||
|
||||
|
||||
class Oled(Extension):
|
||||
def __init__(
|
||||
self,
|
||||
i2c=None,
|
||||
sda=None,
|
||||
scl=None,
|
||||
device_address=0x3C,
|
||||
entries=[],
|
||||
width=128,
|
||||
height=32,
|
||||
flip: bool = False,
|
||||
flip_left: bool = False,
|
||||
flip_right: bool = False,
|
||||
brightness=0.8,
|
||||
brightness_step=0.1,
|
||||
dim_time=20,
|
||||
dim_target=0.1,
|
||||
off_time=60,
|
||||
powersave_dim_time=10,
|
||||
powersave_dim_target=0.1,
|
||||
powersave_off_time=30,
|
||||
):
|
||||
self.device_address = device_address
|
||||
self.flip = flip
|
||||
self.flip_left = flip_left
|
||||
self.flip_right = flip_right
|
||||
self.entries = entries
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.prev_layer = None
|
||||
self.brightness = brightness
|
||||
self.brightness_step = brightness_step
|
||||
self.timer_start = ticks_ms()
|
||||
self.powersave = False
|
||||
self.dim_time_ms = dim_time * 1000
|
||||
self.dim_target = dim_target
|
||||
self.off_time_ms = off_time * 1000
|
||||
self.powersavedim_time_ms = powersave_dim_time * 1000
|
||||
self.powersave_dim_target = powersave_dim_target
|
||||
self.powersave_off_time_ms = powersave_off_time * 1000
|
||||
self.dim_period = PeriodicTimer(50)
|
||||
self.split_side = None
|
||||
# i2c initialization
|
||||
self.i2c = i2c
|
||||
if self.i2c is None:
|
||||
self.i2c = busio.I2C(scl, sda)
|
||||
|
||||
make_key(
|
||||
names=('OLED_BRI',),
|
||||
on_press=self.oled_brightness_increase,
|
||||
on_release=handler_passthrough,
|
||||
)
|
||||
make_key(
|
||||
names=('OLED_BRD',),
|
||||
on_press=self.oled_brightness_decrease,
|
||||
on_release=handler_passthrough,
|
||||
)
|
||||
|
||||
def render(self, layer):
|
||||
splash = displayio.Group()
|
||||
|
||||
for entry in self.entries:
|
||||
if entry.layer != layer and entry.layer is not None:
|
||||
continue
|
||||
if isinstance(entry, TextEntry):
|
||||
splash.append(
|
||||
label.Label(
|
||||
terminalio.FONT,
|
||||
text=entry.text,
|
||||
color=entry.color,
|
||||
background_color=entry.background_color,
|
||||
anchor_point=entry.anchor_point,
|
||||
anchored_position=entry.anchored_position,
|
||||
label_direction=entry.direction,
|
||||
line_spacing=entry.line_spacing,
|
||||
padding_left=1,
|
||||
)
|
||||
)
|
||||
elif isinstance(entry, ImageEntry):
|
||||
splash.append(
|
||||
displayio.TileGrid(
|
||||
entry.image,
|
||||
pixel_shader=entry.image.pixel_shader,
|
||||
x=entry.x,
|
||||
y=entry.y,
|
||||
)
|
||||
)
|
||||
self.display.show(splash)
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
|
||||
for module in keyboard.modules:
|
||||
if isinstance(module, Split):
|
||||
self.split_side = module.split_side
|
||||
|
||||
if self.split_side == SplitSide.LEFT:
|
||||
self.flip = self.flip_left
|
||||
elif self.split_side == SplitSide.RIGHT:
|
||||
self.flip = self.flip_right
|
||||
|
||||
for idx, entry in enumerate(self.entries):
|
||||
if entry.side != self.split_side and entry.side is not None:
|
||||
del self.entries[idx]
|
||||
|
||||
self.display = adafruit_displayio_ssd1306.SSD1306(
|
||||
displayio.I2CDisplay(self.i2c, device_address=self.device_address),
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
rotation=180 if self.flip else 0,
|
||||
brightness=self.brightness,
|
||||
)
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
if self.dim_period.tick():
|
||||
self.dim()
|
||||
if sandbox.active_layers[0] != self.prev_layer:
|
||||
self.prev_layer = sandbox.active_layers[0]
|
||||
self.render(sandbox.active_layers[0])
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
if sandbox.matrix_update or sandbox.secondary_matrix_update:
|
||||
self.timer_start = ticks_ms()
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
self.powersave = True
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
self.powersave = False
|
||||
|
||||
def deinit(self, sandbox):
|
||||
displayio.release_displays()
|
||||
self.i2c.deinit()
|
||||
|
||||
def oled_brightness_increase(self):
|
||||
self.display.brightness = clamp(
|
||||
self.display.brightness + self.brightness_step, 0, 1
|
||||
)
|
||||
self.brightness = self.display.brightness # Save current brightness
|
||||
|
||||
def oled_brightness_decrease(self):
|
||||
self.display.brightness = clamp(
|
||||
self.display.brightness - self.brightness_step, 0, 1
|
||||
)
|
||||
self.brightness = self.display.brightness # Save current brightness
|
||||
|
||||
def dim(self):
|
||||
if self.powersave:
|
||||
|
||||
if (
|
||||
self.powersave_off_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start)
|
||||
> self.powersave_off_time_ms
|
||||
):
|
||||
self.display.sleep()
|
||||
|
||||
elif (
|
||||
self.powersave_dim_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start)
|
||||
> self.powersave_dim_time_ms
|
||||
):
|
||||
self.display.brightness = self.powersave_dim_target
|
||||
|
||||
else:
|
||||
self.display.brightness = self.brightness
|
||||
self.display.wake()
|
||||
|
||||
elif (
|
||||
self.off_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start) > self.off_time_ms
|
||||
):
|
||||
self.display.sleep()
|
||||
|
||||
elif (
|
||||
self.dim_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start) > self.dim_time_ms
|
||||
):
|
||||
self.display.brightness = self.dim_target
|
||||
|
||||
else:
|
||||
self.display.brightness = self.brightness
|
||||
self.display.wake()
|
@ -4,7 +4,7 @@ from math import e, exp, pi, sin
|
||||
from kmk.extensions import Extension
|
||||
from kmk.handlers.stock import passthrough as handler_passthrough
|
||||
from kmk.keys import make_key
|
||||
from kmk.scheduler import create_task
|
||||
from kmk.kmktime import PeriodicTimer
|
||||
from kmk.utils import Debug, clamp
|
||||
|
||||
debug = Debug(__name__)
|
||||
@ -90,10 +90,10 @@ class RGB(Extension):
|
||||
self,
|
||||
pixel_pin,
|
||||
num_pixels=0,
|
||||
rgb_order=(1, 0, 2), # GRB WS2812
|
||||
val_limit=255,
|
||||
hue_default=0,
|
||||
sat_default=255,
|
||||
rgb_order=(1, 0, 2), # GRB WS2812
|
||||
val_default=255,
|
||||
hue_step=4,
|
||||
sat_step=13,
|
||||
@ -109,9 +109,32 @@ class RGB(Extension):
|
||||
pixels=None,
|
||||
refresh_rate=60,
|
||||
):
|
||||
self.pixel_pin = pixel_pin
|
||||
if pixels is None:
|
||||
import neopixel
|
||||
|
||||
pixels = neopixel.NeoPixel(
|
||||
pixel_pin,
|
||||
num_pixels,
|
||||
pixel_order=rgb_order,
|
||||
auto_write=not disable_auto_write,
|
||||
)
|
||||
self.pixels = pixels
|
||||
self.num_pixels = num_pixels
|
||||
self.rgb_order = rgb_order
|
||||
|
||||
# PixelBuffer are already iterable, can't do the usual `try: iter(...)`
|
||||
if issubclass(self.pixels.__class__, PixelBuf):
|
||||
self.pixels = (self.pixels,)
|
||||
|
||||
if self.num_pixels == 0:
|
||||
for pixels in self.pixels:
|
||||
self.num_pixels += len(pixels)
|
||||
|
||||
if debug.enabled:
|
||||
for n, pixels in enumerate(self.pixels):
|
||||
debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')
|
||||
|
||||
self.rgbw = bool(len(rgb_order) == 4)
|
||||
|
||||
self.hue_step = hue_step
|
||||
self.sat_step = sat_step
|
||||
self.val_step = val_step
|
||||
@ -130,11 +153,8 @@ class RGB(Extension):
|
||||
self.reverse_animation = reverse_animation
|
||||
self.user_animation = user_animation
|
||||
self.disable_auto_write = disable_auto_write
|
||||
self.pixels = pixels
|
||||
self.refresh_rate = refresh_rate
|
||||
|
||||
self.rgbw = bool(len(rgb_order) == 4)
|
||||
|
||||
self._substep = 0
|
||||
|
||||
make_key(
|
||||
@ -207,29 +227,7 @@ class RGB(Extension):
|
||||
return
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
if self.pixels is None:
|
||||
import neopixel
|
||||
|
||||
self.pixels = neopixel.NeoPixel(
|
||||
self.pixel_pin,
|
||||
self.num_pixels,
|
||||
pixel_order=self.rgb_order,
|
||||
auto_write=not self.disable_auto_write,
|
||||
)
|
||||
|
||||
# PixelBuffer are already iterable, can't do the usual `try: iter(...)`
|
||||
if issubclass(self.pixels.__class__, PixelBuf):
|
||||
self.pixels = (self.pixels,)
|
||||
|
||||
if self.num_pixels == 0:
|
||||
for pixels in self.pixels:
|
||||
self.num_pixels += len(pixels)
|
||||
|
||||
if debug.enabled:
|
||||
for n, pixels in enumerate(self.pixels):
|
||||
debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')
|
||||
|
||||
self._task = create_task(self.animate, period_ms=(1000 // self.refresh_rate))
|
||||
self._timer = PeriodicTimer(1000 // self.refresh_rate)
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
@ -241,7 +239,7 @@ class RGB(Extension):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
pass
|
||||
self.animate()
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
return
|
||||
@ -249,10 +247,6 @@ class RGB(Extension):
|
||||
def on_powersave_disable(self, sandbox):
|
||||
self._do_update()
|
||||
|
||||
def deinit(self, sandbox):
|
||||
for pixel in self.pixels:
|
||||
pixel.deinit()
|
||||
|
||||
def set_hsv(self, hue, sat, val, index):
|
||||
'''
|
||||
Takes HSV values and displays it on a single LED/Neopixel
|
||||
@ -435,7 +429,7 @@ class RGB(Extension):
|
||||
if self.animation_mode is AnimationModes.STATIC_STANDBY:
|
||||
return
|
||||
|
||||
if self.enable:
|
||||
if self.enable and self._timer.tick():
|
||||
self._animation_step()
|
||||
if self.animation_mode == AnimationModes.BREATHING:
|
||||
self.effect_breathing()
|
||||
|
@ -137,10 +137,3 @@ def ble_disconnect(key, keyboard, *args, **kwargs):
|
||||
|
||||
keyboard._hid_helper.clear_bonds()
|
||||
return keyboard
|
||||
|
||||
|
||||
def any_pressed(key, keyboard, *args, **kwargs):
|
||||
from random import randint
|
||||
|
||||
key.code = randint(4, 56)
|
||||
default_pressed(key, keyboard, *args, **kwargs)
|
||||
|
@ -370,7 +370,6 @@ def maybe_make_firmware_key(candidate: str) -> Optional[Key]:
|
||||
((('HID_SWITCH', 'HID'), handlers.hid_switch)),
|
||||
((('RELOAD', 'RLD'), handlers.reload)),
|
||||
((('RESET',), handlers.reset)),
|
||||
((('ANY',), handlers.any_pressed)),
|
||||
)
|
||||
|
||||
for names, handler in keys:
|
||||
@ -476,9 +475,7 @@ class KeyAttrDict:
|
||||
break
|
||||
|
||||
if not maybe_key:
|
||||
if debug.enabled:
|
||||
debug(f'Invalid key: {name}')
|
||||
return KC.NO
|
||||
raise ValueError(f'Invalid key: {name}')
|
||||
|
||||
if debug.enabled:
|
||||
debug(f'{name}: {maybe_key}')
|
||||
|
@ -1,33 +1,28 @@
|
||||
try:
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional, Tuple
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from collections import namedtuple
|
||||
from keypad import Event as KeyEvent
|
||||
|
||||
from kmk.consts import UnicodeMode
|
||||
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
|
||||
from kmk.keys import KC, Key
|
||||
from kmk.kmktime import ticks_add, ticks_diff
|
||||
from kmk.modules import Module
|
||||
from kmk.scanners.keypad import MatrixScanner
|
||||
from kmk.scheduler import Task, cancel_task, create_task, get_due_task
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug('kmk.keyboard')
|
||||
debug = Debug(__name__)
|
||||
|
||||
KeyBufferFrame = namedtuple(
|
||||
'KeyBufferFrame', ('key', 'is_pressed', 'int_coord', 'index')
|
||||
)
|
||||
|
||||
|
||||
def debug_error(module, message: str, error: Exception):
|
||||
if debug.enabled:
|
||||
debug(
|
||||
message, ': ', error.__class__.__name__, ': ', error, name=module.__module__
|
||||
)
|
||||
|
||||
|
||||
class Sandbox:
|
||||
matrix_update = None
|
||||
secondary_matrix_update = None
|
||||
@ -64,6 +59,7 @@ class KMKKeyboard:
|
||||
matrix_update = None
|
||||
secondary_matrix_update = None
|
||||
matrix_update_queue = []
|
||||
state_changed = False
|
||||
_trigger_powersave_enable = False
|
||||
_trigger_powersave_disable = False
|
||||
i2c_deinit_count = 0
|
||||
@ -79,24 +75,47 @@ class KMKKeyboard:
|
||||
|
||||
_timeouts = {}
|
||||
|
||||
# on some M4 setups (such as klardotsh/klarank_feather_m4, CircuitPython
|
||||
# 6.0rc1) this runs out of RAM every cycle and takes down the board. no
|
||||
# real known fix yet other than turning off debug, but M4s have always been
|
||||
# tight on RAM so....
|
||||
def __repr__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
return ''.join(
|
||||
[
|
||||
'KMKKeyboard(\n',
|
||||
f' debug_enabled={self.debug_enabled}, ',
|
||||
f'diode_orientation={self.diode_orientation}, ',
|
||||
f'matrix={self.matrix},\n',
|
||||
f' unicode_mode={self.unicode_mode}, ',
|
||||
f'_hid_helper={self._hid_helper},\n',
|
||||
f' keys_pressed={self.keys_pressed},\n',
|
||||
f' axes={self.axes},\n',
|
||||
f' _coordkeys_pressed={self._coordkeys_pressed},\n',
|
||||
f' hid_pending={self.hid_pending}, ',
|
||||
f'active_layers={self.active_layers}, ',
|
||||
f'_timeouts={self._timeouts}\n',
|
||||
')',
|
||||
]
|
||||
)
|
||||
|
||||
def _print_debug_cycle(self, init: bool = False) -> None:
|
||||
if debug.enabled:
|
||||
debug(f'coordkeys_pressed={self._coordkeys_pressed}')
|
||||
debug(f'keys_pressed={self.keys_pressed}')
|
||||
|
||||
def _send_hid(self) -> None:
|
||||
if not self._hid_send_enabled:
|
||||
return
|
||||
|
||||
if debug.enabled:
|
||||
if self.keys_pressed:
|
||||
debug('keys_pressed=', self.keys_pressed)
|
||||
if self.axes:
|
||||
debug('axes=', self.axes)
|
||||
if self.axes and debug.enabled:
|
||||
debug(f'axes={self.axes}')
|
||||
|
||||
self._hid_helper.create_report(self.keys_pressed, self.axes)
|
||||
try:
|
||||
self._hid_helper.send()
|
||||
except Exception as err:
|
||||
debug_error(self._hid_helper, 'send', err)
|
||||
except KeyError as e:
|
||||
if debug.enabled:
|
||||
debug(f'HidNotFound(HIDReportType={e})')
|
||||
|
||||
self.hid_pending = False
|
||||
|
||||
@ -106,32 +125,35 @@ class KMKKeyboard:
|
||||
def _handle_matrix_report(self, kevent: KeyEvent) -> None:
|
||||
if kevent is not None:
|
||||
self._on_matrix_changed(kevent)
|
||||
self.state_changed = True
|
||||
|
||||
def _find_key_in_map(self, int_coord: int) -> Key:
|
||||
try:
|
||||
idx = self.coord_mapping.index(int_coord)
|
||||
except ValueError:
|
||||
if debug.enabled:
|
||||
debug('no such int_coord: ', int_coord)
|
||||
debug(f'CoordMappingNotFound(ic={int_coord})')
|
||||
|
||||
return None
|
||||
|
||||
for layer in self.active_layers:
|
||||
try:
|
||||
key = self.keymap[layer][idx]
|
||||
layer_key = self.keymap[layer][idx]
|
||||
except IndexError:
|
||||
key = None
|
||||
layer_key = None
|
||||
if debug.enabled:
|
||||
debug('keymap IndexError: idx=', idx, ' layer=', layer)
|
||||
debug(f'KeymapIndexError(idx={idx}, layer={layer})')
|
||||
|
||||
if not key or key == KC.TRNS:
|
||||
if not layer_key or layer_key == KC.TRNS:
|
||||
continue
|
||||
|
||||
return key
|
||||
return layer_key
|
||||
|
||||
def _on_matrix_changed(self, kevent: KeyEvent) -> None:
|
||||
int_coord = kevent.key_number
|
||||
is_pressed = kevent.pressed
|
||||
if debug.enabled:
|
||||
debug(f'MatrixChange(ic={int_coord}, pressed={is_pressed})')
|
||||
|
||||
key = None
|
||||
if not is_pressed:
|
||||
@ -139,16 +161,18 @@ class KMKKeyboard:
|
||||
key = self._coordkeys_pressed[int_coord]
|
||||
except KeyError:
|
||||
if debug.enabled:
|
||||
debug('release w/o press: ', int_coord)
|
||||
debug(f'KeyNotPressed(ic={int_coord})')
|
||||
|
||||
if key is None:
|
||||
key = self._find_key_in_map(int_coord)
|
||||
|
||||
if key is None:
|
||||
return
|
||||
if key is None:
|
||||
if debug.enabled:
|
||||
debug(f'MatrixUndefinedCoordinate(ic={int_coord})')
|
||||
return self
|
||||
|
||||
if debug.enabled:
|
||||
debug(kevent, ': ', key)
|
||||
debug(f'KeyResolution(key={key})')
|
||||
|
||||
self.pre_process_key(key, is_pressed, int_coord)
|
||||
|
||||
@ -173,7 +197,7 @@ class KMKKeyboard:
|
||||
key = ksf.key
|
||||
|
||||
# Handle any unaccounted-for layer shifts by looking up the key resolution again.
|
||||
if ksf.int_coord is not None:
|
||||
if ksf.int_coord in self._coordkeys_pressed.keys():
|
||||
key = self._find_key_in_map(ksf.int_coord)
|
||||
|
||||
# Resume the processing of the key event and update the HID report
|
||||
@ -214,7 +238,8 @@ class KMKKeyboard:
|
||||
if key is None:
|
||||
break
|
||||
except Exception as err:
|
||||
debug_error(module, 'process_key', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.process_key: {err}')
|
||||
|
||||
if int_coord is not None:
|
||||
if is_pressed:
|
||||
@ -224,20 +249,18 @@ class KMKKeyboard:
|
||||
del self._coordkeys_pressed[int_coord]
|
||||
except KeyError:
|
||||
if debug.enabled:
|
||||
debug('release w/o press:', int_coord)
|
||||
if debug.enabled:
|
||||
debug('coordkeys_pressed=', self._coordkeys_pressed)
|
||||
debug(f'ReleaseKeyError(ic={int_coord})')
|
||||
|
||||
if key:
|
||||
self.process_key(key, is_pressed, int_coord)
|
||||
|
||||
def process_key(
|
||||
self, key: Key, is_pressed: bool, int_coord: Optional[int] = None
|
||||
self, key: Key, is_pressed: bool, coord_int: Optional[int] = None
|
||||
) -> None:
|
||||
if is_pressed:
|
||||
key.on_press(self, int_coord)
|
||||
key.on_press(self, coord_int)
|
||||
else:
|
||||
key.on_release(self, int_coord)
|
||||
key.on_release(self, coord_int)
|
||||
|
||||
def resume_process_key(
|
||||
self,
|
||||
@ -245,9 +268,8 @@ class KMKKeyboard:
|
||||
key: Key,
|
||||
is_pressed: bool,
|
||||
int_coord: Optional[int] = None,
|
||||
reprocess: Optional[bool] = False,
|
||||
) -> None:
|
||||
index = self.modules.index(module) + (0 if reprocess else 1)
|
||||
index = self.modules.index(module) + 1
|
||||
ksf = KeyBufferFrame(
|
||||
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
|
||||
)
|
||||
@ -264,17 +286,60 @@ class KMKKeyboard:
|
||||
def tap_key(self, keycode: Key) -> None:
|
||||
self.add_key(keycode)
|
||||
# On the next cycle, we'll remove the key.
|
||||
self.set_timeout(0, lambda: self.remove_key(keycode))
|
||||
self.set_timeout(False, lambda: self.remove_key(keycode))
|
||||
|
||||
def set_timeout(self, after_ticks: int, callback: Callable[[None], None]) -> [Task]:
|
||||
return create_task(callback, after_ms=after_ticks)
|
||||
def set_timeout(
|
||||
self, after_ticks: int, callback: Callable[[None], None]
|
||||
) -> Tuple[int, int]:
|
||||
# We allow passing False as an implicit "run this on the next process timeouts cycle"
|
||||
if after_ticks is False:
|
||||
after_ticks = 0
|
||||
|
||||
if after_ticks == 0 and self._processing_timeouts:
|
||||
after_ticks += 1
|
||||
|
||||
timeout_key = ticks_add(ticks_ms(), after_ticks)
|
||||
|
||||
if timeout_key not in self._timeouts:
|
||||
self._timeouts[timeout_key] = []
|
||||
|
||||
idx = len(self._timeouts[timeout_key])
|
||||
self._timeouts[timeout_key].append(callback)
|
||||
|
||||
return (timeout_key, idx)
|
||||
|
||||
def cancel_timeout(self, timeout_key: int) -> None:
|
||||
cancel_task(timeout_key)
|
||||
try:
|
||||
self._timeouts[timeout_key[0]][timeout_key[1]] = None
|
||||
except (KeyError, IndexError):
|
||||
if debug.enabled:
|
||||
debug(f'no such timeout: {timeout_key}')
|
||||
|
||||
def _process_timeouts(self) -> None:
|
||||
for task in get_due_task():
|
||||
task()
|
||||
if not self._timeouts:
|
||||
return
|
||||
|
||||
# Copy timeout keys to a temporary list to allow sorting.
|
||||
# Prevent net timeouts set during handling from running on the current
|
||||
# cycle by setting a flag `_processing_timeouts`.
|
||||
current_time = ticks_ms()
|
||||
timeout_keys = []
|
||||
self._processing_timeouts = True
|
||||
|
||||
for k in self._timeouts.keys():
|
||||
if ticks_diff(k, current_time) <= 0:
|
||||
timeout_keys.append(k)
|
||||
|
||||
if timeout_keys and debug.enabled:
|
||||
debug('processing timeouts')
|
||||
|
||||
for k in sorted(timeout_keys):
|
||||
for callback in self._timeouts[k]:
|
||||
if callback:
|
||||
callback()
|
||||
del self._timeouts[k]
|
||||
|
||||
self._processing_timeouts = False
|
||||
|
||||
def _init_sanity_check(self) -> None:
|
||||
'''
|
||||
@ -321,15 +386,14 @@ class KMKKeyboard:
|
||||
self._hid_helper = self._hid_helper(**self._go_args)
|
||||
self._hid_send_enabled = True
|
||||
|
||||
if debug.enabled:
|
||||
debug('hid=', self._hid_helper)
|
||||
|
||||
def _deinit_hid(self) -> None:
|
||||
self._hid_helper.clear_all()
|
||||
self._hid_helper.send()
|
||||
|
||||
def _init_matrix(self) -> None:
|
||||
if self.matrix is None:
|
||||
if debug.enabled:
|
||||
debug('Initialising default matrix scanner.')
|
||||
self.matrix = MatrixScanner(
|
||||
column_pins=self.col_pins,
|
||||
row_pins=self.row_pins,
|
||||
@ -345,124 +409,96 @@ class KMKKeyboard:
|
||||
except TypeError:
|
||||
self.matrix = (self.matrix,)
|
||||
|
||||
if debug.enabled:
|
||||
debug('matrix=', [_.__class__.__name__ for _ in self.matrix])
|
||||
|
||||
def during_bootup(self) -> None:
|
||||
# Modules and extensions that fail `during_bootup` get removed from
|
||||
# their respective lists. This serves as a self-check mechanism; any
|
||||
# modules or extensions that initialize peripherals or data structures
|
||||
# should do that in `during_bootup`.
|
||||
for idx, module in enumerate(self.modules):
|
||||
try:
|
||||
module.during_bootup(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'during_bootup', err)
|
||||
del self.modules[idx]
|
||||
|
||||
if debug.enabled:
|
||||
debug('modules=', [_.__class__.__name__ for _ in self.modules])
|
||||
|
||||
for idx, ext in enumerate(self.extensions):
|
||||
try:
|
||||
ext.during_bootup(self)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'during_bootup', err)
|
||||
del self.extensions[idx]
|
||||
|
||||
if debug.enabled:
|
||||
debug('extensions=', [_.__class__.__name__ for _ in self.extensions])
|
||||
|
||||
def before_matrix_scan(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.before_matrix_scan(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'before_matrix_scan', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.before_matrix_scan: {err}')
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.before_matrix_scan(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'before_matrix_scan', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {ext}.before_matrix_scan: {err}')
|
||||
|
||||
def after_matrix_scan(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.after_matrix_scan(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'after_matrix_scan', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.after_matrix_scan: {err}')
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.after_matrix_scan(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'after_matrix_scan', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {ext}.after_matrix_scan: {err}')
|
||||
|
||||
def before_hid_send(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.before_hid_send(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'before_hid_send', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.before_hid_send: {err}')
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.before_hid_send(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'before_hid_send', err)
|
||||
if debug.enabled:
|
||||
debug(
|
||||
f'Error in {ext}.before_hid_send: {err}',
|
||||
)
|
||||
|
||||
def after_hid_send(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.after_hid_send(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'after_hid_send', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.after_hid_send: {err}')
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.after_hid_send(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'after_hid_send', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {ext}.after_hid_send: {err}')
|
||||
|
||||
def powersave_enable(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.on_powersave_enable(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'powersave_enable', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.on_powersave: {err}')
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.on_powersave_enable(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'powersave_enable', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {ext}.powersave_enable: {err}')
|
||||
|
||||
def powersave_disable(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.on_powersave_disable(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'powersave_disable', err)
|
||||
|
||||
if debug.enabled:
|
||||
debug(f'Error in {module}.powersave_disable: {err}')
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.on_powersave_disable(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'powersave_disable', err)
|
||||
|
||||
def deinit(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.deinit(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'deinit', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.deinit(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'deinit', err)
|
||||
if debug.enabled:
|
||||
debug(f'Error in {ext}.powersave_disable: {err}')
|
||||
|
||||
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs) -> None:
|
||||
self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
|
||||
@ -472,7 +508,6 @@ class KMKKeyboard:
|
||||
finally:
|
||||
debug('Unexpected error: cleaning up')
|
||||
self._deinit_hid()
|
||||
self.deinit()
|
||||
|
||||
def _init(
|
||||
self,
|
||||
@ -484,22 +519,29 @@ class KMKKeyboard:
|
||||
self.hid_type = hid_type
|
||||
self.secondary_hid_type = secondary_hid_type
|
||||
|
||||
if debug.enabled:
|
||||
debug('Initialising ', self)
|
||||
debug('unicode_mode=', self.unicode_mode)
|
||||
|
||||
self._init_sanity_check()
|
||||
self._init_hid()
|
||||
self._init_matrix()
|
||||
self._init_coord_mapping()
|
||||
self.during_bootup()
|
||||
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.during_bootup(self)
|
||||
except Exception as err:
|
||||
if debug.enabled:
|
||||
debug(f'Failed to load module {module}: {err}')
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.during_bootup(self)
|
||||
except Exception as err:
|
||||
if debug.enabled:
|
||||
debug(f'Failed to load extensions {module}: {err}')
|
||||
|
||||
if debug.enabled:
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
debug('mem_info used:', gc.mem_alloc(), ' free:', gc.mem_free())
|
||||
debug(f'init: {self}')
|
||||
|
||||
def _main_loop(self) -> None:
|
||||
self.state_changed = False
|
||||
self.sandbox.active_layers = self.active_layers.copy()
|
||||
|
||||
self.before_matrix_scan()
|
||||
@ -537,6 +579,7 @@ class KMKKeyboard:
|
||||
|
||||
if self.hid_pending:
|
||||
self._send_hid()
|
||||
self.state_changed = True
|
||||
|
||||
self.after_hid_send()
|
||||
|
||||
@ -545,3 +588,6 @@ class KMKKeyboard:
|
||||
|
||||
if self._trigger_powersave_disable:
|
||||
self.powersave_disable()
|
||||
|
||||
if self.state_changed:
|
||||
self._print_debug_cycle()
|
||||
|
@ -41,6 +41,3 @@ class Module:
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def deinit(self, keyboard):
|
||||
pass
|
||||
|
@ -8,9 +8,6 @@ import kmk.handlers.stock as handlers
|
||||
from kmk.keys import Key, make_key
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class _ComboState:
|
||||
@ -217,7 +214,7 @@ class Combos(Module):
|
||||
combo.insert(key, int_coord)
|
||||
combo._state = _ComboState.MATCHING
|
||||
|
||||
key = None
|
||||
key = combo.result
|
||||
break
|
||||
|
||||
else:
|
||||
@ -304,14 +301,10 @@ class Combos(Module):
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord)
|
||||
|
||||
def activate(self, keyboard, combo):
|
||||
if debug.enabled:
|
||||
debug('activate', combo)
|
||||
combo.result.on_press(keyboard)
|
||||
combo._state = _ComboState.ACTIVE
|
||||
|
||||
def deactivate(self, keyboard, combo):
|
||||
if debug.enabled:
|
||||
debug('deactivate', combo)
|
||||
combo.result.on_release(keyboard)
|
||||
combo._state = _ComboState.IDLE
|
||||
|
||||
|
@ -54,7 +54,7 @@ class HoldTap(Module):
|
||||
def __init__(self):
|
||||
self.key_buffer = []
|
||||
self.key_states = {}
|
||||
if KC.get('HT') == KC.NO:
|
||||
if not KC.get('HT'):
|
||||
make_argumented_key(
|
||||
validator=HoldTapKeyMeta,
|
||||
names=('HT',),
|
||||
@ -83,11 +83,6 @@ class HoldTap(Module):
|
||||
if state.activated != ActivationType.PRESSED:
|
||||
continue
|
||||
|
||||
# holdtap isn't interruptable, resolves on ht_release or timeout.
|
||||
if not key.meta.tap_interrupted and not key.meta.prefer_hold:
|
||||
append_buffer = True
|
||||
continue
|
||||
|
||||
# holdtap is interrupted by another key event.
|
||||
if (is_pressed and not key.meta.tap_interrupted) or (
|
||||
not is_pressed and key.meta.tap_interrupted and self.key_buffer
|
||||
@ -98,12 +93,15 @@ class HoldTap(Module):
|
||||
self.ht_activate_on_interrupt(
|
||||
key, keyboard, *state.args, **state.kwargs
|
||||
)
|
||||
append_buffer = True
|
||||
send_buffer = True
|
||||
|
||||
# if interrupt on release: store interrupting keys until one of them
|
||||
# is released.
|
||||
if key.meta.tap_interrupted and is_pressed:
|
||||
if (
|
||||
key.meta.tap_interrupted
|
||||
and is_pressed
|
||||
and not isinstance(current_key.meta, HoldTapKeyMeta)
|
||||
):
|
||||
append_buffer = True
|
||||
|
||||
# apply changes with 'side-effects' on key_states or the loop behaviour
|
||||
@ -112,8 +110,10 @@ class HoldTap(Module):
|
||||
self.key_buffer.append((int_coord, current_key, is_pressed))
|
||||
current_key = None
|
||||
|
||||
if send_buffer:
|
||||
elif send_buffer:
|
||||
self.send_key_buffer(keyboard)
|
||||
keyboard.resume_process_key(self, current_key, is_pressed, int_coord)
|
||||
current_key = None
|
||||
|
||||
return current_key
|
||||
|
||||
@ -221,11 +221,8 @@ class HoldTap(Module):
|
||||
if not self.key_buffer:
|
||||
return
|
||||
|
||||
reprocess = False
|
||||
for (int_coord, key, is_pressed) in self.key_buffer:
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord, reprocess)
|
||||
if isinstance(key.meta, HoldTapKeyMeta):
|
||||
reprocess = True
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord)
|
||||
|
||||
self.key_buffer.clear()
|
||||
|
||||
|
@ -36,15 +36,9 @@ class LayerKeyMeta:
|
||||
class Layers(HoldTap):
|
||||
'''Gives access to the keys used to enable the layer system'''
|
||||
|
||||
_active_combo = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
combo_layers=None,
|
||||
):
|
||||
def __init__(self):
|
||||
# Layers
|
||||
super().__init__()
|
||||
self.combo_layers = combo_layers
|
||||
make_argumented_key(
|
||||
validator=layer_key_validator,
|
||||
names=('MO',),
|
||||
@ -52,7 +46,9 @@ class Layers(HoldTap):
|
||||
on_release=self._mo_released,
|
||||
)
|
||||
make_argumented_key(
|
||||
validator=layer_key_validator, names=('DF',), on_press=self._df_pressed
|
||||
validator=layer_key_validator,
|
||||
names=('DF',),
|
||||
on_press=self._df_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
validator=layer_key_validator,
|
||||
@ -61,10 +57,14 @@ class Layers(HoldTap):
|
||||
on_release=self._lm_released,
|
||||
)
|
||||
make_argumented_key(
|
||||
validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed
|
||||
validator=layer_key_validator,
|
||||
names=('TG',),
|
||||
on_press=self._tg_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
validator=layer_key_validator, names=('TO',), on_press=self._to_pressed
|
||||
validator=layer_key_validator,
|
||||
names=('TO',),
|
||||
on_press=self._to_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
validator=layer_key_validator_lt,
|
||||
@ -83,102 +83,67 @@ class Layers(HoldTap):
|
||||
'''
|
||||
Switches the default layer
|
||||
'''
|
||||
self.activate_layer(keyboard, key.meta.layer, as_default=True)
|
||||
keyboard.active_layers[-1] = key.meta.layer
|
||||
self._print_debug(keyboard)
|
||||
|
||||
def _mo_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Momentarily activates layer, switches off when you let go
|
||||
'''
|
||||
self.activate_layer(keyboard, key.meta.layer)
|
||||
keyboard.active_layers.insert(0, key.meta.layer)
|
||||
self._print_debug(keyboard)
|
||||
|
||||
def _mo_released(self, key, keyboard, *args, **kwargs):
|
||||
self.deactivate_layer(keyboard, key.meta.layer)
|
||||
@staticmethod
|
||||
def _mo_released(key, keyboard, *args, **kwargs):
|
||||
# remove the first instance of the target layer
|
||||
# from the active list
|
||||
# under almost all normal use cases, this will
|
||||
# disable the layer (but preserve it if it was triggered
|
||||
# as a default layer, etc.)
|
||||
# this also resolves an issue where using DF() on a layer
|
||||
# triggered by MO() and then defaulting to the MO()'s layer
|
||||
# would result in no layers active
|
||||
try:
|
||||
del_idx = keyboard.active_layers.index(key.meta.layer)
|
||||
del keyboard.active_layers[del_idx]
|
||||
except ValueError:
|
||||
pass
|
||||
__class__._print_debug(__class__, keyboard)
|
||||
|
||||
def _lm_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
As MO(layer) but with mod active
|
||||
'''
|
||||
keyboard.hid_pending = True
|
||||
keyboard.keys_pressed.add(key.meta.kc)
|
||||
self.activate_layer(keyboard, key.meta.layer)
|
||||
# Sets the timer start and acts like MO otherwise
|
||||
keyboard.add_key(key.meta.kc)
|
||||
self._mo_pressed(key, keyboard, *args, **kwargs)
|
||||
|
||||
def _lm_released(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
As MO(layer) but with mod active
|
||||
'''
|
||||
keyboard.hid_pending = True
|
||||
keyboard.keys_pressed.discard(key.meta.kc)
|
||||
self.deactivate_layer(keyboard, key.meta.layer)
|
||||
keyboard.remove_key(key.meta.kc)
|
||||
self._mo_released(key, keyboard, *args, **kwargs)
|
||||
|
||||
def _tg_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Toggles the layer (enables it if not active, and vise versa)
|
||||
'''
|
||||
# See mo_released for implementation details around this
|
||||
if key.meta.layer in keyboard.active_layers:
|
||||
self.deactivate_layer(keyboard, key.meta.layer)
|
||||
else:
|
||||
self.activate_layer(keyboard, key.meta.layer)
|
||||
try:
|
||||
del_idx = keyboard.active_layers.index(key.meta.layer)
|
||||
del keyboard.active_layers[del_idx]
|
||||
except ValueError:
|
||||
keyboard.active_layers.insert(0, key.meta.layer)
|
||||
|
||||
def _to_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Activates layer and deactivates all other layers
|
||||
'''
|
||||
self._active_combo = None
|
||||
keyboard.active_layers.clear()
|
||||
keyboard.active_layers.insert(0, key.meta.layer)
|
||||
|
||||
def _print_debug(self, keyboard):
|
||||
# debug(f'__getitem__ {key}')
|
||||
if debug.enabled:
|
||||
debug(f'active_layers={keyboard.active_layers}')
|
||||
|
||||
def activate_layer(self, keyboard, layer, as_default=False):
|
||||
if as_default:
|
||||
keyboard.active_layers[-1] = layer
|
||||
else:
|
||||
keyboard.active_layers.insert(0, layer)
|
||||
|
||||
if self.combo_layers:
|
||||
self._activate_combo_layer(keyboard)
|
||||
|
||||
self._print_debug(keyboard)
|
||||
|
||||
def deactivate_layer(self, keyboard, layer):
|
||||
# Remove the first instance of the target layer from the active list
|
||||
# under almost all normal use cases, this will disable the layer (but
|
||||
# preserve it if it was triggered as a default layer, etc.).
|
||||
# This also resolves an issue where using DF() on a layer
|
||||
# triggered by MO() and then defaulting to the MO()'s layer
|
||||
# would result in no layers active.
|
||||
try:
|
||||
del_idx = keyboard.active_layers.index(layer)
|
||||
del keyboard.active_layers[del_idx]
|
||||
except ValueError:
|
||||
if debug.enabled:
|
||||
debug(f'_mo_released: layer {layer} not active')
|
||||
|
||||
if self.combo_layers:
|
||||
self._deactivate_combo_layer(keyboard, layer)
|
||||
|
||||
self._print_debug(keyboard)
|
||||
|
||||
def _activate_combo_layer(self, keyboard):
|
||||
if self._active_combo:
|
||||
return
|
||||
|
||||
for combo, result in self.combo_layers.items():
|
||||
matching = True
|
||||
for layer in combo:
|
||||
if layer not in keyboard.active_layers:
|
||||
matching = False
|
||||
break
|
||||
|
||||
if matching:
|
||||
self._active_combo = combo
|
||||
keyboard.active_layers.insert(0, result)
|
||||
break
|
||||
|
||||
def _deactivate_combo_layer(self, keyboard, layer):
|
||||
if self._active_combo and layer in self._active_combo:
|
||||
keyboard.active_layers.remove(self.combo_layers[self._active_combo])
|
||||
self._active_combo = None
|
||||
|
@ -53,7 +53,7 @@ class OneShot(HoldTap):
|
||||
elif state.activated == ActivationType.INTERRUPTED:
|
||||
if is_pressed:
|
||||
send_buffer = True
|
||||
self.key_buffer.insert(0, (None, key, False))
|
||||
self.key_buffer.insert(0, (0, key, False))
|
||||
|
||||
if send_buffer:
|
||||
self.key_buffer.append((int_coord, current_key, is_pressed))
|
||||
|
@ -42,11 +42,12 @@ class Phrase:
|
||||
self._characters: list[Character] = []
|
||||
self._index: int = 0
|
||||
for char in string:
|
||||
key_code = KC[char]
|
||||
if key_code == KC.NO:
|
||||
try:
|
||||
key_code = KC[char]
|
||||
shifted = char.isupper() or key_code.has_modifiers == {2}
|
||||
self._characters.append(Character(key_code, shifted))
|
||||
except ValueError:
|
||||
raise ValueError(f'Invalid character in dictionary: {char}')
|
||||
shifted = char.isupper() or key_code.has_modifiers == {2}
|
||||
self._characters.append(Character(key_code, shifted))
|
||||
|
||||
def next_character(self) -> None:
|
||||
'''Increment the current index for this phrase'''
|
||||
|
@ -17,7 +17,7 @@ class TapDanceKeyMeta:
|
||||
ht_key = KC.HT(
|
||||
tap=key,
|
||||
hold=key,
|
||||
prefer_hold=True,
|
||||
prefer_hold=False,
|
||||
tap_interrupted=False,
|
||||
tap_time=self.tap_time,
|
||||
)
|
||||
|
@ -1,67 +0,0 @@
|
||||
'''
|
||||
Here we're abusing _asyncios TaskQueue to implement a very simple priority
|
||||
queue task scheduler.
|
||||
Despite documentation, Circuitpython doesn't usually ship with a min-heap
|
||||
module; it does however implement a pairing-heap for `TaskQueue` in native code.
|
||||
'''
|
||||
|
||||
try:
|
||||
from typing import Callable
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from _asyncio import Task, TaskQueue
|
||||
|
||||
from kmk.kmktime import ticks_add, ticks_diff
|
||||
|
||||
_task_queue = TaskQueue()
|
||||
|
||||
|
||||
class PeriodicTaskMeta:
|
||||
def __init__(self, func: Callable[[None], None], period: int) -> None:
|
||||
self._task = Task(self.call)
|
||||
self._coro = func
|
||||
self.period = period
|
||||
|
||||
def call(self) -> None:
|
||||
self._coro()
|
||||
after_ms = ticks_add(self._task.ph_key, self.period)
|
||||
_task_queue.push_sorted(self._task, after_ms)
|
||||
|
||||
|
||||
def create_task(
|
||||
func: Callable[[None], None],
|
||||
*,
|
||||
after_ms: int = 0,
|
||||
period_ms: int = 0,
|
||||
) -> [Task, PeriodicTaskMeta]:
|
||||
if period_ms:
|
||||
r = PeriodicTaskMeta(func, period_ms)
|
||||
t = r._task
|
||||
else:
|
||||
t = r = Task(func)
|
||||
|
||||
if after_ms:
|
||||
after_ms = ticks_add(ticks_ms(), after_ms)
|
||||
_task_queue.push_sorted(t, after_ms)
|
||||
else:
|
||||
_task_queue.push_head(t)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def get_due_task() -> [Callable, None]:
|
||||
while True:
|
||||
t = _task_queue.peek()
|
||||
if not t or ticks_diff(t.ph_key, ticks_ms()) > 0:
|
||||
break
|
||||
_task_queue.pop_head()
|
||||
yield t.coro
|
||||
|
||||
|
||||
def cancel_task(t: [Task, PeriodicTaskMeta]) -> None:
|
||||
if isinstance(t, PeriodicTaskMeta):
|
||||
t = t._task
|
||||
_task_queue.remove(t)
|
13
kmk/utils.py
13
kmk/utils.py
@ -1,8 +1,3 @@
|
||||
try:
|
||||
from typing import Optional
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
|
||||
@ -21,12 +16,8 @@ class Debug:
|
||||
def __init__(self, name: str = __name__):
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *message: str, name: Optional[str] = None) -> None:
|
||||
if not name:
|
||||
name = self.name
|
||||
print(ticks_ms(), end=' ')
|
||||
print(name, end=': ')
|
||||
print(*message, sep='')
|
||||
def __call__(self, message: str) -> None:
|
||||
print(f'{ticks_ms()} {self.name}: {message}')
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
|
@ -6,7 +6,6 @@ from kmk.keys import KC, ModifierKey
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.scanners import DiodeOrientation
|
||||
from kmk.scanners.digitalio import MatrixScanner
|
||||
from kmk.scheduler import _task_queue
|
||||
|
||||
|
||||
class DigitalInOut(Mock):
|
||||
@ -24,8 +23,6 @@ def code2name(code):
|
||||
|
||||
|
||||
class KeyboardTest:
|
||||
loop_delay_ms = 2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
modules,
|
||||
@ -82,7 +79,7 @@ class KeyboardTest:
|
||||
timeout = time.time_ns() + 10 * 1_000_000_000
|
||||
while timeout > time.time_ns():
|
||||
self.do_main_loop()
|
||||
if not _task_queue.peek() and not self.keyboard._resume_buffer:
|
||||
if not self.keyboard._timeouts and not self.keyboard._resume_buffer:
|
||||
break
|
||||
assert timeout > time.time_ns(), 'infinite loop detected'
|
||||
|
||||
@ -131,4 +128,4 @@ class KeyboardTest:
|
||||
|
||||
def do_main_loop(self):
|
||||
self.keyboard._main_loop()
|
||||
time.sleep(self.loop_delay_ms / 1000)
|
||||
time.sleep(0.002)
|
||||
|
@ -9,10 +9,6 @@ class KeyEvent:
|
||||
self.pressed = pressed
|
||||
|
||||
|
||||
def ticks_ms():
|
||||
return (time.time_ns() // 1_000_000) % (1 << 29)
|
||||
|
||||
|
||||
def init_circuit_python_modules_mocks():
|
||||
sys.modules['usb_hid'] = Mock()
|
||||
sys.modules['digitalio'] = Mock()
|
||||
@ -30,8 +26,4 @@ def init_circuit_python_modules_mocks():
|
||||
sys.modules['micropython'].const = lambda x: x
|
||||
|
||||
sys.modules['supervisor'] = Mock()
|
||||
sys.modules['supervisor'].ticks_ms = ticks_ms
|
||||
|
||||
from . import task
|
||||
|
||||
sys.modules['_asyncio'] = task
|
||||
sys.modules['supervisor'].ticks_ms = lambda: time.time_ns() // 1_000_000
|
||||
|
196
tests/task.py
196
tests/task.py
@ -1,196 +0,0 @@
|
||||
# MicroPython uasyncio module
|
||||
# MIT license; Copyright (c) 2019-2020 Damien P. George
|
||||
|
||||
# This file contains the core TaskQueue based on a pairing heap, and the core Task class.
|
||||
# They can optionally be replaced by C implementations.
|
||||
|
||||
# This file is a modified version, based on the extmod in Circuitpython, for
|
||||
# unit testing in KMK only.
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from kmk.kmktime import ticks_diff
|
||||
|
||||
cur_task = None
|
||||
__task_queue = None
|
||||
|
||||
|
||||
class CancelledError(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
# pairing-heap meld of 2 heaps; O(1)
|
||||
def ph_meld(h1, h2):
|
||||
if h1 is None:
|
||||
return h2
|
||||
if h2 is None:
|
||||
return h1
|
||||
lt = ticks_diff(h1.ph_key, h2.ph_key) < 0
|
||||
if lt:
|
||||
if h1.ph_child is None:
|
||||
h1.ph_child = h2
|
||||
else:
|
||||
h1.ph_child_last.ph_next = h2
|
||||
h1.ph_child_last = h2
|
||||
h2.ph_next = None
|
||||
h2.ph_rightmost_parent = h1
|
||||
return h1
|
||||
else:
|
||||
h1.ph_next = h2.ph_child
|
||||
h2.ph_child = h1
|
||||
if h1.ph_next is None:
|
||||
h2.ph_child_last = h1
|
||||
h1.ph_rightmost_parent = h2
|
||||
return h2
|
||||
|
||||
|
||||
# pairing-heap pairing operation; amortised O(log N)
|
||||
def ph_pairing(child):
|
||||
heap = None
|
||||
while child is not None:
|
||||
n1 = child
|
||||
child = child.ph_next
|
||||
n1.ph_next = None
|
||||
if child is not None:
|
||||
n2 = child
|
||||
child = child.ph_next
|
||||
n2.ph_next = None
|
||||
n1 = ph_meld(n1, n2)
|
||||
heap = ph_meld(heap, n1)
|
||||
return heap
|
||||
|
||||
|
||||
# pairing-heap delete of a node; stable, amortised O(log N)
|
||||
def ph_delete(heap, node):
|
||||
if node is heap:
|
||||
child = heap.ph_child
|
||||
node.ph_child = None
|
||||
return ph_pairing(child)
|
||||
# Find parent of node
|
||||
parent = node
|
||||
while parent.ph_next is not None:
|
||||
parent = parent.ph_next
|
||||
parent = parent.ph_rightmost_parent
|
||||
if parent is None or parent.ph_child is None:
|
||||
return heap
|
||||
# Replace node with pairing of its children
|
||||
if node is parent.ph_child and node.ph_child is None:
|
||||
parent.ph_child = node.ph_next
|
||||
node.ph_next = None
|
||||
return heap
|
||||
elif node is parent.ph_child:
|
||||
child = node.ph_child
|
||||
next = node.ph_next
|
||||
node.ph_child = None
|
||||
node.ph_next = None
|
||||
node = ph_pairing(child)
|
||||
parent.ph_child = node
|
||||
else:
|
||||
n = parent.ph_child
|
||||
while node is not n.ph_next:
|
||||
n = n.ph_next
|
||||
if not n:
|
||||
return heap
|
||||
child = node.ph_child
|
||||
next = node.ph_next
|
||||
node.ph_child = None
|
||||
node.ph_next = None
|
||||
node = ph_pairing(child)
|
||||
if node is None:
|
||||
node = n
|
||||
else:
|
||||
n.ph_next = node
|
||||
node.ph_next = next
|
||||
if next is None:
|
||||
node.ph_rightmost_parent = parent
|
||||
parent.ph_child_last = node
|
||||
return heap
|
||||
|
||||
|
||||
# TaskQueue class based on the above pairing-heap functions.
|
||||
class TaskQueue:
|
||||
def __init__(self):
|
||||
self.heap = None
|
||||
|
||||
def peek(self):
|
||||
return self.heap
|
||||
|
||||
def push_sorted(self, v, key):
|
||||
v.data = None
|
||||
v.ph_key = key
|
||||
v.ph_child = None
|
||||
v.ph_next = None
|
||||
self.heap = ph_meld(v, self.heap)
|
||||
|
||||
def push_head(self, v):
|
||||
self.push_sorted(v, ticks_ms())
|
||||
|
||||
def pop_head(self):
|
||||
v = self.heap
|
||||
self.heap = ph_pairing(v.ph_child)
|
||||
# v.ph_child = None
|
||||
return v
|
||||
|
||||
def remove(self, v):
|
||||
self.heap = ph_delete(self.heap, v)
|
||||
|
||||
|
||||
# Task class representing a coroutine, can be waited on and cancelled.
|
||||
class Task:
|
||||
def __init__(self, coro, globals=None):
|
||||
self.coro = coro # Coroutine of this Task
|
||||
self.data = None # General data for queue it is waiting on
|
||||
self.state = True # None, False, True or a TaskQueue instance
|
||||
self.ph_key = 0 # Pairing heap
|
||||
self.ph_child = None # Paring heap
|
||||
self.ph_child_last = None # Paring heap
|
||||
self.ph_next = None # Paring heap
|
||||
self.ph_rightmost_parent = None # Paring heap
|
||||
|
||||
def __await__(self):
|
||||
if not self.state:
|
||||
# Task finished, signal that is has been await'ed on.
|
||||
self.state = False
|
||||
elif self.state is True:
|
||||
# Allocated head of linked list of Tasks waiting on completion of this task.
|
||||
self.state = TaskQueue()
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not self.state:
|
||||
if self.data is None:
|
||||
# Task finished but has already been sent to the loop's exception handler.
|
||||
raise StopIteration
|
||||
else:
|
||||
# Task finished, raise return value to caller so it can continue.
|
||||
raise self.data
|
||||
else:
|
||||
# Put calling task on waiting queue.
|
||||
self.state.push_head(cur_task)
|
||||
# Set calling task's data to this task that it waits on, to double-link it.
|
||||
cur_task.data = self
|
||||
|
||||
def done(self):
|
||||
return not self.state
|
||||
|
||||
def cancel(self):
|
||||
# Check if task is already finished.
|
||||
if not self.state:
|
||||
return False
|
||||
# Can't cancel self (not supported yet).
|
||||
if self is cur_task:
|
||||
raise RuntimeError("can't cancel self")
|
||||
# If Task waits on another task then forward the cancel to the one it's waiting on.
|
||||
while isinstance(self.data, Task):
|
||||
self = self.data
|
||||
# Reschedule Task as a cancelled task.
|
||||
if hasattr(self.data, 'remove'):
|
||||
# Not on the main running queue, remove the task from the queue it's on.
|
||||
self.data.remove(self)
|
||||
__task_queue.push_head(self)
|
||||
elif ticks_diff(self.ph_key, ticks_ms()) > 0:
|
||||
# On the main running queue but scheduled in the future, so bring it forward to now.
|
||||
__task_queue.remove(self)
|
||||
__task_queue.push_head(self)
|
||||
self.data = CancelledError
|
||||
return True
|
@ -10,7 +10,7 @@ from tests.keyboard_test import KeyboardTest
|
||||
class TestCapsWord(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.kb = KeyboardTest(
|
||||
[CapsWord(timeout=2 * KeyboardTest.loop_delay_ms)],
|
||||
[CapsWord()],
|
||||
[
|
||||
[KC.CW, KC.A, KC.Z, KC.N1, KC.N0, KC.SPC],
|
||||
],
|
||||
|
@ -1,36 +1,28 @@
|
||||
import unittest
|
||||
|
||||
from kmk.keys import KC
|
||||
from kmk.modules.combos import Chord, Combo, Combos, Sequence
|
||||
from kmk.modules.combos import Chord, Combos, Sequence
|
||||
from kmk.modules.layers import Layers
|
||||
from tests.keyboard_test import KeyboardTest
|
||||
|
||||
|
||||
class TestCombo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.t_within = 2 * KeyboardTest.loop_delay_ms
|
||||
self.t_after = 7 * KeyboardTest.loop_delay_ms
|
||||
timeout = (self.t_after + self.t_within) // 2
|
||||
|
||||
# overide default timeouts
|
||||
Combo.timeout = timeout
|
||||
Sequence.timeout = timeout
|
||||
|
||||
combos = Combos()
|
||||
layers = Layers()
|
||||
KCMO = KC.MO(1)
|
||||
combos.combos = [
|
||||
Chord((KC.A, KC.B, KC.C), KC.Y),
|
||||
Chord((KC.A, KC.B), KC.X),
|
||||
Chord((KC.C, KC.D), KC.Z, timeout=2 * timeout),
|
||||
Chord((KC.C, KC.D), KC.Z, timeout=80),
|
||||
Chord((KC.C, KCMO), KC.Z),
|
||||
Chord((KC.F, KC.G), KC.Z, timeout=3 * timeout),
|
||||
Sequence((KC.N1, KC.N2, KC.N3), KC.Y),
|
||||
Sequence((KC.N1, KC.N2), KC.X),
|
||||
Sequence((KC.N3, KC.N4), KC.Z, timeout=2 * timeout),
|
||||
Sequence((KC.N1, KC.N1, KC.N1), KC.W),
|
||||
Sequence((KC.N3, KC.N2, KC.N1), KC.Y, fast_reset=False),
|
||||
Sequence((KC.LEADER, KC.N1), KC.V),
|
||||
Chord((KC.F, KC.G), KC.Z, timeout=130),
|
||||
Sequence((KC.N1, KC.N2, KC.N3), KC.Y, timeout=50),
|
||||
Sequence((KC.N1, KC.N2), KC.X, timeout=50),
|
||||
Sequence((KC.N3, KC.N4), KC.Z, timeout=100),
|
||||
Sequence((KC.N1, KC.N1, KC.N1), KC.W, timeout=50),
|
||||
Sequence((KC.N3, KC.N2, KC.N1), KC.Y, timeout=50, fast_reset=False),
|
||||
Sequence((KC.LEADER, KC.N1), KC.V, timeout=50),
|
||||
]
|
||||
self.keyboard = KeyboardTest(
|
||||
[combos, layers],
|
||||
@ -41,6 +33,9 @@ class TestCombo(unittest.TestCase):
|
||||
debug_enabled=False,
|
||||
)
|
||||
|
||||
self.t_within = 40
|
||||
self.t_after = 60
|
||||
|
||||
def test_chord(self):
|
||||
keyboard = self.keyboard
|
||||
t_within = self.t_within
|
||||
|
@ -10,37 +10,20 @@ class TestHoldTap(unittest.TestCase):
|
||||
def setUp(self):
|
||||
KC.clear()
|
||||
|
||||
self.t_within = 2 * KeyboardTest.loop_delay_ms
|
||||
self.t_after = 6 * KeyboardTest.loop_delay_ms
|
||||
tap_time = 5 * KeyboardTest.loop_delay_ms
|
||||
|
||||
# overide default timeouts
|
||||
HoldTap.tap_time = tap_time
|
||||
|
||||
def test_holdtap(self):
|
||||
t_within = self.t_within
|
||||
t_after = self.t_after
|
||||
|
||||
keyboard = KeyboardTest(
|
||||
[Layers(), HoldTap()],
|
||||
[
|
||||
[
|
||||
KC.HT(KC.A, KC.LCTL),
|
||||
KC.LT(1, KC.B),
|
||||
KC.C,
|
||||
KC.D,
|
||||
],
|
||||
[KC.HT(KC.A, KC.LCTL), KC.LT(1, KC.B), KC.C, KC.D],
|
||||
[KC.N1, KC.N2, KC.N3, KC.N4],
|
||||
],
|
||||
debug_enabled=False,
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'HT tap behaviour', [(0, True), t_within, (0, False)], [{KC.A}, {}]
|
||||
)
|
||||
keyboard.test('HT tap behaviour', [(0, True), 100, (0, False)], [{KC.A}, {}])
|
||||
|
||||
keyboard.test(
|
||||
'HT hold behaviour', [(0, True), t_after, (0, False)], [{KC.LCTL}, {}]
|
||||
'HT hold behaviour', [(0, True), 350, (0, False)], [{KC.LCTL}, {}]
|
||||
)
|
||||
|
||||
# TODO test multiple mods being held
|
||||
@ -48,74 +31,74 @@ class TestHoldTap(unittest.TestCase):
|
||||
# HT
|
||||
keyboard.test(
|
||||
'HT within tap time sequential -> tap behavior',
|
||||
[(0, True), t_within, (0, False), (3, True), (3, False)],
|
||||
[(0, True), 100, (0, False), (3, True), (3, False)],
|
||||
[{KC.A}, {}, {KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'HT within tap time rolling -> hold behavior',
|
||||
[(0, True), t_within, (3, True), t_after, (0, False), (3, False)],
|
||||
[(0, True), 100, (3, True), 250, (0, False), (3, False)],
|
||||
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'HT within tap time nested -> hold behavior',
|
||||
[(0, True), t_within, (3, True), (3, False), t_after, (0, False)],
|
||||
[(0, True), 100, (3, True), (3, False), 250, (0, False)],
|
||||
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'HT after tap time sequential -> hold behavior',
|
||||
[(0, True), t_after, (0, False), (3, True), (3, False)],
|
||||
[(0, True), 350, (0, False), (3, True), (3, False)],
|
||||
[{KC.LCTL}, {}, {KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'HT after tap time rolling -> hold behavior',
|
||||
[(0, True), t_after, (3, True), (0, False), (3, False)],
|
||||
[(0, True), 350, (3, True), (0, False), (3, False)],
|
||||
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'HT after tap time nested -> hold behavior',
|
||||
[(0, True), t_after, (3, True), (3, False), (0, False)],
|
||||
[(0, True), 350, (3, True), (3, False), (0, False)],
|
||||
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}],
|
||||
)
|
||||
|
||||
# LT
|
||||
keyboard.test(
|
||||
'LT within tap time sequential -> tap behavior',
|
||||
[(1, True), t_within, (1, False), (3, True), (3, False)],
|
||||
[(1, True), 100, (1, False), (3, True), (3, False)],
|
||||
[{KC.B}, {}, {KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'LT within tap time rolling -> tap behavior',
|
||||
[(1, True), t_within, (3, True), (1, False), (3, False)],
|
||||
[(1, True), 100, (3, True), 250, (1, False), (3, False)],
|
||||
[{KC.B}, {KC.B, KC.D}, {KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'LT within tap time nested -> tap behavior',
|
||||
[(1, True), t_within, (3, True), (3, False), (1, False)],
|
||||
[(1, True), 100, (3, True), (3, False), 250, (1, False)],
|
||||
[{KC.B}, {KC.B, KC.D}, {KC.B}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'LT after tap time sequential -> hold behavior',
|
||||
[(1, True), t_after, (1, False), (3, True), (3, False)],
|
||||
[(1, True), 350, (1, False), (3, True), (3, False)],
|
||||
[{KC.D}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'LT after tap time rolling -> hold behavior',
|
||||
[(1, True), t_after, (3, True), (1, False), (3, False)],
|
||||
[(1, True), 350, (3, True), (1, False), (3, False)],
|
||||
[{KC.N4}, {}],
|
||||
)
|
||||
|
||||
keyboard.test(
|
||||
'LT after tap time nested -> hold behavior',
|
||||
[(1, True), t_after, (3, True), (3, False), (1, False)],
|
||||
[(1, True), 350, (3, True), (3, False), (1, False)],
|
||||
[{KC.N4}, {}],
|
||||
)
|
||||
|
||||
@ -123,9 +106,9 @@ class TestHoldTap(unittest.TestCase):
|
||||
'LT after tap time nested -> hold behavior',
|
||||
[
|
||||
(0, True),
|
||||
t_after,
|
||||
350,
|
||||
(1, True),
|
||||
t_after,
|
||||
350,
|
||||
(3, True),
|
||||
(3, False),
|
||||
(1, False),
|
||||
@ -135,25 +118,26 @@ class TestHoldTap(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_holdtap_chain(self):
|
||||
t_after = self.t_after
|
||||
|
||||
keyboard = KeyboardTest(
|
||||
[HoldTap()],
|
||||
[
|
||||
[
|
||||
KC.N0,
|
||||
KC.HT(KC.N1, KC.LCTL),
|
||||
KC.HT(KC.N2, KC.LSFT, tap_interrupted=True),
|
||||
KC.HT(KC.N1, KC.LCTL, tap_time=50),
|
||||
KC.HT(KC.N2, KC.LSFT, tap_interrupted=True, tap_time=50),
|
||||
KC.HT(
|
||||
KC.N3,
|
||||
KC.LALT,
|
||||
prefer_hold=False,
|
||||
tap_interrupted=True,
|
||||
tap_time=50,
|
||||
),
|
||||
],
|
||||
],
|
||||
debug_enabled=False,
|
||||
)
|
||||
# t_within = 40
|
||||
t_after = 60
|
||||
|
||||
keyboard.test(
|
||||
'chained 0',
|
||||
@ -172,7 +156,7 @@ class TestHoldTap(unittest.TestCase):
|
||||
'chained 1',
|
||||
[(2, True), (1, True), (0, True), (0, False), (1, False), (2, False)],
|
||||
[
|
||||
{KC.LSFT},
|
||||
{KC.LCTL},
|
||||
{KC.LCTL, KC.LSFT},
|
||||
{KC.LCTL, KC.LSFT, KC.N0},
|
||||
{KC.LCTL, KC.LSFT},
|
||||
@ -224,7 +208,7 @@ class TestHoldTap(unittest.TestCase):
|
||||
'chained 5',
|
||||
[(3, True), (1, True), (0, True), (0, False), (1, False), (3, False)],
|
||||
[
|
||||
{KC.N3},
|
||||
{KC.LCTL},
|
||||
{KC.LCTL, KC.N3},
|
||||
{KC.LCTL, KC.N3, KC.N0},
|
||||
{KC.LCTL, KC.N3},
|
||||
@ -291,21 +275,21 @@ class TestHoldTap(unittest.TestCase):
|
||||
# TODO test TT
|
||||
|
||||
def test_holdtap_repeat(self):
|
||||
t_within = self.t_within
|
||||
t_after = self.t_after
|
||||
|
||||
keyboard = KeyboardTest(
|
||||
[HoldTap()],
|
||||
[
|
||||
[
|
||||
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.ALL),
|
||||
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.TAP),
|
||||
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.HOLD),
|
||||
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.ALL, tap_time=50),
|
||||
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.TAP, tap_time=50),
|
||||
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.HOLD, tap_time=50),
|
||||
]
|
||||
],
|
||||
debug_enabled=False,
|
||||
)
|
||||
|
||||
t_within = 40
|
||||
t_after = 60
|
||||
|
||||
keyboard.test(
|
||||
'repeat tap',
|
||||
[
|
||||
@ -317,6 +301,7 @@ class TestHoldTap(unittest.TestCase):
|
||||
(0, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
t_after,
|
||||
],
|
||||
[{KC.A}, {}, {KC.A}, {}, {KC.A}, {}],
|
||||
)
|
||||
@ -332,6 +317,7 @@ class TestHoldTap(unittest.TestCase):
|
||||
(0, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
t_after,
|
||||
],
|
||||
[{KC.B}, {}, {KC.B}, {}, {KC.B}, {}],
|
||||
)
|
||||
@ -348,6 +334,7 @@ class TestHoldTap(unittest.TestCase):
|
||||
t_after,
|
||||
(0, True),
|
||||
(0, False),
|
||||
t_after,
|
||||
],
|
||||
[{KC.A}, {}, {KC.B}, {}, {KC.A}, {}],
|
||||
)
|
@ -123,10 +123,12 @@ class TestKeys_dot(unittest.TestCase):
|
||||
assert primary_key is secondary_key
|
||||
|
||||
def test_invalid_key_upper(self):
|
||||
assert KC.INVALID_KEY == KC.NO
|
||||
with self.assertRaises(ValueError):
|
||||
KC.INVALID_KEY
|
||||
|
||||
def test_invalid_key_lower(self):
|
||||
assert KC.invalid_key == KC.NO
|
||||
with self.assertRaises(ValueError):
|
||||
KC.invalid_key
|
||||
|
||||
def test_custom_key(self):
|
||||
created = make_key(
|
||||
@ -166,10 +168,12 @@ class TestKeys_index(unittest.TestCase):
|
||||
assert upper_key is lower_key
|
||||
|
||||
def test_invalid_key_upper(self):
|
||||
assert KC.INVALID_KEY == KC.NO
|
||||
with self.assertRaises(ValueError):
|
||||
KC['NOT_A_VALID_KEY']
|
||||
|
||||
def test_invalid_key_lower(self):
|
||||
assert KC.invalid_key == KC.NO
|
||||
with self.assertRaises(ValueError):
|
||||
KC['not_a_valid_key']
|
||||
|
||||
def test_custom_key(self):
|
||||
created = make_key(
|
||||
@ -214,10 +218,10 @@ class TestKeys_get(unittest.TestCase):
|
||||
assert primary_key is secondary_key
|
||||
|
||||
def test_invalid_key_upper(self):
|
||||
assert KC.get('INVALID_KEY') is KC.NO
|
||||
assert KC.get('INVALID_KEY') is None
|
||||
|
||||
def test_invalid_key_lower(self):
|
||||
assert KC.get('not_a_valid_key') is KC.NO
|
||||
assert KC.get('not_a_valid_key') is None
|
||||
|
||||
def test_custom_key(self):
|
||||
created = make_key(
|
||||
|
@ -10,13 +10,8 @@ class TestLayers(unittest.TestCase):
|
||||
self.kb = KeyboardTest(
|
||||
[Layers()],
|
||||
[
|
||||
[
|
||||
KC.N0,
|
||||
KC.LM(1, KC.LCTL),
|
||||
KC.LT(1, KC.N2, tap_interrupted=True, prefer_hold=True),
|
||||
KC.LT(1, KC.N3, tap_interrupted=False, prefer_hold=True),
|
||||
],
|
||||
[KC.A, KC.B, KC.C, KC.D],
|
||||
[KC.N0, KC.LM(1, KC.LCTL)],
|
||||
[KC.A, KC.B],
|
||||
],
|
||||
debug_enabled=False,
|
||||
)
|
||||
@ -28,25 +23,6 @@ class TestLayers(unittest.TestCase):
|
||||
[{KC.LCTL}, {KC.LCTL, KC.A}, {KC.A}, {}],
|
||||
)
|
||||
|
||||
def test_layertap(self):
|
||||
self.kb.test(
|
||||
'Layertap roll',
|
||||
[(2, True), (0, True), (2, False), (0, False)],
|
||||
[{KC.N2}, {KC.N0, KC.N2}, {KC.N0}, {}],
|
||||
)
|
||||
|
||||
self.kb.test(
|
||||
'Layertap tap interrupted',
|
||||
[(2, True), (0, True), 200, (0, False), (2, False)],
|
||||
[{KC.A}, {}],
|
||||
)
|
||||
|
||||
self.kb.test(
|
||||
'Layertap tap interrupted by holdtap',
|
||||
[(3, True), (2, True), (2, False), (3, False)],
|
||||
[{KC.C}, {}],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -6,30 +6,25 @@ from kmk.modules.oneshot import OneShot
|
||||
from tests.keyboard_test import KeyboardTest
|
||||
|
||||
|
||||
class TestOneshot(unittest.TestCase):
|
||||
class TestHoldTap(unittest.TestCase):
|
||||
def test_oneshot(self):
|
||||
t_within = 2 * KeyboardTest.loop_delay_ms
|
||||
t_after = 7 * KeyboardTest.loop_delay_ms
|
||||
timeout = (t_after + t_within) // 2
|
||||
|
||||
# overide default timeouts
|
||||
OneShot.tap_time = timeout
|
||||
|
||||
keyboard = KeyboardTest(
|
||||
[Layers(), OneShot()],
|
||||
[
|
||||
[
|
||||
KC.OS(KC.MO(1)),
|
||||
KC.OS(KC.MO(1), tap_time=50),
|
||||
KC.MO(1),
|
||||
KC.C,
|
||||
KC.D,
|
||||
KC.OS(KC.E),
|
||||
KC.OS(KC.F),
|
||||
KC.OS(KC.E, tap_time=50),
|
||||
KC.OS(KC.F, tap_time=50),
|
||||
],
|
||||
[KC.N0, KC.N1, KC.N2, KC.N3, KC.OS(KC.LSFT), KC.TRNS],
|
||||
[KC.N0, KC.N1, KC.N2, KC.N3, KC.OS(KC.LSFT, tap_time=50), KC.TRNS],
|
||||
],
|
||||
debug_enabled=False,
|
||||
)
|
||||
t_within = 40
|
||||
t_after = 60
|
||||
|
||||
keyboard.test(
|
||||
'OS timed out',
|
||||
|
@ -37,8 +37,11 @@ class TestStickyMod(unittest.TestCase):
|
||||
[
|
||||
(4, True),
|
||||
(4, False),
|
||||
100,
|
||||
(4, True),
|
||||
200,
|
||||
(4, False),
|
||||
100,
|
||||
(1, True),
|
||||
(1, False),
|
||||
],
|
||||
@ -58,19 +61,26 @@ class TestStickyMod(unittest.TestCase):
|
||||
(1, True),
|
||||
(1, False),
|
||||
(2, True),
|
||||
200,
|
||||
(0, True),
|
||||
50,
|
||||
(0, False),
|
||||
50,
|
||||
(0, True),
|
||||
50,
|
||||
(0, False),
|
||||
(1, True),
|
||||
(1, False),
|
||||
50,
|
||||
(1, True),
|
||||
(1, False),
|
||||
(0, True),
|
||||
50,
|
||||
(0, False),
|
||||
(3, True),
|
||||
(3, False),
|
||||
(2, False),
|
||||
100,
|
||||
(4, True),
|
||||
(4, False),
|
||||
(1, True),
|
||||
|
@ -7,7 +7,6 @@ from tests.keyboard_test import KeyboardTest
|
||||
|
||||
class TestStringSubstitution(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.delay = KeyboardTest.loop_delay_ms
|
||||
self.symbols = '`-=[]\\;\',./~!@#$%^&*()_+{}|:\"<>?'
|
||||
self.everything = ALL_NUMBERS + ALL_ALPHAS + ALL_ALPHAS.lower() + self.symbols
|
||||
self.test_dictionary = {
|
||||
@ -47,21 +46,21 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
# that results in a corresponding match, as that key is never sent
|
||||
self.keyboard.test(
|
||||
'multi-character key, single-character value',
|
||||
[(0, True), (0, False), (0, True), (0, False), self.delay],
|
||||
[(0, True), (0, False), (0, True), (0, False), 50],
|
||||
[{KC.A}, {}, {KC.BACKSPACE}, {}, {KC.B}, {}],
|
||||
)
|
||||
# note: the pressed key is never sent here, as the event is
|
||||
# intercepted and the replacement is sent instead
|
||||
self.keyboard.test(
|
||||
'multi-character value, single-character key',
|
||||
[(1, True), (1, False), self.delay],
|
||||
[(1, True), (1, False), 50],
|
||||
[{KC.A}, {}, {KC.A}, {}],
|
||||
)
|
||||
# modifiers are force-released if there's a match,
|
||||
# so the keyup event for them isn't sent
|
||||
self.keyboard.test(
|
||||
'shifted alphanumeric or symbol in key and/or value',
|
||||
[(3, True), (2, True), (2, False), (3, False), self.delay],
|
||||
[(3, True), (2, True), (2, False), (3, False), 50],
|
||||
[{KC.LSHIFT}, {KC.LSHIFT, KC.N2}, {}],
|
||||
)
|
||||
self.keyboard.test(
|
||||
@ -75,7 +74,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
(5, False),
|
||||
(5, True),
|
||||
(5, False),
|
||||
self.delay,
|
||||
10,
|
||||
],
|
||||
[
|
||||
{KC.D},
|
||||
@ -94,7 +93,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
)
|
||||
self.keyboard.test(
|
||||
'the presence of non-shift modifiers prevents a multi-character match',
|
||||
[(4, True), (0, True), (0, False), (0, True), (0, False), (4, False)],
|
||||
[(4, True), (0, True), (0, False), (0, True), (0, False), (4, False), 50],
|
||||
[
|
||||
{KC.LCTRL},
|
||||
{KC.LCTRL, KC.A},
|
||||
@ -106,7 +105,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
)
|
||||
self.keyboard.test(
|
||||
'the presence of non-shift modifiers prevents a single-character match',
|
||||
[(4, True), (1, True), (1, False), (4, False)],
|
||||
[(4, True), (1, True), (1, False), (4, False), 50],
|
||||
[
|
||||
{KC.LCTRL},
|
||||
{KC.LCTRL, KC.B},
|
||||
@ -116,7 +115,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
)
|
||||
self.keyboard.test(
|
||||
'the presence of non-shift modifiers resets current potential matches',
|
||||
[(0, True), (0, False), (4, True), (0, True), (0, False), (4, False)],
|
||||
[(0, True), (0, False), (4, True), (0, True), (0, False), (4, False), 50],
|
||||
[
|
||||
{KC.A},
|
||||
{},
|
||||
@ -129,15 +128,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
|
||||
self.keyboard.test(
|
||||
'match found and replaced when there are preceding characters',
|
||||
[
|
||||
(5, True),
|
||||
(5, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
self.delay,
|
||||
],
|
||||
[(5, True), (5, False), (0, True), (0, False), (0, True), (0, False), 50],
|
||||
[
|
||||
{KC.C},
|
||||
{},
|
||||
@ -151,15 +142,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
)
|
||||
self.keyboard.test(
|
||||
'match found and replaced when there are trailing characters, and the trailing characters are sent',
|
||||
[
|
||||
(0, True),
|
||||
(0, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
(5, True),
|
||||
(5, False),
|
||||
self.delay,
|
||||
],
|
||||
[(0, True), (0, False), (0, True), (0, False), (5, True), (5, False), 50],
|
||||
[
|
||||
{KC.A},
|
||||
{},
|
||||
@ -173,7 +156,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
)
|
||||
self.keyboard.test(
|
||||
'no match',
|
||||
[(0, True), (0, False), (2, True), (2, False)],
|
||||
[(0, True), (0, False), (2, True), (2, False), 50],
|
||||
[
|
||||
{KC.A},
|
||||
{},
|
||||
@ -200,7 +183,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
(6, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
10 * self.delay,
|
||||
50,
|
||||
],
|
||||
[
|
||||
{KC.D},
|
||||
@ -258,7 +241,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
# send the unreachable match "cccc" after matching "ccc"
|
||||
(5, True),
|
||||
(5, False),
|
||||
self.delay,
|
||||
10,
|
||||
],
|
||||
[
|
||||
{KC.C},
|
||||
@ -289,7 +272,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
(0, True),
|
||||
(0, False),
|
||||
(7, False),
|
||||
self.delay,
|
||||
10,
|
||||
],
|
||||
[
|
||||
{KC.RSHIFT},
|
||||
@ -320,6 +303,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
(0, False),
|
||||
(4, False),
|
||||
(8, False),
|
||||
10,
|
||||
],
|
||||
[
|
||||
{KC.RALT},
|
||||
@ -341,6 +325,7 @@ class TestStringSubstitution(unittest.TestCase):
|
||||
(1, False),
|
||||
(3, False),
|
||||
(8, False),
|
||||
10,
|
||||
],
|
||||
[
|
||||
{KC.RALT},
|
||||
|
@ -9,39 +9,36 @@ from tests.keyboard_test import KeyboardTest
|
||||
|
||||
class TestTapDance(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.t_within = 2 * KeyboardTest.loop_delay_ms
|
||||
self.t_after = 10 * KeyboardTest.loop_delay_ms
|
||||
tap_time = (self.t_after + self.t_within) // 4 * 3
|
||||
|
||||
TapDance.tap_time = tap_time
|
||||
|
||||
self.keyboard = KeyboardTest(
|
||||
[Layers(), HoldTap(), TapDance()],
|
||||
[
|
||||
[
|
||||
KC.TD(KC.N0, KC.N1),
|
||||
KC.TD(KC.N0, KC.N1, tap_time=50),
|
||||
KC.TD(
|
||||
KC.HT(KC.N1, KC.A),
|
||||
KC.HT(KC.N2, KC.B, tap_time=2 * tap_time),
|
||||
KC.HT(KC.N1, KC.A, tap_time=50),
|
||||
KC.HT(KC.N2, KC.B, tap_time=100),
|
||||
),
|
||||
KC.TD(KC.HT(KC.X, KC.Y), KC.X, tap_time=0),
|
||||
KC.TD(KC.LT(1, KC.N3), KC.X, tap_time=0),
|
||||
KC.TD(KC.HT(KC.X, KC.Y, tap_time=50), KC.X, tap_time=0),
|
||||
KC.TD(KC.LT(1, KC.N3, tap_time=50), KC.X, tap_time=0),
|
||||
KC.N4,
|
||||
],
|
||||
[KC.N9, KC.N8, KC.N7, KC.N6, KC.N5],
|
||||
],
|
||||
debug_enabled=False,
|
||||
)
|
||||
self.t_within = 40
|
||||
self.t_after = 60
|
||||
|
||||
def test_normal_key(self):
|
||||
keyboard = self.keyboard
|
||||
t_within = self.t_within
|
||||
t_after = self.t_after
|
||||
|
||||
keyboard.test('Tap x1', [(0, True), (0, False)], [{KC.N0}, {}])
|
||||
keyboard.test('Tap x1', [(0, True), (0, False), t_after], [{KC.N0}, {}])
|
||||
|
||||
keyboard.test(
|
||||
'Tap x2',
|
||||
[(0, True), (0, False), t_within, (0, True), (0, False)],
|
||||
[(0, True), (0, False), t_within, (0, True), (0, False), t_after],
|
||||
[{KC.N1}, {}],
|
||||
)
|
||||
|
||||
@ -54,6 +51,7 @@ class TestTapDance(unittest.TestCase):
|
||||
(0, False),
|
||||
(0, True),
|
||||
(0, False),
|
||||
t_after,
|
||||
],
|
||||
[{KC.N1}, {}, {KC.N0}, {}],
|
||||
)
|
||||
@ -95,11 +93,11 @@ class TestTapDance(unittest.TestCase):
|
||||
t_within = self.t_within
|
||||
t_after = self.t_after
|
||||
|
||||
keyboard.test('Tap x1', [(1, True), (1, False)], [{KC.N1}, {}])
|
||||
keyboard.test('Tap x1', [(1, True), (1, False), t_after], [{KC.N1}, {}])
|
||||
|
||||
keyboard.test(
|
||||
'Tap x2',
|
||||
[(1, True), (1, False), t_within, (1, True), (1, False)],
|
||||
[(1, True), (1, False), t_within, (1, True), (1, False), 2 * t_after],
|
||||
[{KC.N2}, {}],
|
||||
)
|
||||
|
||||
@ -133,7 +131,7 @@ class TestTapDance(unittest.TestCase):
|
||||
|
||||
keyboard.test(
|
||||
'',
|
||||
[(0, True), (0, False), t_within, (1, True), (1, False)],
|
||||
[(0, True), (0, False), t_within, (1, True), (1, False), t_after],
|
||||
[{KC.N0}, {}, {KC.N1}, {}],
|
||||
)
|
||||
|
||||
@ -147,6 +145,7 @@ class TestTapDance(unittest.TestCase):
|
||||
(2, False),
|
||||
t_after,
|
||||
(0, False),
|
||||
t_after,
|
||||
],
|
||||
[{KC.N1}, {KC.N1, KC.X}, {KC.N1}, {}],
|
||||
)
|
||||
@ -161,6 +160,7 @@ class TestTapDance(unittest.TestCase):
|
||||
(0, False),
|
||||
t_after,
|
||||
(2, False),
|
||||
t_after,
|
||||
],
|
||||
[{KC.X}, {KC.X, KC.N0}, {KC.X}, {}],
|
||||
)
|
||||
@ -172,7 +172,7 @@ class TestTapDance(unittest.TestCase):
|
||||
|
||||
keyboard.test(
|
||||
'',
|
||||
[(3, True), (3, False), t_within, (1, True), (1, False)],
|
||||
[(3, True), (3, False), t_within, (1, True), (1, False), t_after],
|
||||
[{KC.N3}, {}, {KC.N1}, {}],
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user