Skip to content

Commit

Permalink
feat: Extras, tap-hold, and modifier keys
Browse files Browse the repository at this point in the history
Initial implementation of
#146.

Backend representation for mod-tap, layer-tap, and modifiers+basic
keycode, with conversion to and from text format and numeric keycode.

Add shift-select for modifiers with a non-modifier key, extras tab, and
tap-hold section.

Sensitivity needs to be set based on whether shift is held and what is
already selected. Getting this to work properly is awkward with how GTK
modifiers are handled. More things need to be added to extras, the
layout for basic keys needs to be updated for the latest design, and
various style details need to be addressed. And it needs to not show
these things for internal keycodes.
  • Loading branch information
ids1024 committed Aug 30, 2022
1 parent 8527120 commit 3b84291
Show file tree
Hide file tree
Showing 20 changed files with 1,170 additions and 200 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ members = [ "tools", "ffi", "backend", "widgets" ]
[dependencies]
cascade = "1"
futures = "0.3.13"
gtk = { version = "0.15.0", features = ["v3_22"] }
# 3.24 is packaged by focal
gtk = { version = "0.15.0", features = ["v3_24"] }
libc = "0.2"
once_cell = "1.4"
pangocairo = "0.15.0"
Expand Down
1 change: 1 addition & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ license = "GPL-3.0-or-later"
edition = "2018"

