static void add_history_line (const char *);
 #endif
 
+static int override_progress_bars = -1;
+
 /* Currently open libguestfs handle. */
 guestfs_h *g;
 
 int inspector = 0;
 int utf8_mode = 0;
 int have_terminfo = 0;
+int progress_bars = 0;
 
 static void __attribute__((noreturn))
 usage (int status)
              "  -m|--mount dev[:mnt] Mount dev on mnt (if omitted, /)\n"
              "  -n|--no-sync         Don't autosync\n"
              "  -N|--new type        Create prepared disk (test1.img, ...)\n"
+             "  --progress-bars      Enable progress bars even when not interactive\n"
+             "  --no-progress-bars   Disable progress bars\n"
              "  --remote[=pid]       Send commands to remote %s\n"
              "  -r|--ro              Mount read-only\n"
              "  --selinux            Enable SELinux support\n"
     { "new", 1, 0, 'N' },
     { "no-dest-paths", 0, 0, 'D' },
     { "no-sync", 0, 0, 'n' },
+    { "progress-bars", 0, 0, 0 },
+    { "no-progress-bars", 0, 0, 0 },
     { "remote", 2, 0, 0 },
     { "ro", 0, 0, 'r' },
     { "selinux", 0, 0, 0 },
         guestfs_set_selinux (g, 1);
       } else if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
         keys_from_stdin = 1;
+      } else if (STREQ (long_options[option_index].name, "progress-bars")) {
+        override_progress_bars = 1;
+      } else if (STREQ (long_options[option_index].name, "no-progress-bars")) {
+        override_progress_bars = 0;
       } else {
         fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
                  program_name, long_options[option_index].name, option_index);
     }
   }
 
