Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a0a471d
add linux debounce with preprocessor loop
Loosetooth Apr 9, 2025
3e2abb9
add linux_debounce_duration_ms config setting
Loosetooth Apr 10, 2025
16cffef
add documentation for linux-debounce-duration
Loosetooth Apr 10, 2025
55fcd50
add terminating newline, remove tabs on empty lines
Loosetooth Apr 10, 2025
87114e9
do not use preprocessor when debounce time is 0
Loosetooth Apr 10, 2025
e12fdc9
use Arc<Mutex<u16>> for linux_debounce_duration, read debounce_durati…
Loosetooth Apr 10, 2025
10b6d36
first iteration of asym_eager_defer_pk
Loosetooth Apr 14, 2025
8dcab4e
improve debounce loop efficiency
Loosetooth Apr 14, 2025
ef6491d
only start pre processing loop if debounce duration is larger than 0
Loosetooth Apr 14, 2025
e9f2b96
use factory function to create debounce algorithm instance, add info log
Loosetooth Apr 14, 2025
a1ab4a4
return if there are pending events from process_event function
Loosetooth Apr 14, 2025
5f4aedd
use a setting for the debounce algorithm, add name and debounce_time …
Loosetooth Apr 15, 2025
ac5074b
add sym_eager_pk debounce algorithm
Loosetooth Apr 15, 2025
4ad4b24
add a better debounce config example
Loosetooth Apr 15, 2025
a62c7e4
add trailing newlines
Loosetooth Apr 15, 2025
7624c0c
add sym_defer_pk debounce algorithm
Loosetooth Apr 15, 2025
3ae9958
remove unused imports, fix warnings
Loosetooth Apr 15, 2025
91e8937
actually debounce release events in asym_eager_defer_pk
Loosetooth Apr 15, 2025
b876e73
replace info logs by debug
Loosetooth Apr 17, 2025
3387f5a
use enum for debounce algorithm
Loosetooth Apr 23, 2025
d582f12
avoid busy looping
Loosetooth Apr 23, 2025
a08eb3f
update docs
Loosetooth Apr 23, 2025
a1a1206
remove linux prefix from settings and variables
Loosetooth Apr 23, 2025
15c057b
add tests for debounce algorithms
Loosetooth Apr 24, 2025
c7beac3
use existing count_ms_elapsed function instead of own implementation
Loosetooth May 4, 2025
720af90
use vector for deferred events, to ensure same release order
Loosetooth May 4, 2025
c5e9728
correct sym_defer_pk to update deadline on debounced events
Loosetooth May 4, 2025
0ed1f8e
use imported thread and duration in tests
Loosetooth May 4, 2025
48ffff0
sleep fixed duration
Loosetooth May 5, 2025
3e2d752
correct usage of count_ms_elapsed
Loosetooth May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ If you need help, please feel welcome to ask in the GitHub discussions.
;; linux-output-device-bus-type USB
;; linux-output-device-bus-type I8042

;; On Linux, it is possible to configure a debounce algorithm.
;; This is useful in the case of keyboard 'chatter' or 'double taps' due to faulty hardware.
;; The default algorithm is "asym_eager_defer_pk".
;; This feature is not live-reloadable.
;;
;; Example: Set debounce duration to 50ms.
;; debounce-duration 50
;;
;; Example: Set the debounce algorithm to "asym_eager_defer_pk".
;; debounce-algorithm asym_eager_defer_pk
;;
;; Supported debounce algorithms:
;; - asym_eager_defer_pk
;; - sym_eager_pk
;; - sym_defer_pk

;; Example configuration combining both:
;; debounce-algorithm sym_eager_pk
;; debounce-duration 50

;; There is an optional configuration entry for Windows to help mitigate strange
;; behaviour of AltGr if your layout uses that. Uncomment one of the items below
;; to change what kanata does with the key.
Expand Down
21 changes: 21 additions & 0 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4127,6 +4127,27 @@ Thus the output bus type is configurable.
)
----

[[linux-only-debounce-duration]]
=== Linux only: debounce-duration

