├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── resources ├── hhkb2_pro_topre │ ├── 1.wav │ ├── config.yaml │ ├── down_1.wav │ ├── down_2.wav │ ├── down_3.wav │ ├── down_4.wav │ ├── down_5.wav │ ├── down_6.wav │ ├── down_7.wav │ ├── down_8.wav │ ├── down_9.wav │ ├── enter_down_1.wav │ ├── enter_down_2.wav │ ├── enter_up_1.wav │ ├── enter_up_2.wav │ ├── up_1.wav │ ├── up_2.wav │ ├── up_3.wav │ ├── up_4.wav │ ├── up_5.wav │ ├── up_6.wav │ ├── up_7.wav │ ├── up_8.wav │ └── up_9.wav └── modelm │ ├── 10_.wav │ ├── 11_.wav │ ├── 1_.wav │ ├── 2_.wav │ ├── 3_.wav │ ├── 4_.wav │ ├── 5_.wav │ ├── 6_.wav │ ├── 7_.wav │ ├── 8_.wav │ ├── 9_.wav │ ├── config.yaml │ └── spacebar.wav └── src ├── errors.rs ├── ffi ├── linux.rs ├── mod.rs ├── osx.rs └── types.rs ├── keyboard.rs ├── lib.rs ├── macros.rs ├── main.rs └── switch.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - osx 3 | - linux 4 | 5 | language: rust 6 | sudo: required 7 | 8 | rust: 9 | # - nightly 10 | # - beta 11 | - stable 12 | 13 | before_install: 14 | - | 15 | ### Target dependent setup 16 | 17 | # OSX 18 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 19 | brew update 20 | brew uninstall libjpeg libpng libtiff --ignore-dependencies || true 21 | brew install openal-soft libsndfile 22 | fi 23 | 24 | # Linux 25 | if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 26 | sudo apt-get update 27 | sudo apt-get install -y libopenal-dev libsndfile1-dev 28 | fi 29 | 30 | script: 31 | - cargo build 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "modelm" 3 | version = "0.4.1" 4 | authors = ["Joshua Miller "] 5 | 6 | [dependencies] 7 | clap = "1.5.5" 8 | ears = { git = "https://github.com/jhasse/ears", rev = "9fa9f95b09777e8e17422da9fedcb4e7fb19fc22"} 9 | env_logger = "0.3" 10 | libc = "0.1" 11 | log = "0.3" 12 | rand = "0.3" 13 | regex = "0.1" 14 | yaml-rust = "*" 15 | quick-error = "*" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modelm 2 | [![Build Status](https://travis-ci.org/millerjs/modelm.svg?branch=master)](https://travis-ci.org/millerjs/modelm) 3 | 4 | A **Mechanical keyboard audio simulator** for your keyboard written in [Rust](https://www.rust-lang.org/). 5 | 6 | > *Get yourself clickity-clacking.* 7 | 8 | Inspired by the [IBM Model M Keyboard](https://en.wikipedia.org/wiki/Model_M_keyboard) and a disproportionate love of clicky keyboards over non clicky keyboards, this is a simple program to simulate the Model M by providing audible keystroke feedback. 9 | 10 | ## Features 11 | To provide audible feedback for your keystrokes, `modelm` hooks into OSX Quartz Events (on OSX) or your `/dev/input` keyboard device and must be run as `root`(or from a terminal with accessibility features enabled on OSX). 12 | 13 | * **Stereo sounds** - Keys on the left sound like keys on the left. Keys on the right sound like Keys on the right. 14 | * **Custom resource loading** - You can pick your favorite clickity-clacks. Just point `modelm` to a directory with sound bites and a config file. 15 | 16 | ## Requirements 17 | 18 | Install OpenAL audio dependency. 19 | 20 | ### OSX 21 | 22 | ```bash 23 | brew update 24 | brew install openal-soft libsndfile 25 | ``` 26 | ### Debian/Ubuntu 27 | 28 | ```bash 29 | apt-get update 30 | apt-get install libopenal-dev libsndfile1-dev 31 | ``` 32 | 33 | ### Arch 34 | 35 | You may also need pulseaudio as shown 36 | 37 | ```bash 38 | pacman -S openal libsndfile pulseaudio-alsa 39 | ``` 40 | 41 | ## Installation 42 | 43 | 44 | ### Running a pre-compiled binary 45 | 46 | To run the compiled 47 | binary, 48 | [download the latest release](https://github.com/millerjs/modelm/releases/latest) and 49 | simply run the following from within the extracted tarball 50 | 51 | ```bash 52 | ./modelm -d resources/modelm 53 | ``` 54 | 55 | ### Installation from source 56 | 57 | First, install [Rust](https://github.com/rust-lang/rustup) and [Cargo](https://crates.io/). 58 | 59 | ``` 60 | git clone https://github.com/millerjs/modelm.git 61 | cd modelm 62 | cargo run --release 63 | ``` 64 | 65 | ### Usage 66 | 67 | ```bash 68 | # To specify custom clickity clacks: 69 | sudo ./modelm -d path/to/clacks 70 | 71 | # To make the key gradient from left to right more dramatic 72 | sudo ./modelm -x 5.0 73 | 74 | # Or less dramatic 75 | sudo ./modelm -x 0.5 76 | 77 | # Or reverse because you have your headphones on backward, silly 78 | sudo ./modelm -x'-1' 79 | ``` 80 | 81 | #### Note: Linux usage 82 | 83 | Currently, `modelm` defaults to reading from `/dev/input/event0`, but 84 | you can specify which event device to read at runtimefrom by setting the 85 | `MODELM_INPUT_DEVICE` environment variable to a new path. 86 | 87 | 88 | #### Help output 89 | 90 | ``` 91 | modelm 0.3.0 92 | Joshua Miller 93 | Turns your computer into a mechanical keyboard emulator! 94 | 95 | USAGE: 96 | modelm [FLAGS] [OPTIONS] 97 | 98 | FLAGS: 99 | -v, --debug Debug output 100 | -h, --help Prints help information 101 | --version Prints version information 102 | 103 | OPTIONS: 104 | -c, --config Specify the config to parse click options from 105 | -d, --directory Specify the directory to load click sounds from 106 | -V, --volume Adjust the keyboard volume in range [0.0, 1.0] 107 | -x, --x-scale Specify the pan amount for the positional sound of clicks. A decimal (default: 1.0). The larger the value, the further apart the clicks will sound. A value of 0 turns off positional sound. A value < 0 reverses the directionality. 108 | ``` 109 | 110 | #### Example config file 111 | ```yaml 112 | switches: 113 | 114 | ## enter 115 | - keycode_regex: '36' 116 | keydown_paths: 117 | - enter_down_1.wav 118 | - enter_down_2.wav 119 | 120 | keyup_paths: 121 | - enter_up_1.wav 122 | - enter_up_2.wav 123 | 124 | ## all other keys 125 | - keycode_regex: '\d+' 126 | keydown_paths: 127 | - down_1.wav 128 | - down_2.wav 129 | 130 | keyup_paths: 131 | - up_1.wav 132 | - up_2.wav 133 | ``` 134 | 135 | ### Options 136 | 137 | You can pass options through cargo with a `--`, e.g. to change the volume: 138 | ``` 139 | sudo cargo run --release -- -V 0.5 140 | ``` 141 | 142 | ### Credits 143 | 144 | Currently ships with [IBM sounds](https://webwit.nl/input/kbsim/) and [HHKB Pro 2 Topre]( https://www.youtube.com/watch?v=9hXeG_YEkBs) sounds. 145 | -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/1.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/config.yaml: -------------------------------------------------------------------------------- 1 | switches: 2 | 3 | ## enter 4 | - keycode_regex: '36' 5 | keydown_paths: 6 | - enter_down_1.wav 7 | - enter_down_2.wav 8 | 9 | keyup_paths: 10 | - enter_up_1.wav 11 | - enter_up_2.wav 12 | 13 | ## all other keys 14 | - keycode_regex: '\d+' 15 | keydown_paths: 16 | - down_1.wav 17 | - down_2.wav 18 | - down_3.wav 19 | - down_4.wav 20 | - down_5.wav 21 | - down_6.wav 22 | - down_7.wav 23 | - down_8.wav 24 | - down_9.wav 25 | 26 | keyup_paths: 27 | - up_1.wav 28 | - up_2.wav 29 | - up_3.wav 30 | - up_4.wav 31 | - up_5.wav 32 | - up_6.wav 33 | - up_7.wav 34 | - up_8.wav 35 | - up_9.wav 36 | -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_1.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_2.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_3.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_4.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_5.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_6.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_6.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_7.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_7.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_8.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_8.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/down_9.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/down_9.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/enter_down_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/enter_down_1.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/enter_down_2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/enter_down_2.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/enter_up_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/enter_up_1.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/enter_up_2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/enter_up_2.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_1.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_2.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_3.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_4.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_5.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_6.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_6.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_7.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_7.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_8.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_8.wav -------------------------------------------------------------------------------- /resources/hhkb2_pro_topre/up_9.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/hhkb2_pro_topre/up_9.wav -------------------------------------------------------------------------------- /resources/modelm/10_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/10_.wav -------------------------------------------------------------------------------- /resources/modelm/11_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/11_.wav -------------------------------------------------------------------------------- /resources/modelm/1_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/1_.wav -------------------------------------------------------------------------------- /resources/modelm/2_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/2_.wav -------------------------------------------------------------------------------- /resources/modelm/3_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/3_.wav -------------------------------------------------------------------------------- /resources/modelm/4_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/4_.wav -------------------------------------------------------------------------------- /resources/modelm/5_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/5_.wav -------------------------------------------------------------------------------- /resources/modelm/6_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/6_.wav -------------------------------------------------------------------------------- /resources/modelm/7_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/7_.wav -------------------------------------------------------------------------------- /resources/modelm/8_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/8_.wav -------------------------------------------------------------------------------- /resources/modelm/9_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/9_.wav -------------------------------------------------------------------------------- /resources/modelm/config.yaml: -------------------------------------------------------------------------------- 1 | switches: 2 | - keycode_regex: '49' 3 | keydown_paths: 4 | - spacebar.wav 5 | keyup_paths: 6 | - spacebar.wav 7 | - keycode_regex: '\d+' 8 | keydown_paths: 9 | - 1_.wav 10 | - 2_.wav 11 | - 3_.wav 12 | - 4_.wav 13 | - 5_.wav 14 | - 6_.wav 15 | - 7_.wav 16 | - 8_.wav 17 | - 9_.wav 18 | -------------------------------------------------------------------------------- /resources/modelm/spacebar.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerjs/modelm/a80b69ccb7b0d65ccb8d0b865d3b8519cf733120/resources/modelm/spacebar.wav -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use yaml_rust; 2 | use regex; 3 | 4 | quick_error! { 5 | #[derive(Debug)] 6 | pub enum KeyboardError { 7 | /// Parsing Error 8 | ScanError(err: yaml_rust::ScanError) { from() } 9 | /// Config Error 10 | Config(err: String) { from() } 11 | Regex(err: regex::Error) { from() } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ffi/linux.rs: -------------------------------------------------------------------------------- 1 | /// Linux key handler ffi 2 | 3 | use std::fs::File; 4 | use std::io::prelude::*; 5 | use std::sync::mpsc::Sender; 6 | use std::{mem, slice, io, env}; 7 | use super::types::{EventType, KeyEvent}; 8 | 9 | type LinuxEventCode = u16; 10 | type LinuxEventType = u16; 11 | type LinuxEventValue = u32; 12 | type LinuxKernelTime = u64; 13 | 14 | #[repr(C)] 15 | #[derive(Debug)] 16 | struct TimeValue { 17 | __kernel_time_t: LinuxKernelTime, 18 | __kernel_suseconds_t: LinuxKernelTime, 19 | } 20 | 21 | #[repr(C)] 22 | #[derive(Debug)] 23 | struct InputEvent { 24 | time: TimeValue, 25 | etype: LinuxEventType, 26 | code: LinuxEventCode, 27 | value: LinuxEventValue, 28 | } 29 | 30 | /// Returns true if the code is considered a "flag", i.e. a modifier 31 | /// key 32 | fn is_flag(code: LinuxEventCode) -> bool { 33 | match code { 34 | 100 | 125 | 29 | 42 | 54 | 56 | 58 | 97 => true, 35 | _ => false, 36 | } 37 | } 38 | 39 | 40 | impl Into for InputEvent { 41 | fn into(self) -> KeyEvent { 42 | KeyEvent { 43 | code: self.code, 44 | etype: match is_flag(self.code) { 45 | true => EventType::FlagsChanged, 46 | false => match self.value { 47 | 0 => EventType::KeyUp, 48 | _ => EventType::KeyDown, 49 | }, 50 | }, 51 | } 52 | } 53 | } 54 | 55 | 56 | /// Reads event struct from input device 57 | fn read_event(device: &mut File) -> Result { 58 | let mut event: InputEvent = unsafe { mem::zeroed() }; 59 | let event_size = mem::size_of::(); 60 | 61 | unsafe { 62 | try!(device.read_exact(slice::from_raw_parts_mut( 63 | &mut event as *mut _ as *mut u8, 64 | event_size 65 | ))); 66 | } 67 | 68 | if event.etype != 1 { 69 | return read_event(device); 70 | } 71 | 72 | debug!("read input event {:?}", event); 73 | Ok(event) 74 | } 75 | 76 | 77 | /// Opens the input device, will use MODELM_INPUT_DEVICE environment 78 | /// variable if set 79 | fn open_device() -> Result { 80 | let path = env::var("MODELM_INPUT_DEVICE") 81 | .unwrap_or("/dev/input/event0".to_owned()); 82 | File::open(path) 83 | } 84 | 85 | 86 | /// Sends KeyEvents to the channel. Returns only on error. 87 | pub fn start_listener(channel: &Sender) { 88 | let mut device = open_device().expect("unable to open device"); 89 | 90 | loop { 91 | let _ = read_event(&mut device) 92 | .map(|event| channel.send(event.into())) 93 | .map_err(|err| error!("Unable to parse event, {:}", err)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | /// Target OS dependent ffi 2 | 3 | pub mod types; 4 | 5 | use self::types::KeyEvent; 6 | use std::sync::mpsc::Sender; 7 | 8 | 9 | // ====================================================================== 10 | // Import module according to target os 11 | 12 | #[cfg(target_os = "macos")] mod osx; 13 | #[cfg(target_os = "linux")] mod linux; 14 | 15 | 16 | // ====================================================================== 17 | // Compile against os ffi module 18 | 19 | /// Register a listener. 20 | /// - On Linux, this is a no-op because we rely on an input device 21 | /// that is already listening. 22 | /// - On OSX, this registers a Quartz Event Tap 23 | #[allow(unused_variables)] 24 | pub fn register_listener(tx: &Sender) { 25 | #[cfg(target_os = "macos")] self::osx::register_listener(tx); 26 | } 27 | 28 | /// Start listener should never return. The listener will send events 29 | /// via the channel `tx`. (On OSX, `tx` should already have been 30 | /// passed to a registered event tap by calling `register_lisener` and 31 | /// will not be used here.) 32 | #[allow(unused_variables)] 33 | pub fn start_listener(tx: &Sender) { 34 | #[cfg(target_os = "macos")] self::osx::start_listener(); 35 | #[cfg(target_os = "linux")] self::linux::start_listener(tx) 36 | } 37 | -------------------------------------------------------------------------------- /src/ffi/osx.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(improper_ctypes)] 5 | 6 | use libc; 7 | use std::sync::mpsc::Sender; 8 | use super::types::{EventType, KeyEvent}; 9 | 10 | // Opaque Pointer Types 11 | pub type Pointer = *mut libc::c_void; 12 | pub type CGEventRef = Pointer; 13 | pub type CFMachPortRef = Pointer; 14 | 15 | // Integer Types 16 | pub type CGEventField = u32; 17 | pub type CGEventMask = u64; 18 | pub type CGEventTapLocation = u32; 19 | pub type CGEventTapOptions = u32; 20 | pub type CGEventTapPlacement = u32; 21 | pub type CGEventType = u32; 22 | pub type CGKeyCode = u16; 23 | 24 | // Callback Type 25 | pub type CGEventTapCallBack = extern "C" 26 | fn(Pointer, CGEventMask, CGEventRef, &Sender) -> CGEventRef; 27 | 28 | // Constants 29 | pub const kCGEventKeyDown: CGEventType = 10; 30 | pub const kCGEventKeyUp: CGEventType = 11; 31 | pub const kCGEventFlagsChanged: CGEventType = 12; 32 | pub const kCGSessionEventTap: CGEventTapLocation = 1; 33 | pub const kCGHeadInsertEventTap: CGEventTapPlacement = 0; 34 | pub const kCGKeyboardEventKeycode: CGEventField = 9; 35 | pub const kCGTapDisabledByTimeout: CGEventType = 0xFFFFFFFE; 36 | 37 | pub mod ext_quartz { 38 | extern crate libc; 39 | use std::sync::mpsc::Sender; 40 | 41 | // Import types from super 42 | use super::super::types::KeyEvent; 43 | use super::Pointer; 44 | use super::CGEventRef; 45 | use super::CFMachPortRef; 46 | use super::CGEventField; 47 | use super::CGEventMask; 48 | use super::CGEventTapCallBack; 49 | use super::CGEventTapLocation; 50 | use super::CGEventTapOptions; 51 | use super::CGEventTapPlacement; 52 | use super::CGKeyCode; 53 | 54 | // Link to ApplicationServices/ApplicationServices.h and Carbon/Carbon.h 55 | #[link(name = "ApplicationServices", kind = "framework")] 56 | #[link(name = "Carbon", kind = "framework")] 57 | extern { 58 | 59 | /// Pass through to the default loop modes 60 | pub static kCFRunLoopCommonModes: Pointer; 61 | 62 | /// Pass through to the default allocator 63 | pub static kCFAllocatorDefault: Pointer; 64 | 65 | /// Run the current threads loop in default mode 66 | pub fn CFRunLoopRun(); 67 | 68 | /// Obtain the current threads loop 69 | pub fn CFRunLoopGetCurrent() -> Pointer; 70 | 71 | /// Get the code of the event back, e.g. the key code 72 | pub fn CGEventGetIntegerValueField( 73 | event: CGEventRef, 74 | field: CGEventField, 75 | ) -> CGKeyCode; 76 | 77 | /// Create an event tap 78 | /// 79 | /// # Arguments 80 | /// 81 | /// * `place` - The location of the new event tap. Pass one of 82 | /// the constants listed in Event Tap Locations. Only 83 | /// processes running as the root user may locate an 84 | /// event tap at the point where HID events enter the 85 | /// window server; for other users, this function 86 | /// returns NULL. 87 | /// 88 | /// * `options` - The placement of the new event tap in the 89 | /// list of active event taps. Pass one of the 90 | /// constants listed in Event Tap Placement. 91 | /// 92 | /// * `eventsOfInterest` - A constant that specifies whether 93 | /// the new event tap is a passive listener or an 94 | /// active filter. 95 | /// 96 | /// * `callback` - A bit mask that specifies the set of events 97 | /// to be observed. For a list of possible events, 98 | /// see Event Types. For information on how to 99 | /// specify the mask, see CGEventMask. If the event 100 | /// tap is not permitted to monitor one or more of 101 | /// the events specified in the eventsOfInterest 102 | /// parameter, then the appropriate bits in the mask 103 | /// are cleared. If that action results in an empty 104 | /// mask, this function returns NULL. callback 105 | /// 106 | /// * `refcon` - An event tap callback function that you 107 | /// provide. Your callback function is invoked from 108 | /// the run loop to which the event tap is added as a 109 | /// source. The thread safety of the callback is 110 | /// defined by the run loop’s environment. To learn 111 | /// more about event tap callbacks, see 112 | /// CGEventTapCallBack. refcon 113 | /// 114 | /// * `channel` - A pointer to user-defined data. This pointer 115 | /// is passed into the callback function specified in 116 | /// the callback parameter. Here we use it as a mpsc 117 | /// channel. 118 | pub fn CGEventTapCreate( 119 | tap: CGEventTapLocation, 120 | place: CGEventTapPlacement, 121 | options: CGEventTapOptions, 122 | eventsOfInterest: CGEventMask, 123 | callback: CGEventTapCallBack, 124 | channel: &Sender, 125 | ) -> CFMachPortRef; 126 | 127 | /// Creates a CFRunLoopSource object for a CFMachPort 128 | /// object. 129 | /// 130 | /// The run loop source is not automatically added to 131 | /// a run loop. To add the source to a run loop, use 132 | /// CFRunLoopAddSource 133 | pub fn CFMachPortCreateRunLoopSource( 134 | allocator: Pointer, 135 | port: CFMachPortRef, 136 | order: libc::c_int, 137 | ) -> Pointer; 138 | 139 | /// Adds a CFRunLoopSource object to a run loop mode. 140 | pub fn CFRunLoopAddSource( 141 | run_loop: Pointer, 142 | run_loop_source: Pointer, 143 | mode: Pointer, 144 | ); 145 | 146 | pub fn CGEventTapEnable(port: CFMachPortRef, enable: bool); 147 | } 148 | 149 | } 150 | 151 | /// This callback will be registered to be invoked from the run loop 152 | /// to which the event tap is added as a source. 153 | #[no_mangle] 154 | #[allow(unused_variables, private_no_mangle_fns)] 155 | pub extern fn callback(proxy: Pointer, etype: CGEventMask, event: CGEventRef, channel: &Sender) 156 | -> CGEventRef { 157 | unsafe { 158 | let keyCode = ext_quartz::CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); 159 | let event = KeyEvent { 160 | etype: match etype as u32 { 161 | kCGEventKeyDown => EventType::KeyDown, 162 | kCGEventKeyUp => EventType::KeyUp, 163 | kCGEventFlagsChanged => EventType::FlagsChanged, 164 | kCGTapDisabledByTimeout => { 165 | warn!("Quartz event tap disabled because of timeout; attempting to reregister."); 166 | register_listener(channel); 167 | return event; 168 | }, 169 | _ => { 170 | error!("Received unknown EventType: {:}", etype); 171 | return event; 172 | }, 173 | }, 174 | code: keyCode, 175 | }; 176 | debug!("Received event: {:?}", event); 177 | let _ = channel.send(event); 178 | } 179 | event 180 | } 181 | 182 | /// Redefine macro for bitshifting from header as function here 183 | pub fn CGEventMaskBit(eventType: u32) -> CGEventMask { 184 | 1 << (eventType) 185 | } 186 | 187 | /// Safe wrapper around CFRunLoopRun 188 | pub fn start_listener() { 189 | unsafe { 190 | ext_quartz::CFRunLoopRun(); 191 | } 192 | } 193 | 194 | /// Registeres an event tap 195 | pub fn register_listener(tx: &Sender) { 196 | let mask = CGEventMaskBit(kCGEventKeyDown) 197 | | CGEventMaskBit(kCGEventKeyUp) 198 | | CGEventMaskBit(kCGEventFlagsChanged); 199 | 200 | unsafe { 201 | let options = 0; 202 | 203 | // Create the event tap 204 | let event_tap = ext_quartz::CGEventTapCreate( 205 | kCGSessionEventTap, 206 | kCGHeadInsertEventTap, 207 | options, 208 | mask, 209 | callback, 210 | tx, 211 | ); 212 | assert!(!event_tap.is_null(), 213 | "Unable to create event tap. Please make sure you have the correct permissions"); 214 | info!("Created event tap..."); 215 | 216 | let allocator = ext_quartz::kCFAllocatorDefault; 217 | let current_event_loop = ext_quartz::CFRunLoopGetCurrent(); 218 | let mode = ext_quartz::kCFRunLoopCommonModes; 219 | 220 | // Create Run Loop Source 221 | let run_loop_source = ext_quartz::CFMachPortCreateRunLoopSource(allocator, event_tap, 0); 222 | 223 | // Add Run Loop Source to the current event loop 224 | ext_quartz::CFRunLoopAddSource(current_event_loop, run_loop_source, mode); 225 | 226 | // Enable the tap 227 | ext_quartz::CGEventTapEnable(event_tap, true); 228 | 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/ffi/types.rs: -------------------------------------------------------------------------------- 1 | /// Define types to be passed between os and modelm 2 | 3 | pub type KeyCode = u16; 4 | 5 | #[derive(Debug)] 6 | #[repr(C)] 7 | pub enum EventType { 8 | KeyDown, 9 | KeyUp, 10 | FlagsChanged, 11 | } 12 | 13 | #[derive(Debug)] 14 | #[repr(C)] 15 | pub struct KeyEvent { 16 | pub etype: EventType, 17 | pub code: KeyCode, 18 | } 19 | -------------------------------------------------------------------------------- /src/keyboard.rs: -------------------------------------------------------------------------------- 1 | //! Keyboard emulation operations 2 | //! 3 | //! This module contains a Keyboard struct that emulates the sounds of 4 | //! your favorite keyboard. 5 | //! 6 | //! The Keyboard currently uses the `ears` crate to play sounds with 7 | //! OpenAL. 8 | //! 9 | //! # Example 10 | //! ```ignore 11 | //! ears::init(); 12 | //! Keyboard::new("resources/modelm").listen(); 13 | //! ``` 14 | 15 | use ::DEFAULT_SOUND_FILE_REGEX; 16 | use ffi::{register_listener, start_listener}; 17 | use ffi::types::{EventType, KeyCode, KeyEvent}; 18 | use regex::Regex; 19 | use std::collections::HashSet; 20 | use std::fs::read_dir; 21 | use std::sync::mpsc::channel; 22 | use std::thread; 23 | use switch::Switch; 24 | use yaml_rust; 25 | use yaml_rust::Yaml; 26 | use ::errors::KeyboardError; 27 | 28 | /// Keyboard representation 29 | #[repr(C)] 30 | pub struct Keyboard { 31 | switches: Vec, 32 | sound_file_regex: Regex, 33 | keys_down: HashSet, 34 | options: KeyboardOptions, 35 | } 36 | 37 | 38 | pub struct KeyboardOptions { 39 | pub x_scale: f32, 40 | pub volume: f32, 41 | pub modifier_keys: bool, 42 | } 43 | 44 | 45 | impl Default for KeyboardOptions { 46 | fn default() -> Self 47 | { 48 | KeyboardOptions { 49 | x_scale: 1.0, 50 | volume: 1.0, 51 | modifier_keys: false, 52 | } 53 | } 54 | } 55 | 56 | impl Keyboard { 57 | 58 | /// Default constructor for Keyboard 59 | /// 60 | /// Create a new Keyboard with default members 61 | pub fn new() -> Keyboard 62 | { 63 | Keyboard { 64 | keys_down: HashSet::new(), 65 | options: KeyboardOptions::default(), 66 | switches: vec![], 67 | sound_file_regex: Regex::new(DEFAULT_SOUND_FILE_REGEX).unwrap(), 68 | } 69 | } 70 | 71 | /// Default constructor for Keyboard 72 | /// 73 | /// Create a new Keyboard with default members 74 | pub fn with_options(options: KeyboardOptions) -> Keyboard 75 | { 76 | Keyboard { options: options, .. Keyboard::new() } 77 | } 78 | 79 | pub fn load_config_yaml(mut self, config: &str) -> Result 80 | { 81 | let parsed = try!(yaml_rust::YamlLoader::load_from_str(config)); 82 | let yaml = &parsed[0]; 83 | 84 | let switches = try_yaml!(yaml["switches"], Yaml::Array, 85 | "config must have Array [switches]"); 86 | 87 | for switch_config in switches { 88 | self.switches.push(try!(Switch::from_yaml(&switch_config))); 89 | } 90 | 91 | Ok(self) 92 | } 93 | 94 | /// Adds a handler using all the sounds in the given directory 95 | /// 96 | /// # Argument 97 | /// `directory` - Path to read sound files from 98 | pub fn add_default_handler(mut self, directory: &str) -> Result 99 | { 100 | let mut switch = Switch::new(); 101 | for path in read_dir(directory).unwrap() { 102 | let path = path.unwrap().path(); 103 | if self.sound_file_regex.is_match(path.to_str().unwrap()) { 104 | switch = try!(switch.load_sound_keydown(&path)); 105 | } 106 | } 107 | self.switches.push(switch); 108 | Ok(self) 109 | } 110 | 111 | /// Adds a user created handler 112 | /// 113 | /// # Argument 114 | /// `switch` - the Switch object to add 115 | pub fn switch(mut self, switch: Switch) -> Keyboard 116 | { 117 | self.switches.push(switch); 118 | self 119 | } 120 | 121 | /// Sets the volume of the keyboard. 122 | /// 123 | /// Volume should be a decimal between 0 and 1 124 | pub fn set_volume(mut self, volume: f32) -> Keyboard { 125 | self.options.volume = volume; 126 | self 127 | } 128 | 129 | /// Set the pan amount for the positional sound of clicks. 130 | /// 131 | /// A decimal (default: 1.0). The larger the value, the further 132 | /// apart the clicks will sound. A value of 0 turns off positional 133 | /// sound. A value < 0 reverses the directionality. 134 | /// 135 | /// # Argument 136 | /// `x_scale` - Amount to scale sounds left and right. 137 | /// 138 | /// Scale should be a decimal. 139 | pub fn set_x_scale(mut self, x_scale: f32) -> Keyboard { 140 | self.options.x_scale = x_scale; 141 | self 142 | } 143 | 144 | /// Listener to play sound. 145 | /// 146 | /// Play a sound when the an event is added to the channel by the 147 | /// callback 148 | /// 149 | /// # Example 150 | /// ```ignore 151 | /// ears::init(); 152 | /// Keyboard::new("resources"); 153 | /// ``` 154 | pub fn listen(&mut self) { 155 | let (tx, rx) = channel(); 156 | 157 | // create listener thread 158 | thread::spawn(move || { 159 | register_listener(&tx); 160 | info!("Running event listener..."); 161 | info!("Press ^C to exit."); 162 | start_listener(&tx); 163 | }); 164 | 165 | // poll channel for events 166 | loop { 167 | match rx.recv() { 168 | Ok(event) => self.handle_event(event), 169 | Err(err) => return info!("Channel to listener closed, {:}", err), 170 | } 171 | } 172 | } 173 | 174 | /// Returns the index of the handler for a KeyCode 175 | /// 176 | /// # Argument 177 | /// `code` - The key code of the event 178 | fn get_switch_index(&self, code: KeyCode) -> Option { 179 | for (i, switch) in self.switches.iter().enumerate() { 180 | if switch.handles(code) { 181 | return Some(i) 182 | } 183 | } 184 | None 185 | } 186 | 187 | /// Looks-up the handler for a KeyCode and calls the handler with 188 | /// the event. 189 | /// 190 | /// # Argument 191 | /// `event` - The instance of the event to handle 192 | pub fn call_event_handler(&mut self, event: KeyEvent) { 193 | match self.get_switch_index(event.code) { 194 | Some(i) => self.switches[i].handle_event(event, &self.options), 195 | None => (), 196 | }; 197 | } 198 | 199 | /// Adjusts keyboard state given Event and calls a handler. 200 | /// 201 | /// # Argument 202 | /// `event` - The instance of the event to record and handle 203 | pub fn handle_event(&mut self, event: KeyEvent) { 204 | match event.etype { 205 | EventType::KeyDown => { 206 | if !self.keys_down.contains(&event.code) { 207 | self.keys_down.insert(event.code); 208 | self.call_event_handler(event); 209 | } 210 | }, 211 | EventType::KeyUp => { 212 | if self.keys_down.contains(&event.code) { 213 | self.keys_down.remove(&event.code); 214 | self.call_event_handler(event); 215 | } 216 | }, 217 | EventType::FlagsChanged if self.options.modifier_keys => { 218 | if !self.keys_down.contains(&event.code) { 219 | self.keys_down.insert(event.code); 220 | self.call_event_handler(KeyEvent {etype: EventType::KeyDown, .. event}); 221 | } else { 222 | self.keys_down.remove(&event.code); 223 | self.call_event_handler(KeyEvent {etype: EventType::KeyUp, .. event}); 224 | } 225 | }, 226 | _ => (), 227 | }; 228 | } 229 | } 230 | 231 | 232 | #[cfg(test)] 233 | mod test { 234 | #![allow(non_snake_case)] 235 | 236 | use super::Keyboard; 237 | 238 | #[test] 239 | fn keyboard_create_OK() -> () { 240 | let _ = Keyboard::new().load_config_yaml(r"switches: 241 | - keycode_regex: '49' 242 | keydown_paths: 243 | - spacebar.wav 244 | keyup_paths: 245 | - spacebar.wav 246 | - keycode_regex: '\d+' 247 | keydown_paths: 248 | - 1_.wav 249 | "); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "modelm"] 2 | 3 | #[macro_use] 4 | extern crate quick_error; 5 | #[macro_use] 6 | extern crate log; 7 | 8 | extern crate ears; 9 | extern crate libc; 10 | extern crate rand; 11 | extern crate regex; 12 | extern crate yaml_rust; 13 | 14 | #[macro_use] 15 | pub mod macros; 16 | pub mod keyboard; 17 | pub mod ffi; 18 | pub mod switch; 19 | pub mod errors; 20 | 21 | static DEFAULT_SOUND_FILE_REGEX: &'static str = r"\.(wav|mp3)"; 22 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! try_yaml { 2 | ($result: expr, Yaml::Array, $error_msg: expr) => {{ match $result { 3 | Yaml::Array(ref res) => res, 4 | _ => return Err(KeyboardError::Config($error_msg.into())), 5 | }}}; 6 | ($result: expr, Yaml::String, $error_msg: expr) => {{ match $result { 7 | Yaml::String(ref res) => res, 8 | _ => return Err(KeyboardError::Config($error_msg.into())), 9 | }}}; 10 | ($result: expr, Yaml::Hash, $error_msg: expr) => {{ match $result { 11 | Yaml::Hash(ref res) => res, 12 | _ => return Err(KeyboardError::Config($error_msg.into())), 13 | }}}; 14 | } 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Small commandline app that uses your keyboard to emulate 2 | //! mechanical keyboard audio. 3 | 4 | #[macro_use] 5 | extern crate log; 6 | extern crate modelm; 7 | extern crate ears; 8 | extern crate env_logger; 9 | extern crate clap; 10 | 11 | use clap::{Arg, App, ArgMatches}; 12 | use modelm::keyboard::{Keyboard, KeyboardOptions}; 13 | use std::env; 14 | use std::path::Path; 15 | use std::fs::File; 16 | use std::io::prelude::*; 17 | 18 | static DEFAULT_PATH: &'static str = "resources/modelm"; 19 | static DEFAULT_CONFIG_PATH: &'static str = "config.yaml"; 20 | 21 | /// Setup logging (cli arg overwrites env var for dtt crate) 22 | pub fn setup_logging(matches: &ArgMatches) 23 | { 24 | let rust_log = env::var("RUST_LOG").unwrap_or("".to_owned()); 25 | 26 | let log_level = match matches.is_present("DEBUG") { 27 | false => "modelm=info", 28 | true => "modelm=debug", 29 | }; 30 | 31 | env::set_var("RUST_LOG", &*format!("{},{}", rust_log, log_level)); 32 | env_logger::init().unwrap(); 33 | 34 | debug!("Set log level to {}", log_level); 35 | } 36 | 37 | fn main() { 38 | if let Err(error) = ears::init() { 39 | return error!("{}", error) 40 | } 41 | 42 | let matches = App::new("modelm") 43 | .version("0.5.0") 44 | .author("Joshua Miller ") 45 | .about("Turns your computer into a mechanical keyboard emulator!") 46 | .arg(Arg::with_name("VOLUME") 47 | .short("V") 48 | .long("volume") 49 | .help("Adjust the keyboard volume in range [0.0, 1.0]") 50 | .takes_value(true)) 51 | .arg(Arg::with_name("DIR") 52 | .short("d") 53 | .long("directory") 54 | .help("Specify the directory to load click sounds from") 55 | .takes_value(true)) 56 | .arg(Arg::with_name("CONFIG") 57 | .short("c") 58 | .long("config") 59 | .help("Specify the config to parse click options from") 60 | .takes_value(true)) 61 | .arg(Arg::with_name("DEBUG") 62 | .short("v") 63 | .long("debug") 64 | .takes_value(false) 65 | .help("Debug output")) 66 | .arg(Arg::with_name("MODIFIER_KEYS") 67 | .short("m") 68 | .long("with-modifier-keys") 69 | .help("Don't exclude modifier keys (control, alt, shift, etc.)")) 70 | .arg(Arg::with_name("XSCALE") 71 | .short("x") 72 | .long("x-scale") 73 | .help("Specify the pan amount for the positional sound of clicks. \ 74 | A decimal (default: 1.0). The larger the value, the further \ 75 | apart the clicks will sound. A value of 0 turns off positional \ 76 | sound. A value < 0 reverses the directionality.") 77 | .takes_value(true)) 78 | .get_matches(); 79 | 80 | setup_logging(&matches); 81 | 82 | // working directory 83 | let dir = matches.value_of("DIR").unwrap_or(DEFAULT_PATH); 84 | 85 | match env::set_current_dir(&Path::new(&*dir)) { 86 | Ok(_) => (), 87 | Err(error) => error!("Unable to work in dir {}: {:?}", dir, error), 88 | } 89 | 90 | // config path 91 | let config_path = matches.value_of("CONFIG").unwrap_or(DEFAULT_CONFIG_PATH); 92 | 93 | // volume 94 | let volume: f32 = matches.value_of("VOLUME").unwrap_or("1.0").parse() 95 | .expect("Volume must be a decimal between 0 and 1."); 96 | 97 | // x-scale 98 | let x_scale: f32 = matches.value_of("XSCALE").unwrap_or("1.0").parse() 99 | .expect("x-scale must be a decimal. (default: 1.0)"); 100 | 101 | // Read the config file 102 | let mut config = String::new(); 103 | let mut config_file = File::open(&config_path) 104 | .expect(&*format!("unable to open: {}", config_path)); 105 | 106 | config_file.read_to_string(&mut config) 107 | .expect(&*format!("unable to read: {}", config_path)); 108 | 109 | // Create a keyboard 110 | let options = KeyboardOptions { 111 | x_scale: x_scale, 112 | volume: volume, 113 | modifier_keys: matches.is_present("MODIFIER_KEYS"), 114 | }; 115 | 116 | let keyboard = Keyboard::with_options(options) 117 | .load_config_yaml(&*config); 118 | 119 | // Run the keyboard 120 | match keyboard { 121 | Ok(mut keyboard) => keyboard.listen(), 122 | Err(error) => error!("Unable to initialize keyboard: {:?}", error), 123 | }; 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/switch.rs: -------------------------------------------------------------------------------- 1 | //! Representation of keyboard switches 2 | //! 3 | //! This module contains a SwitchSound struct that holds options for 4 | //! keyboard switches in relation to sounds. 5 | 6 | use rand; 7 | use ears::AudioController; 8 | use ears::Sound; 9 | use ffi::types::{KeyEvent, KeyCode, EventType}; 10 | use regex::Regex; 11 | use rand::distributions::IndependentSample; 12 | use rand::distributions::Range; 13 | use keyboard::KeyboardOptions; 14 | use std::path::Path; 15 | use yaml_rust; 16 | use yaml_rust::Yaml; 17 | use ::errors::KeyboardError; 18 | 19 | #[cfg(target_os = "macos")] const MIDDLE: f32 = 25.0; 20 | #[cfg(target_os = "linux")] const MIDDLE: f32 = 50.0; 21 | 22 | 23 | pub struct SwitchSound { 24 | sound: Sound, 25 | name: String, 26 | } 27 | 28 | 29 | pub struct Switch { 30 | pub sounds_keydown: Vec, 31 | pub sounds_keyup: Vec, 32 | pub keycode_regex: Regex, 33 | pub position: [f32; 3], 34 | } 35 | 36 | 37 | impl SwitchSound { 38 | fn from_path(path: &Path) -> Result 39 | { 40 | let path_str = path.to_str().ok_or(format!("Unable to load sound: {:?}", path))?; 41 | let sound = Sound::new(path_str)?; 42 | 43 | let name = try!(path.file_name() 44 | .ok_or(format!("Unable to parse filename: {:?}", path)) 45 | .and_then(|n| Ok(n.to_string_lossy().to_string()))); 46 | 47 | Ok(SwitchSound { 48 | name: name, 49 | sound: sound, 50 | }) 51 | } 52 | } 53 | 54 | macro_rules! play_random_sound { 55 | ($sounds: expr, $position: expr, $options: expr) => { 56 | { 57 | if $sounds.len() > 0 { 58 | let range = Range::new(0, $sounds.len()); 59 | let idx = range.ind_sample(&mut rand::thread_rng()); 60 | let sound = &mut $sounds[idx]; 61 | sound.sound.set_position($position); 62 | sound.sound.set_volume($options.volume); 63 | debug!("Playing {}", sound.name); 64 | sound.sound.play(); 65 | } 66 | } 67 | }; 68 | } 69 | 70 | 71 | impl Switch { 72 | pub fn new() -> Switch 73 | { 74 | Switch { 75 | sounds_keydown: vec![], 76 | sounds_keyup: vec![], 77 | keycode_regex: Regex::new(".*").unwrap(), 78 | position: [0.0, 0.0, 1.0], 79 | } 80 | } 81 | 82 | pub fn with_keycode_regex(mut self, regex: Regex) -> Switch 83 | { 84 | self.keycode_regex = regex; 85 | self 86 | } 87 | 88 | pub fn load_sound_keydown(mut self, path: &Path) -> Result 89 | { 90 | self.sounds_keydown.push(try!(SwitchSound::from_path(path))); 91 | Ok(self) 92 | } 93 | 94 | pub fn load_sound_keyup(mut self, path: &Path) -> Result 95 | { 96 | self.sounds_keyup.push(try!(SwitchSound::from_path(path))); 97 | Ok(self) 98 | } 99 | 100 | pub fn handle_event(&mut self, event: KeyEvent, options: &KeyboardOptions) { 101 | let position = - (MIDDLE - event.code as f32) * options.x_scale / 300.0; 102 | match event.etype { 103 | EventType::KeyDown => { 104 | play_random_sound!(self.sounds_keydown, [position, 0.0, 1.0], options); 105 | }, 106 | EventType::KeyUp => { 107 | play_random_sound!(self.sounds_keyup, [position, 0.0, 1.0], options); 108 | }, 109 | _ => (), 110 | } 111 | } 112 | 113 | pub fn handles(&self, code: KeyCode) -> bool { 114 | self.keycode_regex.is_match(&*format!("{}", code)) 115 | } 116 | 117 | pub fn from_yaml(yaml: &yaml_rust::Yaml) -> Result 118 | { 119 | let hash = try_yaml!(*yaml, Yaml::Hash, "switch must be a Hash [switch]"); 120 | 121 | let regex_str = try_yaml!(yaml["keycode_regex"], Yaml::String, 122 | "config must have Hash [switch.keycode_regex]"); 123 | 124 | info!("Parsed keycode_regex : {}", regex_str); 125 | 126 | let mut switch = Switch::new().with_keycode_regex(try!(Regex::new(&*regex_str))); 127 | 128 | if hash.contains_key(&Yaml::String("keydown_paths".into())){ 129 | let keydown_paths = try_yaml!(yaml["keydown_paths"], Yaml::Array, 130 | "config must have Array [switch.keydown_paths]"); 131 | 132 | for keydown_path in keydown_paths { 133 | let path = try!(keydown_path.as_str() 134 | .ok_or(format!("Unable to parse path: {:?}", keydown_path))); 135 | info!("Parsed keydown path: {}", path); 136 | switch = try!(switch.load_sound_keydown(&Path::new(path))); 137 | } 138 | } 139 | 140 | if hash.contains_key(&Yaml::String("keyup_paths".into())){ 141 | let keyup_paths = try_yaml!(yaml["keyup_paths"], Yaml::Array, 142 | "config must have Array [switch.keyup_paths]"); 143 | 144 | for keyup_path in keyup_paths { 145 | let path = try!(keyup_path.as_str() 146 | .ok_or(format!("Unable to parse path: {:?}", keyup_path))); 147 | info!("Parsed keyup path: {}", path); 148 | switch = try!(switch.load_sound_keyup(&Path::new(path))); 149 | } 150 | } 151 | Ok(switch) 152 | } 153 | } 154 | --------------------------------------------------------------------------------