vimgolf 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b23232451856233bb686ed2dc5936c491ca41a8c467b4e950f55a2d23f70064
4
+ data.tar.gz: 268b52ce2fd8286b75d1c974659d960178db0bd9a532cb65c541105d38d49328
5
+ SHA512:
6
+ metadata.gz: 79484a6f2863cf654efa511bf8fee8dc4e771c445c43007c6bd1a6c80d1fcfedc601d8af59457e2ad0ef6bafbaa59955f168fa78af92da49715b5e67953e6729
7
+ data.tar.gz: 327f122659a3af5ba371a140a55544adef6612862eace4b71bc8da4b392587f4730613b68ac31edeffa0192eaa50d05ce0f7b2aab48a2b72b9d6626b137acca6
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [VimGolf.com](http://www.vimgolf.com) Client
1
+ # [VimGolf.com](http://www.vimgolf.com) CLI Client
2
2
 
3
3
  Real Vim ninjas count every keystroke - do you? Head on over to vimgolf.com, pick a challenge, and show us what you've got!
4
4
 
@@ -14,16 +14,8 @@ When you launch a challenge from the command line, it will be downloaded from th
14
14
  $> gem install vimgolf
15
15
 
16
16
  (Go to vimgolf.com, sign in, and grab your API key)
17
- $> vimgolf setup
17
+ $> vimgolf setup
18
18
 
19
19
  (Pick a challenge on vimgolf.com)
20
20
  $> vimgolf put [challenge ID]
21
21
  ```
22
-
23
- **Emacs**: yes, it is true, you can [play vimgolf in emacs too](https://github.com/igrigorik/vimgolf/tree/master/emacs)!
24
-
25
- Patches, tips and ideas are welcome!
26
-
27
- ## License
28
-
29
- (MIT License) - Copyright (c) 2010 Ilya Grigorik
data/lib/vimgolf.rb CHANGED
@@ -1,9 +1,12 @@
1
+ require 'rubygems'
1
2
  require 'fileutils'
2
3
  require 'net/http'
3
4
  require 'strscan'
4
5
  require 'json'
5
6
  require 'yaml'
6
7
  require 'thor'
8
+ require 'shellwords'
9
+ require 'tempfile'
7
10
 
8
11
  require 'vimgolf/version'
9
12
  require 'vimgolf/config'
@@ -1,6 +1,6 @@
1
1
  module VimGolf
2
2
  class Challenge
3
- attr_reader :id, :type
3
+ attr_reader :id, :type, :otype, :remote
4
4
 
5
5
  def self.path(path)
6
6
  @@path = path if path
@@ -11,14 +11,41 @@ module VimGolf
11
11
  @id = id
12
12
  end
13
13
 
14
+ def local(infile, outfile)
15
+ @remote = false
16
+ @input_path = File.expand_path(infile)
17
+ @output_path = File.expand_path(outfile)
18
+ @type = File.basename(@input_path) # extension? use the whole thing
19
+ @otype = File.basename(@output_path)
20
+
21
+ work_files
22
+ end
23
+
24
+ def work_files
25
+ @vimrc_path = File.expand_path('../vimgolf.vimrc', __FILE__)
26
+
27
+ # keep these Tempfile's around so they don't unlink
28
+ @work = Tempfile.new(['vimgolf', ".#{@type}"])
29
+ @log = Tempfile.new('golflog')
30
+ # close tmp files, but don't unlink
31
+ @work.close
32
+ @log.close
33
+
34
+ @work_path = @work.path()
35
+ @log_path = @log.path()
36
+ end
37
+
14
38
  def download
39
+ @remote = true
15
40
  begin
16
- url = URI.parse("#{GOLFHOST}/challenges/#{@id}.json")
17
- req = Net::HTTP::Get.new(url.path)
18
-
19
- proxy_url, proxy_user, proxy_pass = get_proxy
20
- proxy = Net::HTTP::Proxy(proxy_url.host, proxy_url.port, proxy_user, proxy_pass)
21
- res = proxy.start(url.host, url.port) { |http| http.request(req) }
41
+ url = URI("#{GOLFHOST}/challenges/#{@id}.json")
42
+ res = Net::HTTP.start(
43
+ url.hostname,
44
+ url.port,
45
+ use_ssl: url.scheme == 'https'
46
+ ) do |http|
47
+ http.request_get(url)
48
+ end
22
49
 
23
50
  @data = JSON.parse(res.body)
24
51
 
@@ -34,9 +61,14 @@ module VimGolf
34
61
  @data['in']['data'].gsub!(/\r\n/, "\n")
35
62
  @data['out']['data'].gsub!(/\r\n/, "\n")
36
63
 
37
- @type = @data['in']['type']
64
+ # be sure to sanitize the types
65
+ @type = @data['in']['type'].gsub(/[^\w-]/, '.')
66
+ @otype = @data['out']['type'].gsub(/[^\w-]/, '.')
67
+ @input_path = path + ".input.#{@type}"
68
+ @output_path = path + ".output.#{@otype}"
69
+
38
70
  save
39
- start
71
+ work_files
40
72
  rescue Exception => e
41
73
  debug(e)
42
74
  raise "Uh oh, couldn't download or parse challenge, please verify your challenge id & client version."
@@ -44,65 +76,58 @@ module VimGolf
44
76
  end
45
77
 
46
78
  def start
47
- File.open(work_path, "w") {|f| f.puts @data['in']['data']}
79
+ FileUtils.cp(@input_path, @work_path)
48
80
  end
49
81
 
50
82
  def save
51
83
  File.open(input_path, "w") {|f| f.puts @data['in']['data']}
52
84
  File.open(output_path, "w") {|f| f.puts @data['out']['data']}
53
- File.open(vimrc_path, "w") {|f| f.puts @data['vimrc']}
54
85
  end
55
86
 
56
87
  def upload
57
88
  begin
58
- url = URI.parse("#{GOLFHOST}/entry.json")
59
-
60
- proxy_url, proxy_user, proxy_pass = get_proxy
61
- proxy = Net::HTTP::Proxy(proxy_url.host, proxy_url.port, proxy_user, proxy_pass)
62
-
63
- proxy.start(url.host, url.port) do |http|
64
- request = Net::HTTP::Post.new(url.request_uri)
65
- request.set_form_data({"challenge_id" => @id, "apikey" => Config.load['key'], "entry" => IO.read(log_path)})
66
- request["Accept"] = "application/json"
67
-
68
- res = http.request(request)
69
- res = JSON.parse(res.body)
89
+ url = URI("#{GOLFHOST}/entry.json")
90
+ res = Net::HTTP.start(
91
+ url.hostname,
92
+ url.port,
93
+ use_ssl: url.scheme == 'https'
94
+ ) do |http|
95
+ http.request_post(
96
+ url,
97
+ URI.encode_www_form(
98
+ "challenge_id" => @id,
99
+ "apikey" => Config.load['key'],
100
+ "entry" => IO.binread(log_path)
101
+ ),
102
+ "Accept" => "application/json"
103
+ )
104
+ end
70
105
 
71
- raise if !res.is_a? Hash
72
- res['status'].to_sym
106
+ res = JSON.parse(res.body)
73
107
 
74
- end
108
+ raise if !res.is_a? Hash
109
+ res['status'].to_sym
75
110
  rescue Exception => e
76
111
  debug(e)
77
112
  raise "Uh oh, entry upload has failed, please check your key."
78
113
  end
79
114
  end
80
115
 
81
- def input_path; path + ".#{@type}"; end
82
- def work_path; path + ".work.#{@type}"; end
83
- def output_path; path + ".output"; end
84
- def log_path; path + ".log"; end
85
- def vimrc_path; path + ".golfrc"; end
116
+ attr_reader :input_path
117
+ attr_reader :work_path
118
+ attr_reader :output_path
119
+ attr_reader :log_path
120
+ attr_reader :vimrc_path
121
+
122
+ def correct?
123
+ FileUtils.compare_file(@work_path, @output_path)
124
+ end
86
125
 
87
126
  def path
88
127
  @@path + "/#{@id}"
89
128
  end
90
129
 
91
130
  private
92
- def get_proxy
93
- begin
94
- proxy_url = URI.parse(PROXY)
95
- rescue Exception => e
96
- VimGolf.ui.error "Invalid proxy uri in http_proxy environment variable - will try to run with out proxy"
97
- proxy_url = URI.parse("");
98
- end
99
-
100
- proxy_url.port ||= 80
101
- proxy_user, proxy_pass = proxy_url.userinfo.split(/:/) if proxy_url.userinfo
102
-
103
- return proxy_url, proxy_user, proxy_pass
104
- end
105
-
106
131
  def debug(msg)
107
132
  p [caller.first, msg] if GOLFDEBUG
108
133
  end
data/lib/vimgolf/cli.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  module VimGolf
2
2
 
3
3
  GOLFDEBUG = ENV['GOLFDEBUG'].to_sym rescue false
4
- GOLFHOST = ENV['GOLFHOST'] || "http://vimgolf.com"
5
- GOLFDIFF = ENV['GOLFDIFF'] || 'diff'
4
+ GOLFHOST = ENV['GOLFHOST'] || "https://www.vimgolf.com"
6
5
  GOLFSHOWDIFF = ENV['GOLFSHOWDIFF'] || 'vim -d -n'
7
6
  GOLFVIM = ENV['GOLFVIM'] || 'vim'
8
- PROXY = ENV['http_proxy'] || ''
9
7
 
10
8
  class Error
11
9
  end
@@ -27,12 +25,34 @@ module VimGolf
27
25
  class CLI < Thor
28
26
  include Thor::Actions
29
27
 
30
- def self.start(*)
28
+ def self.initialize_ui
29
+ return if @ui_initialized
31
30
  Thor::Base.shell = VimGolf::CLI::UI
32
31
  VimGolf.ui = VimGolf::CLI::UI.new
32
+ @ui_initialized = true
33
+ end
34
+
35
+ def self.reset_ui
36
+ return unless @ui_initialized
37
+ Thor::Base.shell = Thor::Shell::Color
38
+ VimGolf.ui = nil
39
+ @ui_initialized = false
40
+ end
41
+
42
+ def self.start(*)
43
+ initialize_ui
33
44
  super
34
45
  end
35
46
 
47
+ desc "version", "print version of Vimgolf client"
48
+ long_desc <<-DESC
49
+ Print version of the Vimgolf client.
50
+ DESC
51
+
52
+ def version
53
+ VimGolf.ui.info "Client #{Vimgolf::VERSION}"
54
+ end
55
+
36
56
  desc "setup", "configure your VimGolf credentials"
37
57
  long_desc <<-DESC
38
58
  To participate in the challenge please go to vimgolf.com and register an
@@ -43,7 +63,12 @@ module VimGolf
43
63
  DESC
44
64
 
45
65
  def setup
46
- key = VimGolf.ui.ask "Please specify your VimGolf API key (register on vimgolf.com to get it):"
66
+ VimGolf.ui.info "\nLet's setup your VimGolf key..."
67
+ VimGolf.ui.warn "1) Open vimgolf.com in your browser."
68
+ VimGolf.ui.warn "2) Click \"Sign in with Twitter\"."
69
+ VimGolf.ui.warn "3) Once signed in, copy your key (black box, top right)."
70
+
71
+ key = VimGolf.ui.ask "\nPaste your VimGolf key:"
47
72
 
48
73
  if key =~ /[\w\d]{32}/
49
74
  FileUtils.mkdir_p Config.path
@@ -56,39 +81,72 @@ module VimGolf
56
81
  end
57
82
  end
58
83
 
59
- desc "put [ID]", "launch Vim session"
84
+ desc "put CHALLENGE_ID", "launch vimgolf.com challenge"
60
85
  long_desc <<-DESC
61
86
  Launch a VimGolf session for the specified challenge ID. To find a currently
62
87
  active challenge ID, please visit vimgolf.com!
63
88
  DESC
64
89
 
65
- def put(id = nil)
90
+ def put(id)
91
+ FileUtils.mkdir_p Config.put_path
66
92
  VimGolf.ui.warn "Downloading Vimgolf challenge: #{id}"
67
93
  VimGolf::Challenge.path(Config.put_path)
68
94
  challenge = Challenge.new(id)
69
95
  challenge.download
70
96
 
97
+ play(challenge)
98
+ end
99
+
100
+ desc "local INFILE OUTFILE", "launch local challenge"
101
+ long_desc <<-DESC
102
+ Launch a local VimGolf challenge. A temporary copy of INFILE is made; the original files will not be touched.
103
+ DESC
104
+
105
+ def local(infile, outfile)
106
+ # make sure our files are sane
107
+ if !(File.file?(infile) and File.file?(outfile))
108
+ VimGolf.ui.error "INFILE and OUTFILE must exist and be regular files."
109
+ exit 1
110
+ end
111
+
112
+ challenge = Challenge.new(infile) # use the filename as id
113
+ challenge.local(infile, outfile)
114
+
115
+ play(challenge)
116
+ end
117
+
118
+ private
119
+
120
+ def play(challenge)
71
121
  begin
72
- VimGolf.ui.warn "Launching VimGolf session for challenge: #{id}"
73
- # - n - no swap file, memory only editing
74
- # - +0 - always start on line 0
75
- # - --noplugin - don't load any plugins, lets be fair!
76
- # -i NONE - don't load .viminfo (for saved macros and the like)
77
- # - u - load vimgolf .vimrc to level the playing field
78
- vimcmd = "#{GOLFVIM} -Z -n --noplugin -i NONE +0 -u \"#{challenge.vimrc_path}\" -W \"#{challenge.log_path}\" \"#{challenge.work_path}\""
122
+ challenge.start
123
+ VimGolf.ui.warn "Launching VimGolf session for challenge: #{challenge.id}"
124
+ # -Z - restricted mode, utilities not allowed
125
+ # -n - no swap file, memory only editing
126
+ # --noplugin - don't load any plugins, lets be fair!
127
+ # --nofork - otherwise GOLFVIM=gvim forks and returns immediately
128
+ # -i NONE - don't load .viminfo (for saved macros and the like)
129
+ # +0 - always start on line 0
130
+ # -u vimrc - load vimgolf .vimrc to level the playing field
131
+ # -U NONE - don't load .gvimrc
132
+ # -W logfile - keylog file (overwrites if already exists)
133
+ vimcmd = GOLFVIM.shellsplit + %W{-Z -n --noplugin -i NONE +0 -u #{challenge.vimrc_path} -U NONE -W #{challenge.log_path} #{challenge.work_path}}
134
+ if GOLFVIM == "gvim"
135
+ vimcmd += %W{ --nofork}
136
+ end
79
137
  debug(vimcmd)
80
- system(vimcmd)
138
+ system(*vimcmd) # assembled as an array, bypasses the shell
81
139
 
82
140
  if $?.exitstatus.zero?
83
- diff_files = "\"#{challenge.work_path}\" \"#{challenge.output_path}\""
84
- diff = `#{GOLFDIFF} #{diff_files}`
85
- log = Keylog.new(IO.read(challenge.log_path))
141
+ log = Keylog.new(IO.binread(challenge.log_path))
142
+
143
+ VimGolf.ui.info "\nHere are your keystrokes:"
144
+ VimGolf.ui.print_log log
86
145
 
87
146
  # Handle incorrect solutions
88
- if diff.size > 0
147
+ if !challenge.correct?()
89
148
  VimGolf.ui.error "\nUh oh, looks like your entry does not match the desired output."
90
149
  VimGolf.ui.error "Your score for this failed attempt was: #{log.score}"
91
-
92
150
  loop do
93
151
  VimGolf.ui.warn "[d] Show diff"
94
152
  VimGolf.ui.warn "[r] Retry the current challenge"
@@ -99,10 +157,9 @@ module VimGolf
99
157
  :choices => [:diff, :retry, :quit]
100
158
  when :diff
101
159
  VimGolf.ui.warn "Showing vimdiff of your attempt (left) and correct output (right)"
102
- system("#{GOLFSHOWDIFF} #{diff_files}")
160
+ system(*GOLFSHOWDIFF.shellsplit + [challenge.work_path, challenge.output_path])
103
161
  when :retry
104
162
  VimGolf.ui.warn "Retrying current challenge..."
105
- challenge.start
106
163
  raise RetryException
107
164
  when :quit
108
165
  raise Interrupt
@@ -114,23 +171,32 @@ module VimGolf
114
171
  VimGolf.ui.info "\nSuccess! Your output matches. Your score: #{log.score}"
115
172
 
116
173
  loop do
117
- VimGolf.ui.warn "[w] Upload result and retry the challenge"
118
- VimGolf.ui.warn "[x] Upload result and quit"
174
+ choices = []
175
+ begin
176
+ Config.load # raises error if user hasn't finished setup
177
+ choices = [:w, :x]
178
+ VimGolf.ui.warn "[w] Upload result and retry the challenge"
179
+ VimGolf.ui.warn "[x] Upload result and quit"
180
+ rescue
181
+ choices = [:setup]
182
+ VimGolf.ui.warn "[s] Set up vimgolf.com key to submit result"
183
+ end if challenge.remote
119
184
  VimGolf.ui.warn "[r] Do not upload result and retry the challenge"
120
185
  VimGolf.ui.warn "[q] Do not upload result and quit"
121
186
 
122
187
  case VimGolf.ui.ask_question "Choice> ",
123
188
  :type => :warn,
124
- :choices => [:w, :x, :retry, :quit]
189
+ :choices => choices + [:retry, :quit]
125
190
  when :w
126
191
  next unless upload?(challenge)
127
- challenge.start
128
192
  raise RetryException
129
193
  when :x
130
194
  next unless upload?(challenge)
131
195
  raise Interrupt
196
+ when :setup
197
+ setup
198
+ next # we can hopefully submit this time
132
199
  when :retry
133
- challenge.start
134
200
  raise RetryException
135
201
  when :quit
136
202
  raise Interrupt
@@ -139,8 +205,9 @@ module VimGolf
139
205
 
140
206
  else
141
207
  error = <<-MSG
142
- Uh oh, Vim did not exit properly. If the problem persists, please
143
- report the error on github.com/igrigorik/vimgolf
208
+ Uh oh, Vim did not exit properly.
209
+ Please ensure you can execute 'Vim' from the commandline.
210
+ If the problem persists, please report the error on github.com/igrigorik/vimgolf
144
211
  MSG
145
212
 
146
213
  VimGolf.ui.error error
@@ -157,8 +224,6 @@ module VimGolf
157
224
  VimGolf.ui.error "If the error persists, please report it to github.com/igrigorik/vimgolf"
158
225
  end
159
226
 
160
- private
161
-
162
227
  def upload?(challenge)
163
228
  VimGolf.ui.warn "Uploading to VimGolf..."
164
229
 
@@ -1,225 +1,271 @@
1
+ # encoding: ASCII-8BIT
2
+ # Force encoding of string literals. Must match solution text.
3
+
1
4
  module VimGolf
2
5
  class Keylog
3
6
  include Enumerable
4
7
 
5
- alias_method :convert , :to_s
6
- alias_method :score , :count
7
-
8
- def initialize(input)
9
- @input = input
8
+ def initialize(input, time=Time.now.utc)
9
+ # Force encoding of solution text. Must match string literals.
10
+ # .force_encoding CHANGES THE ORIGINAL STRING!
11
+ @input = input.force_encoding(Encoding::ASCII_8BIT)
12
+ @time = time
10
13
  end
11
14
 
12
15
  def to_s(sep = '')
13
16
  to_a.join(sep)
14
17
  end
15
18
 
19
+ alias_method :convert , :to_s
20
+ alias_method :score , :count
21
+
16
22
  def each
17
23
  scanner = StringScanner.new(@input)
18
- output = ""
19
-
20
- until scanner.eos?
21
- c = scanner.get_byte
22
- n = c.unpack('C').first
23
-
24
- out_char = \
25
- case n
26
-
27
- # Special platform-independent encoding stuff
28
- when 0x80
29
- code = scanner.get_byte + scanner.get_byte
30
-
31
- # This list has been populated by looking at
32
- # :h terminal-options and vim source files:
33
- # keymap.h and misc2.c
34
- case code
35
- when "k1"; "<F1>"
36
- when "k2"; "<F2>"
37
- when "k3"; "<F3>"
38
- when "k4"; "<F4>"
39
- when "k5"; "<F5>"
40
- when "k6"; "<F6>"
41
- when "k7"; "<F7>"
42
- when "k8"; "<F8>"
43
- when "k9"; "<F9>"
44
- when "k;"; "<F10>"
45
- when "F1"; "<F11>"
46
- when "F2"; "<F12>"
47
- when "F3"; "<F13>"
48
- when "F4"; "<F14>"
49
- when "F5"; "<F15>"
50
- when "F6"; "<F16>"
51
- when "F7"; "<F17>"
52
- when "F8"; "<F18>"
53
- when "F9"; "<F19>"
54
-
55
- when "%1"; "<Help>"
56
- when "&8"; "<Undo>"
57
- when "#2"; "<S-Home>"
58
- when "*7"; "<S-End>"
59
- when "K1"; "<kHome>"
60
- when "K4"; "<kEnd>"
61
- when "K3"; "<kPageUp>"
62
- when "K5"; "<kPageDown>"
63
- when "K6"; "<kPlus>"
64
- when "K7"; "<kMinus>"
65
- when "K8"; "<kDivide>"
66
- when "K9"; "<kMultiply>"
67
- when "KA"; "<kEnter>"
68
- when "KB"; "<kPoint>"
69
- when "KC"; "<k0>"
70
- when "KD"; "<k1>"
71
- when "KE"; "<k2>"
72
- when "KF"; "<k3>"
73
- when "KG"; "<k4>"
74
- when "KH"; "<k5>"
75
- when "KI"; "<k6>"
76
- when "KJ"; "<k7>"
77
- when "KK"; "<k8>"
78
- when "KL"; "<k9>"
79
-
80
- when "kP"; "<PageUp>"
81
- when "kN"; "<PageDown>"
82
- when "kh"; "<Home>"
83
- when "@7"; "<End>"
84
- when "kI"; "<Insert>"
85
- when "kD"; "<Del>"
86
- when "kb"; "<BS>"
87
-
88
- when "ku"; "<Up>"
89
- when "kd"; "<Down>"
90
- when "kl"; "<Left>"
91
- when "kr"; "<Right>"
92
- when "#4"; "<S-Left>"
93
- when "%i"; "<S-Right>"
94
-
95
- when "kB"; "<S-Tab>"
96
- when "\xffX"; "<C-@>"
97
-
98
- when "\xfd\x4"; "<S-Up>"
99
- when "\xfd\x5"; "<S-Down>"
100
- when "\xfd\x6"; "<S-F1>"
101
- when "\xfd\x7"; "<S-F2>"
102
- when "\xfd\x8"; "<S-F3>"
103
- when "\xfd\x9"; "<S-F4>"
104
- when "\xfd\xa"; "<S-F5>"
105
- when "\xfd\xb"; "<S-F6>"
106
- when "\xfd\xc"; "<S-F7>"
107
- when "\xfd\xd"; "<S-F9>"
108
- when "\xfd\xe"; "<S-F10>"
109
- when "\xfd\xf"; "<S-F10>"
110
- when "\xfd\x10"; "<S-F11>"
111
- when "\xfd\x11"; "<S-F12>"
112
- when "\xfd\x12"; "<S-F13>"
113
- when "\xfd\x13"; "<S-F14>"
114
- when "\xfd\x14"; "<S-F15>"
115
- when "\xfd\x15"; "<S-F16>"
116
- when "\xfd\x16"; "<S-F17>"
117
- when "\xfd\x17"; "<S-F18>"
118
- when "\xfd\x18"; "<S-F19>"
119
- when "\xfd\x19"; "<S-F20>"
120
- when "\xfd\x1a"; "<S-F21>"
121
- when "\xfd\x1b"; "<S-F22>"
122
- when "\xfd\x1c"; "<S-F23>"
123
- when "\xfd\x1d"; "<S-F24>"
124
- when "\xfd\x1e"; "<S-F25>"
125
- when "\xfd\x1f"; "<S-F26>"
126
- when "\xfd\x20"; "<S-F27>"
127
- when "\xfd\x21"; "<S-F28>"
128
- when "\xfd\x22"; "<S-F29>"
129
- when "\xfd\x23"; "<S-F30>"
130
- when "\xfd\x24"; "<S-F31>"
131
- when "\xfd\x25"; "<S-F32>"
132
- when "\xfd\x26"; "<S-F33>"
133
- when "\xfd\x27"; "<S-F34>"
134
- when "\xfd\x28"; "<S-F35>"
135
- when "\xfd\x29"; "<S-F36>"
136
- when "\xfd\x2a"; "<S-F37>"
137
- when "\xfd\x2b"; "<Mouse>"
138
- when "\xfd\x2c"; "<LeftMouse>"
139
- when "\xfd\x2d"; "<LeftDrag>"
140
- when "\xfd\x2e"; "<LeftRelease>"
141
- when "\xfd\x2f"; "<MiddleMouse>"
142
- when "\xfd\x30"; "<MiddleDrag>"
143
- when "\xfd\x31"; "<MiddleRelease>"
144
- when "\xfd\x32"; "<RightMouse>"
145
- when "\xfd\x33"; "<RightDrag>"
146
- when "\xfd\x34"; "<RightRelease>"
147
- #when "\xfd\x35"; "KE_IGNORE"
148
- #when "\xfd\x36"; "KE_TAB"
149
- #when "\xfd\x37"; "KE_S_TAB_OLD"
150
- #when "\xfd\x38"; "KE_SNIFF"
151
- #when "\xfd\x39"; "KE_XF1"
152
- #when "\xfd\x3a"; "KE_XF2"
153
- #when "\xfd\x3b"; "KE_XF3"
154
- #when "\xfd\x3c"; "KE_XF4"
155
- #when "\xfd\x3d"; "KE_XEND"
156
- #when "\xfd\x3e"; "KE_ZEND"
157
- #when "\xfd\x3f"; "KE_XHOME"
158
- #when "\xfd\x40"; "KE_ZHOME"
159
- #when "\xfd\x41"; "KE_XUP"
160
- #when "\xfd\x42"; "KE_XDOWN"
161
- #when "\xfd\x43"; "KE_XLEFT"
162
- #when "\xfd\x44"; "KE_XRIGHT"
163
- #when "\xfd\x45"; "KE_LEFTMOUSE_NM"
164
- #when "\xfd\x46"; "KE_LEFTRELEASE_NM"
165
- #when "\xfd\x47"; "KE_S_XF1"
166
- #when "\xfd\x48"; "KE_S_XF2"
167
- #when "\xfd\x49"; "KE_S_XF3"
168
- #when "\xfd\x4a"; "KE_S_XF4"
169
- when "\xfd\x4b"; "<MouseDown>"
170
- when "\xfd\x4c"; "<MouseUp>"
171
- when "\xfd\x4d"; "<MouseLeft>"
172
- when "\xfd\x4e"; "<MouseRight>"
173
- #when "\xfd\x4f"; "KE_KINS"
174
- #when "\xfd\x50"; "KE_KDEL"
175
- #when "\xfd\x51"; "KE_CSI"
176
- #when "\xfd\x52"; "KE_SNR"
177
- #when "\xfd\x53"; "KE_PLUG"
178
- #when "\xfd\x54"; "KE_CMDWIN"
179
- when "\xfd\x55"; "<C-Left>"
180
- when "\xfd\x56"; "<C-Right>"
181
- when "\xfd\x57"; "<C-Home>"
182
- when "\xfd\x58"; "<C-End>"
183
- #when "\xfd\x59"; "KE_X1MOUSE"
184
- #when "\xfd\x5a"; "KE_X1DRAG"
185
- #when "\xfd\x5b"; "KE_X1RELEASE"
186
- #when "\xfd\x5c"; "KE_X2MOUSE"
187
- #when "\xfd\x5d"; "KE_X2DRAG"
188
- #when "\xfd\x5e"; "KE_X2RELEASE"
189
- #when "\xfd\x5f"; "KE_DROP"
190
- #when "\xfd\x5e"; "KE_CURSORHOLD"
191
- #when "\xfd\x61"; "KE_NOP"
192
- when "\xfd\x62"; nil # Focus Gained (GVIM)
193
- when "\xfd\x63"; nil # Focus Lost (GVIM)
194
-
195
- else
196
- #puts "Unknown Vim code: #{code.inspect}"
197
- '<%02x-%02x>' % code.unpack('CC')
198
- end
199
-
200
- # Control characters with special names
201
- when 0; "<Nul>"
202
- when 9; "<Tab>"
203
- when 10; "<NL>"
204
- when 13; "<CR>"
205
- when 27; "<Esc>"
206
-
207
- when 127; "<Del>"
208
-
209
- # Otherwise, use <C-x> format
210
- when 0..31; "<C-#{(n + 64).chr}>"
211
-
212
- # The rest of ANSI is printable
213
- when 32..126; c
214
-
215
- else
216
- #puts "Unexpected extended ASCII: #{'%#04x' % n}"
217
- '<%#04x>' % n
218
24
 
25
+ # A Vim keycode is either a single byte, or a 3-byte sequence starting
26
+ # with 0x80.
27
+ while (c = scanner.get_byte)
28
+ n = c.ord
29
+ if n == 0x80
30
+ b2, b3 = scanner.get_byte, scanner.get_byte
31
+ if b2 == "\xfd" && b3 >= "\x38" && @time.between?(*NO_SNIFF_DATE_RANGE)
32
+ # Should we account for KE_SNIFF removal?
33
+ b3 = (b3.ord + 1).chr
34
+ end
35
+ code = KC_MBYTE[b2+b3]
36
+ yield code if code # ignore "nil" keystrokes (like window focus)
37
+ else
38
+ yield KC_1BYTE[n]
219
39
  end
220
-
221
- yield out_char if out_char
222
40
  end
223
41
  end
42
+
43
+ # Quick lookup array for single-byte keycodes
44
+ KC_1BYTE = []
45
+ (0..255).each {|n| KC_1BYTE.push("<%#04x>" % n)} # Fallback for non-ASCII
46
+ (1..127).each {|n| KC_1BYTE[n] = "<C-#{(n ^ 0x40).chr}>"}
47
+ (32..126).each {|c| KC_1BYTE[c] = c.chr } # Printing chars
48
+ KC_1BYTE[0x1b] = "<Esc>" # Special names for a few control chars
49
+ KC_1BYTE[0x0d] = "<CR>"
50
+ KC_1BYTE[0x0a] = "<NL>"
51
+ KC_1BYTE[0x09] = "<Tab>"
52
+
53
+ # Between these dates, assume KE_SNIFF is removed.
54
+ NO_SNIFF_DATE_RANGE = [Time.utc(2016, 4), Time.utc(2017, 7)]
55
+
56
+ KC_MBYTE = Hash.new do |_h,k|
57
+ '<' + k.bytes.map {|b| "%02x" % b}.join('-') + '>' # For missing keycodes
58
+ end.update({
59
+ # This list has been populated by looking at
60
+ # :h terminal-options and vim source files:
61
+ # keymap.h and misc2.c
62
+ "k1" => "<F1>",
63
+ "k2" => "<F2>",
64
+ "k3" => "<F3>",
65
+ "k4" => "<F4>",
66
+ "k5" => "<F5>",
67
+ "k6" => "<F6>",
68
+ "k7" => "<F7>",
69
+ "k8" => "<F8>",
70
+ "k9" => "<F9>",
71
+ "k;" => "<F10>",
72
+ "F1" => "<F11>",
73
+ "F2" => "<F12>",
74
+ "F3" => "<F13>",
75
+ "F4" => "<F14>",
76
+ "F5" => "<F15>",
77
+ "F6" => "<F16>",
78
+ "F7" => "<F17>",
79
+ "F8" => "<F18>",
80
+ "F9" => "<F19>",
81
+
82
+ "%1" => "<Help>",
83
+ "&8" => "<Undo>",
84
+ "#2" => "<S-Home>",
85
+ "*7" => "<S-End>",
86
+ "K1" => "<kHome>",
87
+ "K4" => "<kEnd>",
88
+ "K3" => "<kPageUp>",
89
+ "K5" => "<kPageDown>",
90
+ "K6" => "<kPlus>",
91
+ "K7" => "<kMinus>",
92
+ "K8" => "<kDivide>",
93
+ "K9" => "<kMultiply>",
94
+ "KA" => "<kEnter>",
95
+ "KB" => "<kPoint>",
96
+ "KC" => "<k0>",
97
+ "KD" => "<k1>",
98
+ "KE" => "<k2>",
99
+ "KF" => "<k3>",
100
+ "KG" => "<k4>",
101
+ "KH" => "<k5>",
102
+ "KI" => "<k6>",
103
+ "KJ" => "<k7>",
104
+ "KK" => "<k8>",
105
+ "KL" => "<k9>",
106
+
107
+ "kP" => "<PageUp>",
108
+ "kN" => "<PageDown>",
109
+ "kh" => "<Home>",
110
+ "@7" => "<End>",
111
+ "kI" => "<Insert>",
112
+ "kD" => "<Del>",
113
+ "kb" => "<BS>",
114
+
115
+ "ku" => "<Up>",
116
+ "kd" => "<Down>",
117
+ "kl" => "<Left>",
118
+ "kr" => "<Right>",
119
+ "#4" => "<S-Left>",
120
+ "%i" => "<S-Right>",
121
+
122
+ "kB" => "<S-Tab>",
123
+ "\xffX" => "<C-@>",
124
+
125
+ # This is how you escape literal 0x80
126
+ "\xfeX" => "<0x80>",
127
+
128
+ # These rarely-used modifiers should be combined with the next
129
+ # stroke (like <S-Space>), but let's put them here for now
130
+ "\xfc\x02" => "<S->",
131
+ "\xfc\x04" => "<C->",
132
+ "\xfc\x06" => "<C-S->",
133
+ "\xfc\x08" => "<A->",
134
+ "\xfc\x0a" => "<A-S->",
135
+ "\xfc\x0c" => "<C-A>",
136
+ "\xfc\x0e" => "<C-A-S->",
137
+ "\xfc\x10" => "<M->",
138
+ "\xfc\x12" => "<M-S->",
139
+ "\xfc\x14" => "<M-C->",
140
+ "\xfc\x16" => "<M-C-S->",
141
+ "\xfc\x18" => "<M-A->",
142
+ "\xfc\x1a" => "<M-A-S->",
143
+ "\xfc\x1c" => "<M-C-A>",
144
+ "\xfc\x1e" => "<M-C-A-S->",
145
+
146
+ # KS_EXTRA keycodes (starting with 0x80 0xfd) are defined by an enum in
147
+ # Vim's keymap.h. Sometimes, a new Vim adds or removes a keycode, which
148
+ # changes the binary representation of every keycode after it. Very
149
+ # annoying.
150
+ "\xfd\x4" => "<S-Up>",
151
+ "\xfd\x5" => "<S-Down>",
152
+ "\xfd\x6" => "<S-F1>",
153
+ "\xfd\x7" => "<S-F2>",
154
+ "\xfd\x8" => "<S-F3>",
155
+ "\xfd\x9" => "<S-F4>",
156
+ "\xfd\xa" => "<S-F5>",
157
+ "\xfd\xb" => "<S-F6>",
158
+ "\xfd\xc" => "<S-F7>",
159
+ "\xfd\xd" => "<S-F9>",
160
+ "\xfd\xe" => "<S-F10>",
161
+ "\xfd\xf" => "<S-F10>",
162
+ "\xfd\x10" => "<S-F11>",
163
+ "\xfd\x11" => "<S-F12>",
164
+ "\xfd\x12" => "<S-F13>",
165
+ "\xfd\x13" => "<S-F14>",
166
+ "\xfd\x14" => "<S-F15>",
167
+ "\xfd\x15" => "<S-F16>",
168
+ "\xfd\x16" => "<S-F17>",
169
+ "\xfd\x17" => "<S-F18>",
170
+ "\xfd\x18" => "<S-F19>",
171
+ "\xfd\x19" => "<S-F20>",
172
+ "\xfd\x1a" => "<S-F21>",
173
+ "\xfd\x1b" => "<S-F22>",
174
+ "\xfd\x1c" => "<S-F23>",
175
+ "\xfd\x1d" => "<S-F24>",
176
+ "\xfd\x1e" => "<S-F25>",
177
+ "\xfd\x1f" => "<S-F26>",
178
+ "\xfd\x20" => "<S-F27>",
179
+ "\xfd\x21" => "<S-F28>",
180
+ "\xfd\x22" => "<S-F29>",
181
+ "\xfd\x23" => "<S-F30>",
182
+ "\xfd\x24" => "<S-F31>",
183
+ "\xfd\x25" => "<S-F32>",
184
+ "\xfd\x26" => "<S-F33>",
185
+ "\xfd\x27" => "<S-F34>",
186
+ "\xfd\x28" => "<S-F35>",
187
+ "\xfd\x29" => "<S-F36>",
188
+ "\xfd\x2a" => "<S-F37>",
189
+ "\xfd\x2b" => "<Mouse>",
190
+ "\xfd\x2c" => "<LeftMouse>",
191
+ "\xfd\x2d" => "<LeftDrag>",
192
+ "\xfd\x2e" => "<LeftRelease>",
193
+ "\xfd\x2f" => "<MiddleMouse>",
194
+ "\xfd\x30" => "<MiddleDrag>",
195
+ "\xfd\x31" => "<MiddleRelease>",
196
+ "\xfd\x32" => "<RightMouse>",
197
+ "\xfd\x33" => "<RightDrag>",
198
+ "\xfd\x34" => "<RightRelease>",
199
+ "\xfd\x35" => nil, # KE_IGNORE
200
+ #"\xfd\x36" => "KE_TAB",
201
+ #"\xfd\x37" => "KE_S_TAB_OLD",
202
+
203
+ # Vim 7.4.1433 removed KE_SNIFF. Unfortunately, this changed the
204
+ # offset of every keycode after it.
205
+ # Vim 8.0.0697 added back a KE_SNIFF_UNUSED to fill in for the
206
+ # removed KE_SNIFF.
207
+ # Keycodes after this point should be accurate for vim < 7.4.1433
208
+ # and vim > 8.0.0697.
209
+ #"\xfd\x38" => "KE_SNIFF",
210
+ #"\xfd\x39" => "KE_XF1",
211
+ #"\xfd\x3a" => "KE_XF2",
212
+ #"\xfd\x3b" => "KE_XF3",
213
+ #"\xfd\x3c" => "KE_XF4",
214
+ #"\xfd\x3d" => "KE_XEND",
215
+ #"\xfd\x3e" => "KE_ZEND",
216
+ #"\xfd\x3f" => "KE_XHOME",
217
+ #"\xfd\x40" => "KE_ZHOME",
218
+ #"\xfd\x41" => "KE_XUP",
219
+ #"\xfd\x42" => "KE_XDOWN",
220
+ #"\xfd\x43" => "KE_XLEFT",
221
+ #"\xfd\x44" => "KE_XRIGHT",
222
+ #"\xfd\x45" => "KE_LEFTMOUSE_NM",
223
+ #"\xfd\x46" => "KE_LEFTRELEASE_NM",
224
+ #"\xfd\x47" => "KE_S_XF1",
225
+ #"\xfd\x48" => "KE_S_XF2",
226
+ #"\xfd\x49" => "KE_S_XF3",
227
+ #"\xfd\x4a" => "KE_S_XF4",
228
+ "\xfd\x4b" => "<ScrollWheelUp>",
229
+ "\xfd\x4c" => "<ScrollWheelDown>",
230
+
231
+ # Horizontal scroll wheel support was added in Vim 7.3c. These
232
+ # 2 entries shifted the rest of the KS_EXTRA mappings down 2.
233
+ # Though Vim 7.2 is rare today, it was common soon after
234
+ # vimgolf.com was launched. In cases where the 7.3 code is
235
+ # never used but the 7.2 code was common, it makes sense to use
236
+ # the 7.2 code. There are conflicts though, so some legacy
237
+ # keycodes have to stay wrong.
238
+ "\xfd\x4d" => "<ScrollWheelRight>",
239
+ "\xfd\x4e" => "<ScrollWheelLeft>",
240
+ "\xfd\x4f" => "<kInsert>",
241
+ "\xfd\x50" => "<kDel>",
242
+ "\xfd\x51" => "<0x9b>", # :help <CSI>
243
+ #"\xfd\x52" => "KE_SNR",
244
+ #"\xfd\x53" => "KE_PLUG", # never used
245
+ "\xfd\x53" => "<C-Left>", # 7.2 compat
246
+ #"\xfd\x54" => "KE_CMDWIN", # never used
247
+ "\xfd\x54" => "<C-Right>", # 7.2 compat
248
+ "\xfd\x55" => "<C-Left>", # 7.2 <C-Home> conflict
249
+ "\xfd\x56" => "<C-Right>", # 7.2 <C-End> conflict
250
+ "\xfd\x57" => "<C-Home>",
251
+ "\xfd\x58" => "<C-End>",
252
+ #"\xfd\x59" => "KE_X1MOUSE",
253
+ #"\xfd\x5a" => "KE_X1DRAG",
254
+ #"\xfd\x5b" => "KE_X1RELEASE",
255
+ #"\xfd\x5c" => "KE_X2MOUSE",
256
+ #"\xfd\x5d" => "KE_X2DRAG",
257
+ #"\xfd\x5e" => "KE_X2RELEASE",
258
+ "\xfd\x5e" => nil, # 7.2 compat (I think?)
259
+ #"\xfd\x5f" => "KE_DROP",
260
+ #"\xfd\x60" => "KE_CURSORHOLD",
261
+
262
+ # If you use gvim, you'll get an entry in your keylog every time the
263
+ # window gains or loses focus. These "keystrokes" should not show and
264
+ # should not be counted.
265
+ "\xfd\x60" => nil, # 7.2 Focus Gained compat
266
+ "\xfd\x61" => nil, # Focus Gained (GVIM) (>7.4.1433)
267
+ "\xfd\x62" => nil, # Focus Gained (GVIM)
268
+ "\xfd\x63" => nil, # Focus Lost (GVIM)
269
+ })
224
270
  end
225
271
  end