This option allows you to configure the debounce duration for key press events in milliseconds.
Debouncing prevents rapid repeated key presses from being processed too quickly.
This can be handy in the case of 'chatter' or 'double taps',
where a single press of the keyboard results in multiple key press events because of faulty hardware.

The following different debouncing algorithms are available:
- "asym_eager_defer_pk" (Default)
- "sym_eager_pk"
- "sym_defer_pk"

.Example:
[source]
----
(defcfg
debounce-duration 50 ;; Set debounce duration to 50ms
)
----

[[macos-only-macos-dev-names-include]]
=== macOS only: macos-dev-names-include

Expand Down
35 changes: 35 additions & 0 deletions parser/src/cfg/debounce_algorithm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@


#[cfg(any(target_os = "linux", target_os = "unknown"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DebounceAlgorithm {
AsymEagerDeferPk,
SymEagerPk,
SymDeferPk,
}

#[cfg(any(target_os = "linux", target_os = "unknown"))]
impl std::str::FromStr for DebounceAlgorithm {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"asym_eager_defer_pk" => Ok(DebounceAlgorithm::AsymEagerDeferPk),
"sym_eager_pk" => Ok(DebounceAlgorithm::SymEagerPk),
"sym_defer_pk" => Ok(DebounceAlgorithm::SymDeferPk),
_ => Err(format!("Unknown debounce algorithm: {}", s)),
}
}
}

