Hardware Cut/Copy/Paste with Arduino Leonardo

Since I switched to Programmed Dvorak layout default keybindings for different operations started to annoy me sometimes. I was thinking about hardware cut/copy/paste in apps even before that. But only with Dvorak I realized how useful it can be. I always wondered why there is no hardware support for that on various keyboard that are out there. And then I saw keyboard.io. Project is about hackable ergonomic mechanical keyboards build on top of Teensy/Arduino Micro boards. And I decided to play a little bit with that idea. Lets start with implementing hardware cut/copy/paste using Leonardo and then lets see how far we can push the idea.

Emulating keyboard on Leonardo.

With release of first boards based on ATmega32u4 Keyboard and Mouse libraries were introduced in Arduino IDE. Those libraries allow you to emulate fully functional mouse and keyboard from your Arduino board using USB connection. For more information take a look at the documentation.

Arduino wiring.

Wiring will be very simple. We will have 3 buttons on pins 2, 3 and 4 with pull down resistors.

schematics1

Hardware Cut/Copy/Paste.

So this will be our simplest solution to the my original idea. Here is Arduino sketch:

// version 0.0.1

int cutPin   = 2;
int copyPin  = 3;
int pastePin = 4;

void setup() {
  pinMode(cutPin, INPUT);
  pinMode(copyPin, INPUT);
  pinMode(pastePin, INPUT);
  Keboard.begin()
}

void loop() {
  if (digitalRead(cutpin)   == HIGH) { cut();   }
  if (digitalRead(copypin)  == HIGH) { copy();  }
  if (digitalRead(pastepin) == HIGH) { paste(); }
}

void pressCtrl() {
  Keyboard.press(KEY_LEFT_CTRL);
}

void pressShift() {
  Keyboard.press(KEY_LEFT_SHIFT);
}

void cut() {
  pressCtrl();
  Keyboard.write('x');
  Keyboard.releaseAll();
}

void copy() {
  pressCtrl();
  Keyboard.write('c');
  Keyboard.releaseAll();
}

void paste() {
  pressCtrl();
  Keyboard.write('v');
  Keyboard.releaseAll();
}

It works! But… for example in my terminal I use Ctrl+Shift+C to copy selection. Of course I can press Shift+Copy combination. But maybe there is a better solution.

Automatic detection of key combination.

Idea is simple. We have serial port open on Leonardo and our Linux PC. When I’m pressing copy on Leonardo it will ask through serial port PC about required combination. On PC there will be running ruby script that will detect currently focused window and look up at the configuration file for keys combination. If there is no combination will be found or reply from script will be timed out we will use default combination.

Detecting WM_CLASS from Ruby (2.0.0+).

From my experience with Xmonad best method to detect unique window type is by WM_CLASS string from X properties. Here is Window class for the job:

class Window
  def self.current
    Window.new(`xprop -root`)
  end

  def initialize(data)
    @root_data = data
  end

  def id
    matches = @root_data.lines.grep(/_NET_ACTIVE_WINDOW\(WINDOW\)/)

    if matches
      match_data = matches.first.match(/_NET_ACTIVE_WINDOW\(WINDOW\):.*#\s(.*)\n/)
      match_data[1]
    else
      raise 'No Window id was found'
    end
  end

  def wm_class
    out = `xprop -id '#{id}'`
    matches = out.lines.grep(/WM_CLASS\(STRING\)/)

    if matches
      match_data = matches.first.match(/WM_CLASS\(STRING\)[^"]*(".*")\n/)
      match_data[1].gsub(/"/,'').split(', ')
    else
      raise 'No Window class was found'
    end
  end

  def is_a?(class_string)
    wm_class.any? { |s| s == class_string }
  end
end

Usage examples:

Window.current.wm_class
=> ["gvim", "Gvim"]

Window.current.is_a?("gvim")
=> true

Keys configuration.

For now lets implement simplest class for that and store all configuration in constant.

class Keys
  CONFIG = {
    'terminology' => {
      'copy'  => 'ctrl-shift-c',
      'cut'   => 'ctrl-shift-c',
      'paste' => 'ctrl-shift-v'
    }
  }

  def self.[](key)
    CONFIG[key]
  end

  def self.for(window)
    window.wm_class.map do |k|
      CONFIG[k]
    end.compact.first
  end
end

Usage:

Keys['terminology']['copy']
=> 'ctrl-shift-c'

# When current window is terminology
Keys.for(Window.current)['copy']
=> 'ctrl-shift-c'

Communicating with Arduino via SerialPort.

Code below uses sketch described above with redefined copy/paste/cut functions.

String stringIn;
// Let's assume than combination aren't longer than 4 keys
String collectedStrings[4];
int counter, inByte, i;

void setup(){
  Serial.begin(9600);
  counter = 0;
  stringIn = String("");
}

void cut() {
  Serial.println("cut");
}

void copy() {
  Serial.println("copy");
}

void paste() {
  Serial.println("paste");
}

void resetReader() {
  counter = 0
  stringIn = String("")
  for (i = 0; i <= 4; i++) {
    collectedStrings[i] = String("")
  }
}

void readLine() {
  while(Serial.available()){
    inByte = Serial.read();
    stringIn += inByte;

    if (inByte == '-'){  // Handle delimiter
      collectedStrings[counter] = String(stringIn);
      stringIn = String("");
      counter = counter + 1;
    }

    if(inByte ==  '\r'){  // end of line
      resetReader();
      return;
    }
  }
}

void executeCombination() {
  for(i = 0; i <= 4; i++) {
    pressKey(collectedstrings[i]);
  }

  Keyboard.releaseAll();
}

void pressKeys(String key) {
  switch(key) {
    case "ctrl":
      pressCtrl();
      break;
    case "shift":
      pressShift();
      break;
    default:
      char[] arr = key.toCharArray();
      char k = arr[0];
      Keyboard.write(k);
  }
}

More information on ruby-serialport is here.

require 'serialport'

class Connection
  def initialize(port: nil)
    unless port
      port = `ls /dev/ttyACM*`.lines.first
    end

    @connection = SerialPort.new(port, 9600)
  end

  def loop
    loop do
      begin
        action = @connection.readline
        @connection.write("#{Keys.for(Window.current.wm_class)[action]}\r")
      rescue Exception => e
        p e
      end
    end
  end
end

Usage:

Connection.new.loop # starts infinite loop

PS. This post is more like collection of theoretical pices of code. I have no time (and probably enthusiasm) to put all this together (at least right now). So this implementation can be broken and inaccurate in many ways. Feel free to point out any errors and mistakes and I will fix them.