chiark / gitweb /
A big mess of changes all at once.
authorMark Wooding <mdw@distorted.org.uk>
Tue, 1 Mar 2022 16:18:47 +0000 (16:18 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 1 Mar 2022 16:18:47 +0000 (16:18 +0000)
I'm usually quite good about factoring out all of the changes I make, but
in this case it doesn't seem worth the bother.

.gitignore
Makefile
dvd-cache-keys.c [new file with mode: 0644]
dvd-id.c [new file with mode: 0644]
dvd-sector-copy.c
dvdrip
lib.c [new file with mode: 0644]
lib.h [new file with mode: 0644]
multiprogress.c [new file with mode: 0644]
multiprogress.h [new file with mode: 0644]

index 33147513a7753ea1797961cbae54180af8a09125..de68a31850553764d980788bdb373d4c7b80c02c 100644 (file)
@@ -1,4 +1,6 @@
 *.dep
 *.o
 
+/dvd-cache-keys
+/dvd-id
 /dvd-sector-copy
index e4122783d7859597bb8bef9d9cbd3450cd9af4e5..e0a08056bd47d3dd5e69ff195dd4108f274d598b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -23,10 +23,26 @@ CFLAGS                       = -O2 -g -pedantic -Wall -Werror
 
 LD                      = gcc
 LDFLAGS                         =
+LIBS                    = -ldvdread -lm
+
+CFLAGS.terminfo                += -DUSE_TERMINFO
+LIBS.terminfo          += -ltinfo
+
+CFLAGS.termcap         += -DUSE_TERMCAP
+LIBS.termcap           += -ltermcap
+
+TERMLIB                        ?= terminfo
+CFLAGS                 += $(CFLAGS.$(TERMLIB))
+LIBS                   += $(LIBS.$(TERMLIB))
 
 PROGS                  += dvd-sector-copy
-dvd-sector-copy_SRCS    = dvd-sector-copy.c
-dvd-sector-copy_LIBS    = -ldvdread -lm
+dvd-sector-copy_SRCS    = dvd-sector-copy.c lib.c multiprogress.c
+
+PROGS                  += dvd-cache-keys
+dvd-cache-keys_SRCS     = dvd-cache-keys.c lib.c multiprogress.c
+
+PROGS                  += dvd-id
+dvd-id_SRCS             = dvd-id.c lib.c multiprogress.c
 
 SCRIPTS                        += dvdrip
 SCRIPTS                        += dvdrip-upload
@@ -41,7 +57,7 @@ TARGETS                       += $(PROGS)
 program-objects                 = $(call objects,$($1_SRCS),$2)
 $(PROGS): %: $$(call program-objects,$$*) $$($$*_DEPS)
        $(call v-tag,LD)$(LD) $(LDFLAGS) -o$@ \
-               $(call program-objects,$*) $($*_LIBS)
+               $(call program-objects,$*) $($*_LIBS) $(LIBS)
 
 INSTALL_bin             = $(addprefix inst/,$(PROGS) $(SCRIPTS))
 install: $(INSTALL_bin)
diff --git a/dvd-cache-keys.c b/dvd-cache-keys.c
new file mode 100644 (file)
index 0000000..6a4a7a0
--- /dev/null
@@ -0,0 +1,53 @@
+#include "lib.h"
+
+static void usage(FILE *fp) { fprintf(fp, "usage: %s DEVICE\n", prog); }
+
+static dvd_reader_t *dvd;
+
+static void kick_vob(int index, int titlep)
+{
+  dvd_file_t *vob;
+
+  vob = DVDOpenFile(dvd, index,
+                   titlep ? DVD_READ_TITLE_VOBS : DVD_READ_MENU_VOBS);
+  if (!vob) bail("failed to open %s %d", titlep ? "title" : "menu", index);
+  DVDCloseFile(vob);
+}
+
+int main(int argc, char *argv[])
+{
+  char fn[MAXFNSZ];
+  int opt;
+  unsigned i, f = 0;
+  secaddr start, len;
+#define f_bogus 1u
+
+  set_prog(argv[0]);
+  for (;;) {
+    opt = getopt(argc, argv, "h"); if (opt < 0) break;
+    switch (opt) {
+      case 'h': usage(stderr); exit(0);
+      default: f |= f_bogus; break;
+    }
+  }
+  if (argc - optind != 1) f |= f_bogus;
+  if (f&f_bogus) { usage(stderr); exit(2); }
+
+  setlocale(LC_ALL, "");
+  progress_init(&progress);
+
+  open_dvd(argv[optind], 0, &dvd);
+
+  for (i = 0; i < 100; i++) {
+    store_filename(fn, mkident(VOB, i, 0));
+    start = UDFFindFile(dvd, fn, &len); if (start) kick_vob(i, 0);
+    if (i) {
+      store_filename(fn, mkident(VOB, i, 1));
+      start = UDFFindFile(dvd, fn, &len); if (start) kick_vob(i, 1);
+    }
+  }
+
+  if (dvd) DVDClose(dvd);
+  progress_free(&progress);
+  return (0);
+}
diff --git a/dvd-id.c b/dvd-id.c
new file mode 100644 (file)
index 0000000..971be05
--- /dev/null
+++ b/dvd-id.c
@@ -0,0 +1,54 @@
+#include "lib.h"
+
+static void usage(FILE *fp) { fprintf(fp, "usage: %s DEVICE\n", prog); }
+
+static void puthex(const unsigned char *p, size_t sz, FILE *fp)
+  { while (sz) { fprintf(fp, "%02x", *p++); sz--; } }
+
+int main(int argc, char *argv[])
+{
+  char volid[33];
+  unsigned char volsetid[16], discid[16];
+  int rc, opt;
+  unsigned f = 0;
+  static dvd_reader_t *dvd;
+#define f_bogus 1u
+
+  set_prog(argv[0]);
+  for (;;) {
+    opt = getopt(argc, argv, "h"); if (opt < 0) break;
+    switch (opt) {
+      case 'h': usage(stderr); exit(0);
+      default: f |= f_bogus; break;
+    }
+  }
+  if (argc - optind != 1) f |= f_bogus;
+  if (f&f_bogus) { usage(stderr); exit(2); }
+  setlocale(LC_ALL, "");
+  progress_init(&progress);
+
+  open_dvd(argv[optind], 0, &dvd);
+
+  rc = DVDUDFVolumeInfo(dvd,
+                       volid, sizeof(volid),
+                       volsetid, sizeof(volsetid));
+  if (rc) {
+    moan("failed to read volume info");
+    strcpy(volid, "<error reading volume info>");
+    memset(volsetid, 0xff, sizeof(volsetid));
+  }
+
+  rc = DVDDiscID(dvd, discid);
+  if (rc) {
+    moan("failed to determine disc id");
+    memset(discid, 0xff, sizeof(discid));
+  }
+
+  fputs(volid, stdout); fputc('-', stdout);
+  puthex(volsetid, sizeof(volsetid), stdout); fputc(':', stdout);
+  puthex(discid, sizeof(discid), stdout); fputc('\n', stdout);
+
+  if (dvd) DVDClose(dvd);
+  progress_free(&progress);
+  return (0);
+}
index 5598f6ff74640f7870728738a631d131c7c9431b..7b7e674ec8cf3f875f8c03073f7c8cb81ed82542 100644 (file)
@@ -1,90 +1,20 @@
-#define _GNU_SOURCE
-#define _FILE_OFFSET_BITS 64
-
-#include <assert.h>
-#include <ctype.h>
-#include <errno.h>
-#include <inttypes.h>
-#include <math.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-
-#include <unistd.h>
-#include <fcntl.h>
-#include <sys/ioctl.h>
-#include <sys/stat.h>
-#include <sys/time.h>
-
-#include <getopt.h>
-
-#include <linux/fs.h>
-
-#include <dvdread/dvd_reader.h>
-#include <dvdread/dvd_udf.h>
-#include <dvdread/ifo_read.h>
-#include <dvdread/ifo_types.h>
-
-#define CTYPE_HACK(fn, ch) fn((unsigned char)(ch))
-#define ISDIGIT(ch) CTYPE_HACK(isdigit, ch)
-#define ISSPACE(ch) CTYPE_HACK(isspace, ch)
+#include "lib.h"
 
-#ifdef DEBUG
-#  define D(x) x
-#else
-#  define D(x)
-#endif
-
-#define N(v) (sizeof(v)/sizeof((v)[0]))
-
-#define SECTORSZ 2048
-#define SECTORS(n) (((n) + (SECTORSZ - 1))/SECTORSZ)
-
-static const char *prog = "<unset>";
 static int status = 0;
 
 static void usage(FILE *fp)
 {
   fprintf(fp,
-         "usage: %s [-c] [-R MAP] [-b OUTMAP] [-r [START]-[END]]\n"
-         "\tDEVICE OUTFILE\n",
+         "usage: %s [-c] [-B PARAM=VALUE,...] [-R MAP]\n"
+         "\t[-b OUTMAP] [-r [START]-[END]] DEVICE OUTFILE\n",
          prog);
 }
 
-static void vmoan(const char *fmt, va_list ap)
-  { fprintf(stderr, "%s: ", prog); vfprintf(stderr, fmt, ap); }
-
-__attribute__((format(printf, 1, 2)))
-static void moan(const char *fmt, ...)
+static double tvdiff(const struct timeval *tv_lo,
+                    const struct timeval *tv_hi)
 {
-  va_list ap;
-
-  va_start(ap, fmt); vmoan(fmt, ap); va_end(ap);
-  fputc('\n', stderr);
-}
-
-__attribute__((noreturn, format(printf, 1, 2)))
-static void bail(const char *fmt, ...)
-{
-  va_list ap;
-
-  va_start(ap, fmt); vmoan(fmt, ap); va_end(ap);
-  fputc('\n', stderr);
-  exit(2);
-}
-
-__attribute__((noreturn, format(printf, 2, 3)))
-static void bail_syserr(int err, const char *fmt, ...)
-{
-  va_list ap;
-
-  va_start(ap, fmt); vmoan(fmt, ap); va_end(ap);
-  if (err) fprintf(stderr, ": %s", strerror(errno));
-  fputc('\n', stderr);
-  exit(2);
+  return ((tv_hi->tv_sec - tv_lo->tv_sec) +
+         (tv_hi->tv_usec - tv_lo->tv_usec)/1.0e6);
 }
 
 static void carefully_write(int fd, const void *buf, size_t sz)
@@ -147,45 +77,6 @@ static void carefully_fclose(FILE *fp, const char *what)
   (p) = &(vv)->v[(vv)->n++];                                           \
 } while (0)
 
