Created
September 4, 2012 16:31
-
-
Save jstjohn/3623171 to your computer and use it in GitHub Desktop.
rmate that works by default on both remote SSH servers and localhost
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# encoding: UTF-8 | |
$VERBOSE = true # -w | |
$KCODE = "U" if RUBY_VERSION < "1.9" # -KU | |
require 'optparse' | |
require 'socket' | |
require 'tempfile' | |
require 'yaml' | |
VERSION_STRING = 'rmate version 1.4 (2012-08-14)' | |
class Settings | |
attr_accessor :host, :port, :wait, :force, :verbose | |
def initialize | |
@host, @port = 'auto', 52698 | |
@host = ENV['RMATE_HOST'].to_s if ENV.has_key? 'RMATE_HOST' | |
@port = ENV['RMATE_PORT'].to_i if ENV.has_key? 'RMATE_PORT' | |
@wait = false | |
@force = false | |
@verbose = false | |
read_disk_settings | |
parse_cli_options | |
if @host == 'auto' and (conn = ENV['SSH_CONNECTION']) | |
@host = conn.split(' ').first | |
else | |
@host = 'localhost' | |
end | |
end | |
def read_disk_settings | |
[ "/etc/rmate.rc", "~/.rmate.rc"].each do |current_file| | |
file = File.expand_path current_file | |
if File.exist? file | |
params = YAML::load(File.open(file)) | |
@host = params["host"] unless params["host"].nil? | |
@port = params["port"] unless params["port"].nil? | |
end | |
end | |
end | |
def parse_cli_options | |
OptionParser.new do |o| | |
o.on( '--host=name', "Connect to host.", "Use 'auto' to detect the host from SSH.", "Defaults to #{@host}.") { |v| @host = v } | |
o.on('-p', '--port=#', Integer, "Port number to use for connection.", "Defaults to #{@port}.") { |v| @port = v } | |
o.on('-w', '--[no-]wait', 'Wait for file to be closed by TextMate.') { |v| @wait = v } | |
o.on('-f', '--force', 'Open even if the file is not writable.') { |v| @force = v } | |
o.on('-v', '--verbose', 'Verbose logging messages.') { |v| @verbose = v } | |
o.on_tail('-h', '--help', 'Show this message.') { puts o; exit } | |
o.on_tail( '--version', 'Show version.') { puts VERSION_STRING; exit } | |
o.parse! | |
end | |
end | |
end | |
class Command | |
def initialize(name) | |
@command = name | |
@variables = {} | |
@data = nil | |
@size = nil | |
end | |
def []=(name, value) | |
@variables[name] = value | |
end | |
def read_file(path) | |
@size = File.size(path) | |
@data = File.open(path, "rb") { |io| io.read(@size) } | |
end | |
def send(socket) | |
socket.puts @command | |
@variables.each_pair do |name, value| | |
value = 'yes' if value === true | |
socket.puts "#{name}: #{value}" | |
end | |
if @data | |
socket.puts "data: #{@size}" | |
socket.puts @data | |
end | |
socket.puts | |
end | |
end | |
def handle_save(socket, variables, data) | |
path = variables["token"] | |
$stderr.puts "Saving #{path}" if $settings.verbose | |
begin | |
File.link(path, "#{path}~") if File.exist? path | |
Tempfile.open("rmate", File.dirname(path)) do |temp| | |
temp.close(false) | |
File.chmod(File.stat(path).mode, temp.path) if File.exist? path | |
open(temp.path, 'wb') {|file| file << data } | |
File.rename(temp.path, path) | |
end | |
File.unlink("#{path}~") if File.exist? "#{path}~" | |
rescue | |
# TODO We probably want some way to notify the server app that the save failed | |
$stderr.puts "Save failed! #{$!}" if $settings.verbose | |
end | |
end | |
def handle_close(socket, variables, data) | |
path = variables["token"] | |
$stderr.puts "Closed #{path}" if $settings.verbose | |
end | |
def handle_cmd(socket) | |
cmd = socket.readline.chomp | |
variables = {} | |
data = "" | |
while line = socket.readline.chomp | |
break if line.empty? | |
name, value = line.split(': ', 2) | |
variables[name] = value | |
data << socket.read(value.to_i) if name == "data" | |
end | |
variables.delete("data") | |
case cmd | |
when "save" then handle_save(socket, variables, data) | |
when "close" then handle_close(socket, variables, data) | |
else abort "Received unknown command “#{cmd}”, exiting." | |
end | |
end | |
def connect_and_handle_cmds(host, port, cmds) | |
socket = TCPSocket.new(host, port) | |
server_info = socket.readline.chomp | |
$stderr.puts "Connect: ‘#{server_info}’" if $settings.verbose | |
cmds.each { |cmd| cmd.send(socket) } | |
socket.puts "." | |
handle_cmd(socket) while !socket.eof? | |
socket.close | |
$stderr.puts "Done" if $settings.verbose | |
end | |
## MAIN | |
$settings = Settings.new | |
## Parse arguments. | |
cmds = [] | |
ARGV.each do |path| | |
abort "File #{path} is not writable! Use -f/--force to open anyway." unless $settings.force or File.writable? path or not File.exists? path | |
$stderr.puts "File #{path} is not writable. Opening anyway." if not File.writable? path and File.exists? path and $settings.verbose | |
cmd = Command.new("open") | |
cmd['display-name'] = "#{Socket.gethostname}:#{path}" | |
cmd['real-path'] = File.expand_path(path) | |
cmd['data-on-save'] = true | |
cmd['re-activate'] = true | |
cmd['token'] = path | |
cmd.read_file(path) if File.exist? path | |
cmd['data'] = "0" unless File.exist? path | |
cmds << cmd | |
end | |
unless $settings.wait | |
pid = fork do | |
connect_and_handle_cmds($settings.host, $settings.port, cmds) | |
end | |
Process.detach(pid) | |
else | |
connect_and_handle_cmds($settings.host, $settings.port, cmds) | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment