editserver 0.1.1
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.
- 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
|