docs: Steve Jobs s/is/was/
[techtalk-pse.git] / techtalk-pse.pl
index ae8a9c9..1b38a9a 100755 (executable)
@@ -3,7 +3,7 @@
 # @configure_input@
 #
 # Tech Talk PSE
 # @configure_input@
 #
 # Tech Talk PSE
-# Copyright (C) 2010 Red Hat Inc.
+# Copyright (C) 2010-2012 Red Hat Inc.
 #
 # 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
 #
 # 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
@@ -29,7 +29,9 @@ use Getopt::Long;
 use Cwd qw(getcwd abs_path);
 use Glib qw(TRUE FALSE);
 use Gtk2 -init;
 use Cwd qw(getcwd abs_path);
 use Glib qw(TRUE FALSE);
 use Gtk2 -init;
-use Gtk2::MozEmbed;
+use Gtk2::Gdk::Keysyms;
+use Gtk2::WebKit;
+use Gnome2::Vte;
 
 =encoding utf8
 
 
 =encoding utf8
 
@@ -67,10 +69,26 @@ there is a discussion on L<WHAT MAKES A GOOD TALK>.
 
 =head1 RUNNING THE TOOL FROM THE COMMAND LINE
 
 
 =head1 RUNNING THE TOOL FROM THE COMMAND LINE
 
-A Tech Talk PSE talk is not a single file, but a directory full of
-files.  (If you want to start a new talk, see the L</TUTORIAL> section
-below).  To display or run the talk, change into the directory
-containing all those files and run the C<techtalk-pse> command:
+Tech Talk PSE talks are just directories containing C<*.html>,
+C<*.sh> (shell script) and C<*.term> (terminal) files:
+
+ 0010-introduction.html
+ 0500-demonstration.sh
+ 0600-command-line.term
+ 9900-conclusion.html
+
+The filenames that Tech Talk PSE considers to be slides have to match
+the regular expression:
+
+ ^(\d+)(?:-.*)\.(html|sh|term)$
+
+(any other file or subdirectory is ignored).  Shell scripts and
+terminal files I<must> be executable.
+
+=head2 DISPLAYING AN EXISTING TALK
+
+To display or run a talk, change into the directory containing all
+those files and run the C<techtalk-pse> command:
 
  cd /path/to/talk/; techtalk-pse
 
 
  cd /path/to/talk/; techtalk-pse
 
@@ -116,15 +134,6 @@ The default is to start at the first slide in the talk.
 
 =cut
 
 
 =cut
 
-my $splash = 1;
-
-=item B<--no-splash>
-
-Don't display the initial "splash" screen which advertises Tech Talk
-PSE to your audience.  Just go straight into the talk.
-
-=cut
-
 my $verbose;
 
 =item B<--verbose>
 my $verbose;
 
 =item B<--verbose>
@@ -142,17 +151,9 @@ Display version number and exit.
 
 =cut
 
 
 =cut
 
-my $mozembed;
-my $mozembed_first;
-my $mozembed_last;
-
 GetOptions ("help|?" => \$help,
             "last" => \$last,
 GetOptions ("help|?" => \$help,
             "last" => \$last,
-            "mozembed" => \$mozembed,
-            "mozembed-first" => \$mozembed_first,
-            "mozembed-last" => \$mozembed_last,
             "n=s" => \$start,
             "n=s" => \$start,
-            "splash!" => \$splash,
             "start=s" => \$start,
             "verbose" => \$verbose,
             "version" => \$version,
             "start=s" => \$start,
             "verbose" => \$verbose,
             "version" => \$version,
@@ -170,10 +171,6 @@ if ($version) {
 die "techtalk-pse: cannot use --start and --last options together\n"
     if defined $last && defined $start;
 
 die "techtalk-pse: cannot use --start and --last options together\n"
     if defined $last && defined $start;
 
-# Run with --mozembed: see below.
-run_mozembed () if $mozembed;
-
-# Normal run of the program.
 die "techtalk-pse: too many arguments\n" if @ARGV >= 2;
 
 # Get the true name of the program.
 die "techtalk-pse: too many arguments\n" if @ARGV >= 2;
 
 # Get the true name of the program.
@@ -191,22 +188,143 @@ if (@ARGV > 0) {
     }
 }
 
     }
 }
 
