#! /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"); } }