editserver 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ extra/editserver.scpt
6
+ tags
7
+ test/editserverrc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'editserver/command'
5
+ Editserver::Command.new(ARGV).run
@@ -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
@@ -0,0 +1,3 @@
1
+ class Editserver
2
+ VERSION = '0.1.1'
3
+ 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,5 @@
1
+ $:.unshift File.expand_path('../../../lib', __FILE__)
2
+
3
+ require 'editserver/terminal/emacs'
4
+ require 'minitest/pride' if $stdout.tty?
5
+ require 'minitest/autorun'
@@ -0,0 +1,5 @@
1
+ $:.unshift File.expand_path('../../../lib', __FILE__)
2
+
3
+ require 'editserver/terminal/vim'
4
+ require 'minitest/pride' if $stdout.tty?
5
+ require 'minitest/autorun'
@@ -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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ File.open ARGV[0], 'a' do |f|
5
+ f.write "\nEdited by #{File.basename $0}"
6
+ end
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