[dependencies]
bitflags = "1.3.2"
cascade = "1"
futures = "0.3.13"
futures-timer = "3.0.2"
Expand Down
7 changes: 6 additions & 1 deletion backend/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,12 @@ impl Board {
let mut key_leds = HashMap::new();
for key in self.keys().iter() {
let scancodes = (0..self.layout().meta.num_layers as usize)
.map(|layer| key.get_scancode(layer).unwrap().1)
.map(|layer| {
key.get_scancode(layer)
.unwrap()
.1
.map_or_else(String::new, |x| x.to_string())
})
.collect();
map.insert(key.logical_name.clone(), scancodes);
if !key.leds.is_empty() {
Expand Down
13 changes: 6 additions & 7 deletions backend/src/key.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use glib::prelude::*;
use std::cell::Cell;

use crate::{Board, Daemon, Hs, PhysicalLayoutKey, Rect, Rgb};
use crate::{Board, Daemon, Hs, Keycode, PhysicalLayoutKey, Rect, Rgb};

#[derive(Debug)]
pub struct Key {
Expand Down Expand Up @@ -143,17 +143,16 @@ impl Key {
Ok(())
}

pub fn get_scancode(&self, layer: usize) -> Option<(u16, String)> {
// TODO: operate on keycode enum
pub fn get_scancode(&self, layer: usize) -> Option<(u16, Option<Keycode>)> {
let board = self.board();
let scancode = self.scancodes.get(layer)?.get();
let scancode_name = match board.layout().scancode_to_name(scancode) {
Some(some) => some,
None => String::new(),
};
let scancode_name = board.layout().scancode_to_name(scancode);
Some((scancode, scancode_name))
}

pub async fn set_scancode(&self, layer: usize, scancode_name: &str) -> Result<(), String> {
// TODO: operate on keycode enum
pub async fn set_scancode(&self, layer: usize, scancode_name: &Keycode) -> Result<(), String> {
let board = self.board();
let scancode = board
.layout()
Expand Down
250 changes: 250 additions & 0 deletions backend/src/keycode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// has_keycode logic; convert to/from int
// serialize (Display?)/format
// serde: serialize/deserialize as string

use bitflags::bitflags;

bitflags! {
pub struct Mods: u16 {
const CTRL = 0x1;
const SHIFT = 0x2;
const ALT = 0x4;
const SUPER = 0x8;

const RIGHT = 0x10;

const RIGHT_CTRL = Self::RIGHT.bits | Self::CTRL.bits;
const RIGHT_SHIFT = Self::RIGHT.bits | Self::SHIFT.bits;
const RIGHT_ALT = Self::RIGHT.bits | Self::ALT.bits;
const RIGHT_SUPER = Self::RIGHT.bits | Self::SUPER.bits;
}
}

impl Default for Mods {
fn default() -> Self {
Self::empty()
}
}

impl Mods {
// Convert single modifier from name
pub fn from_mod_str(s: &str) -> Option<Self> {
match s {
"LEFT_CTRL" => Some(Self::CTRL),
"LEFT_SHIFT" => Some(Self::SHIFT),
"LEFT_ALT" => Some(Self::ALT),
"LEFT_SUPER" => Some(Self::SUPER),
"RIGHT_CTRL" => Some(Self::RIGHT_CTRL),
"RIGHT_SHIFT" => Some(Self::RIGHT_SHIFT),
"RIGHT_ALT" => Some(Self::RIGHT_ALT),
"RIGHT_SUPER" => Some(Self::RIGHT_SUPER),
_ => None,
}
}

// Convert to single modifier
pub(crate) fn as_mod_str(self) -> Option<&'static str> {
match self {
Self::CTRL => Some("LEFT_CTRL"),
Self::SHIFT => Some("LEFT_SHIFT"),
Self::ALT => Some("LEFT_ALT"),
Self::SUPER => Some("LEFT_SUPER"),
Self::RIGHT_CTRL => Some("RIGHT_CTRL"),
Self::RIGHT_SHIFT => Some("RIGHT_SHIFT"),
Self::RIGHT_ALT => Some("RIGHT_ALT"),
Self::RIGHT_SUPER => Some("RIGHT_SUPER"),
_ => None,
}
}

pub fn mod_names(self) -> impl Iterator<Item = &'static str> {
[Self::CTRL, Self::SHIFT, Self::ALT, Self::SUPER]
.iter()
.filter_map(move |i| (self & (*i | Self::RIGHT)).as_mod_str())
}

pub fn toggle_mod(self, other: Self) -> Self {
let other_key = other & !Self::RIGHT;
if !self.contains(other_key) {
self | other
} else {
let key = self & !other_key;
if key == Self::RIGHT {
Self::empty()
} else {
key
}
}
}
}

#[derive(Debug, Hash, PartialEq, Eq, Clone, glib::Boxed)]
#[boxed_type(name = "S76Keycode")]
pub enum Keycode {
Basic(Mods, String),
MT(Mods, String),
LT(u8, String),
}

impl Keycode {
pub fn parse(s: &str) -> Option<Self> {
let mut tokens = tokenize(s);
match tokens.next()? {
"MT" => parse_mt(tokens),
"LT" => parse_lt(tokens),
keycode => parse_basic(tokenize(s)),
}
}

pub fn none() -> Self {
Self::Basic(Mods::empty(), "NONE".to_string())
}

pub fn is_none(&self) -> bool {
if let Keycode::Basic(mode, keycode) = self {
mode.is_empty() && keycode.as_str() == "NONE"
} else {
false
}
}

pub fn is_roll_over(&self) -> bool {
if let Keycode::Basic(mode, keycode) = self {
mode.is_empty() && keycode.as_str() == "ROLL_OVER"
} else {
false
}
}
}

impl std::fmt::Display for Keycode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Basic(mods, scancode_name) => {
let mut has_mod = false;
for mod_name in mods.mod_names() {
if has_mod {
write!(f, " | ")?;
}
write!(f, "{}", mod_name)?;
}
if !(scancode_name == "NONE" && has_mod) {
write!(f, "{}", scancode_name)?;
}
}
Self::MT(mods, scancode_name) => {
write!(f, "MT(")?;
let mut has_mod = false;
for mod_name in mods.mod_names() {
if has_mod {
write!(f, " | ")?;
}
write!(f, "{}", mod_name)?;
}
write!(f, ", {})", scancode_name)?;
}
Self::LT(layer, scancode_name) => {
write!(f, "LT({}, {})", layer, scancode_name)?;
}
}
Ok(())
}
}

const SEPARATORS: &[char] = &[',', '|', '(', ')'];

// Tokenize into iterator of &str, splitting on whitespace and putting
// separators in their own tokens.
fn tokenize(mut s: &str) -> impl Iterator<Item = &str> {
std::iter::from_fn(move || {
s = s.trim_start_matches(' ');
let idx = if SEPARATORS.contains(&s.chars().next()?) {
1
} else {
s.find(|c| c == ' ' || SEPARATORS.contains(&c))
.unwrap_or(s.len())
};
let tok = &s[..idx];
s = &s[idx..];
Some(tok)
})
}

fn parse_mt<'a>(mut tokens: impl Iterator<Item = &'a str>) -> Option<Keycode> {
if tokens.next() != Some("(") {
return None;
}

let mut mods = Mods::empty();
loop {
mods |= Mods::from_mod_str(tokens.next()?)?;
match tokens.next()? {
"|" => {}
"," => {
break;
}
_ => {
return None;
}
}
}

let keycode = tokens.next()?.to_string();

if (tokens.next(), tokens.next()) != (Some(")"), None) {
return None;
}

Some(Keycode::MT(mods, keycode))
}

fn parse_lt<'a>(mut tokens: impl Iterator<Item = &'a str>) -> Option<Keycode> {
if tokens.next() != Some("(") {
return None;
}

let layer = tokens.next()?.parse().ok()?;

if tokens.next() != Some(",") {
return None;
}

let keycode = tokens.next()?.to_string();

if (tokens.next(), tokens.next()) != (Some(")"), None) {
return None;
}

Some(Keycode::LT(layer, keycode))
}

// XXX limit to basic if there are mods?
fn parse_basic<'a>(mut tokens: impl Iterator<Item = &'a str>) -> Option<Keycode> {
let mut mods = Mods::empty();
let mut keycode = None;

loop {
let token = tokens.next()?;
if let Some(mod_) = Mods::from_mod_str(token) {
mods |= mod_;
} else if keycode.is_none() && token.chars().next()?.is_alphanumeric() {
keycode = Some(token.to_string());
} else {
return None;
}
match tokens.next() {
Some("|") => {}
Some(_) => {
return None;
}
None => {
break;
}
}
}

Some(Keycode::Basic(
mods,
keycode.unwrap_or_else(|| "NONE".to_string()),
))
}
Loading

0 comments on commit 3b84291

Please sign in to comment.