twiddler-utils 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/twid-utils +52 -0
- data/doc/README +2 -0
- data/doc/Specifications +15 -0
- data/doc/coverage/index.html +170 -0
- data/doc/coverage/jquery-1.3.2.min.js +19 -0
- data/doc/coverage/jquery.tablesorter.min.js +15 -0
- data/doc/coverage/lib-twiddler-config_rb.html +2547 -0
- data/doc/coverage/lib-twiddler-lint_rb.html +417 -0
- data/doc/coverage/lib-twiddler-parser_rb.html +645 -0
- data/doc/coverage/lib-twiddler-rendering_rb.html +201 -0
- data/doc/coverage/lib-twiddler-target_builder_rb.html +783 -0
- data/doc/coverage/print.css +12 -0
- data/doc/coverage/rcov.js +42 -0
- data/doc/coverage/screen.css +270 -0
- data/lib/twiddler/config.rb +414 -0
- data/lib/twiddler/lint.rb +59 -0
- data/lib/twiddler/parser.rb +97 -0
- data/lib/twiddler/rendering.rb +23 -0
- data/lib/twiddler/target_builder.rb +120 -0
- data/spec_help/file-sandbox.rb +164 -0
- data/spec_help/fixtures/TWIDDLER.CFG +0 -0
- data/spec_help/gem_test_suite.rb +17 -0
- data/spec_help/spec_helper.rb +4 -0
- data/spec_help/ungemmer.rb +36 -0
- data/templates/cheatsheet.erb +38 -0
- data/templates/lua.erb +51 -0
- metadata +201 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'twiddler/config'
|
2
|
+
|
3
|
+
module Twiddler
|
4
|
+
module Lint
|
5
|
+
def self.check(config)
|
6
|
+
report = []
|
7
|
+
Rule::registered.each do |rule|
|
8
|
+
report << rule.new(config).check
|
9
|
+
end
|
10
|
+
report
|
11
|
+
end
|
12
|
+
|
13
|
+
class Rule
|
14
|
+
class << self
|
15
|
+
def register
|
16
|
+
@@registered ||= []
|
17
|
+
@@registered << self
|
18
|
+
end
|
19
|
+
|
20
|
+
def registered
|
21
|
+
@@registered || []
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(config)
|
26
|
+
@config = config
|
27
|
+
end
|
28
|
+
|
29
|
+
def check()
|
30
|
+
return ""
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class MissingStrokes < Rule
|
35
|
+
register
|
36
|
+
def check()
|
37
|
+
all_strokes = []
|
38
|
+
@config.keytable.each do |key|
|
39
|
+
all_strokes << [key.code, ""]
|
40
|
+
all_strokes << [key.code, "shift"] if key.has_mod?("shift")
|
41
|
+
end
|
42
|
+
|
43
|
+
@config.keyboard.each do |chord|
|
44
|
+
next unless chord.single?
|
45
|
+
all_strokes.delete(@config.keytable.normalized(chord[0]))
|
46
|
+
end
|
47
|
+
|
48
|
+
if all_strokes.empty?
|
49
|
+
return "Missing: all normal keystrokes accounted for"
|
50
|
+
else
|
51
|
+
return "Config lacks strokes: #{all_strokes.map{|str| @config.keytable[*str]}.inspect}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'twiddler/config'
|
2
|
+
|
3
|
+
module Twiddler
|
4
|
+
class Parser
|
5
|
+
def self.parse(data)
|
6
|
+
version = *data.unpack("c")
|
7
|
+
case version
|
8
|
+
when 4
|
9
|
+
V4Parser.new(data, 1).go.parsed
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class V4Parser
|
14
|
+
def initialize(raw, skipped = 0)
|
15
|
+
@data = raw
|
16
|
+
@refs = Hash.new{|h,k| h[k] = [:missing_ref]}
|
17
|
+
bytes_hex
|
18
|
+
@index = 0
|
19
|
+
consume("x" * skipped)
|
20
|
+
bytes_hex
|
21
|
+
@config = Config.new()
|
22
|
+
end
|
23
|
+
|
24
|
+
def parsed
|
25
|
+
return @config
|
26
|
+
end
|
27
|
+
|
28
|
+
def consume(template)
|
29
|
+
old_len = @data.length
|
30
|
+
values = @data.unpack(template + "a*")
|
31
|
+
@data = values.pop
|
32
|
+
@index += old_len - @data.length
|
33
|
+
return values
|
34
|
+
end
|
35
|
+
|
36
|
+
def bytes_hex(count = 8)
|
37
|
+
"0x%04x:#{' %02x'*count}" % ([@index] + @data.unpack("c" * count))
|
38
|
+
end
|
39
|
+
|
40
|
+
def go()
|
41
|
+
total_size = @data.length + 1
|
42
|
+
@keyboard_chord_table, @mouse_chord_table, @multichar_table = *consume("vvv")
|
43
|
+
|
44
|
+
@config.configs[:raw] = *consume("a#{@keyboard_chord_table - @index}")
|
45
|
+
|
46
|
+
until @index >= @mouse_chord_table
|
47
|
+
key1234, mod_or_ff, keyindex = consume("B16B8c")
|
48
|
+
exit if @index > 685
|
49
|
+
if keyindex == 0 and
|
50
|
+
key1234 == "0000000000000000" and
|
51
|
+
mod_or_ff == "00000000"
|
52
|
+
next
|
53
|
+
end
|
54
|
+
|
55
|
+
chord = Config::KeyChord.new
|
56
|
+
chord.keydata = key1234
|
57
|
+
|
58
|
+
if(mod_or_ff == "11111111")
|
59
|
+
@refs[keyindex] = chord
|
60
|
+
else
|
61
|
+
chord.add_keystroke(mod_or_ff, keyindex)
|
62
|
+
end
|
63
|
+
|
64
|
+
@config.keyboard << chord
|
65
|
+
end
|
66
|
+
|
67
|
+
while @index < @multichar_table
|
68
|
+
key1234, data = consume("B16B8")
|
69
|
+
if key1234 == "0000000000000000" and
|
70
|
+
data == "00000000"
|
71
|
+
next
|
72
|
+
end
|
73
|
+
|
74
|
+
chord_record = Config::MouseChord.new
|
75
|
+
chord_record.keydata = key1234
|
76
|
+
chord_record.data = data
|
77
|
+
@config.mouse << chord_record
|
78
|
+
end
|
79
|
+
|
80
|
+
data_idx = 0
|
81
|
+
until @data.size == 0 do
|
82
|
+
data_size = *consume("v")
|
83
|
+
|
84
|
+
chord_data = []
|
85
|
+
((data_size-1)/2).times do
|
86
|
+
mod, idx = *consume("B8c")
|
87
|
+
next if idx == 0 #Not sure this is sufficient
|
88
|
+
@refs[data_idx].add_keystroke(mod, idx)
|
89
|
+
end
|
90
|
+
data_idx += 1
|
91
|
+
end
|
92
|
+
|
93
|
+
return self
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Twiddler
|
4
|
+
class Rendering
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def embed(name)
|
10
|
+
path = template_path(name)
|
11
|
+
template = File::read(path)
|
12
|
+
erb = ERB.new(template, nil, "%>")
|
13
|
+
erb.filename = path
|
14
|
+
mod = erb.def_module("render")
|
15
|
+
@config.extend(mod)
|
16
|
+
end
|
17
|
+
|
18
|
+
def template_path(name)
|
19
|
+
path = File::expand_path("../../templates/#{name}.erb",
|
20
|
+
File::dirname(__FILE__))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Twiddler
|
4
|
+
module TargetBuilder
|
5
|
+
def self.build(output_dir, dictionary, config)
|
6
|
+
FileUtils::mkdir_p(output_dir)
|
7
|
+
File::open(dictionary, "r") do |dict|
|
8
|
+
Builder::registered.each do |klass|
|
9
|
+
dict.rewind
|
10
|
+
klass.new(output_dir, dict, config).go
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Builder
|
16
|
+
class << self
|
17
|
+
def register
|
18
|
+
@@registered ||= []
|
19
|
+
@@registered << self
|
20
|
+
end
|
21
|
+
|
22
|
+
def registered
|
23
|
+
@@registered || []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(dir, dict, config)
|
28
|
+
@dir = dir
|
29
|
+
@dict = dict
|
30
|
+
@config = config
|
31
|
+
end
|
32
|
+
|
33
|
+
def output_to(name)
|
34
|
+
File::open(File::expand_path("#{name}.list", @dir), "w") do |file|
|
35
|
+
yield(file)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def go
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class RegexpBuilder < Builder
|
44
|
+
def match_any_of(list)
|
45
|
+
return "(?:#{list.map{|it| Regexp::escape(it)}.join(")|(?:")})"
|
46
|
+
end
|
47
|
+
def go
|
48
|
+
output_to(filename) do |file|
|
49
|
+
@dict.grep(regexp) do |word|
|
50
|
+
file.puts(word)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class ShortWords < RegexpBuilder
|
57
|
+
register
|
58
|
+
|
59
|
+
def filename
|
60
|
+
"Short Words"
|
61
|
+
end
|
62
|
+
|
63
|
+
def regexp
|
64
|
+
/^.{1,5}$/
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Macros < RegexpBuilder
|
69
|
+
register
|
70
|
+
|
71
|
+
def filename
|
72
|
+
"Macros"
|
73
|
+
end
|
74
|
+
|
75
|
+
def regexp
|
76
|
+
macros = @config.keyboard.find_all do |chord|
|
77
|
+
chord.keystrokes.length > 1 and chord.keystrokes.all? do |stroke|
|
78
|
+
stroke[1].empty?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
return %r{^.{0,1}(?:#{match_any_of(macros.map{|chord| chord.render_action})}).{0,1}$}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Flow < RegexpBuilder
|
86
|
+
register
|
87
|
+
|
88
|
+
def filename
|
89
|
+
"Flow"
|
90
|
+
end
|
91
|
+
|
92
|
+
def regexp
|
93
|
+
flow = []
|
94
|
+
chords = @config.keyboard.find_all do |chord|
|
95
|
+
chord.single? and chord.keystrokes[0][1].empty? and Config.keytable.is_tagged?(chord.keystrokes[0][0], :letters)
|
96
|
+
end
|
97
|
+
|
98
|
+
chords.each do |first|
|
99
|
+
chords.each do |second|
|
100
|
+
next if first == second
|
101
|
+
pairs = first.rows.zip(second.rows).find_all{|one,two| one != two}
|
102
|
+
case pairs.length
|
103
|
+
when 1
|
104
|
+
flow << first.render_action + second.render_action
|
105
|
+
next
|
106
|
+
when 2
|
107
|
+
next if pairs.find{|pair| !pair.include?(:open)}
|
108
|
+
if pairs[0][0] == pairs[1][1] and pairs[0][1] == pairs[1][0]
|
109
|
+
flow << first.render_action + second.render_action
|
110
|
+
next
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
return %r{^.{0,1}(?:#{match_any_of(flow)}).{0,1}$}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'ftools'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module FileSandbox
|
5
|
+
def self.included(spec)
|
6
|
+
return unless spec.respond_to? :before
|
7
|
+
|
8
|
+
spec.before do
|
9
|
+
setup_sandbox
|
10
|
+
end
|
11
|
+
|
12
|
+
spec.after do
|
13
|
+
teardown_sandbox
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class HaveContents
|
18
|
+
def initialize(contents)
|
19
|
+
@contents = contents
|
20
|
+
end
|
21
|
+
|
22
|
+
def matches?(target)
|
23
|
+
case @contents
|
24
|
+
when Regexp
|
25
|
+
@contents =~ target.contents
|
26
|
+
when String
|
27
|
+
@contents == target.contents
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def have_contents(expected)
|
33
|
+
HaveContents.new(expected)
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :sandbox
|
37
|
+
|
38
|
+
def in_sandbox(&block)
|
39
|
+
raise "I expected to create a sandbox as you passed in a block to me" if !block_given?
|
40
|
+
|
41
|
+
setup_sandbox
|
42
|
+
original_error = nil
|
43
|
+
|
44
|
+
begin
|
45
|
+
yield @sandbox
|
46
|
+
rescue => e
|
47
|
+
original_error = e
|
48
|
+
raise
|
49
|
+
ensure
|
50
|
+
begin
|
51
|
+
teardown_sandbox
|
52
|
+
rescue
|
53
|
+
if original_error
|
54
|
+
STDERR.puts "ALERT: a test raised an error and failed to release some lock(s) in the sandbox directory"
|
55
|
+
raise(original_error)
|
56
|
+
else
|
57
|
+
raise
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def setup_sandbox(path = '__sandbox')
|
64
|
+
unless @sandbox
|
65
|
+
@sandbox = Sandbox.new(path)
|
66
|
+
@__old_path_for_sandbox = Dir.pwd
|
67
|
+
Dir.chdir(@sandbox.root)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def teardown_sandbox
|
72
|
+
if @sandbox
|
73
|
+
Dir.chdir(@__old_path_for_sandbox)
|
74
|
+
@sandbox.clean_up
|
75
|
+
@sandbox = nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Sandbox
|
80
|
+
attr_reader :root
|
81
|
+
|
82
|
+
def initialize(path = '__sandbox')
|
83
|
+
@root = File.expand_path(path)
|
84
|
+
clean_up
|
85
|
+
FileUtils.mkdir_p @root
|
86
|
+
end
|
87
|
+
|
88
|
+
def [](name)
|
89
|
+
SandboxFile.new(File.join(@root, name), name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# usage new :file=>'my file.rb', :with_contents=>'some stuff'
|
93
|
+
def new(options)
|
94
|
+
if options.has_key? :directory
|
95
|
+
dir = self[options.delete(:directory)]
|
96
|
+
FileUtils.mkdir_p dir.path
|
97
|
+
else
|
98
|
+
file = self[options.delete(:file)]
|
99
|
+
if (binary_content = options.delete(:with_binary_content) || options.delete(:with_binary_contents))
|
100
|
+
file.binary_content = binary_content
|
101
|
+
else
|
102
|
+
file.content = (options.delete(:with_content) || options.delete(:with_contents) || '')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
raise "unexpected keys '#{options.keys.join(', ')}'" unless options.empty?
|
107
|
+
|
108
|
+
dir || file
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove(options)
|
112
|
+
name = File.join(@root, options[:file])
|
113
|
+
FileUtils.remove_file name
|
114
|
+
end
|
115
|
+
|
116
|
+
def clean_up
|
117
|
+
FileUtils.rm_rf @root
|
118
|
+
if File.exists? @root
|
119
|
+
raise "Could not remove directory #{@root.inspect}, something is probably still holding a lock on it"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
class SandboxFile
|
126
|
+
attr_reader :path
|
127
|
+
|
128
|
+
def initialize(path, sandbox_path)
|
129
|
+
@path = path
|
130
|
+
@sandbox_path = sandbox_path
|
131
|
+
end
|
132
|
+
|
133
|
+
def inspect
|
134
|
+
"SandboxFile: #@sandbox_path"
|
135
|
+
end
|
136
|
+
|
137
|
+
def exist?
|
138
|
+
File.exist? path
|
139
|
+
end
|
140
|
+
|
141
|
+
def content
|
142
|
+
File.read path
|
143
|
+
end
|
144
|
+
|
145
|
+
def content=(content)
|
146
|
+
FileUtils.mkdir_p File.dirname(@path)
|
147
|
+
File.open(@path, "w") {|f| f << content}
|
148
|
+
end
|
149
|
+
|
150
|
+
def binary_content=(content)
|
151
|
+
FileUtils.mkdir_p File.dirname(@path)
|
152
|
+
File.open(@path, "wb") {|f| f << content}
|
153
|
+
end
|
154
|
+
|
155
|
+
def create
|
156
|
+
self.content = ''
|
157
|
+
end
|
158
|
+
|
159
|
+
alias exists? exist?
|
160
|
+
alias contents content
|
161
|
+
alias contents= content=
|
162
|
+
alias binary_contents= binary_content=
|
163
|
+
end
|
164
|
+
end
|