-enum { RAW, IFO, VOB, BUP };
-typedef uint_least32_t ident;
-
-static inline ident mkident(unsigned kind, unsigned title, unsigned part)
-  { return (((ident)kind << 0) | ((ident)title << 8) | ((ident)part << 16)); }
-static inline unsigned id_kind(ident id) { return ((id >> 0)&0x0ff); }
-static inline unsigned id_title(ident id) { return ((id >> 8)&0x0ff); }
-static inline unsigned id_part(ident id) { return ((id >> 16)&0x0ff); }
-
-#define MAXFNSZ (1 + 8 + 1 + 12 + 1)
-
-static void store_filename(char *buf, ident id)
-{
-  switch (id_kind(id)) {
-    case RAW:
-      sprintf(buf, "#<raw device>");
-      break;
-    case IFO:
-      if (!id_title(id)) sprintf(buf, "/VIDEO_TS/VIDEO_TS.IFO");
-      else sprintf(buf, "/VIDEO_TS/VTS_%02u_0.IFO", id_title(id));
-      break;
-    case BUP:
-      if (!id_title(id)) sprintf(buf, "/VIDEO_TS/VIDEO_TS.BUP");
-      else sprintf(buf, "/VIDEO_TS/VTS_%02u_0.BUP", id_title(id));
-      break;
-    case VOB:
-      if (!id_title(id)) sprintf(buf, "/VIDEO_TS/VIDEO_TS.VOB");
-      else
-       sprintf(buf, "/VIDEO_TS/VTS_%02u_%u.VOB", id_title(id), id_part(id));
-      break;
-    default:
-      abort();
-  }
-}
-
-typedef uint_least32_t secaddr;
-#define PRIuSEC PRIuLEAST32
-#define SECLIMIT 0x00400000
-
 #define MAXFILES (1 + 2*99 + 1)
 struct file {
   ident id;
@@ -314,66 +205,127 @@ static void put_title(dvd_reader_t *dvd, unsigned title)
           start[0], start[npart - 1] + SECTORS(len[npart - 1]));
 }
 
-static int progresslen = 0;
+static secaddr last_pos, limit, nsectors, ndone;
+static struct timeval last_time;
+static double wsum, wcount;
+static struct file *file;
+static secaddr bad_start;
+static unsigned retry, max_retries = 4;
+static int bad_err;
+
+static const char throbber[] = "|<-<|>->";
+static unsigned throbix = 0;
 
-static void clear_progress_internal(void)
+static double scale_bytes(double n, const char **unit_out)
 {
-  while (progresslen) { fputs("\b \b", stdout); progresslen--; }
-  putchar('\r');
+  const char *unit = "";
+
+  if (n > 1600) { n /= 1024; unit = "k"; }
+  if (n > 1600) { n /= 1024; unit = "M"; }
+  if (n > 1600) { n /= 1024; unit = "G"; }
+  if (n > 1600) { n /= 1024; unit = "T"; }
+
+  *unit_out = unit; return (n);
 }
-static void clear_progress(void)
-  { clear_progress_internal(); fflush(stdout); }
-#ifdef DEBUG
-static void debug_clear_progress(void)
-  { if (progresslen) { putchar('\n'); progresslen = 0; } }
-#endif
-static void vappend_progress(const char *fmt, va_list ap)
-  { progresslen += vprintf(fmt, ap); }
-__attribute__((format(printf, 1, 2)))
-static void append_progress(const char *fmt, ...)
+
+static struct progress_item
+  copy_progress, disc_progress,
+  file_progress, badblock_progress;
+
+#define TIMESTRMAX 16
+static char *fmttime(unsigned long t, char *buf)
 {
-  va_list ap;
+  if (t < 60) sprintf(buf, "%ld s", t);
+  else if (t < 3600) sprintf(buf, "%ld:%02ld", t/60, t%60);
+  else sprintf(buf, "%ld:%02ld:%02ld", t/3600, (t/60)%60, t%60);
+  return (buf);
+}
 
-  va_start(ap, fmt);
-  vappend_progress(fmt, ap);
-  va_end(ap);
+static void render_perfstats(struct progress_render_state *render)
+{
+  int eta;
+  char timebuf[TIMESTRMAX];
+  double rate;
+  const char *unit;
+
+  if (!wsum || !wcount) { rate = 0; eta = -1; }
+  else { rate = wsum/wcount; eta = (int)((nsectors - ndone)/rate + 0.5); }
+
+  rate = scale_bytes(rate*SECTORSZ, &unit);
+  progress_putright(render, "ETA %s ", rate ? fmttime(eta, timebuf) : "???");
+  progress_putright(render, "%.1f %sB/s, ", rate, unit);
 }
-__attribute__((format(printf, 1, 2)))
-static void print_progress(const char *fmt, ...)
+
+static void render_copy_progress(struct progress_item *item,
+                                struct progress_render_state *render)
 {
-  va_list ap;
+  double frac = (double)ndone/nsectors;
 
-  va_start(ap, fmt);
-  clear_progress_internal();
-  vappend_progress(fmt, ap);
-  va_end(ap);
+  progress_putleft(render, " %c copied %.1f%%",
+                  throbber[throbix], 100.0*frac);
+  render_perfstats(render);
+  progress_putleft(render, " (%"PRIuSEC" of %"PRIuSEC")", ndone, nsectors);
+
+  progress_showbar(render, frac);
 }
 
-unsigned flags;
-#  define F_ALLPROGRESS 1u
-static secaddr last_pos, limit, nsectors, ndone;
-static struct timeval last_time;
-static double wsum, wcount;
-static struct file *file;
+static void render_disc_progress(struct progress_item *item,
+                                struct progress_render_state *render)
+{
+  double frac = (double)last_pos/limit;
 
-static const char throbber[] = "|<-<|>->";
-static unsigned throbix = 0;
+  progress_putleft(render, "        disc %.1f%% (%"PRIuSEC" of %"PRIuSEC")",
+                  100.0*frac, last_pos, limit);
+  progress_showbar(render, frac);
+}
 
-static void report_progress(secaddr pos)
+static void render_file_progress(struct progress_item *item,
+                                struct progress_render_state *render)
+{
+  secaddr off = last_pos - file->start, len = file->end - file->start;
+  char fn[MAXFNSZ];
+  double frac;
+
+  store_filename(fn, file->id);
+  frac = (double)off/len;
+  progress_putleft(render, "        `%s' %.1f%% (%"PRIuSEC" of %"PRIuSEC")",
+                  fn, 100.0*frac, off, len);
+  progress_showbar(render, frac);
+}
+
+static void render_badblock_progress(struct progress_item *item,
+                                    struct progress_render_state *render)
+{
+  secaddr n = last_pos - bad_start;
+  int bg;
+
+  if (!n) {
+    progress_putleft(render, " Retrying bad sector %"PRIuSEC"", bad_start);
+    progress_putright(render, "attempt %u/%u ", retry + 1, max_retries);
+    bg = 4;
+  } else {
+    progress_putleft(render, " Found %"PRIuSEC" bad %s",
+                    n, n == 1 ? "sector" : "sectors");
+    progress_putright(render, "%"PRIuSEC" .. %"PRIuSEC" ",
+                     bad_start, last_pos);
+    bg = 1;
+  }
+  if (bad_err && bad_err != EIO)
+    progress_putleft(render, " (%s)", strerror(bad_err));
+  progress_shownotice(render, bg, 7);
+}
+
+static void update_progress(secaddr pos)
 {
-  char etastr[32];
   struct timeval now;
-  int eta;
-  double percent, t, f, g, rate;
-  char *unit;
+  double t, f, g;
+
+  gettimeofday(&now, 0);
+  t = tvdiff(&last_time, &now);
 
 #define ALPHA 0.1
 #define BETA (1 - ALPHA)
 
-  gettimeofday(&now, 0);
-  t = (now.tv_sec - last_time.tv_sec) +
-    (now.tv_usec - last_time.tv_usec)/1000000.0;
-
   if (t) {
     g = wcount ? pow(BETA, t) : 0.0; f = (1 - g)/(1 - BETA);
     wsum = f*(pos - last_pos)/t + g*wsum;
@@ -382,37 +334,15 @@ static void report_progress(secaddr pos)
     last_time = now; last_pos = pos;
   }
 
-  if (!wsum || !wcount)
-    { rate = 0; strcpy(etastr, "???"); }
-  else {
-    rate = wsum/wcount;
-    eta = (int)((nsectors - ndone)/rate);
-    sprintf(etastr, "%d:%02d:%02d", eta/3600, (eta/60)%60, eta%60);
-  }
-
-  rate *= SECTORSZ; unit = "";
-  if (rate > 128) { rate /= 1024; unit = "k"; }
-  if (rate > 128) { rate /= 1024; unit = "M"; }
-  if (rate > 128) { rate /= 1024; unit = "G"; }
-
-  if (flags&F_ALLPROGRESS) percent = pos*100.0/limit;
-  else percent = ndone*100.0/nsectors;
-  print_progress
-    ("%c copied %.1f%% (%"PRIuSEC" of %"PRIuSEC"; %.1f %sB/s, ETA %s)",
-     throbber[throbix], percent, pos, limit,  rate, unit, etastr);
-  throbix++; if (!throbber[throbix]) throbix = 0;
-  if (file && id_kind(file->id) == VOB) {
-    append_progress(" -- %s %d %.1f%%",
-                   id_part(file->id) ? "title" : "menu",
-                   id_title(file->id),
-                   (pos - file->start)*100.0/
-                     (file->end - file->start));
-  }
-
 #undef ALPHA
 #undef BETA
+
+  throbix++; if (!throbber[throbix]) throbix = 0;
 }
 
+static void report_progress(secaddr pos)
+  { update_progress(pos); progress_update(&progress); }
+
 static dvd_reader_t *dvd;
 static int dvdfd = -1, outfd = -1;
 static dvd_file_t *vob;
@@ -436,6 +366,9 @@ static int compare_badblock(const void *a, const void *b)
   return (0);
 }
 
