editserver 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +2 -0
- data/README.markdown +28 -0
- data/Rakefile +30 -0
- data/bin/editserver +5 -0
- data/editserver.gemspec +27 -0
- data/extra/editserver.applescript.erb +46 -0
- data/lib/editserver/command.rb +108 -0
- data/lib/editserver/editor.rb +47 -0
- data/lib/editserver/response.rb +60 -0
- data/lib/editserver/terminal/emacs.rb +24 -0
- data/lib/editserver/terminal/vim.rb +26 -0
- data/lib/editserver/version.rb +3 -0
- data/lib/editserver.rb +92 -0
- data/test/editserver/command.test.rb +64 -0
- data/test/editserver/editor.test.rb +74 -0
- data/test/editserver/response.test.rb +51 -0
- data/test/editserver/terminal/emacs.test.rb +5 -0
- data/test/editserver/terminal/vim.test.rb +5 -0
- data/test/editserver.test.rb +160 -0
- data/test/test-editor +6 -0
- metadata +131 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
__ __
|
3
|
+
/\ \ __/\ \__
|
4
|
+
__ \_\ \/\_\ \ ,_\ ____ __ _ __ __ __ __ _ __
|
5
|
+
/'__`\ /'_` \/\ \ \ \/ /',__\ /'__`\/\`'__\/\ \/\ \ /'__`\/\`'__\
|
6
|
+
/\ __//\ \L\ \ \ \ \ \_/\__, `\/\ __/\ \ \/ \ \ \_/ |/\ __/\ \ \/
|
7
|
+
\ \____\ \___,_\ \_\ \__\/\____/\ \____\\ \_\ \ \___/ \ \____\\ \_\
|
8
|
+
\/____/\/__,_ /\/_/\/__/\/___/ \/____/ \/_/ \/__/ \/____/ \/_/
|
9
|
+
|
10
|
+
guns <sung@metablu.com>
|
11
|
+
|
12
|
+
|
13
|
+
# Your favorite editor, as a local web service!
|
14
|
+
|
15
|
+
For use with [Textaid][1], on OS X with included applescript, or with any other
|
16
|
+
http client.
|
17
|
+
|
18
|
+
Everything works, and core tests are in place. More information forthcoming.
|
19
|
+
|
20
|
+
|
21
|
+
### TODO
|
22
|
+
|
23
|
+
* Finish remaining tests
|
24
|
+
* Improve applescript reliability
|
25
|
+
* Clean up `Editserver::` namespace
|
26
|
+
|
27
|
+
|
28
|
+
[1]: https://chrome.google.com/webstore/detail/ppoadiihggafnhokfkpphojggcdigllp
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'erb'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Run all tests'
|
10
|
+
task :test do
|
11
|
+
Dir['test/**/*.test.rb'].each { |f| load f }
|
12
|
+
end
|
13
|
+
|
14
|
+
if RUBY_PLATFORM[/darwin/]
|
15
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
16
|
+
require 'editserver/command'
|
17
|
+
|
18
|
+
@cmd = Editserver::Command.new
|
19
|
+
|
20
|
+
desc "[PORT=#{@cmd.rackopts[:Port]}] Compile included AppleScript and place in ~/Library/Scripts/"
|
21
|
+
task :applescript do
|
22
|
+
@port = ENV['PORT'] || @cmd.rackopts[:Port]
|
23
|
+
buf = ERB.new(File.read 'extra/editserver.applescript.erb').result(binding)
|
24
|
+
outfile = File.expand_path '~/Library/Scripts/editserver.scpt'
|
25
|
+
|
26
|
+
puts "Writing #{outfile}"
|
27
|
+
FileUtils.mkdir_p File.dirname(outfile)
|
28
|
+
system "echo #{buf.shellescape} | osacompile -o #{outfile.shellescape}"
|
29
|
+
end
|
30
|
+
end
|
data/bin/editserver
ADDED
data/editserver.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
4
|
+
require 'editserver/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'editserver'
|
8
|
+
s.version = Editserver::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ['Sung Pae']
|
11
|
+
s.email = ['sung@metablu.com']
|
12
|
+
s.homepage = 'http://github.com/guns/editserver'
|
13
|
+
s.summary = %q{Your favorite editor, in every app}
|
14
|
+
s.description = %Q{Simple local server for editing text in your favorite editor.}
|
15
|
+
|
16
|
+
s.rubyforge_project = 'editserver'
|
17
|
+
|
18
|
+
s.files = %x(git ls-files).split "\n"
|
19
|
+
s.test_files = %x(git ls-files -- {test,spec,features}/*).split "\n"
|
20
|
+
s.executables = %x(git ls-files -- bin/*).split("\n").map { |f| File.basename f }
|
21
|
+
s.require_paths = ['lib']
|
22
|
+
|
23
|
+
s.add_dependency 'rack', '~> 1.2'
|
24
|
+
|
25
|
+
s.add_development_dependency 'minitest', '~> 2.0'
|
26
|
+
s.add_development_dependency 'ruby-debug19' if RUBY_VERSION >= '1.9.0'
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
(*
|
2
|
+
Edit in Editserver
|
3
|
+
|
4
|
+
guns <sung@metablu.com>
|
5
|
+
http://github.com/guns/editserver
|
6
|
+
MIT LICENSE
|
7
|
+
|
8
|
+
"AppleScript sucks"
|
9
|
+
*)
|
10
|
+
|
11
|
+
tell application "System Events"
|
12
|
+
set current_app to name of (first process whose frontmost is true) as text
|
13
|
+
end tell
|
14
|
+
|
15
|
+
tell application current_app
|
16
|
+
activate -- script is probably being executed by a launcher
|
17
|
+
|
18
|
+
-- HACK: tell messages are async, so we delay, since I don't know how
|
19
|
+
-- to register a callback in AppleScript
|
20
|
+
tell application "System Events"
|
21
|
+
delay 0.5
|
22
|
+
keystroke "a" using command down
|
23
|
+
delay 0.5
|
24
|
+
keystroke "c" using command down
|
25
|
+
end tell
|
26
|
+
end tell
|
27
|
+
|
28
|
+
-- URI.escape does not escape `&'s; CGI.escape is more thorough
|
29
|
+
set output to do shell script "/usr/bin/env ruby -r cgi -e '
|
30
|
+
system *%W[curl -s --data id=applescript
|
31
|
+
--data url=#{CGI.escape %q(" & current_app & ")}
|
32
|
+
--data text=#{CGI.escape %x(pbpaste)}
|
33
|
+
http://127.0.0.1:<%= @port %>]
|
34
|
+
' | pbcopy" -- pipe to clipboard
|
35
|
+
|
36
|
+
tell application current_app
|
37
|
+
activate -- make sure we switch back; not all editors will raise focus
|
38
|
+
|
39
|
+
-- HACK: see above
|
40
|
+
tell application "System Events"
|
41
|
+
delay 0.5
|
42
|
+
keystroke "a" using command down
|
43
|
+
delay 0.5
|
44
|
+
keystroke "v" using command down
|
45
|
+
end tell
|
46
|
+
end tell
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
require 'rack'
|
4
|
+
require 'editserver'
|
5
|
+
|
6
|
+
class Editserver
|
7
|
+
class Command
|
8
|
+
def initialize args = []
|
9
|
+
@args = args
|
10
|
+
@opts = { :rcfile => "~/.editserverrc" }
|
11
|
+
|
12
|
+
# keys are strings because YAML.load returns string keys,
|
13
|
+
# and we are not restricting the keys like @rackopts
|
14
|
+
@editoropts = {
|
15
|
+
'default' => nil,
|
16
|
+
'terminal' => nil
|
17
|
+
}
|
18
|
+
|
19
|
+
@rackopts = {
|
20
|
+
:environment => 'development',
|
21
|
+
:pid => nil,
|
22
|
+
:Port => 9999,
|
23
|
+
:Host => '127.0.0.1',
|
24
|
+
:AccessLog => [],
|
25
|
+
:config => ''
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def options
|
30
|
+
OptionParser.new do |opt|
|
31
|
+
opt.summary_width = 20
|
32
|
+
|
33
|
+
opt.banner = %Q(\
|
34
|
+
Usage: #{File.basename $0} [options]
|
35
|
+
|
36
|
+
Options:
|
37
|
+
).gsub /^ +/, ''
|
38
|
+
|
39
|
+
opt.on '-p', '--port NUMBER', Integer, "default: #{rackopts[:Port]}" do |arg|
|
40
|
+
@rackopts[:Port] = arg
|
41
|
+
end
|
42
|
+
|
43
|
+
opt.on '-t', '--terminal CMD', 'Terminal to launch for console editors' do |arg|
|
44
|
+
@editoropts['terminal'] = arg
|
45
|
+
end
|
46
|
+
|
47
|
+
opt.on '--rc PATH', "Path to rc file; #{@opts[:rcfile]} by default",
|
48
|
+
'(Also can be set by exporting EDITSERVERRC to environment)' do |arg|
|
49
|
+
@rcopts = nil # reset cached user opts
|
50
|
+
@opts[:rcfile] = File.expand_path arg
|
51
|
+
end
|
52
|
+
|
53
|
+
opt.on '--no-rc', 'Suppress reading of rc file' do
|
54
|
+
@opts[:norcfile] = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def rcopts
|
60
|
+
@rcopts ||= begin
|
61
|
+
empty = { 'rack' => {}, 'editor' => {} }
|
62
|
+
rcfile = File.expand_path ENV['EDITSERVERRC'] || @opts[:rcfile]
|
63
|
+
|
64
|
+
if @opts[:norcfile]
|
65
|
+
empty
|
66
|
+
elsif File.exists? rcfile
|
67
|
+
opts = YAML.load_file File.expand_path rcfile
|
68
|
+
opts ||= {}
|
69
|
+
opts['rack'] ||= {}
|
70
|
+
opts['editor'] ||= {}
|
71
|
+
opts
|
72
|
+
else
|
73
|
+
empty
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# returns dup of @rackopts masked by rcopts
|
79
|
+
def rackopts
|
80
|
+
(opts = @rackopts.dup).keys.each do |k|
|
81
|
+
v = rcopts['rack'][k.to_s]
|
82
|
+
v = rcopts['rack'][k.to_s.downcase] if v.nil? # be tolerant of lowercase keys
|
83
|
+
opts[k] = v unless v.nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
opts
|
87
|
+
end
|
88
|
+
|
89
|
+
# returns dup of @editoropts merged with rcopts
|
90
|
+
def editoropts
|
91
|
+
@editoropts.dup.merge rcopts['editor']
|
92
|
+
end
|
93
|
+
|
94
|
+
def server
|
95
|
+
# HACK: Fixed in master -- remove when upgrading min rack dependency
|
96
|
+
# http://groups.google.com/group/rack-devel/browse_thread/thread/8f6c3b79c99809ee
|
97
|
+
srv = Rack::Server.new rackopts
|
98
|
+
srv.instance_variable_set :@app, Editserver.new(editoropts)
|
99
|
+
srv
|
100
|
+
end
|
101
|
+
|
102
|
+
def run
|
103
|
+
options.parse @args
|
104
|
+
$0 = 'editserver'
|
105
|
+
server.start
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
class Editserver
|
4
|
+
class Editor
|
5
|
+
class << self
|
6
|
+
attr_accessor :command
|
7
|
+
|
8
|
+
def define_editor editor, *params
|
9
|
+
@command = [%x(which #{editor}).chomp, *params]
|
10
|
+
end
|
11
|
+
|
12
|
+
@@terminal = nil
|
13
|
+
|
14
|
+
def terminal
|
15
|
+
@@terminal
|
16
|
+
end
|
17
|
+
|
18
|
+
def terminal= str
|
19
|
+
@@terminal = case str
|
20
|
+
when String then str.shellsplit
|
21
|
+
when NilClass then nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end # self
|
25
|
+
|
26
|
+
def terminal
|
27
|
+
@@terminal
|
28
|
+
end
|
29
|
+
|
30
|
+
def edit file
|
31
|
+
cmd = self.class.command
|
32
|
+
|
33
|
+
if not File.executable? cmd.first
|
34
|
+
File.open(file, 'w') { |f| f.write "Editor not found: #{cmd.first.inspect}" }
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
out = %x(#{(cmd + [file]).shelljoin} 2>&1).chomp
|
39
|
+
|
40
|
+
if $?.exitstatus.zero?
|
41
|
+
out
|
42
|
+
else
|
43
|
+
raise EditError, out
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'rack/response'
|
3
|
+
|
4
|
+
class Editserver
|
5
|
+
class Response
|
6
|
+
attr_reader :editor, :request, :response
|
7
|
+
|
8
|
+
def initialize editor, request
|
9
|
+
@editor = editor
|
10
|
+
@request = request
|
11
|
+
@response = Rack::Response.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
tempfile = mktemp
|
16
|
+
editor.edit tempfile.path
|
17
|
+
response.write File.read(tempfile.path)
|
18
|
+
response.finish
|
19
|
+
rescue EditError => e
|
20
|
+
response.write e.to_s
|
21
|
+
response.status = 500 # server error
|
22
|
+
response.finish
|
23
|
+
ensure
|
24
|
+
if tempfile
|
25
|
+
tempfile.close
|
26
|
+
tempfile.unlink
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def mktemp
|
33
|
+
file = Tempfile.new filename
|
34
|
+
|
35
|
+
if text = request.params['text']
|
36
|
+
file.write text
|
37
|
+
file.rewind
|
38
|
+
end
|
39
|
+
|
40
|
+
file
|
41
|
+
end
|
42
|
+
|
43
|
+
def filename
|
44
|
+
# `id' and `url' sent by TextAid
|
45
|
+
name = 'editserver'
|
46
|
+
id, url = request.params.values_at 'id', 'url'
|
47
|
+
|
48
|
+
if id or url
|
49
|
+
name << '-' << id if id
|
50
|
+
name << '-' << url if url
|
51
|
+
else
|
52
|
+
if agent = request.env['HTTP_USER_AGENT']
|
53
|
+
name << '-' << agent.split.first
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
name.gsub /[^\w\.]+/, '-'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'editserver/editor'
|
2
|
+
|
3
|
+
class Editserver
|
4
|
+
class Emacs < Editor
|
5
|
+
define_editor *%w[emacsclient --alternate-editor='']
|
6
|
+
|
7
|
+
def start_server
|
8
|
+
pid = fork { exec *(terminal + %w[-e emacs --eval (server-start)]) }
|
9
|
+
sleep 2 # HACK!
|
10
|
+
Process.detach pid
|
11
|
+
end
|
12
|
+
|
13
|
+
def edit file
|
14
|
+
if terminal.nil?
|
15
|
+
File.open(file, 'w') { |f| f.write 'No terminal defined!' }
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
rescue EditError
|
20
|
+
start_server
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'editserver/editor'
|
2
|
+
|
3
|
+
class Editserver
|
4
|
+
class Vim < Editor
|
5
|
+
define_editor *%w[vim --servername EDITSERVER --remote-tab-wait]
|
6
|
+
|
7
|
+
def server_available?
|
8
|
+
%x(vim --serverlist).split("\n").map { |l| l.strip.upcase }.include? 'EDITSERVER'
|
9
|
+
end
|
10
|
+
|
11
|
+
def start_server
|
12
|
+
pid = fork { exec *(terminal + %w[-e vim --servername EDITSERVER]) }
|
13
|
+
sleep 2 # HACK: a moment to initialize before returning
|
14
|
+
Process.detach pid
|
15
|
+
end
|
16
|
+
|
17
|
+
def edit file
|
18
|
+
if terminal.nil?
|
19
|
+
File.open(file, 'w') { |f| f.write 'No terminal defined!' }
|
20
|
+
else
|
21
|
+
start_server unless server_available?
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/editserver.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'rack'
|
3
|
+
require 'editserver/response'
|
4
|
+
require 'editserver/terminal/vim'
|
5
|
+
require 'editserver/terminal/emacs'
|
6
|
+
|
7
|
+
class Editserver
|
8
|
+
class EditError < StandardError; end
|
9
|
+
class RoutingError < StandardError; end
|
10
|
+
|
11
|
+
GUI_EDITORS = {
|
12
|
+
# OS X editors
|
13
|
+
'mate' => 'mate -w',
|
14
|
+
'mvim' => 'mvim --nofork --servername EDITSERVER', # does not return when app must be launched
|
15
|
+
'kod' => 'open -a Kod -W', # app must quit to release control
|
16
|
+
'bbedit' => 'bbedit -w' # does not open file properly when app is launched
|
17
|
+
}
|
18
|
+
|
19
|
+
attr_reader :editors
|
20
|
+
|
21
|
+
# TODO: We are playing with global constants here;
|
22
|
+
# this is simple, but we should stop
|
23
|
+
def initialize options = {}
|
24
|
+
opts = options.dup
|
25
|
+
Editor.terminal = opts.delete 'terminal'
|
26
|
+
|
27
|
+
# seed with terminal-based editors, whose server/client modes are a bit too
|
28
|
+
# complicated to configure dynamically
|
29
|
+
@editors = {
|
30
|
+
'vim' => Vim,
|
31
|
+
'emacs' => Emacs
|
32
|
+
}
|
33
|
+
|
34
|
+
register_editors GUI_EDITORS.merge(opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns Hash of name => EditorClass
|
38
|
+
def register_editors options = {}
|
39
|
+
opts = options.dup
|
40
|
+
|
41
|
+
if default = opts.delete('default')
|
42
|
+
@editors['default'] = default
|
43
|
+
end
|
44
|
+
|
45
|
+
# rest should be editor definitions
|
46
|
+
opts.each do |name, cmd|
|
47
|
+
klass = pascalize name
|
48
|
+
editor, params = cmd.shellsplit.partition.with_index { |w,i| i.zero? }
|
49
|
+
|
50
|
+
self.class.class_eval %Q( # e.g. mate: mate -w
|
51
|
+
class #{klass} < Editor # class Mate < Editor
|
52
|
+
define_editor #{editor[0].inspect}, *#{params.inspect} # define_editor 'mate', *['-w']
|
53
|
+
end # end
|
54
|
+
)
|
55
|
+
|
56
|
+
@editors[name] = self.class.const_get klass.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
@editors
|
60
|
+
end
|
61
|
+
|
62
|
+
# returns Editserver handler based on path
|
63
|
+
def editor path_info
|
64
|
+
path = path_info[%r(\A([/\w\.-]+)), 1]
|
65
|
+
|
66
|
+
if klass = editors[path[/\/(.*)/, 1]]
|
67
|
+
klass
|
68
|
+
elsif path == '/'
|
69
|
+
klass = editors[editors['default']]
|
70
|
+
end
|
71
|
+
|
72
|
+
raise RoutingError, "No handler for #{path}" if klass.nil?
|
73
|
+
klass
|
74
|
+
end
|
75
|
+
|
76
|
+
def call env
|
77
|
+
request = Rack::Request.new env
|
78
|
+
klass = editor request.path_info
|
79
|
+
Response.new(klass.new, request).call
|
80
|
+
rescue RoutingError => e
|
81
|
+
warn e.to_s
|
82
|
+
res = Rack::Response.new
|
83
|
+
res.status = 500
|
84
|
+
res.finish
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def pascalize str
|
90
|
+
str.capitalize.gsub(/[_-]+(.)/) { |m| m[1].chr.upcase }
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'editserver/command'
|
4
|
+
require 'minitest/pride' if $stdout.tty?
|
5
|
+
require 'minitest/autorun'
|
6
|
+
|
7
|
+
describe Editserver::Command do
|
8
|
+
describe :initialize do
|
9
|
+
it 'should take a single optional argument' do
|
10
|
+
Editserver::Command.method(:initialize).arity.must_equal -1
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should set internal state' do
|
14
|
+
cmd = Editserver::Command.new ['--no-rc']
|
15
|
+
cmd.instance_variable_get(:@args).must_equal ['--no-rc']
|
16
|
+
cmd.instance_variable_get(:@opts).must_equal(:rcfile => '~/.editserverrc')
|
17
|
+
cmd.instance_variable_get(:@editoropts).must_equal('default' => nil, 'terminal' => nil)
|
18
|
+
cmd.instance_variable_get(:@rackopts).keys.sort_by(&:to_s).must_equal [
|
19
|
+
:environment, :pid, :Port, :Host, :AccessLog, :config
|
20
|
+
].sort_by &:to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe :options do
|
25
|
+
before { @cmd = Editserver::Command.new ['--no-rc'] }
|
26
|
+
|
27
|
+
it 'should return an OptionParser object' do
|
28
|
+
@cmd.options.must_be_kind_of OptionParser
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should modify internal state when parsing a list of arguments' do
|
32
|
+
@cmd.options.parse %w[--port 1000]
|
33
|
+
@cmd.instance_variable_get(:@rackopts)[:Port].must_equal 1000
|
34
|
+
|
35
|
+
@cmd.options.parse %w[--terminal xterm]
|
36
|
+
@cmd.instance_variable_get(:@editoropts)['terminal'].must_equal 'xterm'
|
37
|
+
|
38
|
+
@cmd.options.parse %w[--rc /dev/null]
|
39
|
+
@cmd.instance_variable_get(:@opts)[:rcfile].must_equal '/dev/null'
|
40
|
+
|
41
|
+
@cmd.options.parse %w[--no-rc]
|
42
|
+
@cmd.instance_variable_get(:@opts)[:norcfile].must_equal true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# TODO: finish tests
|
48
|
+
#
|
49
|
+
|
50
|
+
describe :rcopts do
|
51
|
+
end
|
52
|
+
|
53
|
+
describe :rackopts do
|
54
|
+
end
|
55
|
+
|
56
|
+
describe :editoropts do
|
57
|
+
end
|
58
|
+
|
59
|
+
describe :server do
|
60
|
+
end
|
61
|
+
|
62
|
+
describe :run do
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'editserver/editor'
|
5
|
+
require 'minitest/pride' if $stdout.tty?
|
6
|
+
require 'minitest/autorun'
|
7
|
+
|
8
|
+
describe Editserver::Editor do
|
9
|
+
before do
|
10
|
+
class Editserver
|
11
|
+
class CleverCat < Editor
|
12
|
+
define_editor 'cat', '--clever'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
@klass = Editserver::CleverCat
|
16
|
+
end
|
17
|
+
|
18
|
+
describe :self do
|
19
|
+
describe :define_editor do
|
20
|
+
it "should populate the new class metaclass's @command" do
|
21
|
+
@klass.instance_variable_get(:@command).must_equal %w[/bin/cat --clever]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe :@@terminal do
|
26
|
+
it "should have a reader and writer for the metaclass's @@terminal" do
|
27
|
+
@klass.terminal.must_equal nil
|
28
|
+
@klass.terminal = 'xterm -fg white -bg black'
|
29
|
+
@klass.terminal.must_equal %w[xterm -fg white -bg black]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end # self
|
33
|
+
|
34
|
+
describe :terminal do
|
35
|
+
it 'should have a reader for @@terminal at the instance level' do
|
36
|
+
@klass.terminal = nil
|
37
|
+
@klass.new.terminal.must_equal nil
|
38
|
+
@klass.terminal = 'urxvt -g 80x24'
|
39
|
+
@klass.new.terminal.must_equal %w[urxvt -g 80x24]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe :edit do
|
44
|
+
before do
|
45
|
+
class Editserver
|
46
|
+
class TestEditor < Editor
|
47
|
+
define_editor File.expand_path('../../test-editor', __FILE__)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
@klass = Editserver::TestEditor
|
51
|
+
@file = Tempfile.new File.basename($0)
|
52
|
+
end
|
53
|
+
|
54
|
+
after { @file.close; @file.unlink }
|
55
|
+
|
56
|
+
it 'should write file with error message is editor is not found' do
|
57
|
+
class Editserver
|
58
|
+
class MysteryPony < Editor
|
59
|
+
define_editor 'magic-pony', '--mysterious'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Editserver::MysteryPony.new.edit @file.path
|
64
|
+
@file.read.must_match /not found:/
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should write the passed file' do
|
68
|
+
@file.write 'MAGICPONY'
|
69
|
+
@file.rewind
|
70
|
+
@klass.new.edit @file.path
|
71
|
+
@file.read.must_match /\AMAGICPONY.*test-editor\z/m
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'rack/mock'
|
4
|
+
require 'editserver/response'
|
5
|
+
require 'minitest/pride' if $stdout.tty?
|
6
|
+
require 'minitest/autorun'
|
7
|
+
|
8
|
+
describe Editserver::Response do
|
9
|
+
before do
|
10
|
+
class Editserver
|
11
|
+
class TestEditor < Editor
|
12
|
+
define_editor File.expand_path('../../test-editor', __FILE__)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
@editor = Editserver::TestEditor.new
|
17
|
+
@uri = 'http://127.0.0.1:9999'
|
18
|
+
@req = lambda do |uri|
|
19
|
+
Rack::Request.new Rack::MockRequest.env_for(uri)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe :initialize do
|
24
|
+
it 'should take two arguments' do
|
25
|
+
lambda { Editserver::Response.new }.must_raise ArgumentError
|
26
|
+
lambda { Editserver::Response.new @editor }.must_raise ArgumentError
|
27
|
+
lambda { Editserver::Response.new @editor, @req.call(@uri); raise RuntimeError }.must_raise RuntimeError
|
28
|
+
end
|
29
|
+
|
30
|
+
# implicitly tests attr readers as well
|
31
|
+
it 'should set public internal state' do
|
32
|
+
req = @req.call @uri
|
33
|
+
res = Editserver::Response.new @editor, req
|
34
|
+
res.editor.must_equal @editor
|
35
|
+
res.request.must_equal req
|
36
|
+
res.response.must_be_kind_of Rack::Response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe :call do
|
41
|
+
it 'should modify the body of the response' do
|
42
|
+
res = Editserver::Response.new @editor, @req.call(@uri)
|
43
|
+
res.call[2].body.join.must_match /test-editor\z/
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should prepend contents of text param to response body if present' do
|
47
|
+
res = Editserver::Response.new @editor, @req.call(@uri + '/?text=SUGARPLUM')
|
48
|
+
res.call[2].body.join.must_match /\ASUGARPLUM.*test-editor\z/m
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
$:.unshift File.dirname(__FILE__)
|
3
|
+
|
4
|
+
require 'rack/mock'
|
5
|
+
require 'minitest/pride' if $stdout.tty?
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'editserver'
|
8
|
+
require 'editserver/version'
|
9
|
+
require 'editserver/command'
|
10
|
+
|
11
|
+
describe Editserver do
|
12
|
+
describe :VERSION do
|
13
|
+
it 'should have a VERSION constant' do
|
14
|
+
Editserver::VERSION.must_match /\d+\.\d+\.\d+/
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe :ErrorClasses do
|
19
|
+
it 'should have some custom error classes' do
|
20
|
+
Editserver::EditError.new.must_be_kind_of StandardError
|
21
|
+
Editserver::RoutingError.new.must_be_kind_of StandardError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe :GUI_EDITORS do
|
26
|
+
it 'should be a Hash of string keys and string values' do
|
27
|
+
Editserver::GUI_EDITORS.must_be_kind_of Hash
|
28
|
+
Editserver::GUI_EDITORS.each do |k,v|
|
29
|
+
k.must_be_kind_of String
|
30
|
+
v.must_be_kind_of String
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe :editors do
|
36
|
+
it 'should be a Hash of string keys and class values' do
|
37
|
+
(srv = Editserver.new).editors.must_be_kind_of Hash
|
38
|
+
srv.editors.each do |k,v|
|
39
|
+
k.must_be_kind_of String
|
40
|
+
v.must_be_kind_of Class
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe :initialize do
|
46
|
+
it 'should optionally accept an options Hash' do
|
47
|
+
Editserver.method(:initialize).arity.must_equal -1
|
48
|
+
lambda { Editserver.new 'string' }.must_raise TypeError
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should not mutate passed options Hash' do
|
52
|
+
Editserver.new(opts = { 'terminal' => 'xterm' })
|
53
|
+
opts['terminal'].must_equal 'xterm'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should set Editor::terminal' do
|
57
|
+
Editserver.new 'terminal' => 'xterm -fg white'
|
58
|
+
Editserver::Editor.terminal.must_equal %w[xterm -fg white] # gross; on the todo list
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should set @editors' do
|
62
|
+
srv = Editserver.new
|
63
|
+
{ 'vim' => Editserver::Vim, 'emacs' => Editserver::Emacs }.each do |k,v|
|
64
|
+
srv.editors[k].must_equal v
|
65
|
+
end
|
66
|
+
|
67
|
+
srv = Editserver.new 'kitten' => 'cat --with-qte'
|
68
|
+
srv.editors['kitten'].must_equal Editserver::Kitten
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe :register_editors do
|
73
|
+
before { @srv = Editserver.new }
|
74
|
+
|
75
|
+
it 'should not mutate passed options Hash' do
|
76
|
+
opts = { 'default' => 'cat', 'cat' => 'cat --regular' }
|
77
|
+
@srv.register_editors opts
|
78
|
+
opts['default'].must_equal 'cat'
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should set the default key' do
|
82
|
+
@srv.register_editors 'default' => 'vim'
|
83
|
+
@srv.editors['default'].must_equal 'vim'
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should create new subclasses of Editor, and place them in the @editors Hash' do
|
87
|
+
@srv.register_editors 'kitty_kitty' => 'cat --here-kitty-kitty'
|
88
|
+
const = Editserver.constants.first.is_a?(String) ? 'KittyKitty' : :KittyKitty
|
89
|
+
Editserver.constants.must_include const
|
90
|
+
@srv.editors['kitty_kitty'].must_equal Editserver::KittyKitty
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should split editor command like a Bourne shell' do
|
94
|
+
@srv.register_editors 'adorable_cat' => 'cat --with "really long whiskers"'
|
95
|
+
Editserver::AdorableCat.instance_variable_get(:@command).must_equal ['/bin/cat', '--with', 'really long whiskers']
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should alter @editors and return the value of @editors' do
|
99
|
+
@srv.register_editors('ugly_cat' => 'cat --manginess=100').must_equal @srv.editors
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe :editor do
|
104
|
+
before { @srv = Editserver.new }
|
105
|
+
|
106
|
+
it 'should take a single string argument' do
|
107
|
+
@srv.method(:editor).arity.must_equal 1
|
108
|
+
lambda { @srv.editor %w[/foo] }.must_raise TypeError
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should raise RoutingError if no handler found for path' do
|
112
|
+
lambda { @srv.editor '/escape-meta-alt-control-shift' }.must_raise Editserver::RoutingError
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should return an existing subclass of Editor when a handler exists' do
|
116
|
+
@srv.editor('/vim').ancestors.must_include Editserver::Editor
|
117
|
+
@srv.register_editors 'kitty_cat' => 'cat --with-nick'
|
118
|
+
@srv.editor('/kitty_cat').ancestors.must_include Editserver::Editor
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should return a default handler for / if defined' do
|
122
|
+
lambda { @srv.editor '/' }.must_raise Editserver::RoutingError
|
123
|
+
@srv.register_editors 'default' => 'emacs'
|
124
|
+
@srv.editor('/').must_equal Editserver::Emacs
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe :call do
|
129
|
+
before do
|
130
|
+
bin = File.expand_path '../test-editor', __FILE__
|
131
|
+
@srv = Editserver.new 'default' => 'test-editor', 'test-editor' => bin
|
132
|
+
@env_for = Rack::MockRequest.method :env_for
|
133
|
+
@uri = 'http://127.0.0.1:9999'
|
134
|
+
|
135
|
+
# don't clutter test output
|
136
|
+
$stderr.reopen '/dev/null'
|
137
|
+
end
|
138
|
+
|
139
|
+
after { $stderr.reopen STDERR }
|
140
|
+
|
141
|
+
it 'should take a rack env hash' do
|
142
|
+
@srv.method(:call).arity.must_equal 1
|
143
|
+
lambda { @srv.call :foo => 'bar' }.must_raise NoMethodError
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should return a rack array' do
|
147
|
+
res = @srv.call @env_for.call(@uri)
|
148
|
+
res.must_be_kind_of Array
|
149
|
+
res.length.must_equal 3
|
150
|
+
res[0].must_be_kind_of Fixnum
|
151
|
+
res[1].must_be_kind_of Hash
|
152
|
+
res[2].must_be_kind_of Rack::Response
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should spawn the specified editor and return an edited string' do
|
156
|
+
@srv.call(@env_for.call @uri)[2].body.join.must_match /test-editor\z/
|
157
|
+
@srv.call(@env_for.call @uri + '/?text=foobarbaz')[2].body.join.must_match /\Afoobarbaz.*test-editor\z/m
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/test/test-editor
ADDED
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: editserver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Sung Pae
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-02-16 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rack
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 2
|
31
|
+
version: "1.2"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: minitest
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 2
|
44
|
+
- 0
|
45
|
+
version: "2.0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: ruby-debug19
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
type: :development
|
60
|
+
version_requirements: *id003
|
61
|
+
description: Simple local server for editing text in your favorite editor.
|
62
|
+
email:
|
63
|
+
- sung@metablu.com
|
64
|
+
executables:
|
65
|
+
- editserver
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files: []
|
69
|
+
|
70
|
+
files:
|
71
|
+
- .gitignore
|
72
|
+
- Gemfile
|
73
|
+
- README.markdown
|
74
|
+
- Rakefile
|
75
|
+
- bin/editserver
|
76
|
+
- editserver.gemspec
|
77
|
+
- extra/editserver.applescript.erb
|
78
|
+
- lib/editserver.rb
|
79
|
+
- lib/editserver/command.rb
|
80
|
+
- lib/editserver/editor.rb
|
81
|
+
- lib/editserver/response.rb
|
82
|
+
- lib/editserver/terminal/emacs.rb
|
83
|
+
- lib/editserver/terminal/vim.rb
|
84
|
+
- lib/editserver/version.rb
|
85
|
+
- test/editserver.test.rb
|
86
|
+
- test/editserver/command.test.rb
|
87
|
+
- test/editserver/editor.test.rb
|
88
|
+
- test/editserver/response.test.rb
|
89
|
+
- test/editserver/terminal/emacs.test.rb
|
90
|
+
- test/editserver/terminal/vim.test.rb
|
91
|
+
- test/test-editor
|
92
|
+
has_rdoc: true
|
93
|
+
homepage: http://github.com/guns/editserver
|
94
|
+
licenses: []
|
95
|
+
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
requirements: []
|
118
|
+
|
119
|
+
rubyforge_project: editserver
|
120
|
+
rubygems_version: 1.3.7
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Your favorite editor, in every app
|
124
|
+
test_files:
|
125
|
+
- test/editserver.test.rb
|
126
|
+
- test/editserver/command.test.rb
|
127
|
+
- test/editserver/editor.test.rb
|
128
|
+
- test/editserver/response.test.rb
|
129
|
+
- test/editserver/terminal/emacs.test.rb
|
130
|
+
- test/editserver/terminal/vim.test.rb
|
131
|
+
- test/test-editor
|