+  /* Decide if we display progress bars. */
+  progress_bars =
+    override_progress_bars >= 0
+    ? override_progress_bars
+    : (optind >= argc && isatty (0));
+
+  if (progress_bars)
+    guestfs_set_progress_callback (g, progress_callback, NULL);
+
   /* Interactive, shell script, or command(s) on the command line? */
   if (optind >= argc) {
     if (isatty (0))
   int pid = 0;
   int i, r;
 
+  reset_progress_bar ();
+
   /* This counts the commands issued, starting at 1. */
   command_num++;
 
 
 I<-N> creates a preformatted disk with a filesystem and adds it.
 See L</PREPARED DISK IMAGES> below.
 
+=item B<--progress-bars>
+
+Enable progress bars, even when guestfish is used non-interactively.
+
+Progress bars are enabled by default when guestfish is used as an
+interactive shell.
+
+=item B<--no-progress-bars>
+
+Disable progress bars.
+
 =item B<--remote[=pid]>
 
 Send remote commands to C<$GUESTFISH_PID> or C<pid>.  See section
 
  guestfish -N disk:200M
 
+=head1 PROGRESS BARS
+
+Some (not all) long-running commands send progress notification
+messages as they are running.  Guestfish turns these messages into
+progress bars.
+
+When a command that supports progress bars takes longer than two
+seconds to run, and if progress bars are enabled, then you will see
+one appearing below the command:
+
+ ><fs> copy-size /large-file /another-file 2048M
+ / 10% [#####-----------------------------------------] 00:30
+
+The spinner on the left hand side moves round once for every progress
+notification received from the backend.  This is a (reasonably) golden
+assurance that the command is "doing something" even if the progress
+bar is not moving, because the command is able to send the progress
+notifications.  When the bar reaches 100% and the command finishes,
+the spinner disappears.
+
+Progress bars are enabled by default when guestfish is used
+interactively.  You can enable them even for non-interactive modes
+using I<--progress-bars>, and you can disable them completely using
+I<--no-progress-bars>.
+
 =head1 GUESTFISH COMMANDS
 
 The commands in this section are guestfish convenience commands, in
 
--- /dev/null
+/* guestfish - the filesystem interactive shell
+ * Copyright (C) 2010 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
+ * 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 WARRANTY; 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <math.h>
+
+#include <guestfs.h>
+
+#include "fish.h"
+#include "rmsd.h"
+
+/* Include these last since they redefine symbols such as 'lines'
+ * which seriously breaks other headers.
+ */
+#include <term.h>
+#include <curses.h>
+
+/* Provided by termcap or terminfo emulation, but not defined
+ * in any header file.
+ */
+extern const char *UP;
+
+static const char *
+spinner (int count)
+{
+  /* Choice of unicode spinners.
+   *
+   * For basic dingbats, see:
+   * http://www.fileformat.info/info/unicode/block/geometric_shapes/utf8test.htm
+   * http://www.fileformat.info/info/unicode/block/dingbats/utf8test.htm
+   *
+   * Arrows are a mess in unicode.  This page helps a lot:
+   * http://xahlee.org/comp/unicode_arrows.html
+   *
+   * I prefer something which doesn't point, just spins.
+   */
+  /* Black pointing triangle. */
+  //static const char *us[] = { "\u25b2", "\u25b6", "\u25bc", "\u25c0" };
+  /* White pointing triangle. */
+  //static const char *us[] = { "\u25b3", "\u25b7", "\u25bd", "\u25c1" };
+  /* Circle with half black. */
+  static const char *us[] = { "\u25d0", "\u25d3", "\u25d1", "\u25d2" };
+  /* White square white quadrant. */
+  //static const char *us[] = { "\u25f0", "\u25f3", "\u25f2", "\u25f1" };
+  /* White circle white quadrant. */
+  //static const char *us[] = { "\u25f4", "\u25f7", "\u25f6", "\u25f5" };
+  /* Black triangle. */
+  //static const char *us[] = { "\u25e2", "\u25e3", "\u25e4", "\u25e5" };
+  /* Spinning arrow in 8 directions. */
+  //static const char *us[] = { "\u2190", "\u2196", "\u2191", "\u2197",
+  //                            "\u2192", "\u2198", "\u2193", "\u2199" };
+
+  /* ASCII spinner. */
+  static const char *as[] = { "/", "-", "\\", "|" };
+
+  const char **s;
+  size_t n;
+
+  if (utf8_mode) {
+    s = us;
+    n = sizeof us / sizeof us[0];
+  }
+  else {
+    s = as;
+    n = sizeof as / sizeof as[0];
+  }
+
+  return s[count % n];
+}
+
+static double start;         /* start time of command */
+static int count;            /* number of progress notifications per cmd */
+static struct rmsd rmsd;     /* running mean and standard deviation */
+
+/* This function is called just before we issue any command. */
+void
+reset_progress_bar (void)
+{
+  /* The time at which this command was issued. */
+  struct timeval start_t;
+  gettimeofday (&start_t, NULL);
+
+  start = start_t.tv_sec + start_t.tv_usec / 1000000.;
+
+  count = 0;
+
+  rmsd_init (&rmsd);
+}
+
+/* Return remaining time estimate (in seconds) for current call.
+ *
+ * This returns the running mean estimate of remaining time, but if
+ * the latest estimate of total time is greater than two s.d.'s from
+ * the running mean then we don't print anything because we're not
+ * confident that the estimate is meaningful.  (Returned value is <0.0
+ * when nothing should be printed).
+ */
+static double
+estimate_remaining_time (double ratio)
+{
+  if (ratio <= 0.)
+    return -1.0;
+
+  struct timeval now_t;
+  gettimeofday (&now_t, NULL);
+
+  double now = now_t.tv_sec + now_t.tv_usec / 1000000.;
+  /* We've done 'ratio' of the work in 'now - start' seconds. */
+  double time_passed = now - start;
+
+  double total_time = time_passed / ratio;
+
+  /* Add total_time to running mean and s.d. and then see if our
+   * estimate of total time is meaningful.
+   */
+  rmsd_add_sample (&rmsd, total_time);
+
+  double mean = rmsd_get_mean (&rmsd);
+  double sd = rmsd_get_standard_deviation (&rmsd);
+  if (fabs (total_time - mean) >= 2.0*sd)
+    return -1.0;
+
+  /* Don't return early estimates. */
+  if (time_passed < 3.0)
+    return -1.0;
+
+  return total_time - time_passed;
+}
+
+/* The overhead is how much we subtract before we get to the progress
+ * bar itself.
+ *
+ * / 100% [########---------------] xx:xx
+ * | |    |                       | |
+ * | |    |                       | time (5 cols)
+ * | |    |                       |
+ * | |    open paren + close paren + space (3 cols)
+ * | |
+ * | percentage and space (5 cols)
+ * |
+ * spinner and space (2 cols)
+ *
+ * Total = 2 + 5 + 3 + 5 = 15
+ */
+#define COLS_OVERHEAD 15
+
+/* Callback which displays a progress bar. */
+void
+progress_callback (guestfs_h *g, void *data,
+                   int proc_nr, int serial,
+                   uint64_t position, uint64_t total)
+{
+  if (have_terminfo == 0) {
+  dumb:
+    printf ("%" PRIu64 "/%" PRIu64 "\n", position, total);
+  } else {
+    int cols = tgetnum ((char *) "co");
+    if (cols < 32) goto dumb;
+
+    /* Update an existing progress bar just printed? */
+    if (count > 0)
+      tputs (UP, 2, putchar);
+    count++;
+
+    double ratio = (double) position / total;
+    if (ratio < 0) ratio = 0; else if (ratio > 1) ratio = 1;
+
+    if (ratio < 1) {
+      int percent = 100.0 * ratio;
+      printf ("%s%3d%% ", spinner (count), percent);
+    } else {
+      fputs (" 100% ", stdout);
+    }
+
+    int dots = ratio * (double) (cols - COLS_OVERHEAD);
+
+    const char *s_open, *s_dot, *s_dash, *s_close;
+    if (utf8_mode) {
+      s_open = "\u27e6"; s_dot = "\u2589"; s_dash = "\u2550"; s_close = "\u27e7";
+    } else {
+      s_open = "["; s_dot = "#"; s_dash = "-"; s_close = "]";
+    }
+
+    fputs (s_open, stdout);
+    int i;
+    for (i = 0; i < dots; ++i)
+      fputs (s_dot, stdout);
+    for (i = dots; i < cols - COLS_OVERHEAD; ++i)
+      fputs (s_dash, stdout);
+    fputs (s_close, stdout);
+    fputc (' ', stdout);
+
+    /* Time estimate. */
+    double estimate = estimate_remaining_time (ratio);
+    if (estimate >= 100.0 * 60.0 * 60.0 /* >= 100 hours */) {
+      /* Display hours<h> */
+      estimate /= 60. * 60.;
+      int hh = floor (estimate);
+      printf (">%dh", hh);
+    } else if (estimate >= 100.0 * 60.0 /* >= 100 minutes */) {
+      /* Display hours<h>minutes */
+      estimate /= 60. * 60.;
+      int hh = floor (estimate);
+      double ignore;
+      int mm = floor (modf (estimate, &ignore) * 60.);
+      printf ("%02dh%02d", hh, mm);
+    } else if (estimate >= 0.0) {
+      /* Display minutes:seconds */
+      estimate /= 60.;
+      int mm = floor (estimate);
+      double ignore;
+      int ss = floor (modf (estimate, &ignore) * 60.);
+      printf ("%02d:%02d", mm, ss);
+    }
+    else /* < 0 means estimate was not meaningful */
+      fputs ("--:--", stdout);
+
+    fputc ('\n', stdout);
+  }
+}
 
--- /dev/null
+/* libguestfs - guestfish shell
+ * Copyright (C) 2010 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
+ * 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 WARRANTY; 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.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef FISH_RMSD_H
+#define FISH_RMSD_H
+
+/* Compute the running mean and standard deviation from the
+ * series of estimated values.
+ *
+ * Method:
+ * http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
+ * Checked in a test program against answers given by Wolfram Alpha.
+ */
+struct rmsd {
+  double a;                     /* mean */
+  double i;                     /* number of samples */
+  double q;
+};
+
+static void
+rmsd_init (struct rmsd *r)
+{
+  r->a = 0;
+  r->i = 1;
+  r->q = 0;
+}
+
+static void
+rmsd_add_sample (struct rmsd *r, double x)
+{
+  double a_next, q_next;
+
+  a_next = r->a + (x - r->a) / r->i;
+  q_next = r->q + (x - r->a) * (x - a_next);
+  r->a = a_next;
+  r->q = q_next;
+  r->i += 1.0;
+}
+
+static double
+rmsd_get_mean (const struct rmsd *r)
+{
+  return r->a;
+}
+
+static double
+rmsd_get_standard_deviation (const struct rmsd *r)
+{
+  return sqrt (r->q / (r->i - 1.0));
+}
+
+#endif /* FISH_RMSD_H */