+static double bad_block_delay = 0.0;
+static double good_block_delay = 0.0;
+
 static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
 {
   ssize_t n, done;
@@ -447,7 +380,7 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
   if (badblocks.n) {
     best = 0; lo = 0; hi = badblocks.n;
 #ifdef DEBUG
-    debug_clear_progress();
+    progress_clear();
     printf(";; searching badblocks for %"PRIuSEC" .. %"PRIuSEC"\n",
           pos, pos + want);
 #endif
@@ -467,7 +400,7 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
             best->start, best->end);
 #endif
     if (best && pos + want > best->start)
-      { want = best->start - pos; fakeerr = EIO; }
+      { want = best->start - pos; fakeerr = EIO; sit(bad_block_delay); }
   }
   done = 0;
   while (want) {
@@ -493,46 +426,61 @@ static ssize_t read_sectors(secaddr pos, void *buf, secaddr want)
     } else if (errno != EINTR) break;
   }
   if (fakeerr && !errno) errno = fakeerr;
+  else if (done > 0 && good_block_delay) sit(done*good_block_delay);
   return (!done && errno ? -1 : done);
 }
 
-static void report_bad_blocks_progress(secaddr lo, secaddr hi, int err)
+static void record_bad_sectors(secaddr bad_lo, secaddr bad_hi)
 {
-  report_progress(hi);
+  char fn[MAXFNSZ];
 
-  if (lo == hi) append_progress(": retrying bad sector");
-  else
-    append_progress(": %"PRIuSEC" bad %s",
-                   hi - lo, hi == lo + 1 ? "sector" : "sectors");
-  if (err && err != EIO) append_progress(" (%s)", strerror(err));
-  fflush(stdout);
+  if (!mapfile) return;
+
+  open_file_on_demand(mapfile, &mapfp, "bad-sector region map");
+  fprintf(mapfp, "%"PRIuSEC" %"PRIuSEC"", bad_lo, bad_hi);
+
+  if (file && id_kind(file->id) != RAW) {
+    store_filename(fn, file->id);
+    fprintf(mapfp, " # `%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC"",
+           fn, bad_lo - file->start, bad_hi - file->start,
+           file->end - file->start);
+  }
+
+  fputc('\n', mapfp);
+  check_write(mapfp, "bad-sector region map");
 }
 
 static void recovered(secaddr bad_lo, secaddr bad_hi)
 {
-  clear_progress();
-  moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC")",
-       bad_hi - bad_lo, bad_lo, bad_hi);
-  if (mapfile) {
-    open_file_on_demand(mapfile, &mapfp, "bad-sector region map");
-    fprintf(mapfp, "%"PRIuSEC" %"PRIuSEC"", bad_lo, bad_hi);
-    if (file && id_kind(file->id) != RAW)
-      fprintf(mapfp, " # %s #%d %"PRIuSEC"..%"PRIuSEC" of %"PRIuSEC" (%.1f%%)",
-             id_part(file->id) ? "title" : "menu",
-             id_title(file->id),
-             bad_lo - file->start, bad_hi - file->start,
-             file->end - file->start,
-             (bad_lo - file->start)*100.0/(file->end - file->start));
-    fputc('\n', mapfp);
-    check_write(mapfp, "bad-sector region map");
+  char fn[MAXFNSZ];
+
+  progress_clear(&progress);
+
+  if (!file || id_kind(file->id) == RAW)
+    moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC")",
+        bad_hi - bad_lo, bad_lo, bad_hi);
+  else {
+    store_filename(fn, file->id);
+    moan("skipping %"PRIuSEC" bad sectors (%"PRIuSEC" .. %"PRIuSEC"; "
+        "`%s' %"PRIuSEC" .. %"PRIuSEC" of %"PRIuSEC")",
+        bad_hi - bad_lo, bad_lo, bad_hi,
+        fn, bad_lo - file->start, bad_hi - file->start,
+        file->end - file->start);
   }
+
+  record_bad_sectors(bad_lo, bad_hi);
+
   if (lseek(outfd, (off_t)(bad_hi - bad_lo)*SECTORSZ, SEEK_CUR) < 0)
     bail_syserr(errno, "failed to seek past bad sectors");
