[TxMt] LaTeX Watch

Robin Houston robin.houston at gmail.com
Thu Mar 29 09:43:57 UTC 2007


Here's a script I hacked together last night.

If you're writing something with lots of complicated diagrams (like my
thesis, to pick an example at random) then you often have to go through
several compile-view-edit cycles before any particular diagram looks right.
This involves a lot of tedious waiting. The obvious way to speed it up is to
try and reduce the time between making an edit and seeing the results.

Hence this script. The idea is that you can 'watch' a LaTeX document, so
that whenever you save a change the  display is updated as quickly as
possible to reflect the change.

The script is currently somewhat inflexible in its assumptions. It compiles
.tex -> .dvi -> ps, and uses gv as the PostScript viewer. If other people
are interested, I can certainly make it more configurable. That's why I'm
posting it here.

The reason for the aparently-perverse choice of gv as the viewer is that
ps2pdf is really, really slow, so if you want a fast cycle – and you're
using PostScript specials – you need to view the DVI or PS directly. I don't
know a DVI previewer that works with the PostScript specials I use, so that
leaves PS. I don't know of anything else for the mac that can display PS as
quickly as gv (I'd love to be enlightened here), and gv is very pleasant to
use once you get used to its idiosyncratic interface.

Anyway, that means you'll need to have X11.app and gv installed for it to
work. As I say, this can potentially be changed, but that's the current
situation. You also need to have gv in your $PATH.

Apart from avoiding PS->PDF conversion, the main trick I use to speed up the
update cycle is to build a custom TeX format file for the document. The
preamble – i.e. anything before \begin{document} – is compiled into a
special format when the file is watched. Thereafter, when the file is
updated, the preamble is merely inspected to see whether it has changed. If
it hasn't, the previously-generated format is used. Assuming you don't
change the preamble very often, this is a big win if you load a lot of large
packages. It halves the compile time of the document I'm currently working
on.

Attached are two files:

Watch document.tmcommand
  A TextMate command that invokes the watcher. Copy this into your favourite
bundle. (If you try to watch a file that's already being watched, you're
given the option to stop watching it. Alternatively you can just close gv,
and it will stop watching. Note that quitting TextMate will *not* stop the
watcher.)

latex_watch.pl
  A Perl script that does the real work. This should be placed in the
directory Support/bin inside the same bundle that you copied the command
into. (You probably need to create that directory.)

If something doesn't work, you _should_ get a nice error dialog telling you
what went wrong.

I'd really appreciate any feedback. I think that, with some more
flexibility, something like this could be useful to a lot of people.

Robin
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macromates.com/textmate/attachments/20070329/802b579a/attachment.html>
-------------- next part --------------
#! /usr/bin/perl

# LaTeX Watch, version 1.0
#	- by Robin Houston, March 2007.

use strict;
use warnings;
use POSIX;
use File::Copy 'copy';

# Debugging flag (currently 0 to disable, or 1 to enable)
use constant DEBUG => 0;

# Add teTeX path
$ENV{PATH} .= ":/usr/local/teTeX/bin/".`/usr/local/teTeX/bin/highesttexbin.pl`;

# PostScript viewer
my @ps_viewer = qw(gv -spartan -scale 2 -nocenter -antialias -nowatch);

# Location of CocoaDialog binary
my $CocoaDialog = "$ENV{TM_SUPPORT_PATH}/bin/CocoaDialog.app/Contents/MacOS/CocoaDialog";

# Explain what's happening (if we're debugging)
sub debug_msg {
	system($CocoaDialog, "msgbox",
		"--button1" => "OK",
		"--text" => $_[0])
	if DEBUG;
}

# Display an error dialog and exit with exit-code 1
sub fail {
	my ($message, $explanation) = @_;
	system($CocoaDialog, "msgbox",
		"--button1" => "Cancel",
		"--title" => "LaTeX Watch error",
		"--text" => "Error: $message",
		"--informative-text" => "$explanation.");
	exit(1)
}

sub fail_unless_system {
	system(@_);
	if ($? == -1) {
		fail("Failed to execute $_[0]",
			"The command '@_' failed to execute: $!");
	}
	elsif ($? & 127) {
		fail("Command failed",
			"The command '@_' caused $_[0] to die with signal ".($? & 127));
	}
	elsif ($? >>= 8) {
		fail("Command failed",
			"The command '@_' failed (error code $?)");
	}
}

my $filepath = $ENV{TM_FILEPATH};
fail("File not saved", "You must save the file before it can be watched")
	if !defined($filepath);

my ($wd, $name);
if ($filepath =~ m!(.*)/!) {
	$wd = $1;
	my $fullname = $';
	if ($fullname =~ /\.tex$/) {
		$name = $`;
	}
	else {
		fail("Filename doesn't end in .tex",
			"The filename ($fullname) does not end with the .tex extension");
	}
}
else {
	fail("Path does not contain /", "The file path ($filepath) does not contain a '/'");
}
if (! -W $wd) {
	fail("Directory not writeable", "I can't write to the directory $wd");
}

my ($preamble, $viewer_pid);

# Clean up if we're interrupted or die
sub clean_up {
	debug_msg("Cleaning up");
	unlink(map("$wd/.$name.$_", qw(ini fmt tex dvi ps bbl log watcher_pid)));
	kill (2, $viewer_pid) if defined $viewer_pid;
}
END { clean_up() }
$SIG{INT} = $SIG{TERM} = sub { exit(0) };

my $mtime;
while(1) {
	my $current_mtime = -M $filepath;
	if (!defined($current_mtime)) {
		fail("Failed to get modification time",
			"I failed to find the modification time of the file '$filepath': $!");
	}
	if (!defined($mtime) or $current_mtime < $mtime) {
		debug_msg("Reloading file");
		$mtime = $current_mtime;
		reload();
		compile();
		view();
	}
	if (defined($viewer_pid) and waitpid($viewer_pid, POSIX::WNOHANG)) {
		my $r = $?;
		debug_msg("Viewer appears to have been closed ($r). Exiting.");
		if ($r & 127) {
			fail("Viewer failed",
				"The PostScript viewer died with signal ".($r & 127));
		}
		elsif ($r >>= 8) {
			fail("Viewer failed",
				"The PostScript viewer exited with an error (error code $r)");
		}
		exit;
	}
	sleep(1);
}

sub reload {
	open (my $f, "<", $filepath)
		or fail ("Failed to open file",
			"I couldn't open the file '$filepath' for reading: $!");

	local $/ = "\\begin{document}";
	my $new_preamble = <$f>;
	chomp ($new_preamble)
		or fail ("No \\begin{document} found",
			"I couldn't find the command \\begin{document} in your file");

	if ($new_preamble ne $preamble) {
		debug_msg("Preamble has changed. Regenerating format.");
		regenerate_format($new_preamble);
	}

	undef $/;
	save_body(<$f>);

	close $f
		or fail ("Failed to close file",
			"I got an error closing the file '$filepath': $!");
}

sub regenerate_format {
	($preamble) = @_;
	open (my $ini, ">", "$wd/.$name.ini")
		or fail("Failed to create file",
			"The file '$wd/.$name.ini' could not be opened for writing: $!");
	print $ini ($preamble, "\n\\dump\n");
	close $ini
		or fail("Failed to close file",
			"The file '$wd/.$name.ini' gave an error on closing: $!");

	copy("$wd/$name.bbl", "$wd/.$name.bbl"); # Ignore errors
	fail_unless_system("etex", "-ini", "&latex", "$wd/.$name.ini");
}

sub save_body {
	open (my $f, ">", "$wd/.$name.tex")
		or fail("Failed to create file",
			"I couldn't create the file '$wd/.$name.tex': $!");

	print $f ("\\begin{document}\n", @_);

	close($f)
		or fail("Failed to close file",
			"I got an error on closing the file '$wd/.$name.tex': $!");
}

sub compile {
	copy("$wd/$name.bbl", "$wd/.$name.bbl"); # Ignore errors
	copy("$wd/$name.aux", "$wd/.$name.aux"); # Ignore errors
	fail_unless_system("etex", "&.$name", "$wd/.$name.tex");
	rename("$wd/.$name.aux", "$wd/$name.aux");
	fail_unless_system("dvips", "$wd/.$name.dvi", "-o");
}

sub view {
	if (defined($viewer_pid)) {
		kill(1, $viewer_pid)
			or fail("Failed to signal viewer",
				"I failed to signal the PostScript viewer (PID $viewer_pid) to reload: $!");
	}
	else {
		$viewer_pid = start_viewer("$wd/.$name.ps");
	}
}

sub start_viewer {
	my ($ps_file) = @_;
	my $pid = fork();
	if ($pid) {
		# In parent
		return $pid;
	}
	else {
		# In child
		POSIX::setsid(); # detach from terminal
		close STDOUT; open(STDOUT, ">", "/dev/null");
		close STDERR; open(STDERR, ">", "/dev/null");
		
		debug_msg("Starting PostScript viewer ($$)");
		exec(@ps_viewer, $ps_file);
		fail("Failed to start PostScript viewer",
			"I failed to run the PostScript viewer: @ps_viewer");
	}
}
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Watch document.tmCommand
Type: application/octet-stream
Size: 1553 bytes
Desc: not available
URL: <http://lists.macromates.com/textmate/attachments/20070329/802b579a/attachment.tmCommand>


More information about the textmate mailing list