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 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