+
+  progress_removeitem(&progress, &badblock_progress);
+  progress_update(&progress);
 }
 
 struct recoverybuf {
   unsigned char *buf;
   secaddr sz, pos, start, end;
+  secaddr good_lo, good_hi;
 };
 
 static void rearrange_sectors(struct recoverybuf *r,
@@ -544,14 +492,14 @@ static void rearrange_sectors(struct recoverybuf *r,
 }
 
 #ifdef DEBUG
-__attribute__((format(printf, 2, 3)))
-static void show_recovery_buffer_map(const struct recoverybuf *r,
-                                    const char *what, ...)
+static PRINTF_LIKE(2, 3)
+  void show_recovery_buffer_map(const struct recoverybuf *r,
+                               const char *what, ...)
 {
   va_list ap;
 
   va_start(ap, what);
-  debug_clear_progress();
+  progress_clear();
   printf(";; recovery buffer (");
   vprintf(what, ap);
   printf("): "
@@ -577,14 +525,14 @@ static ssize_t recovery_read_sectors(struct recoverybuf *r,
   return (n);
 }
 
-static ssize_t recovery_read(struct recoverybuf *r,
-                            secaddr pos, secaddr want)
+static ssize_t recovery_read_buffer(struct recoverybuf *r,
+                                   secaddr pos, secaddr want)
 {
   secaddr diff, pp, nn;
   ssize_t n;
 
 #ifdef DEBUG
-  debug_clear_progress();
+  progress_clear();
   show_recovery_buffer_map(r, "begin(%"PRIuSEC", %"PRIuSEC")", pos, want);
 #endif
 
@@ -606,7 +554,7 @@ static ssize_t recovery_read(struct recoverybuf *r,
   } else if (pos > r->pos + r->end) {
       r->pos = pos; r->start = r->end = 0;
 #ifdef DEBUG
-      show_recovery_buffer_map(r, "cleared; beyond previous region");
+p      show_recovery_buffer_map(r, "cleared; beyond previous region");
 #endif
   } else if (pos + want > r->pos + r->sz) {
     diff = (pos + want) - (r->pos + r->sz);
@@ -675,61 +623,126 @@ end:
   return (n);
 }
 
-static secaddr run_length_wanted(secaddr pos, secaddr badlen,
-                                secaddr sz, secaddr end)
+static ssize_t recovery_read_multiple(struct recoverybuf *r,
+                                     secaddr pos, secaddr want)
+{
+  ssize_t n;
+  secaddr skip, want0 = want;
+
+  while (want > r->sz) {
+    skip = want - r->sz;
+    n = recovery_read_buffer(r, pos + skip, r->sz);
+    if (n < r->sz) return (skip + (n >= 0 ? n : 0));
+    want -= r->sz;
+  }
+  n = recovery_read_buffer(r, pos, want);
+  if (n < 0 || n < want) return (n);
+  return (want0);
+}
+
+static ssize_t recovery_read(struct recoverybuf *r,
+                            secaddr pos, secaddr want)
+{
+  secaddr lo = pos, hi = pos + want, span;
+  ssize_t n;
+
+  if (hi < r->good_lo || lo > r->good_hi) {
+    n = recovery_read_multiple(r, lo, hi - lo);
+    if (n > 0) { r->good_lo = lo; r->good_hi = lo + n; }
+    return (n);
+  }
+
+  if (hi > r->good_hi) {
+    span = hi - r->good_hi;
+    n = recovery_read_multiple(r, r->good_hi, span);
+    if (n > 0) r->good_hi += n;
+    if (n < 0 || n < span) return (r->good_hi - lo);
+  }
+
+  if (lo < r->good_lo) {
+    span = r->good_lo - lo;
+    n = recovery_read_multiple(r, lo, span);
+    if (n == span) r->good_lo = lo;
+    else return (n);
+  }
+
+  n = r->good_hi - pos; if (n > want) n = want;
+  if (!n) { errno = EIO; n = -1; }
+  return (n);
+}
+
+static double clear_factor = 1.5;
+static secaddr clear_min = 1, clear_max = SECLIMIT;
+static double step_factor = 2.0;
+static secaddr step_min = 1, step_max = 0;
+
+static secaddr run_length_wanted(secaddr pos, secaddr badlen, secaddr end)
 {
   secaddr want;
 
-  want = 3*badlen/2;
-  if (!want) want = 1;
+  want = clear_factor*badlen;
+  if (want < clear_min) want = clear_min;
   if (want > end - pos) want = end - pos;
-  if (want > sz) want = sz;
+  if (clear_max && want > clear_max) want = clear_max;
   return (want);
 }
 
+static void report_bad_blocks_progress(secaddr bad_hi, int err)
+  { bad_err = err; report_progress(bad_hi); }
+
 static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
                                unsigned char *buf, secaddr sz)
 {
-  int i;
   secaddr pos = *pos_inout, bad_lo, bad_hi, good, step, want;
   struct recoverybuf r;
   ssize_t n;
 
+  bad_start = pos; bad_err = errno;
+  badblock_progress.render = render_badblock_progress;
+  progress_additem(&progress, &badblock_progress);
+
   r.buf = buf; r.sz = sz; r.pos = r.start = r.end = 0;
-  report_bad_blocks_progress(pos, pos, errno);
+  r.good_lo = r.good_hi = 0;
 
   want = sz; if (want > end - pos) want = end - pos;
-  for (i = 0; i < 4; i++) {
+  for (retry = 0; retry < max_retries; retry++) {
+    report_bad_blocks_progress(pos, errno);
     n = recovery_read(&r, pos, want);
 #ifdef DEBUG
-    debug_clear_progress();
+    progress_clear(&progress);
     printf(";; [retry] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
           pos, pos + want, n);
 #endif
     if (n > 0) {
-      clear_progress();
+      progress_clear(&progress);
       moan("sector %"PRIuSEC" read ok after retry", pos);
+      progress_removeitem(&progress, &badblock_progress);
+      progress_update(&progress);
       return (n);
     }
   }
 
   bad_lo = pos; bad_hi = pos + 1;
   for (;;) {
-    report_bad_blocks_progress(bad_lo, bad_hi, errno);
+    report_bad_blocks_progress(bad_hi, errno);
 #ifdef DEBUG
-    debug_clear_progress();
+    progress_clear(&progress);
     printf(";; bounding bad-block region: "
           "%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"\n",
           bad_lo, bad_hi - bad_lo, bad_hi);
 #endif
     if (bad_hi >= end) {
-      clear_progress();
+      progress_clear(&progress);
       moan("giving up on this extent");
-      recovered(bad_lo, end); *pos_inout = end; return (0);
+      recovered(bad_lo, end); *pos_inout = end;
+      return (0);
     }
-    step = 2*(bad_hi - bad_lo); if (step > end - bad_lo) step = end - bad_lo;
+    step = step_factor*(bad_hi - bad_lo);
+    if (step < step_min) step = step_min;
+    if (step_max && step > step_max) step = step_max;
+    if (step > end - bad_lo) step = end - bad_lo;
     pos = bad_lo + step - 1;
-    want = run_length_wanted(pos, step, sz, end);
+    want = run_length_wanted(pos, step, end);
     n = recovery_read(&r, pos, want);
 #ifdef DEBUG
     printf(";; [bound] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
@@ -742,16 +755,16 @@ static ssize_t find_good_sector(secaddr *pos_inout, secaddr end,
 
   good = pos;
   while (good > bad_hi) {
-    report_bad_blocks_progress(bad_lo, bad_hi, errno);
+    report_bad_blocks_progress(bad_hi, errno);
 #ifdef DEBUG
-    debug_clear_progress();
+    progress_clear(&progress);
     printf(";; limiting bad-block region: "
           "%"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC" ..%"PRIuSEC".. %"PRIuSEC"\n",
           bad_lo, bad_hi - bad_lo, bad_hi, good - bad_hi, good);
 #endif
     pos = bad_hi + (good - bad_hi)/2;
     step = pos - bad_lo;
-    want = run_length_wanted(pos, step, sz, end);
+    want = run_length_wanted(pos, step, end);
     n = recovery_read(&r, pos, want);
 #ifdef DEBUG
     printf(";; [limit] try reading %"PRIuSEC" .. %"PRIuSEC" -> %zd\n",
@@ -817,7 +830,7 @@ static void emit(secaddr start, secaddr end)
        vob = 0;
        break;
       case VOB:
-       if (first_time) { clear_progress(); first_time = 0; }
+       if (first_time) { progress_clear(&progress); first_time = 0; }
        vob = DVDOpenFile(dvd, id_title(file->id),
                          id_part(file->id)
                            ? DVD_READ_TITLE_VOBS
@@ -826,12 +839,18 @@ static void emit(secaddr start, secaddr end)
          bail("failed to open %s %u",
               id_part(file->id) ? "title" : "menu",
               id_title(file->id));
+       progress_update(&progress);
        break;
       default:
        abort();
     }
   }
 
+  if (file && id_kind(file->id) != RAW) {
+    file_progress.render = render_file_progress;
+    progress_additem(&progress, &file_progress);
+  }
+
   pos = start;
   while (pos < end) {
     want = end - pos; if (want > BUFSECTORS) want = BUFSECTORS;
@@ -839,33 +858,17 @@ static void emit(secaddr start, secaddr end)
 
     if (n <= 0) n = find_good_sector(&pos, end, buf, BUFSECTORS);
     if (n > 0) { carefully_write(outfd, buf, n*SECTORSZ); pos += n; }
-    report_progress(pos); fflush(stdout);
+    report_progress(pos);
   }
 
   if (vob) { DVDCloseFile(vob); vob = 0; }
 
-#undef BUFSECTORS
-}
+  if (file && id_kind(file->id) != RAW)
+    progress_removeitem(&progress, &file_progress);
+  progress_update(&progress);
 
-#ifdef notdef
-static void logfn(void *p, dvd_logger_level_t lev,
-                 const char *fmt, va_list ap)
-{
-  switch (lev) {
-    case DVD_LOGGER_LEVEL_ERROR:
-      fprintf("%s (libdvdread error): ", prog);
-      break;
-    case DVD_LOGGER_LEVEL_WARN:
-      fprintf("%s (libdvdread warning): ", prog);
-      break;
-    default:
-      return;
-  }
-  vfprintf(stderr, fmt, ap);
-  fputc('\n', stderr);
+#undef BUFSECTORS
 }
-static const dvd_logger_cb logger = { logfn };
-#endif
 
 struct buf {
   char *p;
@@ -902,6 +905,38 @@ static int read_line(FILE *fp, struct buf *b)
   return (0);
 }
 
+static double parse_float(const char **p_inout, double min, double max,
+                         const char *what)
+{
+  const char *p;
+  char *q;
+  double x;
+  int err;
+
+  err = errno; errno = 0;
+  p = *p_inout;
+  x = strtod(p, &q);
+  if (errno || x < min || x > max) bail("bad %s `%s'", what, p);
+  *p_inout = q; errno = err;
+  return (x);
+}
+
+static long parse_int(const char **p_inout, long min, long max,
+                     const char *what)
+{
+  const char *p;
+  char *q;
+  long x;
+  int err;
+
+  err = errno; errno = 0;
+  p = *p_inout;
+  x = strtoul(p, &q, 0);
+  if (errno || x < min || x > max) bail("bad %s `%s'", what, p);
+  *p_inout = q; errno = err;
+  return (x);
+}
+
 #define PRF_HYPHEN 1u
 static int parse_range(const char *p, unsigned f,
                       secaddr *start_out, secaddr *end_out)
@@ -948,7 +983,7 @@ end:
 int main(int argc, char *argv[])
 {
   unsigned f = 0;
-  char *p;
+  const char *p;
   uint64_t volsz;
   secaddr pos;
   off_t off;
@@ -961,6 +996,10 @@ int main(int argc, char *argv[])
   size_t i;
   FILE *fp;
   struct buf buf = BUF_INIT;
+  struct timeval tv0, tv1;
+  double t, rate, tot;
+  const char *rateunit, *totunit;
+  char timebuf[TIMESTRMAX];
   struct stat st;
 #ifdef DEBUG
   const struct file *file;
@@ -970,13 +1009,45 @@ int main(int argc, char *argv[])
 #define f_bogus 1u
 #define f_continue 2u
 #define f_fixup 4u
+#define f_stats 8u
 #define f_write 256u
 
-  p = strrchr(argv[0], '/'); prog = p ? p + 1 : argv[0];
+  set_prog(argv[0]);
   for (;;) {
-    opt = getopt(argc, argv, "hE:FR:X:b:cr:"); if (opt < 0) break;
+    opt = getopt(argc, argv, "hB:E:FR:X:b:cr:s"); if (opt < 0) break;
     switch (opt) {
       case 'h': usage(stderr); exit(0);
+      case 'B':
+       p = optarg;
+#define SKIP_PREFIX(s)                                                 \
+       (STRNCMP(p, ==, s "=", sizeof(s)) && (p += sizeof(s), 1))
+       for (;;) {
+         if (SKIP_PREFIX("cf"))
+           clear_factor = parse_float(&p, 0, DBL_MAX, "clear factor");
+         else if (SKIP_PREFIX("cmin"))
+           clear_min = parse_int(&p, 1, SECLIMIT, "clear minimum");
+         else if (SKIP_PREFIX("cmax"))
+           clear_max = parse_int(&p, 1, SECLIMIT, "clear maximum");
+         else if (SKIP_PREFIX("sf"))
+           step_factor = parse_float(&p, 0, DBL_MAX, "step factor");
+         else if (SKIP_PREFIX("smin"))
+           step_min = parse_int(&p, 1, SECLIMIT - 1, "step minimum");
+         else if (SKIP_PREFIX("smax"))
+           step_max = parse_int(&p, 1, SECLIMIT - 1, "step maximum");
+         else if (SKIP_PREFIX("retry"))
+           max_retries = parse_int(&p, 0, INT_MAX, "retries");
+         else if (SKIP_PREFIX("_badwait"))
+           bad_block_delay = parse_float(&p, 0, DBL_MAX, "bad-block delay");
+         else if (SKIP_PREFIX("_blkwait"))
+           good_block_delay = parse_float(&p, 0, DBL_MAX, "good block delay");
+         else
+           bail("unknown bad blocks parameter `%s'", p);
+         if (!*p) break;
+         else if (*p != ',') bail("unexpected junk in parameters");
+         p++;
+       }
+#undef SKIP_PREFIX
+       break;
       case 'E': errfile = optarg; break;
       case 'F': f |= f_fixup; break;
       case 'R':
@@ -1038,12 +1109,15 @@ int main(int argc, char *argv[])
          if (end <= SECLIMIT) put_event(EV_STOP, 0, end);
        }
        break;
+      case 's': f |= f_stats; break;
       default: f |= f_bogus; break;
     }
   }
   if (argc - optind != 2) f |= f_bogus;
   if (f&f_bogus) { usage(stderr); exit(2); }
 
+  setlocale(LC_ALL, "");
+  progress_init(&progress);
   device = argv[optind]; outfile = argv[optind + 1];
 
   if (badblocks.n) {
@@ -1057,9 +1131,7 @@ int main(int argc, char *argv[])
 #endif
   }
 
-  dvdfd = open(device, O_RDONLY);
-  if (dvdfd < 0)
-    bail_syserr(errno, "failed to open device `%s'", device);
+  open_dvd(device, &dvdfd, &dvd);
   if (fstat(dvdfd, &st))
     bail_syserr(errno, "failed to stat device `%s'", device);
   if (S_ISREG(st.st_mode)) {
@@ -1079,11 +1151,9 @@ int main(int argc, char *argv[])
     bail("device `%s' volume size %"PRIu64" not a multiple of %d",
         device, volsz, SECTORSZ);
 
-  if (outfile) {
-    outfd = open(outfile, O_WRONLY | O_CREAT, 0666);
-    if (outfd < 0)
-      bail_syserr(errno, "failed to create output file `%s'", outfile);
-  }
+  outfd = open(outfile, O_WRONLY | O_CREAT, 0666);
+  if (outfd < 0)
+    bail_syserr(errno, "failed to create output file `%s'", outfile);
 
   if (f&f_continue) {
     off = lseek(outfd, 0, SEEK_END);
@@ -1094,13 +1164,6 @@ int main(int argc, char *argv[])
   } else if (!eventq.n && !(f&f_fixup))
     put_event(EV_WRITE, 0, 0);
 
-#ifdef notdef
-  dvd = DVDOpen2(0, &logger, device);
-#else
-  dvd = DVDOpen(device);
-#endif
-  if (!dvd) bail("failed to open DVD on `%s'", device);
-
   /* It's fast enough just to check everything. */
   put_menu(dvd, 0);
   for (i = 1; i < 100; i++) {
@@ -1147,7 +1210,8 @@ int main(int argc, char *argv[])
     ev = &eventq.v[i];
     switch (ev->ev) {
       case EV_WRITE: start = ev->pos; f |= f_write; break;
-      case EV_STOP: nsectors += ev->pos - start; f &= ~f_write; break;
+      case EV_STOP:
+       nsectors += ev->pos - start; f &= ~f_write; break;
     }
     if (ev->pos >= limit) break;
     if (f&f_fixup) start = ev->pos;
@@ -1161,19 +1225,27 @@ int main(int argc, char *argv[])
     nsectors += limit - start;
     put_event(EV_STOP, 0, limit);
   }
-  if (n == 1 && (f&f_write)) flags |= F_ALLPROGRESS;
-  f &= ~f_write;
+
+  copy_progress.render = render_copy_progress;
+  progress_additem(&progress, &copy_progress);
+  if (nsectors != limit) {
+    disc_progress.render = render_disc_progress;
+    progress_additem(&progress, &disc_progress);
+  }
+
+  if (f&f_stats) gettimeofday(&tv0, 0);
 
 #ifdef DEBUG
   printf("\n;; event sweep:\n");
 #endif
+  f &= ~f_write;
   for (pos = 0, i = 0; i < eventq.n; i++) {
     ev = &eventq.v[i];
     if (ev->pos > pos) {
       if (f&f_write) emit(pos, ev->pos);
       pos = ev->pos;
 #ifdef DEBUG
-      debug_clear_progress();
+      progress_clear(&progress);
       printf(";;\n");
 #endif
     }
@@ -1182,7 +1254,7 @@ int main(int argc, char *argv[])
        set_live(ev->file);
 #ifdef DEBUG
        store_filename(fn, filetab.v[ev->file].id);
-       debug_clear_progress();
+       progress_clear(&progress);
        printf(";; %8"PRIuSEC": begin `%s'\n", pos, fn);
 #endif
        break;
@@ -1194,7 +1266,7 @@ int main(int argc, char *argv[])
                      "(sector %"PRIuSEC") in output file `%s'",
                      ev->pos, outfile);
 #ifdef DEBUG
-       debug_clear_progress();
+       progress_clear(&progress);
        printf(";; %8"PRIuSEC": begin write\n", pos);
 #endif
        f |= f_write;
@@ -1202,7 +1274,7 @@ int main(int argc, char *argv[])
       case EV_STOP:
        f &= ~f_write;
 #ifdef DEBUG
-       debug_clear_progress();
+       progress_clear(&progress);
        printf(";; %8"PRIuSEC": end write\n", pos);
 #endif
        break;
@@ -1210,7 +1282,7 @@ int main(int argc, char *argv[])
        clear_live(ev->file);
 #ifdef DEBUG
        store_filename(fn, filetab.v[ev->file].id);
-       debug_clear_progress();
+       progress_clear(&progress);
        printf(";; %8"PRIuSEC": end `%s'\n", pos, fn);
 #endif
        break;
@@ -1218,20 +1290,30 @@ int main(int argc, char *argv[])
     }
   }
 
-  if (progresslen) putchar('\n');
+  progress_clear(&progress);
 
   if (ftruncate(outfd, (off_t)limit*SECTORSZ) < 0)
     bail_syserr(errno, "failed to set output file `%s' length", outfile);
 
+  if (f&f_stats) {
+    gettimeofday(&tv1, 0); t = tvdiff(&tv0, &tv1);
+    tot = scale_bytes((double)nsectors*SECTORSZ, &totunit);
+    rate = scale_bytes((double)nsectors*SECTORSZ/t, &rateunit);
+    moan("all done: %.1f %sB in %s -- %.1f %sB/s",
+        tot, totunit, fmttime(t, timebuf), rate, rateunit);
+  }
+
   if (dvd) DVDClose(dvd);
   if (dvdfd >= 0) close(dvdfd);
   if (outfd >= 0) close(outfd);
   carefully_fclose(mapfp, "bad-sector region map");
   carefully_fclose(errfp, "bad-sector error log");
+  progress_free(&progress);
 
 #undef f_bogus
 #undef f_continue
 #undef f_fixup
+#undef f_stats
 #undef f_write
 
   return (status);
diff --git a/dvdrip b/dvdrip
index 3dc10539414c10ee76af78635e3286b447a7e137..a4b5900accde3011caf438c8440bf9dbbf5a7fdd 100755 (executable)
--- a/dvdrip
+++ b/dvdrip
@@ -6,17 +6,20 @@ tmp=${DVDRIP_TMPDIR-${HOME?}/tmp/dvdrip}
 archive=${DVDRIP_ARCHIVE-jem.distorted.org.uk:/mnt/dvd/archive}
 : ${DVD_SECTOR_COPY=dvd-sector-copy}
 : ${DVDRIP_UPLOAD=dvdrip-upload}
-backup=nil eject=nil force=nil retry=nil verbose=nil bogus=nil
+backup=nil ding=nil eject=nil force=nil retry=nil verbose=nil bogus=nil
+unset params
 usage () {
   cat <<EOF
-usage: $prog [-efrv] [-D DEV] [-a ARCH] [-t TMP] TITLE
+usage: $prog [-defrv] [-D DEV] [-a ARCH] [-t TMP] TITLE
 EOF
 }
-while getopts "hD:a:efrt:v" opt; do
+while getopts "hB:D:a:defrt:v" opt; do
   case $opt in
     h) usage; exit 0 ;;
+    B) params=${params+$params,}$OPTARG ;;
     D) dev=$OPTARG ;;
     a) archive=$OPTARG ;;
+    d) ding=t ;;
     e) eject=t ;;
     f) force=t ;;
     r) retry=t ;;
