Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.4.0-dev
2.3.0-dev
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
source "http://rubygems.org"

gem "activesupport"
gem "colored"
gem "colored"
17 changes: 8 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
GEM
remote: http://rubygems.org/
specs:
activesupport (4.2.5.1)
activesupport (5.1.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
colored (1.2)
i18n (0.7.0)
json (1.8.3)
minitest (5.8.4)
thread_safe (0.3.5)
tzinfo (1.2.2)
concurrent-ruby (1.0.5)
i18n (0.8.6)
minitest (5.10.3)
thread_safe (0.3.6)
tzinfo (1.2.3)
thread_safe (~> 0.1)

PLATFORMS
Expand All @@ -23,4 +22,4 @@ DEPENDENCIES
colored

BUNDLED WITH
1.11.2
1.14.6
109 changes: 61 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,74 @@ _usage gif_

![usage_gif](macros.gif)

### Summary
- This uses the evtest linux program to get a stream of all keyboard events.
- The events are parsed using Regex and keypresses are tracked.
- Phrases are mapped to Ruby methods which are called when the phrase is typed anywhere.
- Text can be programmatically added / removed under the cursor using xdotool helper methods.
- This program evolved from my [artoo-keyboard-macros](https://github.com/maxpleaner/artoo-keyboard-macros) project.
- The reason for redoing it was that artoo-keyboard doesnt support global listeners [link to github issue](https://github.com/hybridgroup/artoo-keyboard/issues/6)
- [pty](http://ruby-doc.org/stdlib-2.2.3/libdoc/pty/rdoc/PTY.html) from Ruby's stdlib is used for the streaming I/O
- I tried to make this project easy to extend. The source code
(minus dependencies) is only ~250 lines including comments, and it's all one file. Obfuscation and labyrinthine OOP is avoided,
and the source code is attemptedly structured to place the most-often-changed sections at the top.
- I'm not packaging it up as a gem because editing the source code is required to add macros.
This uses the evtest linux program to get a stream of all keyboard events.

The events are parsed using Regex and keypresses are tracked.

Phrases are mapped to Ruby methods which are called when the phrase is typed anywhere.

Text can be programmatically added / removed under the cursor using xdotool helper methods.

This program evolved from my [artoo-keyboard-macros](https://github.com/maxpleaner/artoo-keyboard-macros) project.
The reason for redoing it was that artoo-keyboard doesnt support global listeners [link to github issue](https://github.com/hybridgroup/artoo-keyboard/issues/6)

[pty](http://ruby-doc.org/stdlib-2.2.3/libdoc/pty/rdoc/PTY.html) from Ruby's stdlib is used for the streaming I/O

### Installation
- install dependencies:
- `sudo apt-get install xdotool evtest`
- clone the repo, `cd` in and run `bundle install`.
- Then run the program: `./macros_server.rb`
- Type anywhere (not just the terminal window) and notice how the text is captured.
- A list of available macros (and the name of the Ruby method they trigger) can be seen in the terminal when the
program is running.
- try typing `hello world`, which will open artoo.io in `chromium-browser`
- or type `text entry`, which will type 'hello world' under the cursor.
Note that the programmatically triggered keystrokes are **not** searched for
additional macros.

### Usage: How to add a macro:
1. create an instance method in `CommandParser` (this is the event that is fired)
2. map the event to a phrase by adding an entry to `@@macro_method_mappings` in `CommandParser`
3. Note that characters supported in macro trigger strings are: `0-9, a-z (lowercase), and whitespace.`

first install dependencies:

- `sudo apt-get install xdotool evtest`

Then clone the repo, `cd` in and run `bundle install`.

**_note_: if this fails try removing Gemfile.lock first**

Then run the program: `./macros_server.rb`

Type anywhere (not just the terminal window) and notice how the text is captured.

A list of available macros (and the name of the Ruby method they trigger) can be seen in the terminal when the
program is running.

try typing `hello world`, which will open artoo.io in `chromium-browser`
or type `text entry`, which will type 'hello world' under the cursor.

Note that the programmatically triggered keystrokes are **not** searched for
additional macros.

### How to add a macro:

1. create an instance method in `CommandParser` (this is the event that is fired)
2. map the event to a phrase by adding an entry to `@@macro_method_mappings` in `CommandParser`
3. Note that characters supported in macro trigger strings are: `0-9, a-z (lowercase), and whitespace.`

### Usage: How to trigger key presses / deletes
- **How to program a macro to enter text for me?**
- There are three helper methods:
- `CommandParser.trigger_deletes(n)` will trigger the 'BackSpace' key n times using `xdotool`.
- `CommandParser.trigger_keystrokes(string)` will translate the string into `xdotool` instructions and enter the keystrokes.
- `CommandParser.trigger_for(method_name)` looks inside `@@macro_method_mappings` to find the macro string which triggers a particular
### How to trigger key presses / deletes

**How to program a macro to enter text?**

There are three helper methods:

- `CommandParser.trigger_deletes(n)` will trigger the 'BackSpace' key n times using `xdotool`.
- `CommandParser.trigger_keystrokes(string)` will translate the string into `xdotool` instructions and enter the keystrokes.
- `CommandParser.trigger_for(method_name)` looks inside `@@macro_method_mappings` to find the macro string which triggers a particular
ruby method (event). This is used in conjunction with `trigger_deletes` to delete the trigger text. i.e.:
`CommandParser.trigger_deletes(CommandParser.trigger_for("my_ruby_method").length)` will delete whatever text was used to trigger the method.
- More characters are accepted when triggering keypresses than when
defining macro phrases. In addition to supporting `0-9, 'a'-'z' and whitespace` like macro phrases,
triggered keypresses can also include `'/', ':', ';', '@', '?', '&', and '.'`. This is so that urls and email addresses can be
supported.
- More characters are accepted when triggering keypresses than when
defining macro phrases. In addition to supporting `0-9, 'a'-'z' and whitespace` like macro phrases,
triggered keypresses can also include `'/', ':', ';', '@', '?', '&', and '.'`. This is so that urls and email addresses can be
supported.

### Caveat regarding sudo
- This script uses sudo when calling evtest (which requires it)
- **The script does not ask for sudo, and will just hang if the current user needs to use a password to use sudo.**
- For just testing this out one can run a command like `sudo pwd` and then run `macros_server.rb` in the next 5 minutes.
- The timeout of sudo can be increased by running `sudo visudo` and changing the value of `Defaults:user_name timestamp_timeout`.
- Alternatively, the script can be run like `sudo ruby macros_server.rb`. This uses sudo's ruby version (for Ubuntu, the standard is still 1.9.3).
In this case, all gems need to be installed as sudo, and different paths are needed to call system programs like `chromium-browser`.

- If the script errors after "checking for sudo access", try `sudo bundle` and `sudo ruby macros_server.rb`

### Contributing / todos
- The following features would benefit the program.
1. Being able to trigger any keystroes, not just `0-9, 'a'-'z', whitespace, '/', ':', ';', '@', '?', '&', and '.'`. This could be done by editing `CommandParser.trigger_keystrokes`.
3. Supporting more characters in macro phrases, including single-keypress named characters like `,./';[]=-` and multi-keypress characters like `!@#$%^&*()_+?><:"{}`.
3. Supporting variables in macros, i.e. `email maxpleaner@gmail.com` could use the email address as a varaible.

The main thing I want to do is add a macros YAML file

Also:

1. Being able to trigger any keystroes, not just `0-9, 'a'-'z', whitespace, '/', ':', ';', '@', '?', '&', and '.'`. This could be done by editing `CommandParser.trigger_keystrokes`.
3. Supporting more characters in macro phrases, including single-keypress named characters like `,./';[]=-` and multi-keypress characters like `!@#$%^&*()_+?><:"{}`.
3. Supporting variables in macros, i.e. `email maxpleaner@gmail.com` could use the email address as a varaible.
54 changes: 54 additions & 0 deletions lib/macro_definitions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module MacroDefinitions

def try_to_call(method_name)
method_name_sym = method_name&.to_sym

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method_name**&**.to_sym

That seems like a writing mistake

if method_name_sym && respond_to?(method_name_sym)
CommandParser.class_exec do
trigger_deletes(trigger_for(method_name_sym.to_s).length)
end
CommandParser.class_exec(&send(method_name_sym))
end
end

def linkedin_url; -> {
trigger_keystrokes "http://linkedin.com/in/maxpleaner"
}; end

def github_url; -> {
trigger_keystrokes "http://github.com/maxpleaner"
}; end

def website_url; -> {
trigger_keystrokes "http://maxpleaner.com"
}; end

def email_url; -> {
trigger_keystrokes "maxpleaner@gmail.com"
}; end

def portfolio_url; -> {
trigger_keystrokes "http://maxpleaner.github.io"
}; end

def cover_letter; -> {
trigger_keystrokes <<-TXT.strip_heredoc

Hello,

My name is Max Pleaner and I'm a web developer in the San Francisco area.
I've been programming since 2013. Prior to that I worked/studied in politics.
My strongest languages are Ruby, Javascript, and Elixir.
I'm confident in many facets of full-stack development, from deployment, APIs,
and data on the backend to animation, games, build tools, realtime, and
reactive frameworks on the frontend. I enjoy constantly learning and strive
to follow best practices, including testing and documentation. I'm eager to work
together with a team and have held a few roles in the industry.

Thanks for your consideration, Max Pleaner
http://maxpleaner.github.io
http://maxpleaner.com/resume

TXT
}; end

end
8 changes: 8 additions & 0 deletions macros.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---

"q laa": "linkedin_url"
"q gaa": "github_url"
"q waa": "website_url"
"q eaa": "email_url"
"q paa": "portfolio_url"
"q cover": "cover_letter"
62 changes: 32 additions & 30 deletions macros_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@
# @@macro_method_mappings maps macro-strings to CommandParser instance methods
# ---------------------------------------------------------

require 'yaml'
require 'byebug'
require_relative "./lib/macro_definitions.rb"

class CommandParser

include MacroDefinitions

# ---------------------------------------------------------
# Macro triggers - add macro phrases here
# ---------------------------------------------------------
Expand All @@ -23,30 +29,17 @@ class CommandParser
@@max_phrase_length = 15

# Declarations of 'phrase' => 'event' mappings
# Add something here when creating a new macro
@@macro_method_mappings = {
# key: the macro trigger phrase
# val: a CommandParser instance method
"hello world" => "hello_world",
"text entry" => "test_text_entry",
}

# ---------------------------------------------------------
# CommandParser instance methods - add macro events here
# ---------------------------------------------------------
# Add something here when creating a new macro
@@macro_method_mappings = {
# key: the macro trigger phrase
# val: a CommandParser instance method
}

if File.exists? "macros.yml"
@@macro_method_mappings.merge! YAML.load File.read "macros.yml"
end

def hello_world
self.class.trigger_deletes(self.class.trigger_for("hello_world").length)
`chromium-browser http://artoo.io`
end

def test_text_entry
self.class.trigger_deletes(self.class.trigger_for("test_text_entry").length)
self.class.trigger_keystrokes("hello world")
# these triggered keystrokes are not scanned for additional macros,
# and even though "hello world" is a macro phrase,
# the hello_world method is not called
end

# ---------------------------------------------------------
# CommandParser configuration
Expand Down Expand Up @@ -86,8 +79,9 @@ def self.add_key(key)
puts "#{"matching method".green}: #{matching_method.to_s}" if matching_method
puts "#{"current phrase".yellow}: #{@@current_phrase}"
# Run event for a matched phrase, if one was found
CommandParser::ParserInstance.try(matching_method.to_sym)
# Print available methods each time a key is typed.
# THIS IS DONE THROUGH A DELEGATOR FUNCTION IN MacroDefinitions
CommandParser::ParserInstance.try_to_call(matching_method)
# Print available methods each time a key is typed. q waa
print_available_methods
end

Expand All @@ -113,6 +107,10 @@ def self.trigger_keystrokes(string='')
'semicolon'
when '.'
'period'
when "\n"
"Return"
when ","
"comma"
else
char
end
Expand All @@ -133,7 +131,11 @@ def self.trigger_keystrokes(string='')
`xdotool key 7`
`xdotool keyup shift`
else
`xdotool key #{translated_char}`
if ["-"].include? translated_char
`xdotool type #{translated_char}`
else
`xdotool key #{translated_char}`
end
end
end
end
Expand Down Expand Up @@ -162,7 +164,8 @@ def self.shell_thread(cmd)
PTY.spawn( cmd ) do |stdout, stdin, pid|
begin
stdout.each { |line| Macros.process_line(line) } # send each output line to Macros.process_line
rescue Errno::EIO
rescue Errno::EIO => e
puts e, e.backtrace
puts "Errno:EIO error, but this probably just means " +
"that the process has finished giving output"
end
Expand Down Expand Up @@ -196,13 +199,10 @@ def self.process_line(line)
# Environment / dependency configuration
# --------------

## stdlib dependencies
require 'pty' # pseudo-terminal

## local file dependencies
require_relative './lib/run_with_timeout.rb'

## gems
require 'active_support/all'
require 'colored'

Expand All @@ -225,7 +225,9 @@ class NilClass; def to_sym; :nil; end; end;
# this is passed to Macros.shell_thread(cmd) when the script is run (see end of file)
# evtest produces a streaming log of system events
# '3' is echoed to the process to select 'keyboard' events
EventsStreamShellCommand = "(echo '3';) | sudo -S evtest" # The -S is necessary here to used saved sudo password.
# CHANGE THIS NUMBER IF NEEDED.
# USE sudo evtest TO FIND CORRECT DEVICE NUMBER
EventsStreamShellCommand = "(echo '6';) | sudo -S evtest" # The -S is necessary here to used saved sudo password.

# ---------------------
# Script execution
Expand Down