+# Get the talk directory and set environment variable $talkdir
+# which is inherited by all the scripts.
+my $talkdir = getcwd;
+$ENV{talkdir} = $talkdir;
+
 # Get the files.
 my @files;
 my %files;
 # Get the files.
 my @files;
 my %files;
+my $current;
+my $pid;
+my $pipeline;
+
+&reread_directory ();
+
+print STDERR "read ", 0+@files, " files\n" if $verbose;
+if (@files == 0) {
+    warn "techtalk-pse: no files found, continuing anyway ...\n"
+}
+
+my $w = Gtk2::Window->new ();
+my $vbox = Gtk2::VBox->new ();
+my $webkit = Gtk2::WebKit::WebView->new ();
+my $vte = Gnome2::Vte::Terminal->new ();
+my $notebook = Gtk2::Notebook->new ();
+my $splash = make_splash_page ();
+my $emptylabel = Gtk2::Label->new ();
+
+my $webkitscroll = Gtk2::ScrolledWindow->new ();
+$webkitscroll->add ($webkit);
+$webkitscroll->set_policy('automatic', 'automatic');
+
+my $webkitpage = $notebook->append_page ($webkitscroll);
+my $shpage = $notebook->append_page ($emptylabel);
+my $vtepage = $notebook->append_page ($vte);
+my $splashpage = $notebook->append_page ($splash);
+
+my ($bbox, $bquit, $breload, $bnext, $bback, $brestart) = make_button_bar ();
+
+$vbox->pack_start($bbox, 0, 0, 0);
+$vbox->pack_start($notebook, 1, 1, 0);
+
+$notebook->set_show_tabs(0);
+$notebook->set_show_border(0);
+
+# Default font size is almost certainly too small
+# for audience to see.
+# XXX we should make font size configurable via
+# @ARGV.
+# XXX any way we can scale WebKit programmatically
+# to set base size which CSS is relative to ?
+# NB careful setting it too big, because it will
+# force a min size on the terminal. Scaling 1.3
+# is biggest we can do while fitting 1024x768
+my $font = $vte->get_font;
+$font->set_size($font->get_size * 1.3);
+
+# When an external command exits, automatically
+# go to the next slide
+$vte->signal_connect (
+    'child-exited' => sub {
+       if ($pid) {
+           $pid = 0;
+           &switch_slide("NEXT");
+       }
+    });
+
+# Exit if the window is closed
+$w->signal_connect (
+    destroy => sub {
+       Gtk2->main_quit;
+       return FALSE;
+    });
+
+# Handle left/right arrows, page up/down & home/end
+# as slide navigation commands. But not when there
+# is a shell running
+$w->signal_connect (
+    'key-press-event' => sub {
+       my $src = shift;
+       my $ev = shift;
+
+       # If a shell is running, don't trap keys
+       if ($pid) {
+           return 0;
+       }
+
+       if ($ev->keyval == $Gtk2::Gdk::Keysyms{Right} ||
+           $ev->keyval == $Gtk2::Gdk::Keysyms{Page_Down}) {
+           &switch_slide("NEXT");
+           return 1;
+       } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{Left} ||
+                $ev->keyval == $Gtk2::Gdk::Keysyms{Page_Up}) {
+           &switch_slide("PREV");
+           return 1;
+       } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{Home}) {
+           &switch_slide("FIRST");
+           return 1;
+       } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{End}) {
+           &switch_slide("LAST");
+           return 1;
+       } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{q} ||
+                $ev->keyval == $Gtk2::Gdk::Keysyms{Escape}) {
+           Gtk2->main_quit;
+           return 1;
+       }
+       return 0;
+    });
+
+
+$w->add ($vbox);
+
+# This allows us to resize the window in window_in_corner().
+$w->set_geometry_hints ($w, { min_width => 100 }, qw(min-size));
+
+$w->show_all ();
+
+window_fullscreen ();
+
+&update_slide();
+
+Gtk2->main();
+
+exit 0;
+
 sub reread_directory
 {
     @files = ();
 
     my $i = 0;
     foreach (glob ("*")) {
 sub reread_directory
 {
     @files = ();
 
     my $i = 0;
     foreach (glob ("*")) {
-        if (/^(\d+)(?:-.*)\.(html|sh)$/) {
+        if (/^(\d+)(?:-.*)\.(html|sh|term)$/) {
             print STDERR "reading $_\n" if $verbose;
 
             my $seq = $1;
             my $ext = $2;
             warn "techtalk-pse: $_: command file is not executable (+x)\n"
             print STDERR "reading $_\n" if $verbose;
 
             my $seq = $1;
             my $ext = $2;
             warn "techtalk-pse: $_: command file is not executable (+x)\n"
-                if $ext eq "sh" && ! -x $_;
+                if ($ext eq "sh" || $ext eq "term") && ! -x $_;
 
             my $h = { name => $_, seq => $1, ext => $2, i => $i };
             push @files, $h;
 
             my $h = { name => $_, seq => $1, ext => $2, i => $i };
             push @files, $h;
@@ -221,222 +339,309 @@ sub reread_directory
         $files[0]->{first} = 1;
         $files[$#files]->{last} = 1;
     }
         $files[0]->{first} = 1;
         $files[$#files]->{last} = 1;
     }
+
+    # Work out what slide we're starting on.
+    if (@files && !$current) {
+       if ($start) {
+           foreach my $file (@files) {
+               if ($file->{name} =~ /^$start/) {
+                   $current = $file;
+                   last;
+               }
+           }
+       } elsif ($last) {
+           $current = $files[$#files];
+       }
+       if (!$current) {
+           $current = $files[0];
+       }
+    }
 }
 }
-reread_directory ();
-print STDERR "read ", 0+@files, " files\n" if $verbose;
-if (@files == 0) {
-    warn "techtalk-pse: no files found, continuing anyway ...\n"
+
+sub window_fullscreen
+{
+    $w->set_decorated (0);
+    $w->fullscreen ();
+    $w->move (0,0);
 }
 
 }
 
-# Work out what slide we're starting on.
-my $current;
-if (defined $current) {
-    die "start slide not implemented yet XXX"
+sub window_in_corner
+{
+    $w->set_decorated (1);
+    $w->unfullscreen ();
+
+    my $root = Gtk2::Gdk->get_default_root_window ();
+    my ($width, $height) = $root->get_size;
+
+    $w->resize ($width/2, $height/4);
+    $w->move ($width/2, 64);
 }
 }
-elsif (@files) {
-    $current = $files[0];
+
+sub run_process
+{
+    $pid = $vte->fork_command("./" . $current->{name}, [], [], undef, 0, 0, 0);
 }
 }
-# else $current is undefined
-
-if ($splash) {
-    my $w = Gtk2::AboutDialog->new;
-    $w->set_authors ("Richard W.M. Jones");
-    $w->set_comments (
-        "Superior technical demonstration software\n".
-        "\n".
-        "Keys\n".
-        "↑ — Go back one slide\n".
-        "↓ — Go forward one slide\n"
-        );
-    $w->set_program_name ("Tech Talk Platinum Supreme Edition (PSE)");
-    $w->set_version ("@VERSION@");
-    $w->set_website ("http://people.redhat.com/~rjones");
-    $w->set_license ("GNU General Public License v2 or above");
-    $w->run;
-    print STDERR "calling \$w->destroy on about dialog\n" if $verbose;
-    $w->destroy;
+
+sub kill_process
+{
+    print STDERR "sending TERM signal to process group $pid\n"
+       if $verbose;
+    kill "TERM", -$pid;
+
+    # Clears out any current displayed text
+    $vte->reset(1, 1);
+    $vte->set_default_colors();
+    $pid = 0;
 }
 
 }
 
-MAIN: while (1) {
-    if (defined $current) {
-        my $go = show_slide ($current);
-        if (defined $go) {
-            print STDERR "go = $go\n" if $verbose;
-            last MAIN if $go eq "QUIT";
-
-            my $i = $current->{i};
-            print STDERR "i = $i\n" if $verbose;
-            $i-- if $go eq "PREV" && $i > 0;
-            $i++ if $go eq "NEXT" && $i+1 < @files;
-            $current = $files[$i];
-        }
+sub switch_slide
+{
+    my $action = shift;
+
+    window_fullscreen ();
+
+    if ($pid) {
+       kill_process ();
+    }
+    if ($pipeline) {
+       $pipeline->set_state('ready');
+       $pipeline = undef;
+    }
+    print STDERR "action = $action\n" if $verbose;
+
+    my $i = defined $current ? $current->{i} : 0;
+
+    print STDERR "i = $i\n" if $verbose;
+    if ($action eq "PREV") {
+       if (defined $current) {
+           $i--;
+       } else {
+           $i = $#files;
+       }
+    } elsif ($action eq "NEXT") {
+       $i++;
+    } elsif ($action eq "FIRST") {
+       $i = 0;
+    } elsif ($action eq "LAST") {
+       $i = $#files;
+    } elsif ($action =~ /^I_(\d+)$/) {
+       $i = $1;
+    }
+
+    $i = 0 if $i < 0;
+    if ($i > $#files) {
+       $current = undef;
     } else {
     } else {
-        print "No slides found.  Press any key to reload directory ...\n";
-        $_ = <STDIN>;
+       $current = $files[$i];
     }
 
     }
 
-    # Reread directory between slides.
-    reread_directory ();
+    &update_slide ();
 
 
-    if (defined $current && !exists $files{$current->{name}}) {
-        # Current slide was deleted.
-        undef $current;
-        $current = $files[0] if @files;
-    }
 }
 
 }
 
-sub show_slide
+sub update_slide
 {
 {
-    my $slide = shift;
-
-    # Display an HTML page.
-    if ($slide->{ext} eq "html") {
-        # MozEmbed is incredibly crashy, so we run ourself as a
-        # subprocess, so when it segfaults we don't care.
-        my @cmd = ($0, "--mozembed");
-        push @cmd, "--mozembed-first" if exists $slide->{first};
-        push @cmd, "--mozembed-last" if exists $slide->{last};
-        my $cwd = getcwd;
-        my $url = "file://" . $cwd . "/" . $slide->{name};
-        push @cmd, $url;
-        system (@cmd);
-        die "failed to execute subcommand: ", join(" ", @cmd), ": $!\n"
-            if $? == -1;
-        if ($? & 127) {
-            # Subcommand probably segfaulted, just continue to next slide.
-            return "NEXT";
-        } else {
-            my $r = $? >> 8;
-            if ($r == 0) {
-                return "NEXT";
-            } elsif ($r == 1) {
-                return "PREV";
-            } elsif ($r == 2) {
-                return "QUIT";
-            }
-        }
+    if ($current) {
+       # Display an HTML page.
+       if ($current->{ext} eq "html") {
+           $notebook->set_current_page ($webkitpage);
+           my $name = $current->{name};
+           my $url = "file://$talkdir/$name";
+
+           $webkit->load_uri ($url);
+           $webkit->grab_focus ();
+       }
+       # Run a shell command.
+       elsif ($current->{ext} eq "sh") {
+            window_in_corner ();
+
+           $notebook->set_current_page ($shpage);
+           run_process ();
+       }
+        # Open a VTE terminal.
+       elsif ($current->{ext} eq "term") {
+           $notebook->set_current_page ($vtepage);
+           $vte->grab_focus ();
+           run_process ();
+       }
+    } else {
+       $notebook->set_current_page ($splashpage);
     }
     }
-    # Run a shell command.
-    elsif ($slide->{ext} eq "sh") {
-        my $pid;
-        # http://docstore.mik.ua/orelly/perl/cookbook/ch10_17.htm
-        local *run_process = sub {
-            $pid = fork ();
-            die "fork: $!" unless defined $pid;
-            unless ($pid) {
-                # Child.
-                POSIX::setsid ();
-                exec ("./".$slide->{name});
-                die "failed to execute command: ", $slide->{name}, ": $!";
-            }
-            # Parent returns.
-        };
-        local *kill_process = sub {
-            print STDERR "sending TERM signal to process group $pid\n"
-                if $verbose;
-            kill "TERM", -$pid;
-        };
-        run_process ();
-
-        my $r = "NEXT";
-
-        my $w = Gtk2::Window->new ();
-
-        my $s = $w->get_screen;
-        $w->set_default_size ($s->get_width, -1);
-        $w->move (0, 0);
-        $w->set_decorated (0);
-
-        my $bbox = Gtk2::HButtonBox->new ();
-        $bbox->set_layout ('start');
-
-        my $bnext = Gtk2::Button->new ("Next slide");
-        $bnext->signal_connect (clicked => sub { $r = "NEXT"; $w->destroy });
-        $bnext->set_sensitive (!(exists $slide->{last}));
-        $bbox->add ($bnext);
-
-        my $bback = Gtk2::Button->new ("Back");
-        $bback->signal_connect (clicked => sub { $r = "PREV"; $w->destroy });
-        $bback->set_sensitive (!(exists $slide->{first}));
-        $bbox->add ($bback);
-
-        my $brestart = Gtk2::Button->new ("Kill & restart");
-        $brestart->signal_connect (clicked => sub {
-            kill_process ();
-            run_process ();
-        });
-        $bbox->add ($brestart);
-
-        my $bquit = Gtk2::Button->new ("Quit");
-        $bquit->signal_connect (clicked => sub { $r = "QUIT"; $w->destroy });
-        $bbox->add ($bquit);
-        $bbox->set_child_secondary ($bquit, 1);
-
-        $w->add ($bbox);
-
-        $w->signal_connect (destroy => sub {
-            Gtk2->main_quit;
-            return FALSE;
-        });
-        $w->show_all ();
-
-        Gtk2->main;
-
-        kill_process ();
-        print STDERR "returning r=$r\n" if $verbose;
-        return $r;
+
+    if ($pid) {
+       $brestart->show ();
+    } else {
+       $brestart->hide ();
     }
     }
+
+    if (defined $current) {
+       $bquit->hide ();
+       $breload->hide ();
+       $bnext->set_sensitive (1);
+       $bback->set_sensitive (!exists $current->{first});
+    } else {
+       $bquit->show ();
+       if (@files) {
+           $breload->hide ();
+       } else {
+           $breload->show ();
+       }
+       $bnext->set_sensitive (0);
+       $bback->set_sensitive (int(@files));
+    }
+}
+
+
+sub make_splash_page {
+    my $box = Gtk2::VBox->new();
+
+    my $title = Gtk2::Label->new ("<b><span size='x-large'>Tech Talk Platinum Supreme Edition (PSE)</span></b>");
+    $title->set_use_markup (1);
+
+    $box->pack_start ($title, 0, 1, 0);
+
+    my $vers = Gtk2::Label->new ("<b><span size='large'>@VERSION@</span></b>");
+    $vers->set_use_markup (1);
+    $box->pack_start ($vers, 0, 1, 0);
+
+    my $tagline = Gtk2::Label->new ("<i><span size='large'>Superior technical demonstration software</span></i>");
+    $tagline->set_use_markup (1);
+
+    $box->pack_start ($tagline, 0, 1, 0);
+    $box->pack_start (Gtk2::Label->new (""), 0, 1, 0);
+    $box->pack_start (Gtk2::Label->new ("Author: Richard W.M. Jones"), 0, 1, 0);
+
+    my $url = Gtk2::Label->new ("<a href='http://people.redhat.com/~rjones'>http;//people.redhat.com/~rjones/</a>");
+    $url->set_use_markup (1);
+    $box->pack_start ($url, 0, 1, 0);
+    $box->pack_start (Gtk2::Label->new ("GNU General Public License v2 or above"), 0, 1, 0);
+
+    return $box;
 }
 
 }
 
-# If invoked with the --mozembed parameter then we just display a
-# single page.  This is just to prevent crashes in MozEmbed from
-# killing the whole program.
-sub run_mozembed
+# Make the standard button bar across the top of the page.
+sub make_button_bar
 {
 {
-    my $r = 0;
+    my $bbox = Gtk2::Toolbar->new ();
+    $bbox->set_style ("GTK_TOOLBAR_TEXT");
 
 
-    my $w = Gtk2::Window->new ();
-    my $vbox = Gtk2::VBox->new ();
-    my $moz = Gtk2::MozEmbed->new ();
+    my $i = 0;
 
 
-    my $bbox = Gtk2::HButtonBox->new ();
-    $bbox->set_layout ('start');
+    my $bquit = Gtk2::ToolButton->new (undef, "Quit");
+    $bquit->signal_connect (clicked => sub { Gtk2->main_quit });
+    $bbox->insert ($bquit, $i++);
+
+    my $breload = Gtk2::ToolButton->new (undef, "Reload");
+    $breload->signal_connect (clicked => sub { reread_directory () });
+    $bbox->insert ($breload, $i++);
+
+    my $bnext = Gtk2::ToolButton->new (undef, "Next slide");
+    $bnext->signal_connect (clicked => sub { &switch_slide ("NEXT") });
+    $bbox->insert ($bnext, $i++);
+
+    my $bback = Gtk2::ToolButton->new (undef, "Back");
+    $bback->signal_connect (clicked => sub { &switch_slide ("PREV") });
+    $bbox->insert ($bback, $i++);
+
+    $bbox->insert (Gtk2::SeparatorToolItem->new (), $i++);
+
+    my $brestart = Gtk2::ToolButton->new (undef, "Kill & restart");
+    $brestart->signal_connect (clicked =>
+                              sub {
+                                  kill_process ();
+                                  run_process ();
+                              });
+    $bbox->insert ($brestart, $i++);
+
+    my $sep = Gtk2::SeparatorToolItem->new ();
+    $sep->set_expand (TRUE);
+    $sep->set_draw (FALSE);
+    $bbox->insert ($sep, $i++);
+
+    my $optsmenu = Gtk2::Menu->new ();
+
+    my $mfirst = Gtk2::MenuItem->new ("First slide");
+    $mfirst->signal_connect (activate => sub { &switch_slide ("FIRST") });
+    $mfirst->show ();
+    $optsmenu->append ($mfirst);
+
+    my $mlast = Gtk2::MenuItem->new ("Last slide");
+    $mlast->signal_connect (activate => sub { &switch_slide ("LAST") });
+    $mlast->show ();
+    $optsmenu->append ($mlast);
+
+    my $slidesmenu = Gtk2::Menu->new ();
+    foreach (@files) {
+        my $item = Gtk2::MenuItem->new ($_->{name});
+        my $index = $_->{i};
+        $item->signal_connect (activate => sub { &switch_slide ("I_$index") });
+        $item->set_sensitive ($current->{i} != $index);
+        $item->show ();
+        $slidesmenu->append ($item);
+    }
 
 
-    $vbox->pack_start ($bbox, 0, 0, 0);
-    $vbox->add ($moz);
-    $w->fullscreen ();
-    #$w->set_default_size (640, 480);
-    $w->add ($vbox);
-
-    my $bnext = Gtk2::Button->new ("Next slide");
-    $bnext->signal_connect (clicked => sub { $r = 0; $w->destroy });
-    $bnext->set_sensitive (!$mozembed_last);
-    $bbox->add ($bnext);
-
-    my $bback = Gtk2::Button->new ("Back");
-    $bback->signal_connect (clicked => sub { $r = 1; $w->destroy });
-    $bback->set_sensitive (!$mozembed_first);
-    $bbox->add ($bback);
-
-    my $bquit = Gtk2::Button->new ("Quit");
-    $bquit->signal_connect (clicked => sub { $r = 2; $w->destroy });
-    $bbox->add ($bquit);
-    $bbox->set_child_secondary ($bquit, 1);
-
-    $w->signal_connect (destroy => sub {
-        Gtk2->main_quit;
-        return FALSE;
-    });
-    $w->show_all ();
+    my $mslides = Gtk2::MenuItem->new ("Slides");
+    $mslides->set_submenu ($slidesmenu);
+    $mslides->show ();
+    $optsmenu->append ($mslides);
+
+    my $sep2 = Gtk2::SeparatorMenuItem->new ();
+    $sep2->show ();
+    $optsmenu->append ($sep2);
+
+    my $mscreenshot = Gtk2::MenuItem->new ("Take a screenshot");
+    $mscreenshot->signal_connect (activate => sub { screenshot () });
+    $mscreenshot->show ();
+    $optsmenu->append ($mscreenshot);
+
+    my $sep3 = Gtk2::SeparatorMenuItem->new ();
+    $sep3->show ();
+    $optsmenu->append ($sep3);
+
+    my $mquit = Gtk2::MenuItem->new ("Quit");
+    $mquit->signal_connect (activate => sub { Gtk2->main_quit });
+    $mquit->show ();
+    $optsmenu->append ($mquit);
+
+    my $moptions = Gtk2::MenuToolButton->new (undef, "Options");
+    #$boptions->signal_connect (clicked =>
+    #  sub { $optsmenu->popup (undef, undef, undef, undef, ?, ?) } );
+    $bbox->insert ($moptions, $i++);
+    $moptions->set_menu ($optsmenu);
+
+    return ($bbox, $bquit, $breload, $bnext, $bback, $brestart);
+}
+
+# Try running the external "gnome-screenshot" program, if it's
+# available, else take a screenshot using gdk routines.
+sub screenshot
+{
+    system ("gnome-screenshot");
 
 
-    $moz->load_url ($ARGV[0]);
-    Gtk2->main;
+    if ($? == -1) {
+        # We are going to save the entire screen.
+        my $root = Gtk2::Gdk->get_default_root_window ();
+        my ($width, $height) = $root->get_size;
 
 
-    exit $r;
+        # Create blank pixbuf to hold the image.
+        my $gdkpixbuf = Gtk2::Gdk::Pixbuf->new ('rgb',
+                                                0, 8, $width, $height);
+
+        $gdkpixbuf->get_from_drawable ($root, $root->get_colormap (),
+                                       0, 0, 0, 0, $width, $height);
+
+        my $i = 0;
+        $i++ while -f "screenshot$i.png";
+        $gdkpixbuf->save ("screenshot$i.png", 'png');
+    }
+
+    return FALSE;
 }
 
 1;
 
 }
 
 1;
 
+__END__
+
 =head1 TUTORIAL
 
 =head2 START WRITING A TALK
 =head1 TUTORIAL
 
 =head2 START WRITING A TALK
@@ -452,8 +657,8 @@ somewhere:
 
 A tech talk consists of HTML files ("slides") and shell scripts.  The
 filenames must start with a number, followed optionally by a
 
 A tech talk consists of HTML files ("slides") and shell scripts.  The
 filenames must start with a number, followed optionally by a
-description, followed by the extension (C<.html> or C<.sh>).  So to
-start our talk with two slides:
+description, followed by the extension (C<.html>, C<.sh> or C<.term>).
+So to start our talk with two slides:
 
  echo "This is the introduction" > 0010-introduction.html
  echo "This is the second slide" > 0020-second.html
 
  echo "This is the introduction" > 0010-introduction.html
  echo "This is the second slide" > 0020-second.html
@@ -521,9 +726,10 @@ styles as I edit them.  Just start firefox in the talk directory:
 When you edit an HTML file, click the Firefox reload button to
 immediately see your changes.
 
 When you edit an HTML file, click the Firefox reload button to
 immediately see your changes.
 
-Tech Talk PSE uses Mozilla embedding to display HTML, which uses the
-same Mozilla engine as Firefox, so what you should see in Firefox
-should be identical to what Tech Talk PSE displays.
+Tech Talk PSE uses WebKit embedding to display HTML.  HTML is
+standardized enough nowadays that what you see in Firefox and other
+browsers should be the same as what Tech Talk PSE displays.
+WebKit-based browsers (Chrome, Safari) should be identical.
 
 =head2 CREATING FIGURES
 
 
 =head2 CREATING FIGURES
 
@@ -533,8 +739,8 @@ an C<E<lt>imgE<gt>> tag, eg:
 
  <img src="fig1.gif">
 
 
  <img src="fig1.gif">
 
-Suitable tools include: XFig, GnuPlot, GraphViz, and many TeX tools
-such as PicTex and in particular TikZ.
+Suitable tools include: Inkscape, XFig, GnuPlot, GraphViz, and many
+TeX tools such as PicTex and in particular TikZ.
 
 =head2 EMBEDDING VIDEOS, ANIMATIONS, ETC.
 
 
 =head2 EMBEDDING VIDEOS, ANIMATIONS, ETC.
 
@@ -563,9 +769,14 @@ either rename or make a symbolic link to the slide name:
 
 =head2 TIPS FOR WRITING SHELL SCRIPTS
 
 
 =head2 TIPS FOR WRITING SHELL SCRIPTS
 
-Make sure each C<*.sh> file you write is executable, otherwise Tech
-Talk PSE won't be able to run it.  (The program gives a warning if you
-forget this).
+Make sure each C<*.sh> or C<*.term> file you write is executable,
+otherwise Tech Talk PSE won't be able to run it.  (The program gives a
+warning if you forget this).
+
+The difference between C<*.sh> (shell script) and C<*.term> (a
+terminal script) is that a shell script runs any commands, usually
+graphical commands, whereas a terminal script runs in a full screen
+terminal.
 
 A good idea is to start each script by sourcing some common functions.
 All my scripts start with:
 
 A good idea is to start each script by sourcing some common functions.
 All my scripts start with:
@@ -580,9 +791,16 @@ terminal.
 In C<functions>, I have:
 
  # -*- shell-script -*-
 In C<functions>, I have:
 
  # -*- shell-script -*-
+ # Place any local environment variables required in 'local'.
+ if [ -f local ]; then source local; fi
  export PS1="$ "
  export PS1="$ "
- export HISTFILE=/tmp/history
+ export HISTFILE=$talkdir/history
  rm -f $HISTFILE
  rm -f $HISTFILE
+ touch $HISTFILE
  
  add_history ()
  {
  
  add_history ()
  {
@@ -591,29 +809,25 @@ In C<functions>, I have:
  
  terminal ()
  {
  
  terminal ()
  {
-     exec \
-         gnome-terminal \
-         --window \
-         --geometry=+100+100 \
-         --hide-menubar \
-         --disable-factory \
-         -e '/bin/bash --norc' \
-         "$@"
+     # Make $HISTFILE unwritable so the shell won't update it
+     # when it exits.
+     chmod -w $HISTFILE
+     # Execute a shell.
+     bash --norc "$@"
  }
 
 By initializing the shell history, during your talk you can rapidly
 recall commands to start parts of the demonstration just by hitting
  }
 
 By initializing the shell history, during your talk you can rapidly
 recall commands to start parts of the demonstration just by hitting
-the Up arrow.  A complete shell script from one of my talks would look
-like this:
+the Up arrow.  A complete terminal script from one of my talks would
+look like this:
 
  #!/bin/bash -
  source functions
  add_history guestfish -i debian.img
 
  #!/bin/bash -
  source functions
  add_history guestfish -i debian.img
- terminal --title="Examining a Debian guest image in guestfish"
+ terminal
 
 
-This is just a starting point for your own scripts.  You may want to
-use a different terminal, such as xterm, and you may want to adjust
-terminal fonts.
+This is just a starting point for your own scripts.
 
 =head1 REFERENCE
 
 
 =head1 REFERENCE
 
@@ -623,7 +837,7 @@ Tech Talk PSE displays the slides in the directory in lexicographic
 order (the same order as C<LANG=C ls -1>).  Only files matching the
 following regexp are considered:
 
 order (the same order as C<LANG=C ls -1>).  Only files matching the
 following regexp are considered:
 
- ^(\d+)(?:-.*)\.(html|sh)$
+ ^(\d+)(?:-.*)\.(html|sh|term)$
 
 For future compatibility, you should ensure that every slide has a
 unique numeric part (ie. I<don't> have C<0010-aaa.html> and
 
 For future compatibility, you should ensure that every slide has a
 unique numeric part (ie. I<don't> have C<0010-aaa.html> and
@@ -651,6 +865,13 @@ When running shell scripts, the current directory is also set to be
 the directory containing the talk files, so the same rules about using
 relative paths apply there too.
 
 the directory containing the talk files, so the same rules about using
 relative paths apply there too.
 
+The environment variable C<$talkdir> is exported to scripts and it
+contains the absolute path of the directory containing the talk files.
+When a script is run, the current directory is the same as
+C<$talkdir>, but if your script changes directory (eg. into a
+subdirectory containing supporting files) then it can be useful to use
+C<$talkdir> to refer back to the original directory.
+
 =head1 WHAT MAKES A GOOD TALK
 
 I like what Edward Tufte writes, for example his evisceration of
 =head1 WHAT MAKES A GOOD TALK
 
 I like what Edward Tufte writes, for example his evisceration of
@@ -684,7 +905,7 @@ that it took to get them all in one place.
 
 I don't think you can get away with spending less than two full days
 preparing a talk, if you want to master the topic and draw up accurate
 
 I don't think you can get away with spending less than two full days
 preparing a talk, if you want to master the topic and draw up accurate
-slides.  Steve Jobs is reputed to spend weeks preparing his annual
+slides.  Steve Jobs was reputed to spend weeks preparing his annual
 sales talk to the Apple faithful.
 
 B<Thirdly:> Now that you're going to write your talk as an essay, what
 sales talk to the Apple faithful.
 
 B<Thirdly:> Now that you're going to write your talk as an essay, what