@@ -72,6 +75,26 @@ case $rc,$force in
   0,t) warn "output file already exists; will overwrite" ;;
 esac
 
+mkdir -p "$tmp/$tag"
+
+discid=$(dvd-id "$dev")
+if [ -f "$tmp/$tag/discid" ]; then
+  read oldid <"$tmp/$tag/discid"
+  case $force,$oldid in
+    t,"$discid" | nil,"$discid")
+      ;;
+    nil,*)
+      fail "discid mismatch: expected \`$oldid' but drive has \`$discid'"
+      ;;
+    t,*)
+      warn "discid mismatch: expected \`$oldid' but drive has \`$discid'; continuing anway"
+      ;;
+  esac
+fi
+info "copying \`$discid'"
+echo "$discid" >"$tmp/$tag/discid.new"
+mv "$tmp/$tag/discid.new" "$tmp/$tag/discid"
+
 accumulate_badblocks () {
   if [ -f "$tmp/$tag/badblocks.new" ]; then
     if [ ! -f "$tmp/$tag/badblocks" ]; then
@@ -83,7 +106,6 @@ accumulate_badblocks () {
 }
 
 set --
-mkdir -p "$tmp/$tag"
 any=nil
 for i in "$tmp/$tag/dest.new" "$tmp/$tag/dest" "$tmp/$tag/dest.seen"; do
   if [ -f "$tmp/$tag/dest.new" ]; then any=t; fi
@@ -91,6 +113,14 @@ done
 case $any in
   nil) printf "%s\n" "$title.iso" >"$tmp/$tag/dest.new" ;;
 esac
+case $eject in
+  t) touch "$tmp/$tag/eject" ;;
+  nil) rm -f "$tmp/$tag/eject" ;;
+esac
+case $ding in
+  t) touch "$tmp/$tag/ding" ;;
+  nil) rm -f "$tmp/$tag/ding" ;;
+esac
 
 accumulate_badblocks
 case $retry in
@@ -109,8 +139,8 @@ case $retry in
     ;;
 esac
 if [ ! -f "$tmp/$tag/iso" ]; then
-  run "$DVD_SECTOR_COPY" -cs -b"$tmp/$tag/badblocks.new" "$@" \
-      "$dev" "$tmp/$tag/iso.new"
+  run "$DVD_SECTOR_COPY" -cs ${params+"-B$params"} \
+      -b"$tmp/$tag/badblocks.new" "$@" "$dev" "$tmp/$tag/iso.new"
   run mv "$tmp/$tag/iso.new" "$tmp/$tag/iso"
   accumulate_badblocks
   case $retry in t) rm -f "$tmp/$tag/badblocks.retry" ;; esac
@@ -120,6 +150,17 @@ if [ ! -f "$tmp/$tag/iso" ]; then
 fi
 
 run mv "$tmp/$tag/dest.new" "$tmp/$tag/dest"
+if [ -f "$tmp/$tag/eject" ]; then eject=t; else eject=nil; fi
+if [ -f "$tmp/$tag/ding" ]; then ding=t; else ding=nil; fi
 run "$DVDRIP_UPLOAD"
 case $eject in t) run eject "$dev" ;; esac