#[cfg(any(target_os = "linux", target_os = "unknown"))]
impl std::fmt::Display for DebounceAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let algorithm_name = match self {
DebounceAlgorithm::AsymEagerDeferPk => "asym_eager_defer_pk",
DebounceAlgorithm::SymEagerPk => "sym_eager_pk",
DebounceAlgorithm::SymDeferPk => "sym_defer_pk",
};
write!(f, "{}", algorithm_name)
}
}
21 changes: 21 additions & 0 deletions parser/src/cfg/defcfg.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::debounce_algorithm::DebounceAlgorithm;
use super::sexpr::SExpr;
use super::HashSet;
use super::{error::*, TrimAtomQuotes};
Expand Down Expand Up @@ -34,6 +35,8 @@ pub struct CfgLinuxOptions {
pub linux_use_trackpoint_property: bool,
pub linux_output_bus_type: LinuxCfgOutputBusType,
pub linux_device_detect_mode: Option<DeviceDetectMode>,
pub debounce_duration_ms: u16,
pub debounce_algorithm: DebounceAlgorithm,
}
#[cfg(any(target_os = "linux", target_os = "unknown"))]
impl Default for CfgLinuxOptions {
Expand All @@ -51,6 +54,8 @@ impl Default for CfgLinuxOptions {
linux_use_trackpoint_property: false,
linux_output_bus_type: LinuxCfgOutputBusType::BusI8042,
linux_device_detect_mode: None,
debounce_duration_ms: 0,
debounce_algorithm: DebounceAlgorithm::AsymEagerDeferPk,
}
}
}
Expand Down Expand Up @@ -393,6 +398,22 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
cfg.linux_opts.linux_device_detect_mode = detect_mode;
}
}
"debounce-duration" => {
#[cfg(any(target_os = "linux", target_os = "unknown"))]
{
cfg.linux_opts.debounce_duration_ms =
parse_cfg_val_u16(val, label, false)?;
}
}
"debounce-algorithm" => {
#[cfg(any(target_os = "linux", target_os = "unknown"))]
{
let algorithm = sexpr_to_str_or_err(val, label)?;
cfg.linux_opts.debounce_algorithm = algorithm
.parse::<DebounceAlgorithm>()
.map_err(|e| anyhow_expr!(val, "{}", e))?;
}
}
"windows-altgr" => {
#[cfg(any(target_os = "windows", target_os = "unknown"))]
{
Expand Down
2 changes: 2 additions & 0 deletions parser/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ use kanata_keyberon::key_code::*;
use kanata_keyberon::layout::*;
use sexpr::*;

pub mod debounce_algorithm;

#[cfg(test)]
mod tests;
#[cfg(test)]
Expand Down
101 changes: 101 additions & 0 deletions src/kanata/debounce/asym_eager_defer_pk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::collections::HashMap;
use std::time::{Duration, Instant};
use kanata_parser::cfg::debounce_algorithm::DebounceAlgorithm;

use crate::kanata::{KeyEvent, KeyValue, OsCode};
use std::sync::mpsc::SyncSender as Sender;
use crate::kanata::debounce::debounce::{try_send_panic, Debounce};

/// Implementation of the asym_eager_defer_pk algorithm
/// See: https://github.com/qmk/qmk_firmware/blob/6ef97172889ccd5db376b2a9f8825489e24fdac4/docs/feature_debounce_type.md
pub struct AsymEagerDeferPk {
debounce_duration: Duration,
last_key_event_time: HashMap<OsCode, Instant>,
release_deadlines: Vec<(OsCode, Instant)>,
}

impl AsymEagerDeferPk {
pub fn new(debounce_duration_ms: u16) -> Self {
Self {
debounce_duration: Duration::from_millis(debounce_duration_ms.into()),
last_key_event_time: HashMap::new(),
release_deadlines: Vec::new(), // Initialize as an empty Vec
}
}
}

impl Debounce for AsymEagerDeferPk {
fn name(&self) -> DebounceAlgorithm {
DebounceAlgorithm::AsymEagerDeferPk
}

fn debounce_time(&self) -> u16 {
self.debounce_duration.as_millis() as u16
}

fn process_event(&mut self, event: KeyEvent, process_tx: &Sender<KeyEvent>) -> bool {
let now = Instant::now();
let oscode = event.code;

match event.value {
KeyValue::Press => {
// Cancel any pending release for this key
self.release_deadlines.retain(|(code, _)| *code != oscode);

// Check if the key press is within the debounce duration
if let Some(&last_time) = self.last_key_event_time.get(&oscode) {
if now.duration_since(last_time) < self.debounce_duration {
log::debug!("Debouncing key press for {:?}", oscode);
return !self.release_deadlines.is_empty(); // Skip processing this event
}
}

// Eagerly process key-down events
self.last_key_event_time.insert(oscode, now);
try_send_panic(process_tx, event);
}
KeyValue::Release => {
// Check if pending release event is already scheduled
if self.release_deadlines.iter().any(|(code, _)| *code == oscode) {
log::debug!("Release event already scheduled for {:?}", oscode);
return !self.release_deadlines.is_empty(); // Skip processing this event
}
// Schedule the release event for later
self.release_deadlines.push((oscode, now + self.debounce_duration));
}
KeyValue::Repeat => {
// Forward repeat events immediately
log::debug!("Forwarding repeat event for {:?}", oscode);
try_send_panic(process_tx, event);
}
_ => {
// Forward other key events without debouncing
log::debug!("Forwarding other event for {:?}", oscode);
try_send_panic(process_tx, event);
}
}

// Return true if there are still pending deadlines
!self.release_deadlines.is_empty()
}

fn tick(&mut self, process_tx: &Sender<KeyEvent>, now: Instant) -> bool {
// Process any release events whose deadlines have passed
self.release_deadlines.retain(|(oscode, deadline)| {
if now >= *deadline {
log::debug!("Emitting key release for {:?}", oscode);
let release_event = KeyEvent {
code: *oscode,
value: KeyValue::Release,
};
try_send_panic(process_tx, release_event);
false // Remove this item from the Vec
} else {
true // Keep this item in the Vec
}
});

// Return true if there are still pending deadlines
!self.release_deadlines.is_empty()
}
}
37 changes: 37 additions & 0 deletions src/kanata/debounce/debounce.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::{kanata::KeyEvent, sym_defer_pk::SymDeferPk, sym_eager_pk::SymEagerPk};
use std::{sync::mpsc::SyncSender as Sender, time::Instant};
use crate::kanata::debounce::asym_eager_defer_pk::AsymEagerDeferPk;
use kanata_parser::cfg::debounce_algorithm::DebounceAlgorithm;

/// Trait for debounce algorithms
pub trait Debounce: Send + Sync {
/// Returns the name of the debounce algorithm
fn name(&self) -> DebounceAlgorithm;

/// Returns the debounce time in milliseconds
fn debounce_time(&self) -> u16;

fn process_event(&mut self, event: KeyEvent, process_tx: &Sender<KeyEvent>) -> bool;

/// Optional tick function to process delayed events (deadlines),
/// returns whether there are pending events
fn tick(&mut self, _process_tx: &Sender<KeyEvent>, _now: Instant) -> bool {
return false; // Default implementation: no pending events
}
}

/// Factory function to create debounce algorithm instances
pub fn create_debounce_algorithm(algorithm: DebounceAlgorithm, debounce_duration_ms: u16) -> Box<dyn Debounce> {
match algorithm {
DebounceAlgorithm::AsymEagerDeferPk => Box::new(AsymEagerDeferPk::new(debounce_duration_ms)),
DebounceAlgorithm::SymEagerPk => Box::new(SymEagerPk::new(debounce_duration_ms)),
DebounceAlgorithm::SymDeferPk => Box::new(SymDeferPk::new(debounce_duration_ms)),
}
}

/// Helper function to send events and panic on failure
pub fn try_send_panic(tx: &Sender<KeyEvent>, kev: KeyEvent) {
if let Err(e) = tx.try_send(kev) {
panic!("failed to send on channel: {e:?}");
}
}
7 changes: 7 additions & 0 deletions src/kanata/debounce/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod debounce;
pub mod asym_eager_defer_pk;
pub mod sym_eager_pk;
pub mod sym_defer_pk;

#[cfg(test)]
mod test;
88 changes: 88 additions & 0 deletions src/kanata/debounce/sym_defer_pk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use std::time::{Duration, Instant};
use kanata_parser::cfg::debounce_algorithm::DebounceAlgorithm;

use crate::kanata::{KeyEvent, KeyValue};
use std::sync::mpsc::SyncSender as Sender;
use crate::kanata::debounce::debounce::{try_send_panic, Debounce};

/// Implementation of the sym_defer_pk algorithm
/// Debouncing per key. On any state change, a per-key timer is set.
/// When DEBOUNCE milliseconds of no changes have occurred on that key,
/// the key status change is pushed.
pub struct SymDeferPk {
debounce_duration: Duration,
pending_events: Vec<(KeyEvent, Instant)>,
}

impl SymDeferPk {
pub fn new(debounce_duration_ms: u16) -> Self {
Self {
debounce_duration: Duration::from_millis(debounce_duration_ms.into()),
pending_events: Vec::new(),
}
}
}

impl Debounce for SymDeferPk {
fn name(&self) -> DebounceAlgorithm {
DebounceAlgorithm::SymDeferPk
}

fn debounce_time(&self) -> u16 {
self.debounce_duration.as_millis() as u16
}

fn process_event(&mut self, event: KeyEvent, process_tx: &Sender<KeyEvent>) -> bool {
let now = Instant::now();
let oscode = event.code;

match event.value {
KeyValue::Repeat => {
// Forward repeat events immediately
log::debug!("Forwarding repeat event for {:?}", oscode);
try_send_panic(process_tx, event);
}
_ => {
let new_deadline = now + self.debounce_duration;

// Check if there is a pending event for this key
if let Some(pos) = self.pending_events.iter().position(|(pending_event, _)| pending_event.code == oscode) {
// If the event is already pending, update the deadline
log::debug!(
"Updating pending event for {:?} (value: {:?}) to new deadline: {:?}",
oscode,
event.value,
new_deadline
);
self.pending_events[pos].1 = new_deadline;
} else {
// No pending event for this key. Add the new event.
log::debug!(
"Deferring event for {:?} (value: {:?}) until debounce duration passes",
oscode, event.value
);
self.pending_events.push((event, new_deadline));
}
}
}

// Return true if there are still pending events
!self.pending_events.is_empty()
}

fn tick(&mut self, process_tx: &Sender<KeyEvent>, now: Instant) -> bool {
// Process any events whose debounce duration has passed
self.pending_events.retain(|(event, deadline)| {
if now >= *deadline {
log::debug!("Emitting deferred event for {:?}", event.code);
try_send_panic(process_tx, event.clone());
false // Remove this item from the Vec
} else {
true // Keep this item in the Vec
}
});

// Return true if there are still pending events
!self.pending_events.is_empty()
}
}
Loading