#! /usr/bin/perl # LaTeX Watch, version 1.2 # - by Robin Houston, March 2007. # Changes # 1.1: # - Include $! in error message if ps_viewer fails to start # - run etex in batchmode # - deal sensibly with compilation errors (don't just quit, offer to show log) # - use 'gv -scale 1' (x 1.414) instead of '-scale 2' (x 2) # # 1.2: # - Add Fink path (/sw/bin) to $PATH # - Improved error handling in the command; also don't assume this script is executable. 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`; # Add TextMate support path $ENV{PATH} .= ":$ENV{TM_SUPPORT_PATH}/bin"; # Add Fink path $ENV{PATH} .= ":/sw/bin"; # PostScript viewer my @ps_viewer = qw(gv -spartan -scale 1 -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() and 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", -interaction => "batchmode", "&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 my @tex_command = ("etex", -interaction => "batchmode", "&.$name", "$wd/.$name.tex"); system(@tex_command); if ($? == -1) { fail("Failed to execute tex", "The command '@tex_command' failed to execute: $!"); } elsif ($? & 127) { fail("Command failed", "The command '@tex_command' caused $tex_command[0] to die with signal ".($? & 127)); } elsif ($? >>= 8) { if ($? == 1) { # Probably an error in the document offer_to_show_log(); return; } else { fail("Command failed", "The command '@tex_command' exited with unexpected error code $?"); } } else { # Success! rename("$wd/.$name.aux", "$wd/$name.aux"); fail_unless_system("dvips", "$wd/.$name.dvi", "-o"); return 1; } } sub offer_to_show_log { pipe (my $rh, my $wh); if (my $pid = fork()) { # Parent my $button = <$rh>; waitpid($pid, 0); if ($?) { # If we failed to show the dialog, there's not much sense # in trying to put up another dialog to explain what happened! # Reluctantly ignore it. debug_msg("Failed to offer to show log"); } elsif ($button == 1) { # OK button pressed fail_unless_system("mate", "$wd/.$name.log"); } } else { close(STDOUT); open(STDOUT, ">&", $wh); # Talk to the pipe! exec($CocoaDialog, "ok-msgbox", "--title" => "LaTeX Watch: compilation error", "--text" => "Error compiling $name.tex", "--informative-text" => "TeX gave an error compiling the file. Shall I show the log?"); # If there's an error, just exit with a non-zero code. debug_msg("Child process failed to offer to show log."); POSIX::_exit(2); # Use _exit so we don't trigger cleanup code. } } 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): $!"); } }