-printf "\a"
+case $ding in
+  t)
+    if [ -t 1 ]; then exec 3>&1
+    elif [ -t 2 ]; then exec 3>&2
+    else exec 3>/dev/tty
+    fi
+    printf "\a" >&3
+    exec 3>&-
+    ;;
+esac
diff --git a/lib.c b/lib.c
new file mode 100644 (file)
index 0000000..1c815d0
--- /dev/null
+++ b/lib.c
@@ -0,0 +1,150 @@
+#include "lib.h"
+
+const char *prog = "<unset>";
+
+void set_prog(const char *p)
+  { const char *q = strrchr(p, '/'); prog = q ? q + 1 : p; }
+
+void vmoan(const char *fmt, va_list ap)
+  { fprintf(stderr, "%s: ", prog); vfprintf(stderr, fmt, ap); }
+
+__attribute__((format(printf, 1, 2)))
+void moan(const char *fmt, ...)
+{
+  va_list ap;
+
+  va_start(ap, fmt); vmoan(fmt, ap); va_end(ap);
+  fputc('\n', stderr);
+}
+
+__attribute__((noreturn, format(printf, 1, 2)))
+void bail(const char *fmt, ...)
+{
+  va_list ap;
+
+  va_start(ap, fmt); vmoan(fmt, ap); va_end(ap);
+  fputc('\n', stderr);
+  exit(2);
+}
+
+__attribute__((noreturn, format(printf, 2, 3)))
+void bail_syserr(int err, const char *fmt, ...)
+{
+  va_list ap;
+
+  va_start(ap, fmt); vmoan(fmt, ap); va_end(ap);
+  if (err) fprintf(stderr, ": %s", strerror(errno));
+  fputc('\n', stderr);
+  exit(2);
+}
+
+void sit(double t)
+{
+  struct timeval tv;
+  double whole = floor(t);
+
+  if (t) {
+    tv.tv_sec = whole; tv.tv_usec = floor((t - whole)*1.0e6) + 1;
+    if (select(0, 0, 0, 0, &tv) < 0) bail_syserr(errno, "failed to sleep");
+  }
+}
+
+void store_filename(char *buf, ident id)
+{
+  switch (id_kind(id)) {
+    case RAW:
+      sprintf(buf, "#<raw device>");
+      break;
+    case IFO:
+      if (!id_title(id)) sprintf(buf, "/VIDEO_TS/VIDEO_TS.IFO");
+      else sprintf(buf, "/VIDEO_TS/VTS_%02u_0.IFO", id_title(id));
+      break;
+    case BUP:
+      if (!id_title(id)) sprintf(buf, "/VIDEO_TS/VIDEO_TS.BUP");
+      else sprintf(buf, "/VIDEO_TS/VTS_%02u_0.BUP", id_title(id));
+      break;
+    case VOB:
+      if (!id_title(id)) sprintf(buf, "/VIDEO_TS/VIDEO_TS.VOB");
+      else
+       sprintf(buf, "/VIDEO_TS/VTS_%02u_%u.VOB", id_title(id), id_part(id));
+      break;
+    default:
+      abort();
+  }
+}
+
+struct progress_state progress = PROGRESS_STATE_INIT;
+static struct banner_progress_item banner_progress;
+
+static void render_banner_progress(struct progress_item *item,
+                           struct progress_render_state *render)
+{
+  struct banner_progress_item *bi = (struct banner_progress_item *)item;
+
+  progress_putleft(render, " %s", bi->msg);
+  progress_shownotice(render, 4, 7);
+}
+
+void show_banner(const char *msg)
+{
+  banner_progress._base.render = render_banner_progress;
+  progress_additem(&progress, &banner_progress._base);
+  banner_progress.msg = msg;
+  progress_update(&progress);
+}
+
+void hide_banner(void)
+{
+  if (!progress_removeitem(&progress, &banner_progress._base))
+    progress_update(&progress);
+}
+
+#ifdef notdef
+static void logfn(void *p, dvd_logger_level_t lev,
+                 const char *fmt, va_list ap)
+{
+  switch (lev) {
+    case DVD_LOGGER_LEVEL_ERROR:
+      fprintf("%s (libdvdread error): ", prog);
+      break;
+    case DVD_LOGGER_LEVEL_WARN:
+      fprintf("%s (libdvdread warning): ", prog);
+      break;
+    default:
+      return;
+  }
+  vfprintf(stderr, fmt, ap);
+  fputc('\n', stderr);
+}
+static const dvd_logger_cb logger = { logfn };
+#endif
+
+void open_dvd(const char *device, int *fd_out, dvd_reader_t **dvd_out)
+{
+  int fd;
+  dvd_reader_t *dvd;
+  int bannerp = 0;
+
+  for (;;) {
+    fd = open(device, O_RDONLY);
+    if (fd >= 0 || errno != ENOMEDIUM) break;
+    if (!bannerp) {
+      show_banner("Waiting for disc to be inserted...");
+      bannerp = 1;
+    }
+    sit(0.2);
+  }
+  if (bannerp) hide_banner();
+  if (fd < 0) bail_syserr(errno, "failed to open device `%s'", device);
+  if (dvd_out) {
+#ifdef notdef
+    dvd = DVDOpen2(0, &logger, device);
+#else
+    dvd = DVDOpen(device);
+#endif
+    if (!dvd) bail("failed to open DVD on `%s'", device);
+    *dvd_out = dvd;
+  }
+  if (fd_out) *fd_out = fd;
+  else close(fd);
+}
diff --git a/lib.h b/lib.h
new file mode 100644 (file)
index 0000000..1f08848
--- /dev/null
+++ b/lib.h
@@ -0,0 +1,100 @@
+#ifndef LIB_H
+#define LIB_H
+
+#define _GNU_SOURCE
+#define _FILE_OFFSET_BITS 64
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <float.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <locale.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <getopt.h>
+
+#include <linux/fs.h>
+
+#include <dvdread/dvd_reader.h>
+#include <dvdread/dvd_udf.h>
+#include <dvdread/ifo_read.h>
+#include <dvdread/ifo_types.h>
+
+#include "multiprogress.h"
+
+#define CTYPE_HACK(fn, ch) fn((unsigned char)(ch))
+#define ISDIGIT(ch) CTYPE_HACK(isdigit, ch)
+#define ISSPACE(ch) CTYPE_HACK(isspace, ch)
+
+#define STRCMP(a, op, b) (strcmp((a), (b)) op 0)
+#define STRNCMP(a, op, b, n) (strncmp((a), (b), (n)) op 0)
+
+#ifdef DEBUG
+#  define D(x) x
+#else
+#  define D(x)
+#endif
+
+#define N(v) (sizeof(v)/sizeof((v)[0]))
+
+#define SECTORSZ 2048
+#define SECTORS(n) (((n) + (SECTORSZ - 1))/SECTORSZ)
+typedef uint_least32_t secaddr;
+#define PRIuSEC PRIuLEAST32
+#define SECLIMIT 0x00400000
+
+#define PRINTF_LIKE(fmt, dots) __attribute__((format(printf, fmt, dots)))
+#define NORETURN __attribute__((noreturn))
+
+extern const char *prog;
+
+extern void set_prog(const char *p);
+extern void vmoan(const char *fmt, va_list ap);
+extern PRINTF_LIKE(1, 2) void moan(const char *fmt, ...);
+extern PRINTF_LIKE(1, 2) NORETURN void bail(const char *fmt, ...);
+extern PRINTF_LIKE(2, 3) NORETURN
+  void bail_syserr(int err, const char *fmt, ...);
+
+extern void sit(double t);
+
+enum { RAW, IFO, VOB, BUP };
+typedef uint_least32_t ident;
+
+static inline ident mkident(unsigned kind, unsigned title, unsigned part)
+  { return (((ident)kind << 0) | ((ident)title << 8) | ((ident)part << 16)); }
+static inline unsigned id_kind(ident id) { return ((id >> 0)&0x0ff); }
+static inline unsigned id_title(ident id) { return ((id >> 8)&0x0ff); }
+static inline unsigned id_part(ident id) { return ((id >> 16)&0x0ff); }
+
+#define MAXFNSZ (1 + 8 + 1 + 12 + 1)
+extern void store_filename(char *buf, ident id);
+
+struct banner_progress_item {
+  struct progress_item _base;
+  const char *msg;
+};
+
+extern struct progress_state progress;
+
+extern void show_banner(const char *msg);
+extern void hide_banner(void);
+
+extern void open_dvd(const char *device,
+                    int *fd_out, dvd_reader_t **dvd_out);
+
+#endif
diff --git a/multiprogress.c b/multiprogress.c
new file mode 100644 (file)
index 0000000..efad38d
--- /dev/null
@@ -0,0 +1,587 @@
+#define _XOPEN_SOURCE
+
+#include <limits.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wchar.h>
+
+#include <unistd.h>
+#include <termios.h>
+#include <sys/ioctl.h>
+
+#if defined(USE_TERMINFO)
+#  include <curses.h>
+#  include <term.h>
+#elif defined(USE_TERMCAP)
+#  include <termcap.h>
+#endif
+
+#include "multiprogress.h"
+
+static FILE *dup_stream(int fd)
+{
+  FILE *fp;
+  int newfd;
+
+  newfd = dup(fd); if (newfd < 0) return (0);
+  fp = fdopen(newfd, "r+"); if (!fp) return (0);
+  return (fp);
+}
+
+int progress_init(struct progress_state *progress)
+{
+#ifdef USE_TERMCAP
+  char *term, *capcur;
+#endif
+#ifdef USE_TERMINFO
+  int err;
+#endif
+  struct progress_ttyinfo *tty;
+  const char *t;
+  int n;
+
+  tty = &progress->tty;
+  tty->fp = 0;
+  tty->termbuf = tty->capbuf = 0;
+  tty->cap.f = 0;
+  tty->cap.cr = tty->cap.up = tty->cap.ce = tty->cap.cd =
+    tty->cap.mr = tty->cap.md = tty->cap.me =
+    tty->cap.af = tty->cap.ab = tty->cap.op = 0;
+
+  progress->items = progress->end_item = 0;
+  progress->nitems = 0; progress->last_lines = 0;
+  progress->tv_update.tv_sec = 0; progress->tv_update.tv_usec = 0;
+
+  if (isatty(1)) tty->fp = dup_stream(1);
+  else if (isatty(2)) tty->fp = dup_stream(2);
+  else tty->fp = fopen("/dev/tty", "r+");
+  if (!tty->fp) return (-1);
+
+#define SETDIM(dim, var, getcap, dflt) do {                            \
+  t = getenv(var); if (t) { n = atoi(t); if (n) { tty->dim = n; break; } } \
+  n = getcap; if (n > 0) { tty->dim = n; break; }                      \
+  tty->dim = dflt;                                                     \
+} while (0)
+
+#if defined(USE_TERMINFO)
+
+  if (setupterm(0, fileno(tty->fp), &err) != OK || err < 1) return (-1);
+
+  tty->cap.cr = tigetstr("cr");
+  tty->cap.up = tigetstr("cuu1");
+  tty->cap.ce = tigetstr("el");
+  tty->cap.cd = tigetstr("ed");
+
+  if (tigetnum("xmc") < 1) {
+    tty->cap.mr = tigetstr("rev");
+    tty->cap.md = tigetstr("bold");
+    tty->cap.me = tigetstr("sgr0");
+
+    tty->cap.af = tigetstr("setaf");
+    tty->cap.ab = tigetstr("setab");
+    tty->cap.op = tigetstr("op");
+  }
+
+  if (tigetflag("bce") > 0) tty->cap.f |= TCF_BCE;
+
+  SETDIM(defwd, "COLUMNS", tigetnum("co"), 80);
+  SETDIM(defht, "LINES", tigetnum("li"), 25);
+
+#elif defined(USE_TERMCAP)
+
+  term = getenv("TERM"); if (!term) return (-1);
+  if (tgetent(tty->termbuf, term) < 1) return (-1);
+
+  tty->termbuf = malloc(4096); if (!tty->termbuf) return (-1);
+  tty->capbuf = malloc(4096); if (!tty->capbuf) return (-1);
+
+  capcur = tty->capbuf;
+  tty->cap.cr = tgetstr("cr", &capcur);
+  tty->cap.up = tgetstr("up", &capcur);
+  tty->cap.ce = tgetstr("ce", &capcur);
+  tty->cap.cd = tgetstr("cd", &capcur);
+
+  if (tgetnum("sg") < 1) {
+    tty->cap.mr = tgetstr("mr", &capcur);
+    tty->cap.md = tgetstr("md", &capcur);
+    tty->cap.me = tgetstr("me", &capcur);
+
+    tty->cap.af = tgetstr("AF", &capcur);
+    tty->cap.ab = tgetstr("AB", &capcur);
+    tty->cap.op = tgetstr("op", &capcur);
+  }
+
+  if (tgetflag("ut") > 0) tty->cap.f |= TCF_BCE;
+
+  t = tgetstr("pc", &capcur); PC = t ? *t : 0;
+
+  SETDIM(defwd, "COLUMNS", tgetnum("co"), 80);
+  SETDIM(defht, "LINES", tgetnum("li"), 25);
+
+#else
+
+  SETDIM(defwd, "COLUMNS", -1, 80);
+  SETDIM(defht, "LINES", -1, 25);
+
+#endif
+
+#undef SETDIM
+
+  if (!tty->cap.cr || !tty->cap.up || !tty->cap.ce || !tty->cap.cd)
+    { fclose(tty->fp); tty->fp = 0; return (-1); }
+  if (!tty->cap.af || !tty->cap.ab || !tty->cap.op) tty->cap.op = 0;
+  if (!tty->cap.me) tty->cap.mr = tty->cap.md = 0;
+  return (0);
+}
+
+void progress_free(struct progress_state *progress)
+{
+  struct progress_ttyinfo *tty = &progress->tty;
+
+  if (tty->fp) { fclose(tty->fp); tty->fp = 0; }
+  free(tty->termbuf); free(tty->capbuf); tty->termbuf = tty->capbuf = 0;
+}
+
+#if defined(USE_TERMINFO)
+static const struct progress_ttyinfo *curtty = 0;
+static int putty(int ch) { return (putc(ch, curtty->fp)); }
+static void put_sequence(const struct progress_ttyinfo *tty,
+                        const char *p, unsigned nlines)
+  { if (p) { curtty = tty; tputs(p, nlines, putty); } }
+static void set_fgcolour(const struct progress_ttyinfo *tty, int colour)
+  { put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); }
+static void set_bgcolour(const struct progress_ttyinfo *tty, int colour)
+  { put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); }
+#elif defined(USE_TERMCAP)
+static const struct progress_ttyinfo *curtty = 0;
+static int putty(int ch) { return (putc(ch, curtty->fp)); }
+static void put_sequence(const struct progress_ttyinfo *tty,
+                        const char *p, unsigned nlines)
+  { if (p) { curtty = tty; tputs(p, nlines, putty); } }
+static void set_fgcolour(const struct progress_ttyinfo *tty, int colour)
+  { put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); }
+static void set_bgcolour(const struct progress_ttyinfo *tty, int colour)
+  { put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); }
+#else
+static void put_sequence(const struct progress_ttyinfo *tty,
+                        const char *p, unsigned nlines) { ; }
+static void set_fgcolour(const struct progress_ttyinfo *tty, int colour)
+  { ; }
+static void set_bgcolour(const struct progress_ttyinfo *tty, int colour)
+  { ; }
+#endif
+
+#define CLRF_ALL 1u
+static int clear_progress(struct progress_state *progress,
+                         struct progress_render_state *render, unsigned f)
+{
+  const struct progress_ttyinfo *tty = &progress->tty;
+  unsigned ndel, nleave;
+  unsigned i;
+
+  if (!tty->fp) return (-1);
+
+  put_sequence(tty, tty->cap.cr, 1);
+  if (progress->last_lines) {
+    if (f&CLRF_ALL)
+      { ndel = progress->last_lines; nleave = 0; }
+    else {
+      if (progress->nitems >= progress->last_lines) ndel = 0;
+      else ndel = progress->last_lines - progress->nitems;
+      nleave = progress->last_lines - ndel;
+    }
+    if (!ndel)
+      for (i = 0; i < nleave - 1; i++) put_sequence(tty, tty->cap.up, 1);
+    else {
+      for (i = 0; i < ndel - 1; i++) put_sequence(tty, tty->cap.up, 1);
+      put_sequence(tty, tty->cap.cd, ndel);
+      for (i = 0; i < nleave; i++) put_sequence(tty, tty->cap.up, 1);
+    }
+  }
+  progress->last_lines = 0;
+  if (ferror(tty->fp)) return (-1);
+  return (0);
+}
+
+static int grow_linebuf(struct progress_render_state *render, size_t want)
+{
+  char *newbuf; size_t newsz;
+
+  if (want <= render->linesz) return (0);
+  if (!render->linesz) newsz = 4*render->width + 1;
+  else newsz = render->linesz;
+  while (newsz < want) newsz *= 2;
+  newbuf = malloc(newsz + 1); if (!newbuf) return (-1);
+  newbuf[newsz] = 0;
+  if (render->leftsz)
+    memcpy(newbuf, render->linebuf, render->leftsz);
+  if (render->rightsz)
+    memcpy(newbuf + newsz - render->rightsz,
+          render->linebuf + render->linesz - render->rightsz,
+          render->rightsz);
+  free(render->linebuf); render->linebuf = newbuf; render->linesz = newsz;
+  return (0);
+}
+
+static int grow_tempbuf(struct progress_render_state *render, size_t want)
+{
+  char *newbuf; size_t newsz;
+
+  if (want <= render->tempsz) return (0);
+  if (!render->tempsz) newsz = 4*render->width + 1;
+  else newsz = render->tempsz;
+  while (newsz < want) newsz *= 2;
+  newbuf = malloc(newsz + 1); if (!newbuf) return (-1);
+  newbuf[newsz] = 0;
+  if (render->tempsz) memcpy(newbuf, render->tempbuf, render->tempsz);
+  free(render->tempbuf); render->tempbuf = newbuf; render->tempsz = newsz;
+  return (0);
+}
+
+static int setup_render_state(struct progress_state *progress,
+                             struct progress_render_state *render)
+{
+  const struct progress_ttyinfo *tty = &progress->tty;
+  struct winsize wsz;
+  int rc = 0;
+
+  render->tty = tty;
+  render->linebuf = 0; render->linesz = 0;
+  render->tempbuf = 0; render->tempsz = 0;
+
+#ifdef USE_TERMCAP
+  render->old_bc = BC; BC = 0;
+  render->old_up = UP; UP = 0;
+#endif
+
+  if (!ioctl(fileno(tty->fp), TIOCGWINSZ, &wsz))
+    { render->width = wsz.ws_col; render->height = wsz.ws_row; }
+  else
+    { render->width = tty->defwd; render->height = tty->defht; rc = -1; }
+
+  if (render->width && !tty->cap.op && !tty->cap.mr) render->width--;
+
+  return (rc);
+}
+
+static void free_render_state(struct progress_render_state *render)
+{
+  fflush(render->tty->fp);
+  free(render->linebuf); render->linebuf = 0; render->linesz = 0;
+  free(render->tempbuf); render->tempbuf = 0; render->tempsz = 0;
+#ifdef USE_TERMCAP
+  UP = render->old_up;
+  BC = render->old_bc;
+#endif
+}
+
+#define CONV_MORE ((size_t)-2)
+#define CONV_BAD ((size_t)-1)
+
+struct measure {
+  mbstate_t ps;
+  const char *p; size_t i, sz;
+  unsigned wd;
+};
+
+static void init_measure(struct measure *m, const char *p, size_t sz)
+{
+  m->p = p; m->sz = sz; m->i = 0; m->wd = 0;
+  memset(&m->ps, 0, sizeof(m->ps));
+}
+
+static int advance_measure(struct measure *m)
+{
+  wchar_t wch;
+  unsigned chwd;
+  size_t n;
+
+  n = mbrtowc(&wch, m->p + m->i, m->sz - m->i, &m->ps);
+  if (!n) { chwd = 0; n = m->sz - m->i; }
+  else if (n == CONV_MORE) { chwd = 2; n = m->sz - m->i; }
+  else if (n == CONV_BAD) { chwd = 2; n = 1; }
+  else chwd = wcwidth(wch);
+
+  m->i += n; m->wd += chwd;
+  return (m->i < m->sz);
+}
+
+static unsigned string_width(const char *p, size_t sz)
+{
+  struct measure m;
+
+  init_measure(&m, p, sz);
+  while (advance_measure(&m));
+  return (m.wd);
+}
+
+static size_t split_string(const char *p, size_t sz,
+                          unsigned *wd_out, unsigned maxwd)
+{
+  struct measure m;
+  size_t lasti; unsigned lastwd;
+  int more;
+
+  init_measure(&m, p, sz);
+  for (;;) {
+    lasti = m.i; lastwd = m.wd;
+    more = advance_measure(&m);
+    if (m.wd > maxwd) { *wd_out = lastwd; return (lasti); }
+    else if (!more) { *wd_out = m.wd; return (sz); }
+  }
+}
+
+enum { LEFT, RIGHT };
+static int putstr(struct progress_render_state *render, unsigned side,
+                 const char *p, size_t n)
+{
+  unsigned newwd = string_width(p, n);
+  size_t want;
+
+  if (newwd >= render->width - render->leftwd - render->rightwd) return (-1);
+  want = render->leftsz + render->rightsz + n;
+  if (want > render->linesz && grow_linebuf(render, want)) return (-1);
+  switch (side) {
+    case LEFT:
+      memcpy(render->linebuf + render->leftsz, p, n);
+      render->leftsz += n; render->leftwd += newwd;
+      break;
+    case RIGHT:
+      memcpy(render->linebuf + render->linesz - render->rightsz - n, p, n);
+      render->rightsz += n; render->rightwd += newwd;
+      break;
+    default:
+      return (-1);
+  }
+  return (0);
+}
+
+static int vputf(struct progress_render_state *render, unsigned side,
+                const char *fmt, va_list ap)
+{
+  va_list bp;
+  int rc;
+
+  if (!render->tempsz && grow_tempbuf(render, 2*strlen(fmt))) return (-1);
+  for (;;) {
+    va_copy(bp, ap);
+    rc = vsnprintf(render->tempbuf, render->tempsz, fmt, bp);
+    va_end(bp);
+    if (rc < 0) return (-1);
+    if (rc <= render->tempsz) break;
+    if (grow_tempbuf(render, 2*(rc + 1))) return (-1);
+  }
+  if (putstr(render, side, render->tempbuf, rc)) return (-1);
+  return (0);
+}
+
+int progress_vputleft(struct progress_render_state *render,
+                     const char *fmt, va_list ap)
+  { return (vputf(render, LEFT, fmt, ap)); }
+
+int progress_vputright(struct progress_render_state *render,
+                      const char *fmt, va_list ap)
+  { return (vputf(render, RIGHT, fmt, ap)); }
+
+int progress_putleft(struct progress_render_state *render,
+                    const char *fmt, ...)
+{
+  va_list ap;
+  int rc;
+
+  va_start(ap, fmt); rc = vputf(render, LEFT, fmt, ap); va_end(ap);
+  return (rc);
+}
+
+int progress_putright(struct progress_render_state *render,
+                     const char *fmt, ...)
+{
+  va_list ap;
+  int rc;
+
+  va_start(ap, fmt); rc = vputf(render, RIGHT, fmt, ap); va_end(ap);
+  return (rc);
+}
+
+enum {
+  LEFT_COLOUR,
+  LEFT_MONO,
+  LEFT_SIMPLE,
+  RIGHT_ANY,
+  STOP
+};
+
+struct bar_state {
+  const struct progress_render_state *render;
+  unsigned pos, nextpos, state;
+};
+
+static void advance_bar_state(struct bar_state *bar)
+{
+  const struct progress_render_state *render = bar->render;
+  const struct progress_ttyinfo *tty = render->tty;
+  size_t here = bar->nextpos;
+
+  while (bar->nextpos == here) {
+    switch (bar->state) {
+      case LEFT_COLOUR: set_bgcolour(tty, 3); goto right;
+      case LEFT_MONO: put_sequence(tty, tty->cap.me, 1); goto right;
+      case LEFT_SIMPLE: putc('|', tty->fp); goto right;
+      right: bar->state = RIGHT_ANY; bar->nextpos = render->width; break;
+      case RIGHT_ANY: bar->state = STOP; bar->nextpos = UINT_MAX; break;
+    }
+  }
+}
+
+static void put_str(FILE *fp, const char *p, size_t sz)
+  { while (sz--) putc(*p++, fp); }
+static void put_spc(FILE *fp, unsigned n)
+  { while (n--) putc(' ', fp); }
+
+static void put_barstr(struct bar_state *bar, const char *p, size_t sz)
+{
+  unsigned wd;
+  size_t n;
+
+  for (;;) {
+    n = split_string(p, sz, &wd, bar->nextpos - bar->pos);
+    if (n == sz && wd < bar->nextpos - bar->pos) break;
+    put_str(bar->render->tty->fp, p, n); bar->pos += wd;
+    advance_bar_state(bar);
+    p += n; sz -= n;
+  }
+  put_str(bar->render->tty->fp, p, sz); bar->pos += wd;
+}
+
+static void put_barspc(struct bar_state *bar, unsigned n)
+{
+  unsigned step;
+
+  for (;;) {
+    step = bar->nextpos - bar->pos;
+    if (n < step) break;
+    put_spc(bar->render->tty->fp, step); bar->pos += step;
+    advance_bar_state(bar);
+    n -= step;
+  }
+  put_spc(bar->render->tty->fp, n); bar->pos += n;
+}
+
+int progress_showbar(struct progress_render_state *render, double frac)
+{
+  const struct progress_ttyinfo *tty = render->tty;
+  struct bar_state bar;
+
+  if (!tty->fp) return (-1);
+
+  bar.render = render; bar.pos = 0; bar.nextpos = frac*render->width + 0.5;
+
+  if (tty->cap.op) {
+    set_fgcolour(tty, 0); bar.state = LEFT_COLOUR;
+    if (bar.nextpos) set_bgcolour(tty, 2);
+    else advance_bar_state(&bar);
+  } else if (tty->cap.mr) {
+    if (bar.nextpos)
+      { bar.state = LEFT_MONO; put_sequence(tty, tty->cap.mr, 1); }
+    else
+      { bar.state = RIGHT; bar.nextpos = render->width; }
+  } else
+    bar.state = LEFT_SIMPLE;
+
+  put_barstr(&bar, render->linebuf, render->leftsz);
+  put_barspc(&bar, render->width - render->leftwd - render->rightwd);
+  put_barstr(&bar,
+            render->linebuf + render->linesz - render->rightsz,
+            render->rightsz);
+
+  put_sequence(tty, tty->cap.me, 1);
+  put_sequence(tty, tty->cap.op, 1);
+
+  return (0);
+}
+
+int progress_shownotice(struct progress_render_state *render, int bg, int fg)
+{
+  const struct progress_ttyinfo *tty = render->tty;
+
+  if (!tty->fp) return (-1);
+
+  if (tty->cap.op) { set_fgcolour(tty, fg); set_bgcolour(tty, bg); }
+  else if (tty->cap.mr) put_sequence(tty, tty->cap.mr, 1);
+  if (tty->cap.md) put_sequence(tty, tty->cap.md, 1);
+
+  put_str(tty->fp, render->linebuf, render->leftsz);
+  if (!render->rightsz && (tty->cap.f&TCF_BCE) && tty->cap.ce)
+    put_sequence(tty, tty->cap.ce, 1);
+  else {
+    put_spc(tty->fp, render->width - render->leftwd - render->rightwd);
+    put_str(tty->fp,
+           render->linebuf + render->linesz - render->rightsz,
+           render->rightsz);
+  }
+
+  put_sequence(tty, tty->cap.me, 1);
+  put_sequence(tty, tty->cap.op, 1);
+
+  return (0);
+}
+
+int progress_additem(struct progress_state *progress,
+                    struct progress_item *item)
+{
+  if (item->parent) return (-1);
+  item->prev = progress->end_item; item->next = 0;
+  if (progress->end_item) progress->end_item->next = item;
+  else progress->items = item;
+  progress->end_item = item; item->parent = progress;
+  progress->nitems++;
+
+  return (0);
+}
+
+int progress_clear(struct progress_state *progress)
+{
+  struct progress_render_state render;
+
+  if (!progress->tty.fp) return (-1);
+  if (setup_render_state(progress, &render)) return (-1);
+  clear_progress(progress, &render, CLRF_ALL);
+  free_render_state(&render);
+  return (0);
+}
+
+int progress_update(struct progress_state *progress)
+{
+  struct progress_render_state render;
+  struct progress_item *item;
+  unsigned f = 0;
+#define f_any 1u
+
+  if (!progress->tty.fp) return (-1);
+  if (setup_render_state(progress, &render)) return (-1);
+  clear_progress(progress, &render, 0);
+
+  for (item = progress->items; item; item = item->next) {
+    if (f&f_any) fputs("\r\n", progress->tty.fp);
+    render.leftsz = render.rightsz = 0;
+    render.leftwd = render.rightwd = 0;
+    item->render(item, &render); progress->last_lines++; f |= f_any;
+    if (progress->last_lines > render.height) break;
+  }
+  free_render_state(&render);
+  return (0);
+}
+
+int progress_removeitem(struct progress_state *progress,
+                       struct progress_item *item)
+{
+  if (!item->parent) return (-1);
+  if (item->next) item->next->prev = item->prev;
+  else (progress->end_item) = item->prev;
+  if (item->prev) item->prev->next = item->next;
+  else (progress->items) = item->next;
+  progress->nitems--; item->parent = 0;
+
+  return (0);
+}
diff --git a/multiprogress.h b/multiprogress.h
new file mode 100644 (file)
index 0000000..1db97a8
--- /dev/null
@@ -0,0 +1,82 @@
+#ifndef MULTIPROGRESS_H
+#define MULTIPROGRESS_H
+
+#include <stdio.h>
+#include <sys/time.h>
+
+struct progress_ttyinfo {
+  FILE *fp;                            /* terminal stream */
+  char *termbuf, *capbuf;              /* buffers for termcap */
+  struct {                             /* terminal capabilities */
+    unsigned f;                                /*   various flags */
+#define TCF_BCE 1u                     /*   erases to background colour */
+    const char *cr, *up, *ce, *cd;     /*   cursor motion */
+    const char *mr, *md, *me;          /*   reverse video, bold */
+    const char *af, *ab, *op;          /*   colour */
+  } cap;
+    unsigned defwd, defht;             /* default width and height */
+};
+#define PROGRESS_TTYINFO_INIT { 0, 0, 0, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }
+
+struct progress_state {
+  struct progress_ttyinfo tty;         /* terminal state */
+  struct progress_item *items, *end_item; /* list of progress items */
+  unsigned nitems;                     /* number of items */
+  unsigned last_lines;                 /* number written last time */
+  struct timeval tv_update;            /* last update time */
+};
+#define PROGRESS_STATE_INIT { PROGRESS_TTYINFO_INIT, 0, 0, 0, 0, { 0, 0 } }
+
+struct progress_render_state {
+  const struct progress_ttyinfo *tty;  /* terminal state */
+  unsigned width, height;              /* terminal size, in characters */
+  char *linebuf; size_t linesz;                /* output buffer */
+  char *tempbuf; size_t tempsz;                /* scratch buffer */
+  size_t leftsz, rightsz;              /* left and right cursors */
+  unsigned leftwd, rightwd;            /* left and right widths */
+  char *old_bc, *old_up;               /* old fixup strings */
+};
+
+struct progress_item {
+  struct progress_state *parent;       /* controlling progress state */
+  struct progress_item *next, *prev;   /* forward and backward links */
+  void (*render)(struct progress_item */*item*/, /* render function */
+                struct progress_render_state */*rs*/);
+};
+#define PROGRESS_ITEM_INIT { 0, 0, 0, 0 }
+
+extern int progress_init(struct progress_state */*progress*/);
+extern void progress_free(struct progress_state */*progress*/);
+
+extern int progress_clear(struct progress_state */*progress*/);
+
+extern int progress_update(struct progress_state */*progress*/);
+
+extern int progress_additem(struct progress_state */*progress*/,
+                           struct progress_item */*item*/);
+
+extern int progress_removeitem(struct progress_state */*progress*/,
+                              struct progress_item */*item*/);
+
+
+extern int progress_vputleft(struct progress_render_state */*render*/,
+                            const char */*fmt*/, va_list /*ap*/);
+
+extern int progress_vputright(struct progress_render_state */*render*/,
+                             const char */*fmt*/, va_list /*ap*/);
+
+__attribute__((format(printf, 2, 3)))
+extern int progress_putleft(struct progress_render_state */*render*/,
+                           const char */*fmt*/, ...);
+
+__attribute__((format(printf, 2, 3)))
+extern int progress_putright(struct progress_render_state */*render*/,
+                            const char */*fmt*/, ...);
+
+extern int progress_showbar(struct progress_render_state */*render*/,
+                           double /*frac*/);
+
+extern int progress_shownotice(struct progress_render_state */*render*/,
+                              int /*bg*/, int /*fg*/);
+
+#endif