vimgolf 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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