rubberin/rubberin

212 lines
5.8 KiB
Ruby
Executable file

#!/usr/bin/env ruby
#
# rubberin - a rubber wrapper using inotify
#
# Copyright (C) 2007 Paul van Tilburg <paul@luon.net>
#
# This script can assist the authoring of LaTeX documents. It acts as an
# drop-in replacement for rubber. While rubber would just perform a
# compilation of the LaTeX file, rubberin will perform a compilation (using
# rubber) each time it detects a modification in the LaTeX file or any of
# its dependencies (include files, BibTeX files, image, etc.) until
# rubberin is interrupted. Additionally, rubberin spawns a viewer to view
# the result of the LaTeX compilation and forces it to reload every time
# rubberin has recompiled the LaTeX file.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WAR RANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
require "English"
require "getoptlong"
require "rb-inotify"
require "pathname"
# Small necessary Pathname class extension.
class Pathname
def extname=(ext)
raise "Invalid extension" unless ext.is_a?(String) && ext.match(/^\./)
@path.sub!(/#{extname}$/, ext)
end
end
## Helper methods
# Print the usage.
def usage
puts "Usage: #{PROGRAM} [options]... [tex-file] [rubber-options]..."
end
# Print the command line help.
def help
usage
puts "Wrapper for rubber that facilitates automatic compiling and viewer updating"
puts "when the input file or dependancy files are modified."
puts
puts "Available options:"
puts " -d, --pdf produce a PDF file (via ps2pdf if used with -p)"
puts " -h, --help display this help"
puts " -p, --ps produce a PostScript file"
puts "The options --pdf and --ps are passed to rubber like all options not listed above."
end
# Compile the input file using the right options based on the mode.
def compile(infile, mode)
mode_opt = { pdf: "--pdf",
ps: "--ps",
pspdf: "--ps --pdf" }[mode]
params = ARGV.join(" ")
err_file = infile.with_extname("err")
system "rubber --inplace #{mode_opt} #{params} #{infile} 2> #{err_file}"
File.open(err_file) { |file| puts file.read }
end
# Start the right viewer based on the mode.
def view(infile, mode)
case mode
when :dvi
system "xdvi.bin",
"-watchfile", "1",
"-name", "xdvi", infile.with_extname("dvi").to_s
when :ps
system "evince", infile.with_extname("ps").to_s
when :pdf, :pspdf
system "evince", infile.with_extname("pdf").to_s
end
end
# Clean up cruft related to compilation of the input file.
def clean(infile)
err_file = infile.with_extname("err")
File.unlink(err_file) if err_file.exist?
system "rubber --clean --inplace #{infile}"
end
# Reload the right viewer based on the mode.
def reload(infile, mode)
case mode
when :ps
system "evince", infile.with_extname("ps").to_s
when :pdf, :pspdf
system "evince", infile.with_extname("pdf").to_s
end
end
## Initialisation
PROGRAM = File.basename($PROGRAM_NAME).freeze
VERSION = "0.9".freeze
# Parse the command line options.
# Determine the compile mode from the options.
mode = :dvi
opts = GetoptLong.new(["--help", "-h", GetoptLong::NO_ARGUMENT],
["--ps", "-p", GetoptLong::NO_ARGUMENT],
["--pdf", "-d", GetoptLong::NO_ARGUMENT],
["--version", "-v", GetoptLong::NO_ARGUMENT])
opts.ordering = GetoptLong::REQUIRE_ORDER
opts.quiet = true
opts.each do |opt, _arg|
case opt
when "--help"
help
exit 0
when "--ps"
mode = :ps
when "--pdf"
mode = if mode == :ps
:pspdf
else
:pdf
end
when "--version"
puts "#{PROGRAM} #{VERSION}"
exit 0
end
end
# Get the input file from the arguments.
if ARGV.empty?
usage
exit 1
else
infile = Pathname.new(ARGV.shift)
def infile.with_extname(ext)
file = dirname + basename(extname)
file.extname = ".#{ext}"
file
end
end
# Check the input file.
begin
File.open(infile) {}
rescue SystemCallError => e
puts "#{PROGRAM}: #{e}"
exit 2
end
# Find the dependencies of the input file using rubber-info and
# do an initial run.
files = `rubber-info --deps #{infile}`.chomp.split
compile(infile, mode)
puts
# Spawn a viewer based on the mode.
pid = $PROCESS_ID
viewer_pid =
fork do
view(infile, mode)
# If xdvi/evince exits, this program should exit too.
puts "#{PROGRAM}: viewer exited, so will I!"
Process.kill("HUP", pid)
end
## Main event loop
# Add input file with dependancies to the watch list and start event loop.
notifier = INotify::Notifier.new
dirs = files.map { |file| Pathname.new(file).dirname }.uniq
dirs.each do |dir|
# Set up a watch per directory
notifier.watch(dir.to_s, :close_write) do |ev|
# Only compile if a dependency of the input file has been modified
file_path = (dir + ev.name).to_s
next unless files.include? file_path
puts "#{PROGRAM}: file #{ev.name} modified, compiling #{infile}..."
compile(infile, mode)
reload(infile, mode)
puts
end
end
# Handle signals during the main event loop.
["INT", "TERM", "QUIT"].each do |sig|
Signal.trap(sig) do
puts "#{PROGRAM}: caught signal #{sig}, stopping the viewer..."
Process.kill(sig, viewer_pid)
notifier.stop
end
end
Signal.trap("HUP") { notifier.stop }
# Run the main event loop.
notifier.run
# Clean before finishing!
clean(infile)