veyron/lib/textutil: Add textutil package, including LineWriter.

LineWriter implements an io.Writer filter that formats input text
into output lines with a given target width in runes.  It
performs word-wrapping.

In order to implement this as an io.Writer filter, I also added
UTF8ChunkDecoder which decodes a stream of UTF-8 encoded runes
that may be arbitrarily chunked.  There's also a RuneChunkDecoder
interface to allow us to easily add UTF-16 support in the future.

I've updated the cmdline package to use this new support, and
updated the vdl tool docs with the new formatting to give an idea
of the changes.  I'll re-generate all docs in a subsequent CL.

Change-Id: Ia83cffe71d6fd43a0806fe028ff92d0d4ff079c3
diff --git a/lib/cmdline/cmdline.go b/lib/cmdline/cmdline.go
index 391b5d5..5c71607 100644
--- a/lib/cmdline/cmdline.go
+++ b/lib/cmdline/cmdline.go
@@ -20,7 +20,10 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"strconv"
 	"strings"
+
+	"veyron.io/veyron/veyron/lib/textutil"
 )
 
 // ErrExitCode may be returned by the Run function of a Command to cause the
@@ -138,53 +141,78 @@
 	fmt.Fprint(cmd.stderr, "ERROR: ")
 	fmt.Fprintf(cmd.stderr, format, v...)
 	fmt.Fprint(cmd.stderr, "\n\n")
-	cmd.usage(cmd.stderr, true)
+	cmd.writeUsage(cmd.stderr)
 	return ErrUsage
 }
 
+// Have a reasonable default for the output width in runes.
+const defaultWidth = 80
+
+func outputWidth() int {
+	if width, err := strconv.Atoi(os.Getenv("CMDLINE_WIDTH")); err == nil && width != 0 {
+		return width
+	}
+	if _, width, err := textutil.TerminalSize(); err == nil && width != 0 {
+		return width
+	}
+	return defaultWidth
+}
+
+func (cmd *Command) writeUsage(w io.Writer) {
+	lineWriter := textutil.NewUTF8LineWriter(w, outputWidth())
+	cmd.usage(lineWriter, true)
+	lineWriter.Flush()
+}
+
 // usage prints the usage of cmd to the writer.  The firstCall boolean is set to
 // false when printing usage for multiple commands, and is used to avoid
 // printing redundant information (e.g. help command, global flags).
-func (cmd *Command) usage(w io.Writer, firstCall bool) {
-	printLong(w, cmd.Long)
+func (cmd *Command) usage(w *textutil.LineWriter, firstCall bool) {
+	fmt.Fprintln(w, cmd.Long)
+	fmt.Fprintln(w)
 	// Usage line.
 	hasFlags := numFlags(&cmd.Flags) > 0
-	fmt.Fprintf(w, "\nUsage:\n")
-	path := namePath(cmd)
+	fmt.Fprintln(w, "Usage:")
+	path := cmd.namePath()
 	pathf := "   " + path
 	if hasFlags {
 		pathf += " [flags]"
 	}
 	if len(cmd.Children) > 0 {
-		fmt.Fprintf(w, "%s <command>\n", pathf)
+		fmt.Fprintln(w, pathf, "<command>")
 	}
 	if cmd.Run != nil {
 		if cmd.ArgsName != "" {
-			fmt.Fprintf(w, "%s %s\n", pathf, cmd.ArgsName)
+			fmt.Fprintln(w, pathf, cmd.ArgsName)
 		} else {
-			fmt.Fprintf(w, "%s\n", pathf)
+			fmt.Fprintln(w, pathf)
 		}
 	}
 	if len(cmd.Children) == 0 && cmd.Run == nil {
 		// This is a specification error.
-		fmt.Fprintf(w, "%s [ERROR: neither Children nor Run is specified]\n", pathf)
+		fmt.Fprintln(w, pathf, "[ERROR: neither Children nor Run is specified]")
 	}
 	// Commands.
+	const minNameWidth = 11
 	if len(cmd.Children) > 0 {
-		fmt.Fprintf(w, "\nThe %s commands are:\n", cmd.Name)
-		// Compute the size of the largest command name
-		namelen := 11
+		fmt.Fprintln(w)
+		fmt.Fprintln(w, "The", path, "commands are:")
+		nameWidth := minNameWidth
 		for _, child := range cmd.Children {
-			if len(child.Name) > namelen {
-				namelen = len(child.Name)
+			if len(child.Name) > nameWidth {
+				nameWidth = len(child.Name)
 			}
 		}
+		// Print as a table with aligned columns Name and Short.
+		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
 		for _, child := range cmd.Children {
 			// Don't repeatedly list default help command.
 			if !child.isDefaultHelp || firstCall {
-				fmt.Fprintf(w, "   %-[1]*[2]s %[3]s\n", namelen, child.Name, child.Short)
+				fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, child.Name, child.Short)
+				w.Flush()
 			}
 		}
+		w.SetIndents()
 		if firstCall {
 			fmt.Fprintf(w, "Run \"%s help [command]\" for command usage.\n", path)
 		}
@@ -192,32 +220,45 @@
 	// Args.
 	if cmd.Run != nil && cmd.ArgsLong != "" {
 		fmt.Fprintln(w)
-		printLong(w, cmd.ArgsLong)
+		fmt.Fprintln(w, cmd.ArgsLong)
 	}
 	// Help topics.
 	if len(cmd.Topics) > 0 {
-		fmt.Fprintf(w, "\nThe %s additional help topics are:\n", cmd.Name)
+		fmt.Fprintln(w)
+		fmt.Fprintln(w, "The", path, "additional help topics are:")
+		nameWidth := minNameWidth
 		for _, topic := range cmd.Topics {
-			fmt.Fprintf(w, "   %-11s %s\n", topic.Name, topic.Short)
+			if len(topic.Name) > nameWidth {
+				nameWidth = len(topic.Name)
+			}
 		}
+		// Print as a table with aligned columns Name and Short.
+		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
+		for _, topic := range cmd.Topics {
+			fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, topic.Name, topic.Short)
+			w.Flush()
+		}
+		w.SetIndents()
 		if firstCall {
 			fmt.Fprintf(w, "Run \"%s help [topic]\" for topic details.\n", path)
 		}
 	}
 	// Flags.
 	if hasFlags {
-		fmt.Fprintf(w, "\nThe %s flags are:\n", cmd.Name)
+		fmt.Fprintln(w)
+		fmt.Fprintln(w, "The", path, "flags are:")
 		printFlags(w, &cmd.Flags)
 	}
 	// Global flags.
 	if numFlags(flag.CommandLine) > 0 && firstCall {
-		fmt.Fprintf(w, "\nThe global flags are:\n")
+		fmt.Fprintln(w)
+		fmt.Fprintln(w, "The global flags are:")
 		printFlags(w, flag.CommandLine)
 	}
 }
 
 // namePath returns the path of command names up to cmd.
-func namePath(cmd *Command) string {
+func (cmd *Command) namePath() string {
 	var path []string
 	for ; cmd != nil; cmd = cmd.parent {
 		path = append([]string{cmd.Name}, path...)
@@ -225,11 +266,6 @@
 	return strings.Join(path, " ")
 }
 
-func printLong(w io.Writer, long string) {
-	fmt.Fprint(w, strings.Trim(long, "\n"))
-	fmt.Fprintln(w)
-}
-
 func numFlags(set *flag.FlagSet) (num int) {
 	set.VisitAll(func(*flag.Flag) {
 		num++
@@ -237,12 +273,19 @@
 	return
 }
 
-func printFlags(w io.Writer, set *flag.FlagSet) {
+func printFlags(w *textutil.LineWriter, set *flag.FlagSet) {
 	set.VisitAll(func(f *flag.Flag) {
-		fmt.Fprintf(w, "   -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
+		fmt.Fprintf(w, " -%s=%s", f.Name, f.DefValue)
+		w.SetIndents(spaces(3))
+		fmt.Fprintln(w, f.Usage)
+		w.SetIndents()
 	})
 }
 
+func spaces(count int) string {
+	return strings.Repeat(" ", count)
+}
+
 // newDefaultHelp creates a new default help command.  We need to create new
 // instances since the parent for each help command is different.
 func newDefaultHelp() *Command {
@@ -252,8 +295,16 @@
 		Short: "Display help for commands or topics",
 		Long: `
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
 `,
 		ArgsName: "[command/topic ...]",
 		ArgsLong: `
@@ -261,7 +312,9 @@
 `,
 		Run: func(cmd *Command, args []string) error {
 			// Help applies to its parent - e.g. "foo help" applies to the foo command.
-			return runHelp(cmd.stdout, cmd.parent, args, helpStyle)
+			lineWriter := textutil.NewUTF8LineWriter(cmd.stdout, outputWidth())
+			defer lineWriter.Flush()
+			return runHelp(lineWriter, cmd.parent, args, helpStyle)
 		},
 		isDefaultHelp: true,
 	}
@@ -272,7 +325,7 @@
 const helpName = "help"
 
 // runHelp runs the "help" command.
-func runHelp(w io.Writer, cmd *Command, args []string, style style) error {
+func runHelp(w *textutil.LineWriter, cmd *Command, args []string, style style) error {
 	if len(args) == 0 {
 		cmd.usage(w, true)
 		return nil
@@ -291,20 +344,21 @@
 	// Try to display help for the help topic.
 	for _, topic := range cmd.Topics {
 		if topic.Name == subName {
-			printLong(w, topic.Long)
+			fmt.Fprintln(w, topic.Long)
 			return nil
 		}
 	}
-	return cmd.UsageErrorf("%s: unknown command or topic %q", cmd.Name, subName)
+	return cmd.UsageErrorf("%s: unknown command or topic %q", cmd.namePath(), subName)
 }
 
 // recursiveHelp prints help recursively via DFS from this cmd onward.
-func recursiveHelp(w io.Writer, cmd *Command, style style, firstCall bool) {
+func recursiveHelp(w *textutil.LineWriter, cmd *Command, style style, firstCall bool) {
 	if !firstCall {
-		// Title-case required for godoc to recognize this as a section header.
-		header := strings.Title(namePath(cmd))
 		lineBreak(w, style)
-		fmt.Fprintf(w, "%s\n\n", header)
+		// Title-case required for godoc to recognize this as a section header.
+		header := strings.Title(cmd.namePath())
+		fmt.Fprintln(w, header)
+		fmt.Fprintln(w)
 	}
 	cmd.usage(w, firstCall)
 	for _, child := range cmd.Children {
@@ -314,29 +368,47 @@
 		}
 	}
 	for _, topic := range cmd.Topics {
-		// Title-case required for godoc to recognize this as a section header.
-		header := strings.Title(namePath(cmd)+" "+topic.Name) + " - help topic"
 		lineBreak(w, style)
-		fmt.Fprintf(w, "%s\n\n", header)
-		printLong(w, topic.Long)
+		// Title-case required for godoc to recognize this as a section header.
+		header := strings.Title(cmd.namePath()+" "+topic.Name) + " - help topic"
+		fmt.Fprintln(w, header)
+		fmt.Fprintln(w)
+		fmt.Fprintln(w, topic.Long)
 	}
 }
 
-func lineBreak(w io.Writer, style style) {
+func lineBreak(w *textutil.LineWriter, style style) {
+	w.Flush()
 	switch style {
 	case styleText:
-		fmt.Fprintln(w, strings.Repeat("=", 80))
+		width := w.Width()
+		if width < 0 {
+			// If the user has chosen an "unlimited" word-wrapping width, we still
+			// need a reasonable width for our visual line break.
+			width = defaultWidth
+		}
+		fmt.Fprintln(w, strings.Repeat("=", width))
 	case styleGoDoc:
 		fmt.Fprintln(w)
 	}
+	w.Flush()
 }
 
+func trimNewlines(s *string) { *s = strings.Trim(*s, "\n") }
+
 // Init initializes all nodes in the command tree rooted at cmd.  Init must be
 // called before Execute.
 func (cmd *Command) Init(parent *Command, stdout, stderr io.Writer) {
 	cmd.parent = parent
 	cmd.stdout = stdout
 	cmd.stderr = stderr
+	trimNewlines(&cmd.Short)
+	trimNewlines(&cmd.Long)
+	trimNewlines(&cmd.ArgsLong)
+	for tx := range cmd.Topics {
+		trimNewlines(&cmd.Topics[tx].Short)
+		trimNewlines(&cmd.Topics[tx].Long)
+	}
 	// Add help command, if it doesn't already exist.
 	hasHelp := false
 	for _, child := range cmd.Children {
@@ -366,6 +438,7 @@
 
 func mergeFlags(dst, src *flag.FlagSet) {
 	src.VisitAll(func(f *flag.Flag) {
+		trimNewlines(&f.Usage)
 		if dst.Lookup(f.Name) == nil {
 			dst.Var(f.Value, f.Name, f.Usage)
 		}
@@ -378,13 +451,14 @@
 // there are usage errors, otherwise it is whatever the leaf command returns
 // from its Run function.
 func (cmd *Command) Execute(args []string) error {
+	path := cmd.namePath()
 	// Parse the merged flags.
 	if err := cmd.parseFlags.Parse(args); err != nil {
 		if err == flag.ErrHelp {
-			cmd.usage(cmd.stdout, true)
+			cmd.writeUsage(cmd.stdout)
 			return nil
 		}
-		return cmd.UsageErrorf(err.Error())
+		return cmd.UsageErrorf("%s: %v", path, err)
 	}
 	args = cmd.parseFlags.Args()
 	// Look for matching children.
@@ -400,19 +474,19 @@
 	if cmd.Run != nil {
 		if cmd.ArgsName == "" && len(args) > 0 {
 			if len(cmd.Children) > 0 {
-				return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0])
+				return cmd.UsageErrorf("%s: unknown command %q", path, args[0])
 			}
-			return cmd.UsageErrorf("%s doesn't take any arguments", cmd.Name)
+			return cmd.UsageErrorf("%s doesn't take any arguments", path)
 		}
 		return cmd.Run(cmd, args)
 	}
 	switch {
 	case len(cmd.Children) == 0:
-		return cmd.UsageErrorf("%s: neither Children nor Run is specified", cmd.Name)
+		return cmd.UsageErrorf("%s: neither Children nor Run is specified", path)
 	case len(args) > 0:
-		return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0])
+		return cmd.UsageErrorf("%s: unknown command %q", path, args[0])
 	default:
-		return cmd.UsageErrorf("%s: no command specified", cmd.Name)
+		return cmd.UsageErrorf("%s: no command specified", path)
 	}
 }
 
@@ -426,7 +500,7 @@
 		if code, ok := err.(ErrExitCode); ok {
 			os.Exit(int(code))
 		}
-		fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
+		fmt.Fprintln(os.Stderr, "ERROR:", err)
 		os.Exit(2)
 	}
 }
diff --git a/lib/cmdline/cmdline_test.go b/lib/cmdline/cmdline_test.go
index e0de532..6cf6b0e 100644
--- a/lib/cmdline/cmdline_test.go
+++ b/lib/cmdline/cmdline_test.go
@@ -5,6 +5,7 @@
 	"errors"
 	"flag"
 	"fmt"
+	"os"
 	"regexp"
 	"strings"
 	"testing"
@@ -61,15 +62,16 @@
 }
 
 func init() {
+	os.Setenv("CMDLINE_WIDTH", "80") // make sure the formatting stays the same.
 	flag.StringVar(&globalFlag1, "global1", "", "global test flag 1")
 	globalFlag2 = flag.Int64("global2", 0, "global test flag 2")
 }
 
-func matchOutput(actual, expect string) bool {
+func stripOutput(got string) string {
 	// The global flags include the flags from the testing package, so strip them
 	// out before the comparison.
-	re := regexp.MustCompile("   -test.*\n")
-	return re.ReplaceAllLiteralString(actual, "") == expect
+	re := regexp.MustCompile(" -test[^\n]+\n(?:   [^\n]+\n)+")
+	return re.ReplaceAllLiteralString(got, "")
 }
 
 func runTestCases(t *testing.T, cmd *Command, tests []testCase) {
@@ -86,19 +88,19 @@
 		// Run the execute function and check against expected results.
 		cmd.Init(nil, &stdout, &stderr)
 		if err := cmd.Execute(test.Args); err != test.Err {
-			t.Errorf("Ran with args %q\nEXPECTED error:\n%q\nACTUAL error:\n%q", test.Args, test.Err, err)
+			t.Errorf("Ran with args %q\n GOT error:\n%q\nWANT error:\n%q", test.Args, err, test.Err)
 		}
-		if !matchOutput(stdout.String(), test.Stdout) {
-			t.Errorf("Ran with args %q\nEXPECTED stdout:\n%q\nACTUAL stdout:\n%q", test.Args, test.Stdout, stdout.String())
+		if got, want := stripOutput(stdout.String()), test.Stdout; got != want {
+			t.Errorf("Ran with args %q\n GOT stdout:\n%q\nWANT stdout:\n%q", test.Args, got, want)
 		}
-		if !matchOutput(stderr.String(), test.Stderr) {
-			t.Errorf("Ran with args %q\nEXPECTED stderr:\n%q\nACTUAL stderr:\n%q", test.Args, test.Stderr, stderr.String())
+		if got, want := stripOutput(stderr.String()), test.Stderr; got != want {
+			t.Errorf("Ran with args %q\n GOT stderr:\n%q\nWANT stderr:\n%q", test.Args, got, want)
 		}
-		if globalFlag1 != test.GlobalFlag1 {
-			t.Errorf("Value for global1 flag %q\nEXPECTED %q", globalFlag1, test.GlobalFlag1)
+		if got, want := globalFlag1, test.GlobalFlag1; got != want {
+			t.Errorf("global1 flag got %q, want %q", got, want)
 		}
-		if *globalFlag2 != test.GlobalFlag2 {
-			t.Errorf("Value for global2 flag %q\nEXPECTED %q", globalFlag2, test.GlobalFlag2)
+		if got, want := *globalFlag2, test.GlobalFlag2; got != want {
+			t.Errorf("global2 flag got %q, want %q", got, want)
 		}
 	}
 }
@@ -122,8 +124,10 @@
    nocmds [ERROR: neither Children nor Run is specified]
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -137,8 +141,10 @@
    nocmds [ERROR: neither Children nor Run is specified]
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
@@ -181,8 +187,10 @@
 Run "onecmd help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -201,8 +209,10 @@
 Run "onecmd help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -218,8 +228,10 @@
 Run "onecmd help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -232,27 +244,40 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
 			Args: []string{"help", "help"},
 			Stdout: `Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    onecmd help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The onecmd help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -268,8 +293,10 @@
 Run "onecmd help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Onecmd Echo
 
@@ -283,16 +310,25 @@
 Onecmd Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    onecmd help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The onecmd help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -311,8 +347,10 @@
 Run "onecmd help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -336,8 +374,10 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
@@ -395,11 +435,14 @@
 Run "multi help [command]" for command usage.
 
 The multi flags are:
-   -extra=false: Print an extra arg
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -416,11 +459,14 @@
 Run "multi help [command]" for command usage.
 
 The multi flags are:
-   -extra=false: Print an extra arg
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -437,11 +483,14 @@
 Run "multi help [command]" for command usage.
 
 The multi flags are:
-   -extra=false: Print an extra arg
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Multi Echo
 
@@ -461,22 +510,32 @@
 
 [args] are arbitrary strings that will be echoed.
 
-The echoopt flags are:
-   -n=false: Do not output trailing newline
+The multi echoopt flags are:
+ -n=false
+   Do not output trailing newline
 ================================================================================
 Multi Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    multi help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The multi help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -489,8 +548,10 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -502,12 +563,15 @@
 
 [args] are arbitrary strings that will be echoed.
 
-The echoopt flags are:
-   -n=false: Do not output trailing newline
+The multi echoopt flags are:
+ -n=false
+   Do not output trailing newline
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -527,11 +591,14 @@
 Run "multi help [command]" for command usage.
 
 The multi flags are:
-   -extra=false: Print an extra arg
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -585,7 +652,7 @@
 		{
 			Args: []string{"echo", "-n", "foo", "bar"},
 			Err:  ErrUsage,
-			Stderr: `ERROR: flag provided but not defined: -n
+			Stderr: `ERROR: multi echo: flag provided but not defined: -n
 
 Echo prints any strings passed in to stdout.
 
@@ -595,14 +662,16 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
 			Args: []string{"-nosuchflag", "echo", "foo", "bar"},
 			Err:  ErrUsage,
-			Stderr: `ERROR: flag provided but not defined: -nosuchflag
+			Stderr: `ERROR: multi: flag provided but not defined: -nosuchflag
 
 Multi has two variants of echo.
 
@@ -616,11 +685,14 @@
 Run "multi help [command]" for command usage.
 
 The multi flags are:
-   -extra=false: Print an extra arg
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
@@ -706,11 +778,14 @@
 Run "toplevelprog help [topic]" for topic details.
 
 The toplevelprog flags are:
-   -tlextra=false: Print an extra arg for all commands
+ -tlextra=false
+   Print an extra arg for all commands
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -732,11 +807,14 @@
 Run "toplevelprog help [topic]" for topic details.
 
 The toplevelprog flags are:
-   -tlextra=false: Print an extra arg for all commands
+ -tlextra=false
+   Print an extra arg for all commands
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -758,11 +836,14 @@
 Run "toplevelprog help [topic]" for topic details.
 
 The toplevelprog flags are:
-   -tlextra=false: Print an extra arg for all commands
+ -tlextra=false
+   Print an extra arg for all commands
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Toplevelprog Echoprog
 
@@ -771,15 +852,16 @@
 Usage:
    toplevelprog echoprog [flags] <command>
 
-The echoprog commands are:
+The toplevelprog echoprog commands are:
    echo        Print strings on stdout
    echoopt     Print strings on stdout, with opts
 
-The echoprog additional help topics are:
+The toplevelprog echoprog additional help topics are:
    topic3      Help topic 3 short
 
-The echoprog flags are:
-   -extra=false: Print an extra arg
+The toplevelprog echoprog flags are:
+ -extra=false
+   Print an extra arg
 ================================================================================
 Toplevelprog Echoprog Echo
 
@@ -799,8 +881,9 @@
 
 [args] are arbitrary strings that will be echoed.
 
-The echoopt flags are:
-   -n=false: Do not output trailing newline
+The toplevelprog echoprog echoopt flags are:
+ -n=false
+   Do not output trailing newline
 ================================================================================
 Toplevelprog Echoprog Topic3 - help topic
 
@@ -818,16 +901,25 @@
 Toplevelprog Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    toplevelprog help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The toplevelprog help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 ================================================================================
 Toplevelprog Topic1 - help topic
 
@@ -845,22 +937,25 @@
 Usage:
    toplevelprog echoprog [flags] <command>
 
-The echoprog commands are:
+The toplevelprog echoprog commands are:
    echo        Print strings on stdout
    echoopt     Print strings on stdout, with opts
    help        Display help for commands or topics
 Run "toplevelprog echoprog help [command]" for command usage.
 
-The echoprog additional help topics are:
+The toplevelprog echoprog additional help topics are:
    topic3      Help topic 3 short
 Run "toplevelprog echoprog help [topic]" for topic details.
 
-The echoprog flags are:
-   -extra=false: Print an extra arg
+The toplevelprog echoprog flags are:
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -880,22 +975,25 @@
 Usage:
    toplevelprog echoprog [flags] <command>
 
-The echoprog commands are:
+The toplevelprog echoprog commands are:
    echo        Print strings on stdout
    echoopt     Print strings on stdout, with opts
    help        Display help for commands or topics
 Run "toplevelprog echoprog help [command]" for command usage.
 
-The echoprog additional help topics are:
+The toplevelprog echoprog additional help topics are:
    topic3      Help topic 3 short
 Run "toplevelprog echoprog help [topic]" for topic details.
 
-The echoprog flags are:
-   -extra=false: Print an extra arg
+The toplevelprog echoprog flags are:
+ -extra=false
+   Print an extra arg
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Toplevelprog Echoprog Echo
 
@@ -915,22 +1013,32 @@
 
 [args] are arbitrary strings that will be echoed.
 
-The echoopt flags are:
-   -n=false: Do not output trailing newline
+The toplevelprog echoprog echoopt flags are:
+ -n=false
+   Do not output trailing newline
 ================================================================================
 Toplevelprog Echoprog Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    toplevelprog echoprog help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The toplevelprog echoprog help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 ================================================================================
 Toplevelprog Echoprog Topic3 - help topic
 
@@ -946,12 +1054,15 @@
 
 [args] are arbitrary strings that will be echoed.
 
-The echoopt flags are:
-   -n=false: Do not output trailing newline
+The toplevelprog echoprog echoopt flags are:
+ -n=false
+   Do not output trailing newline
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -974,8 +1085,10 @@
 [strings] are arbitrary strings that will be printed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1000,11 +1113,14 @@
 Run "toplevelprog help [topic]" for topic details.
 
 The toplevelprog flags are:
-   -tlextra=false: Print an extra arg for all commands
+ -tlextra=false
+   Print an extra arg for all commands
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1054,7 +1170,7 @@
 		{
 			Args: []string{"hello", "--extra", "foo", "bar"},
 			Err:  ErrUsage,
-			Stderr: `ERROR: flag provided but not defined: -extra
+			Stderr: `ERROR: toplevelprog hello: flag provided but not defined: -extra
 
 Hello prints any strings passed in to stdout preceded by "Hello".
 
@@ -1064,14 +1180,16 @@
 [strings] are arbitrary strings that will be printed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
 			Args: []string{"-extra", "echoprog", "echoopt", "foo", "bar"},
 			Err:  ErrUsage,
-			Stderr: `ERROR: flag provided but not defined: -extra
+			Stderr: `ERROR: toplevelprog: flag provided but not defined: -extra
 
 Toplevelprog has the echo subprogram and the hello command.
 
@@ -1090,11 +1208,14 @@
 Run "toplevelprog help [topic]" for topic details.
 
 The toplevelprog flags are:
-   -tlextra=false: Print an extra arg for all commands
+ -tlextra=false
+   Print an extra arg for all commands
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
@@ -1200,8 +1321,10 @@
 Run "prog1 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1219,8 +1342,10 @@
 Run "prog1 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1238,8 +1363,10 @@
 Run "prog1 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Prog1 Hello11
 
@@ -1266,7 +1393,7 @@
 Usage:
    prog1 prog2 <command>
 
-The prog2 commands are:
+The prog1 prog2 commands are:
    hello21     Print strings on stdout preceded by "Hello"
    prog3       Set of hello commands
    hello22     Print strings on stdout preceded by "Hello"
@@ -1287,7 +1414,7 @@
 Usage:
    prog1 prog2 prog3 <command>
 
-The prog3 commands are:
+The prog1 prog2 prog3 commands are:
    hello31     Print strings on stdout preceded by "Hello"
    hello32     Print strings on stdout preceded by "Hello"
 ================================================================================
@@ -1321,16 +1448,25 @@
 Prog1 Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    prog1 help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The prog1 help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -1340,7 +1476,7 @@
 Usage:
    prog1 prog2 <command>
 
-The prog2 commands are:
+The prog1 prog2 commands are:
    hello21     Print strings on stdout preceded by "Hello"
    prog3       Set of hello commands
    hello22     Print strings on stdout preceded by "Hello"
@@ -1348,8 +1484,10 @@
 Run "prog1 prog2 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Prog1 Prog2 Hello21
 
@@ -1367,7 +1505,7 @@
 Usage:
    prog1 prog2 prog3 <command>
 
-The prog3 commands are:
+The prog1 prog2 prog3 commands are:
    hello31     Print strings on stdout preceded by "Hello"
    hello32     Print strings on stdout preceded by "Hello"
 ================================================================================
@@ -1401,16 +1539,25 @@
 Prog1 Prog2 Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    prog1 prog2 help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The prog1 prog2 help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -1420,15 +1567,17 @@
 Usage:
    prog1 prog2 prog3 <command>
 
-The prog3 commands are:
+The prog1 prog2 prog3 commands are:
    hello31     Print strings on stdout preceded by "Hello"
    hello32     Print strings on stdout preceded by "Hello"
    help        Display help for commands or topics
 Run "prog1 prog2 prog3 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Prog1 Prog2 Prog3 Hello31
 
@@ -1451,16 +1600,25 @@
 Prog1 Prog2 Prog3 Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    prog1 prog2 prog3 help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The prog1 prog2 prog3 help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -1470,15 +1628,17 @@
 Usage:
    prog1 prog2 prog3 <command>
 
-The prog3 commands are:
+The prog1 prog2 prog3 commands are:
    hello31     Print strings on stdout preceded by "Hello"
    hello32     Print strings on stdout preceded by "Hello"
    help        Display help for commands or topics
 Run "prog1 prog2 prog3 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Prog1 Prog2 Prog3 Hello31
 
@@ -1501,16 +1661,25 @@
 Prog1 Prog2 Prog3 Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    prog1 prog2 prog3 help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The prog1 prog2 prog3 help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -1528,8 +1697,10 @@
 Run "prog1 help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 
 Prog1 Hello11
 
@@ -1556,7 +1727,7 @@
 Usage:
    prog1 prog2 <command>
 
-The prog2 commands are:
+The prog1 prog2 commands are:
    hello21     Print strings on stdout preceded by "Hello"
    prog3       Set of hello commands
    hello22     Print strings on stdout preceded by "Hello"
@@ -1577,7 +1748,7 @@
 Usage:
    prog1 prog2 prog3 <command>
 
-The prog3 commands are:
+The prog1 prog2 prog3 commands are:
    hello31     Print strings on stdout preceded by "Hello"
    hello32     Print strings on stdout preceded by "Hello"
 
@@ -1611,16 +1782,25 @@
 Prog1 Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    prog1 help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The prog1 help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 	}
@@ -1675,8 +1855,10 @@
 [strings] are arbitrary strings that will be printed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1689,8 +1871,10 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1709,8 +1893,10 @@
 [strings] are arbitrary strings that will be printed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Cmdargs Echo
 
@@ -1724,16 +1910,25 @@
 Cmdargs Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    cmdargs help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The cmdargs help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -1755,8 +1950,10 @@
 [strings] are arbitrary strings that will be printed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1780,8 +1977,10 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
@@ -1830,8 +2029,10 @@
 Run "cmdrun help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1848,8 +2049,10 @@
 Run "cmdrun help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1862,8 +2065,10 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1880,8 +2085,10 @@
 Run "cmdrun help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 ================================================================================
 Cmdrun Echo
 
@@ -1895,16 +2102,25 @@
 Cmdrun Help
 
 Help with no args displays the usage of the parent command.
+
 Help with args displays the usage of the specified sub-command or help topic.
+
 "help ..." recursively displays help for all commands and topics.
 
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
 Usage:
    cmdrun help [flags] [command/topic ...]
 
 [command/topic ...] optionally identifies a specific sub-command or help topic.
 
-The help flags are:
-   -style=text: The formatting style for help output, either "text" or "godoc".
+The cmdrun help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
 `,
 		},
 		{
@@ -1924,8 +2140,10 @@
 Run "cmdrun help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 		{
@@ -1949,8 +2167,10 @@
 [strings] are arbitrary strings that will be echoed.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
@@ -1960,8 +2180,8 @@
 func TestLongCommandsHelp(t *testing.T) {
 	cmdLong := &Command{
 		Name:  "thisisaverylongcommand",
-		Short: "description of very long command.",
-		Long:  "blah blah blah.",
+		Short: "the short description of the very long command is very long, and will have to be wrapped",
+		Long:  "The long description of the very long command is also very long, and will similarly have to be wrapped",
 		Run:   runEcho,
 	}
 	cmdShort := &Command{
@@ -1986,13 +2206,31 @@
 
 The program commands are:
    x                      description of short command.
-   thisisaverylongcommand description of very long command.
+   thisisaverylongcommand the short description of the very long command is very
+                          long, and will have to be wrapped
    help                   Display help for commands or topics
 Run "program help [command]" for command usage.
 
 The global flags are:
-   -global1=: global test flag 1
-   -global2=0: global test flag 2
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
+`,
+		},
+		{
+			Args: []string{"help", "thisisaverylongcommand"},
+			Stdout: `The long description of the very long command is also very long, and will
+similarly have to be wrapped
+
+Usage:
+   program thisisaverylongcommand
+
+The global flags are:
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
 `,
 		},
 	}
diff --git a/lib/textutil/doc.go b/lib/textutil/doc.go
new file mode 100644
index 0000000..cd28c8c
--- /dev/null
+++ b/lib/textutil/doc.go
@@ -0,0 +1,5 @@
+// Package textutil implements utilities for handling human-readable text.
+//
+// This package includes a combination of low-level and high-level utilities.
+// The main high-level utility is available as NewUTF8LineWriter.
+package textutil
diff --git a/lib/textutil/line_writer.go b/lib/textutil/line_writer.go
new file mode 100644
index 0000000..0b70ae7
--- /dev/null
+++ b/lib/textutil/line_writer.go
@@ -0,0 +1,445 @@
+package textutil
+
+import (
+	"fmt"
+	"io"
+	"unicode"
+)
+
+// LineWriter implements an io.Writer filter that formats input text into output
+// lines with a given target width in runes.
+//
+// Each input rune is classified into one of three kinds:
+//   EOL:    end-of-line, consisting of \f, \n, \r, \v, U+2028 or U+2029
+//   Space:  defined by unicode.IsSpace
+//   Letter: everything else
+//
+// The input text is expected to consist of words, defined as sequences of
+// letters.  Sequences of words form paragraphs, where paragraphs are separated
+// by either blank lines (that contain no letters), or an explicit U+2029
+// ParagraphSeparator.  Input lines with leading spaces are treated verbatim.
+//
+// Paragraphs are output as word-wrapped lines; line breaks only occur at word
+// boundaries.  Output lines are usually no longer than the target width.  The
+// exceptions are single words longer than the target width, which are output on
+// their own line, and verbatim lines, which may be arbitrarily longer or
+// shorter than the width.
+//
+// Output lines never contain trailing spaces.  Only verbatim output lines may
+// contain leading spaces.  Spaces separating input words are output verbatim,
+// unless it would result in a line with leading or trailing spaces.
+//
+// EOL runes within the input text are never written to the output; the output
+// line terminator and paragraph separator may be configured, and some EOL may
+// be output as a single space ' ' to maintain word separation.
+//
+// The algorithm greedily fills each output line with as many words as it can,
+// assuming that all Unicode code points have the same width.  Invalid UTF-8 is
+// silently transformed to the replacement character U+FFFD and treated as a
+// single rune.
+//
+// Flush must be called after the last call to Write; the input is buffered.
+//
+//   Implementation note: line breaking is a complicated topic.  This approach
+//   attempts to be simple and useful; a full implementation conforming to
+//   Unicode Standard Annex #14 would be complicated, and is not implemented.
+//   Languages that don't use spaces to separate words (e.g. CJK) won't work
+//   well under the current approach.
+//
+//   http://www.unicode.org/reports/tr14 [Unicode Line Breaking Algorithm]
+//   http://www.unicode.org/versions/Unicode4.0.0/ch05.pdf [5.8 Newline Guidelines]
+type LineWriter struct {
+	// State configured by the user.
+	w            io.Writer
+	runeDecoder  RuneChunkDecoder
+	width        runePos
+	lineTerm     []byte
+	paragraphSep string
+	indents      []string
+
+	// The buffer contains a single output line.
+	lineBuf byteRuneBuffer
+
+	// Keep track of the previous state and rune.
+	prevState state
+	prevRune  rune
+
+	// Keep track of blank input lines.
+	inputLineHasLetter bool
+
+	// lineBuf positions where the line starts (after separators and indents), a
+	// new word has started and the last word has ended.
+	lineStart    bytePos
+	newWordStart bytePos
+	lastWordEnd  bytePos
+
+	// Keep track of paragraph terminations and line indices, so we can output the
+	// paragraph separator and indents correctly.
+	terminateParagraph bool
+	paragraphLineIndex int
+	wroteFirstLine     bool
+}
+
+type state int
+
+const (
+	stateWordWrap  state = iota // Perform word-wrapping [start state]
+	stateVerbatim               // Verbatim output-line, no word-wrapping
+	stateSkipSpace              // Skip spaces in input line.
+)
+
+// NewLineWriter returns a new LineWriter with the given target width in runes,
+// producing output on the underlying writer w.  The dec and enc are used to
+// respectively decode runes from Write calls, and encode runes to w.
+func NewLineWriter(w io.Writer, width int, dec RuneChunkDecoder, enc RuneEncoder) *LineWriter {
+	ret := &LineWriter{
+		w:            w,
+		runeDecoder:  dec,
+		width:        runePos(width),
+		lineTerm:     []byte("\n"),
+		paragraphSep: "\n",
+		prevState:    stateWordWrap,
+		prevRune:     LineSeparator,
+		lineBuf:      byteRuneBuffer{enc: enc},
+	}
+	ret.resetLine()
+	return ret
+}
+
+// NewUTF8LineWriter returns a new LineWriter filter that implements io.Writer,
+// and decodes and encodes runes in UTF-8.
+func NewUTF8LineWriter(w io.Writer, width int) *LineWriter {
+	return NewLineWriter(w, width, &UTF8ChunkDecoder{}, UTF8Encoder{})
+}
+
+// Width returns the target width in runes.  If width < 0 the width is
+// unlimited; each paragraph is output as a single line.
+func (w *LineWriter) Width() int { return int(w.width) }
+
+// SetLineTerminator sets the line terminator for subsequent Write calls.  Every
+// output line is terminated with term; EOL runes from the input are never
+// written to the output.  A new LineWriter instance uses "\n" as the default
+// line terminator.
+//
+// Calls Flush internally, and returns any Flush error.
+func (w *LineWriter) SetLineTerminator(term string) error {
+	if err := w.Flush(); err != nil {
+		return err
+	}
+	w.lineTerm = []byte(term)
+	w.resetLine()
+	return nil
+}
+
+// SetParagraphSeparator sets the paragraph separator for subsequent Write
+// calls.  Every consecutive pair of non-empty paragraphs is separated with sep;
+// EOL runes from the input are never written to the output.  A new LineWriter
+// instance uses "\n" as the default paragraph separator.
+//
+// Calls Flush internally, and returns any Flush error.
+func (w *LineWriter) SetParagraphSeparator(sep string) error {
+	if err := w.Flush(); err != nil {
+		return err
+	}
+	w.paragraphSep = sep
+	w.resetLine()
+	return nil
+}
+
+// SetIndents sets the indentation for subsequent Write calls.  Multiple indents
+// may be set, corresponding to the indent to use for the corresponding
+// paragraph line.  E.g. SetIndents("AA", "BBB", C") means the first line in
+// each paragraph is indented with "AA", the second line in each paragraph is
+// indented with "BBB", and all subsequent lines in each paragraph are indented
+// with "C".
+//
+// SetIndents() is equivalent to SetIndents(""), SetIndents("", ""), etc.
+//
+// A new LineWriter instance has no idents by default.
+//
+// Calls Flush internally, and returns any Flush error.
+func (w *LineWriter) SetIndents(indents ...string) error {
+	if err := w.Flush(); err != nil {
+		return err
+	}
+	// Copy indents in case the user passed the slice via SetIndents(p...), and
+	// canonicalize the all empty case to nil.
+	allEmpty := true
+	w.indents = make([]string, len(indents))
+	for ix, indent := range indents {
+		w.indents[ix] = indent
+		if indent != "" {
+			allEmpty = false
+		}
+	}
+	if allEmpty {
+		w.indents = nil
+	}
+	w.resetLine()
+	return nil
+}
+
+// Write implements io.Writer by buffering data into the LineWriter w.  Actual
+// writes to the underlying writer may occur, and may include data buffered in
+// either this Write call or previous Write calls.
+//
+// Flush must be called after the last call to Write.
+func (w *LineWriter) Write(data []byte) (int, error) {
+	return RuneChunkWrite(w.runeDecoder, w.addRune, data)
+}
+
+// Flush flushes any remaining buffered text, and resets the paragraph line
+// count back to 0, so that indents will be applied starting from the first
+// line.  It does not imply a paragraph separator; repeated calls to Flush with
+// no intervening calls to other methods is equivalent to a single Flush.
+//
+// Flush must be called after the last call to Write, and may be called an
+// arbitrary number of times before the last Write.
+func (w *LineWriter) Flush() error {
+	if err := RuneChunkFlush(w.runeDecoder, w.addRune); err != nil {
+		return err
+	}
+	// Add U+2028 to force the last line (if any) to be written.
+	if err := w.addRune(LineSeparator); err != nil {
+		return err
+	}
+	// Reset the paragraph line count.
+	w.paragraphLineIndex = 0
+	w.resetLine()
+	return nil
+}
+
+// addRune is called every time w.runeDecoder decodes a full rune.
+func (w *LineWriter) addRune(r rune) error {
+	state, lineBreak := w.nextState(r, w.updateRune(r))
+	if lineBreak {
+		if err := w.writeLine(); err != nil {
+			return err
+		}
+	}
+	w.bufferRune(r, state, lineBreak)
+	w.prevState = state
+	w.prevRune = r
+	return nil
+}
+
+// We classify each incoming rune into three kinds for easier handling.
+type kind int
+
+const (
+	kindEOL kind = iota
+	kindSpace
+	kindLetter
+)
+
+func runeKind(r rune) kind {
+	switch r {
+	case '\f', '\n', '\r', '\v', LineSeparator, ParagraphSeparator:
+		return kindEOL
+	}
+	if unicode.IsSpace(r) {
+		return kindSpace
+	}
+	return kindLetter
+}
+
+func (w *LineWriter) updateRune(r rune) bool {
+	forceLineBreak := false
+	switch kind := runeKind(r); kind {
+	case kindEOL:
+		// Update lastWordEnd if the last word just ended.
+		if w.newWordStart != -1 {
+			w.newWordStart = -1
+			w.lastWordEnd = w.lineBuf.ByteLen()
+		}
+		switch {
+		case w.prevRune == '\r' && r == '\n':
+			// Treat "\r\n" as a single EOL; we've already handled the logic for '\r',
+			// so there's nothing to do when we see '\n'.
+		case r == LineSeparator:
+			// Treat U+2028 as a pure line break; it's never a paragraph break.
+			forceLineBreak = true
+		case r == ParagraphSeparator || !w.inputLineHasLetter:
+			// The paragraph has just been terminated if we see an explicit U+2029, or
+			// if we see a blank line, which may contain spaces.
+			forceLineBreak = true
+			w.terminateParagraph = true
+		}
+		w.inputLineHasLetter = false
+	case kindSpace:
+		// Update lastWordEnd if the last word just ended.
+		if w.newWordStart != -1 {
+			w.newWordStart = -1
+			w.lastWordEnd = w.lineBuf.ByteLen()
+		}
+	case kindLetter:
+		// Update newWordStart if a new word just started.
+		if w.newWordStart == -1 {
+			w.newWordStart = w.lineBuf.ByteLen()
+		}
+		w.inputLineHasLetter = true
+		w.terminateParagraph = false
+	default:
+		panic(fmt.Errorf("textutil: updateRune unhandled kind %d", kind))
+	}
+	return forceLineBreak
+}
+
+// nextState returns the next state and whether we should break the line.
+//
+// Here's a handy table that describes all the scenarios in which we will line
+// break input text, grouped by the reason for the break.  The current position
+// is the last non-* rune in each pattern, which is where we decide to break.
+//
+//              w.prevState   Next state   Buffer reset
+//              -----------   ----------   ------------
+//   ===== Force line break (U+2028 / U+2029, blank line) =====
+//   a..*|***   *             wordWrap     empty
+//   a._.|***   *             wordWrap     empty
+//   a+**|***   *             wordWrap     empty
+//
+//   ===== verbatim: wait for any EOL =====
+//   _*.*|***   verbatim      wordWrap     empty
+//
+//   ===== wordWrap: switch to verbatim =====
+//   a._*|***   wordWrap      verbatim     empty
+//
+//   ===== wordWrap: line is too wide =====
+//   abc.|***   wordWrap      wordWrap     empty
+//   abcd|.**   wordWrap      wordWrap     empty
+//   abcd|e.*   wordWrap      wordWrap     empty
+//   a_cd|.**   wordWrap      wordWrap     empty
+//
+//   abc_|***   wordWrap      skipSpace    empty
+//   abcd|_**   wordWrap      skipSpace    empty
+//   abcd|e_*   wordWrap      skipSpace    empty
+//   a_cd|_**   wordWrap      skipSpace    empty
+//
+//   a_cd|e**   wordWrap      start        newWordStart
+//
+//   LEGEND
+//     abcde  Letter
+//     .      End-of-line
+//     +      End-of-line (only U+2028 / U+2029)
+//     _      Space
+//     *      Any rune (letter, line-end or space)
+//     |      Visual indication of width=4, has no width itself.
+//
+// Note that Flush calls behave exactly as if an explicit U+2028 line separator
+// were added to the end of all buffered data.
+func (w *LineWriter) nextState(r rune, forceLineBreak bool) (state, bool) {
+	if forceLineBreak {
+		return stateWordWrap, true
+	}
+	kind := runeKind(r)
+	// Handle non word-wrap states, which are easy.
+	switch w.prevState {
+	case stateVerbatim:
+		if kind == kindEOL {
+			return stateWordWrap, true
+		}
+		return stateVerbatim, false
+	case stateSkipSpace:
+		if kind == kindSpace {
+			return stateSkipSpace, false
+		}
+		return stateWordWrap, false
+	}
+	// Handle stateWordWrap, which is more complicated.
+
+	// Switch to the verbatim state when we see a space right after an EOL.
+	if runeKind(w.prevRune) == kindEOL && kind == kindSpace {
+		return stateVerbatim, true
+	}
+	// Break on EOL or space when the line is too wide.  See above table.
+	if w.width >= 0 && w.width <= w.lineBuf.RuneLen()+1 {
+		switch kind {
+		case kindEOL:
+			return stateWordWrap, true
+		case kindSpace:
+			return stateSkipSpace, true
+		}
+		// case kindLetter falls through
+	}
+	// Handle the newWordStart case in the above table.
+	if w.width >= 0 && w.width < w.lineBuf.RuneLen()+1 && w.newWordStart != w.lineStart {
+		return stateWordWrap, true
+	}
+	// Stay in the wordWrap state and don't break the line.
+	return stateWordWrap, false
+}
+
+func (w *LineWriter) writeLine() error {
+	if w.lastWordEnd == -1 {
+		// Don't write blank lines, but we must reset the line in case the paragraph
+		// has just been terminated.
+		w.resetLine()
+		return nil
+	}
+	// Write the line (without trailing spaces) followed by the line terminator.
+	line := w.lineBuf.Bytes()[:w.lastWordEnd]
+	if _, err := w.w.Write(line); err != nil {
+		return err
+	}
+	if _, err := w.w.Write(w.lineTerm); err != nil {
+		return err
+	}
+	// Reset the line buffer.
+	w.wroteFirstLine = true
+	w.paragraphLineIndex++
+	if w.newWordStart != -1 {
+		// If we have an unterminated new word, we must be in the newWordStart case
+		// in the table above.  Handle the special buffer reset here.
+		newWord := string(w.lineBuf.Bytes()[w.newWordStart:])
+		w.resetLine()
+		w.newWordStart = w.lineBuf.ByteLen()
+		w.lineBuf.WriteString(newWord)
+	} else {
+		w.resetLine()
+	}
+	return nil
+}
+
+func (w *LineWriter) resetLine() {
+	w.lineBuf.Reset()
+	w.newWordStart = -1
+	w.lastWordEnd = -1
+	// Write the paragraph separator if the previous paragraph has terminated.
+	// This consumes no runes from the line width.
+	if w.wroteFirstLine && w.terminateParagraph {
+		w.lineBuf.WriteString0Runes(w.paragraphSep)
+		w.paragraphLineIndex = 0
+	}
+	// Add indent; a non-empty indent consumes runes from the line width.
+	var indent string
+	switch {
+	case w.paragraphLineIndex < len(w.indents):
+		indent = w.indents[w.paragraphLineIndex]
+	case len(w.indents) > 0:
+		indent = w.indents[len(w.indents)-1]
+	}
+	w.lineBuf.WriteString(indent)
+	w.lineStart = w.lineBuf.ByteLen()
+}
+
+func (w *LineWriter) bufferRune(r rune, state state, lineBreak bool) {
+	// Never add leading spaces to the buffer in the wordWrap state.
+	wordWrapNoLeadingSpaces := state == stateWordWrap && !lineBreak
+	switch kind := runeKind(r); kind {
+	case kindEOL:
+		// When we're word-wrapping and we see a letter followed by EOL, we convert
+		// the EOL into a single space in the buffer, to break the previous word
+		// from the next word.
+		if wordWrapNoLeadingSpaces && runeKind(w.prevRune) == kindLetter {
+			w.lineBuf.WriteRune(' ')
+		}
+	case kindSpace:
+		if wordWrapNoLeadingSpaces || state == stateVerbatim {
+			w.lineBuf.WriteRune(r)
+		}
+	case kindLetter:
+		w.lineBuf.WriteRune(r)
+	default:
+		panic(fmt.Errorf("textutil: bufferRune unhandled kind %d", kind))
+	}
+}
diff --git a/lib/textutil/line_writer_test.go b/lib/textutil/line_writer_test.go
new file mode 100644
index 0000000..29db23f
--- /dev/null
+++ b/lib/textutil/line_writer_test.go
@@ -0,0 +1,268 @@
+package textutil
+
+import (
+	"bytes"
+	"io"
+	"strings"
+	"testing"
+)
+
+type lp struct {
+	line, para string
+}
+
+var (
+	allIndents  = [][]int{nil, {}, {1}, {2}, {1, 2}, {2, 1}}
+	allIndents1 = [][]int{{1}, {2}, {1, 2}, {2, 1}}
+)
+
+func TestLineWriter(t *testing.T) {
+	tests := []struct {
+		Width   int
+		Indents [][]int
+		In      string // See xlateIn for details on the format
+		Want    string // See xlateWant for details on the format
+	}{
+		// Completely blank input yields empty output.
+		{4, allIndents, "", ""},
+		{4, allIndents, " ", ""},
+		{4, allIndents, "  ", ""},
+		{4, allIndents, "   ", ""},
+		{4, allIndents, "    ", ""},
+		{4, allIndents, "     ", ""},
+		{4, allIndents, "      ", ""},
+		{4, allIndents, "F N  R   V    L     P      ", ""},
+		// Single words never get word-wrapped, even if they're long.
+		{4, allIndents, "a", "0a."},
+		{4, allIndents, "ab", "0ab."},
+		{4, allIndents, "abc", "0abc."},
+		{4, allIndents, "abcd", "0abcd."},
+		{4, allIndents, "abcde", "0abcde."},
+		{4, allIndents, "abcdef", "0abcdef."},
+		// Word-wrapping boundary conditions.
+		{4, allIndents, "abc ", "0abc."},
+		{4, allIndents, "abc  ", "0abc."},
+		{4, allIndents, "abcN", "0abc."},
+		{4, allIndents, "abcN ", "0abc."},
+		{4, allIndents, "abcd ", "0abcd."},
+		{4, allIndents, "abcd  ", "0abcd."},
+		{4, allIndents, "abcdN", "0abcd."},
+		{4, allIndents, "abcdN ", "0abcd."},
+		{4, [][]int{nil}, "a cd", "0a cd."},
+		{4, [][]int{nil}, "a cd ", "0a cd."},
+		{4, [][]int{nil}, "a cdN", "0a cd."},
+		{4, allIndents1, "a cd", "0a.1cd."},
+		{4, allIndents1, "a cd ", "0a.1cd."},
+		{4, allIndents1, "a cdN", "0a.1cd."},
+		{4, allIndents, "a cde", "0a.1cde."},
+		{4, allIndents, "a cde ", "0a.1cde."},
+		{4, allIndents, "a cdeN", "0a.1cde."},
+		{4, [][]int{nil}, "a  d", "0a  d."},
+		{4, [][]int{nil}, "a  d ", "0a  d."},
+		{4, [][]int{nil}, "a  dN", "0a  d."},
+		{4, allIndents1, "a  d", "0a.1d."},
+		{4, allIndents1, "a  d ", "0a.1d."},
+		{4, allIndents1, "a  dN", "0a.1d."},
+		{4, allIndents, "a  de", "0a.1de."},
+		{4, allIndents, "a  de ", "0a.1de."},
+		{4, allIndents, "a  deN", "0a.1de."},
+		// Multi-line word-wrapping boundary conditions.
+		{4, allIndents, "abc e", "0abc.1e."},
+		{4, allIndents, "abc.e", "0abc.1e."},
+		{4, allIndents, "abc efgh", "0abc.1efgh."},
+		{4, allIndents, "abc.efgh", "0abc.1efgh."},
+		{4, allIndents, "abc efghi", "0abc.1efghi."},
+		{4, allIndents, "abc.efghi", "0abc.1efghi."},
+		{4, [][]int{nil}, "abc e gh", "0abc.1e gh."},
+		{4, [][]int{nil}, "abc.e.gh", "0abc.1e gh."},
+		{4, allIndents1, "abc e gh", "0abc.1e.2gh."},
+		{4, allIndents1, "abc.e.gh", "0abc.1e.2gh."},
+		{4, allIndents, "abc e ghijk", "0abc.1e.2ghijk."},
+		{4, allIndents, "abc.e.ghijk", "0abc.1e.2ghijk."},
+		// Verbatim lines.
+		{4, allIndents, " b", "0 b."},
+		{4, allIndents, "  bc", "0  bc."},
+		{4, allIndents, "   bcd", "0   bcd."},
+		{4, allIndents, "    bcde", "0    bcde."},
+		{4, allIndents, "     bcdef", "0     bcdef."},
+		{4, allIndents, "      bcdefg", "0      bcdefg."},
+		{4, allIndents, " b de ghijk", "0 b de ghijk."},
+		// Verbatim lines before word-wrapped lines.
+		{4, allIndents, " b.vw yz", "0 b.1vw.2yz."},
+		{4, allIndents, "  bc.vw yz", "0  bc.1vw.2yz."},
+		{4, allIndents, "   bcd.vw yz", "0   bcd.1vw.2yz."},
+		{4, allIndents, "    bcde.vw yz", "0    bcde.1vw.2yz."},
+		{4, allIndents, "     bcdef.vw yz", "0     bcdef.1vw.2yz."},
+		{4, allIndents, "      bcdefg.vw yz", "0      bcdefg.1vw.2yz."},
+		{4, allIndents, " b de ghijk.vw yz", "0 b de ghijk.1vw.2yz."},
+		// Verbatim lines after word-wrapped lines.
+		{4, allIndents, "vw yz. b", "0vw.1yz.2 b."},
+		{4, allIndents, "vw yz.  bc", "0vw.1yz.2  bc."},
+		{4, allIndents, "vw yz.   bcd", "0vw.1yz.2   bcd."},
+		{4, allIndents, "vw yz.    bcde", "0vw.1yz.2    bcde."},
+		{4, allIndents, "vw yz.     bcdef", "0vw.1yz.2     bcdef."},
+		{4, allIndents, "vw yz.      bcdefg", "0vw.1yz.2      bcdefg."},
+		{4, allIndents, "vw yz. b de ghijk", "0vw.1yz.2 b de ghijk."},
+		// Verbatim lines between word-wrapped lines.
+		{4, allIndents, "vw yz. b.mn pq", "0vw.1yz.2 b.2mn.2pq."},
+		{4, allIndents, "vw yz.  bc.mn pq", "0vw.1yz.2  bc.2mn.2pq."},
+		{4, allIndents, "vw yz.   bcd.mn pq", "0vw.1yz.2   bcd.2mn.2pq."},
+		{4, allIndents, "vw yz.    bcde.mn pq", "0vw.1yz.2    bcde.2mn.2pq."},
+		{4, allIndents, "vw yz.     bcdef.mn pq", "0vw.1yz.2     bcdef.2mn.2pq."},
+		{4, allIndents, "vw yz.      bcdefg.mn pq", "0vw.1yz.2      bcdefg.2mn.2pq."},
+		{4, allIndents, "vw yz. b de ghijk.mn pq", "0vw.1yz.2 b de ghijk.2mn.2pq."},
+		// Multi-paragraphs via explicit U+2029, and multi-newline.
+		{4, allIndents, "ab de ghPij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.ghPij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de gh Pij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.gh Pij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de ghNNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.ghNNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de ghNNNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.ghNNNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de gh N Nij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.gh N Nij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de gh N N Nij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.gh N N Nij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		// Special-case /r/n is a single EOL, but may be combined.
+		{4, allIndents, "ab de ghRNij lm op", "0ab.1de.2gh.2ij.2lm.2op."},
+		{4, allIndents, "ab.de.ghRNij.lm.op", "0ab.1de.2gh.2ij.2lm.2op."},
+		{4, allIndents, "ab de gh RNij lm op", "0ab.1de.2gh.2ij.2lm.2op."},
+		{4, allIndents, "ab.de.gh RNij.lm.op", "0ab.1de.2gh.2ij.2lm.2op."},
+		{4, allIndents, "ab de ghRNRNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.ghRNRNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de gh RN RNij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.gh RN RNij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab de ghR Nij lm op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		{4, allIndents, "ab.de.ghR Nij.lm.op", "0ab.1de.2gh.:0ij.1lm.2op."},
+		// Line separator via explicit U+2028 ends lines, but not paragraphs.
+		{4, allIndents, "aLcd", "0a.1cd."},
+		{4, allIndents, "a Lcd", "0a.1cd."},
+		{4, allIndents, "aLLcd", "0a.1cd."},
+		{4, allIndents, "a LLcd", "0a.1cd."},
+		// 0 width ends up with one word per line, except verbatim lines.
+		{0, allIndents, "a c e", "0a.1c.2e."},
+		{0, allIndents, "a cd fghij", "0a.1cd.2fghij."},
+		{0, allIndents, "a. cd fghij.l n", "0a.1 cd fghij.2l.2n."},
+		// -1 width ends up with all words on same line, except verbatim lines.
+		{-1, allIndents, "a c e", "0a c e."},
+		{-1, allIndents, "a cd fghij", "0a cd fghij."},
+		{-1, allIndents, "a. cd fghij.l n", "0a.1 cd fghij.2l n."},
+	}
+	for _, test := range tests {
+		// Run with a variety of chunk sizes.
+		for _, sizes := range [][]int{nil, {1}, {2}, {1, 2}, {2, 1}} {
+			// Run with a variety of line terminators and paragraph separators.
+			for _, lp := range []lp{{}, {"\n", "\n"}, {"L", "P"}, {"LLL", "PPP"}} {
+				// Run with a variety of indents.
+				if len(test.Indents) == 0 {
+					t.Errorf("%d %q %q has no indents, use [][]int{nil} rather than nil", test.Width, test.In, test.Want)
+				}
+				for _, indents := range test.Indents {
+					var buf bytes.Buffer
+					w := newUTF8LineWriter(t, &buf, test.Width, lp, indents)
+					lineWriterWriteFlush(t, w, xlateIn(test.In), sizes)
+					if got, want := buf.String(), xlateWant(test.Want, lp, indents); got != want {
+						t.Errorf("%q sizes:%v lp:%q indents:%v got %q, want %q", test.In, sizes, lp, indents, got, want)
+					}
+				}
+			}
+		}
+	}
+}
+
+// xlateIn translates our test.In pattern into an actual input string to feed
+// into the writer.  The point is to make it easy to specify the various control
+// sequences in a single character, so it's easier to understand.
+func xlateIn(text string) string {
+	text = strings.Replace(text, "F", "\f", -1)
+	text = strings.Replace(text, "N", "\n", -1)
+	text = strings.Replace(text, ".", "\n", -1) // Also allow . for easier reading
+	text = strings.Replace(text, "R", "\r", -1)
+	text = strings.Replace(text, "V", "\v", -1)
+	text = strings.Replace(text, "L", "\u2028", -1)
+	text = strings.Replace(text, "P", "\u2029", -1)
+	return text
+}
+
+// xlateWant translates our test.Want pattern into an actual expected string to
+// compare against the output.  The point is to make it easy to read and write
+// the expected patterns, and to make it easy to test various indents.
+func xlateWant(text string, lp lp, indents []int) string {
+	// Dot "." and colon ":" in the want string indicate line terminators and
+	// paragraph separators, respectively.
+	line := lp.line
+	if line == "" {
+		line = "\n"
+	}
+	text = strings.Replace(text, ".", line, -1)
+	para := lp.para
+	if para == "" {
+		para = "\n"
+	}
+	text = strings.Replace(text, ":", para, -1)
+	// The numbers in the want string indicate paragraph line numbers, to make it
+	// easier to automatically replace for various indent configurations.
+	switch len(indents) {
+	case 0:
+		text = strings.Replace(text, "0", "", -1)
+		text = strings.Replace(text, "1", "", -1)
+		text = strings.Replace(text, "2", "", -1)
+	case 1:
+		text = strings.Replace(text, "0", spaces(indents[0]), -1)
+		text = strings.Replace(text, "1", spaces(indents[0]), -1)
+		text = strings.Replace(text, "2", spaces(indents[0]), -1)
+	case 2:
+		text = strings.Replace(text, "0", spaces(indents[0]), -1)
+		text = strings.Replace(text, "1", spaces(indents[1]), -1)
+		text = strings.Replace(text, "2", spaces(indents[1]), -1)
+	case 3:
+		text = strings.Replace(text, "0", spaces(indents[0]), -1)
+		text = strings.Replace(text, "1", spaces(indents[1]), -1)
+		text = strings.Replace(text, "2", spaces(indents[2]), -1)
+	}
+	return text
+}
+
+func spaces(count int) string {
+	return strings.Repeat(" ", count)
+}
+
+func newUTF8LineWriter(t *testing.T, buf io.Writer, width int, lp lp, indents []int) *LineWriter {
+	w := NewUTF8LineWriter(buf, width)
+	if lp.line != "" || lp.para != "" {
+		if err := w.SetLineTerminator(lp.line); err != nil {
+			t.Errorf("SetLineTerminator(%q) got %v, want nil", lp.line, err)
+		}
+		if err := w.SetParagraphSeparator(lp.para); err != nil {
+			t.Errorf("SetParagraphSeparator(%q) got %v, want nil", lp.para, err)
+		}
+	}
+	if indents != nil {
+		indentStrs := make([]string, len(indents))
+		for ix, indent := range indents {
+			indentStrs[ix] = spaces(indent)
+		}
+		if err := w.SetIndents(indentStrs...); err != nil {
+			t.Errorf("SetIndents(%v) got %v, want nil", indentStrs, err)
+		}
+	}
+	return w
+}
+
+func lineWriterWriteFlush(t *testing.T, w *LineWriter, text string, sizes []int) {
+	// Write chunks of different sizes until we've exhausted the input.
+	remain := text
+	for ix := 0; len(remain) > 0; ix++ {
+		var chunk []byte
+		chunk, remain = nextChunk(remain, sizes, ix)
+		got, err := w.Write(chunk)
+		if want := len(chunk); got != want || err != nil {
+			t.Errorf("%q Write(%q) got (%d,%v), want (%d,nil)", text, chunk, got, err, want)
+		}
+	}
+	// Flush the writer.
+	if err := w.Flush(); err != nil {
+		t.Errorf("%q Flush() got %v, want nil", text, err)
+	}
+}
diff --git a/lib/textutil/rune.go b/lib/textutil/rune.go
new file mode 100644
index 0000000..57db02e
--- /dev/null
+++ b/lib/textutil/rune.go
@@ -0,0 +1,119 @@
+package textutil
+
+import (
+	"bytes"
+)
+
+// TODO(toddw): Add UTF16 support.
+
+const (
+	EOF                = rune(-1) // Indicates the end of a rune stream.
+	LineSeparator      = '\u2028' // Unicode line separator rune.
+	ParagraphSeparator = '\u2029' // Unicode paragraph separator rune.
+)
+
+// RuneEncoder is the interface to an encoder of a stream of runes into
+// bytes.Buffer.
+type RuneEncoder interface {
+	// Encode encodes r into buf.
+	Encode(r rune, buf *bytes.Buffer)
+}
+
+// RuneStreamDecoder is the interface to a decoder of a contiguous stream of
+// runes.
+type RuneStreamDecoder interface {
+	// Next returns the next rune.  Invalid encodings are returned as U+FFFD.
+	// Returns EOF at the end of the stream.
+	Next() rune
+	// BytePos returns the current byte position in the original data buffer.
+	BytePos() int
+}
+
+// RuneChunkDecoder is the interface to a decoder of a stream of encoded runes
+// that may be arbitrarily chunked.
+//
+// Implementations of RuneChunkDecoder are commonly used to implement io.Writer
+// wrappers, to handle buffering when chunk boundaries may occur in the middle
+// of an encoded rune.
+type RuneChunkDecoder interface {
+	// Decode returns a RuneStreamDecoder that decodes the data chunk.  Call Next
+	// repeatedly on the returned stream until it returns EOF to decode the chunk.
+	Decode(chunk []byte) RuneStreamDecoder
+	// DecodeLeftover returns a RuneStreamDecoder that decodes leftover buffered
+	// data.  Call Next repeatedly on the returned stream until it returns EOF to
+	// ensure all buffered data is processed.
+	DecodeLeftover() RuneStreamDecoder
+}
+
+// RuneChunkWrite is a helper that calls d.Decode(data) and repeatedly calls
+// Next in a loop, calling fn for every rune that is decoded.  Returns the
+// number of bytes in data that were successfully processed.  If fn returns an
+// error, Write will return with that error, without processing any more data.
+//
+// This is a convenience for implementing io.Writer, given a RuneChunkDecoder.
+func RuneChunkWrite(d RuneChunkDecoder, fn func(rune) error, data []byte) (int, error) {
+	stream := d.Decode(data)
+	for r := stream.Next(); r != EOF; r = stream.Next() {
+		if err := fn(r); err != nil {
+			return stream.BytePos(), err
+		}
+	}
+	return stream.BytePos(), nil
+}
+
+// RuneChunkFlush is a helper that calls d.DecodeLeftover and repeatedly calls
+// Next in a loop, calling fn for every rune that is decoded.  If fn returns an
+// error, Flush will return with that error, without processing any more data.
+//
+// This is a convenience for implementing an additional Flush() call on an
+// implementation of io.Writer, given a RuneChunkDecoder.
+func RuneChunkFlush(d RuneChunkDecoder, fn func(rune) error) error {
+	stream := d.DecodeLeftover()
+	for r := stream.Next(); r != EOF; r = stream.Next() {
+		if err := fn(r); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// bytePos and runePos distinguish positions that are used in either domain;
+// we're trying to avoid silly mistakes like adding a bytePos to a runePos.
+type bytePos int
+type runePos int
+
+// byteRuneBuffer maintains a buffer with both byte and rune based positions.
+type byteRuneBuffer struct {
+	enc     RuneEncoder
+	buf     bytes.Buffer
+	runeLen runePos
+}
+
+func (b *byteRuneBuffer) ByteLen() bytePos { return bytePos(b.buf.Len()) }
+func (b *byteRuneBuffer) RuneLen() runePos { return b.runeLen }
+func (b *byteRuneBuffer) Bytes() []byte    { return b.buf.Bytes() }
+
+func (b *byteRuneBuffer) Reset() {
+	b.buf.Reset()
+	b.runeLen = 0
+}
+
+// WriteRune writes r into b.
+func (b *byteRuneBuffer) WriteRune(r rune) {
+	b.enc.Encode(r, &b.buf)
+	b.runeLen++
+}
+
+// WriteString writes str into b.
+func (b *byteRuneBuffer) WriteString(str string) {
+	for _, r := range str {
+		b.WriteRune(r)
+	}
+}
+
+// WriteString0Runes writes str into b, not incrementing the rune length.
+func (b *byteRuneBuffer) WriteString0Runes(str string) {
+	for _, r := range str {
+		b.enc.Encode(r, &b.buf)
+	}
+}
diff --git a/lib/textutil/utf8.go b/lib/textutil/utf8.go
new file mode 100644
index 0000000..349033e
--- /dev/null
+++ b/lib/textutil/utf8.go
@@ -0,0 +1,167 @@
+package textutil
+
+import (
+	"bytes"
+	"fmt"
+	"unicode/utf8"
+)
+
+// UTF8Encoder implements RuneEncoder for the UTF-8 encoding.
+type UTF8Encoder struct{}
+
+var _ RuneEncoder = UTF8Encoder{}
+
+// Encode encodes r into buf in the UTF-8 encoding.
+func (UTF8Encoder) Encode(r rune, buf *bytes.Buffer) { buf.WriteRune(r) }
+
+// UTF8ChunkDecoder implements RuneChunkDecoder for a stream of UTF-8 data that
+// is arbitrarily chunked.
+//
+// UTF-8 is a byte-wise encoding that may use multiple bytes to encode a single
+// rune.  This decoder buffers partial runes that have been split across chunks,
+// so that a full rune is returned when the subsequent data chunk is provided.
+//
+// This is commonly used to implement an io.Writer wrapper over UTF-8 text.  It
+// is useful since the data provided to Write calls may be arbitrarily chunked.
+//
+// The zero UTF8ChunkDecoder is a decoder with an empty buffer.
+type UTF8ChunkDecoder struct {
+	// The only state we keep is the last partial rune we've encountered.
+	partial    [utf8.UTFMax]byte
+	partialLen int
+}
+
+var _ RuneChunkDecoder = (*UTF8ChunkDecoder)(nil)
+
+// Decode returns a RuneStreamDecoder that decodes the data chunk.  Call Next
+// repeatedly on the returned stream until it returns EOF to decode the chunk.
+//
+// If the data is chunked in the middle of an encoded rune, the final partial
+// rune in the chunk will be buffered, and the next call to Decode will continue
+// by combining the buffered data with the next chunk.
+//
+// Invalid encodings are transformed into U+FFFD, one byte at a time.  See
+// unicode/utf8.DecodeRune for details.
+func (d *UTF8ChunkDecoder) Decode(chunk []byte) RuneStreamDecoder {
+	return &utf8Stream{d, chunk, 0}
+}
+
+// DecodeLeftover returns a RuneStreamDecoder that decodes leftover buffered
+// data.  Call Next repeatedly on the returned stream until it returns EOF to
+// ensure all buffered data is processed.
+//
+// Since the only data that is buffered is the final partial rune, the returned
+// RuneStreamDecoder will only contain U+FFFD or EOF.
+func (d *UTF8ChunkDecoder) DecodeLeftover() RuneStreamDecoder {
+	return &utf8LeftoverStream{d, 0}
+}
+
+// nextRune decodes the next rune, logically combining any previously buffered
+// data with the data chunk.  It returns the decoded rune and the byte size of
+// the data that was used for the decoding.
+//
+// The returned size may be > 0 even if the returned rune == EOF, if a partial
+// rune was detected and buffered.  The returned size may be 0 even if the
+// returned rune != EOF, if previously buffered data was decoded.
+func (d *UTF8ChunkDecoder) nextRune(data []byte) (rune, int) {
+	if d.partialLen > 0 {
+		return d.nextRunePartial(data)
+	}
+	r, size := utf8.DecodeRune(data)
+	if r == utf8.RuneError && !utf8.FullRune(data) {
+		// Initialize the partial rune buffer with remaining data.
+		d.partialLen = copy(d.partial[:], data)
+		return d.verifyPartial(d.partialLen, data)
+	}
+	return r, size
+}
+
+// nextRunePartial implements nextRune when there is a previously buffered
+// partial rune.
+func (d *UTF8ChunkDecoder) nextRunePartial(data []byte) (rune, int) {
+	// Append as much data as we can to the partial rune, and see if it's full.
+	oldLen := d.partialLen
+	d.partialLen += copy(d.partial[oldLen:], data)
+	if !utf8.FullRune(d.partial[:d.partialLen]) {
+		// We still don't have a full rune - keep waiting.
+		return d.verifyPartial(d.partialLen-oldLen, data)
+	}
+	// We finally have a full rune.
+	r, size := utf8.DecodeRune(d.partial[:d.partialLen])
+	if size < oldLen {
+		// This occurs when we have a multi-byte rune that has the right number of
+		// bytes, but is an invalid code point.
+		//
+		// Say oldLen=2, and we just received the third byte of a 3-byte rune which
+		// isn't a UTF-8 trailing byte.  In this case utf8.DecodeRune returns U+FFFD
+		// and size=1, to indicate we should skip the first byte.
+		//
+		// We shift the unread portion of the old partial data forward, and update
+		// the partial len so that it's strictly decreasing.  The strictly
+		// decreasing property isn't necessary for correctness, but helps avoid
+		// repeatedly copying data into the partial buffer unecessarily.
+		copy(d.partial[:], d.partial[size:oldLen])
+		d.partialLen = oldLen - size
+		return r, 0
+	}
+	// We've used all the old buffered data; start decoding directly from data.
+	d.partialLen = 0
+	return r, size - oldLen
+}
+
+// verifyPartial is called when we don't have a full rune, and ncopy bytes have
+// been copied from data into the decoder partial rune buffer.  We expect that
+// all data has been buffered and we return EOF and the total size of the data.
+func (d *UTF8ChunkDecoder) verifyPartial(ncopy int, data []byte) (rune, int) {
+	if ncopy < len(data) {
+		// Something's very wrong if we managed to fill d.partial without copying
+		// all the data; any sequence of utf8.UTFMax bytes must be a full rune.
+		panic(fmt.Errorf("UTF8ChunkDecoder: partial rune %v with leftover data %v", d.partial[:d.partialLen], data[ncopy:]))
+	}
+	return EOF, len(data)
+}
+
+// utf8Stream implements UTF8ChunkDecoder.Decode.
+type utf8Stream struct {
+	d    *UTF8ChunkDecoder
+	data []byte
+	pos  int
+}
+
+var _ RuneStreamDecoder = (*utf8Stream)(nil)
+
+func (s *utf8Stream) Next() rune {
+	if s.pos == len(s.data) {
+		return EOF
+	}
+	r, size := s.d.nextRune(s.data[s.pos:])
+	s.pos += size
+	return r
+}
+
+func (s *utf8Stream) BytePos() int {
+	return s.pos
+}
+
+// utf8LeftoverStream implements UTF8ChunkDecoder.DecodeLeftover.
+type utf8LeftoverStream struct {
+	d   *UTF8ChunkDecoder
+	pos int
+}
+
+var _ RuneStreamDecoder = (*utf8LeftoverStream)(nil)
+
+func (s *utf8LeftoverStream) Next() rune {
+	if s.d.partialLen == 0 {
+		return EOF
+	}
+	r, size := utf8.DecodeRune(s.d.partial[:s.d.partialLen])
+	copy(s.d.partial[:], s.d.partial[size:])
+	s.d.partialLen -= size
+	s.pos += size
+	return r
+}
+
+func (s *utf8LeftoverStream) BytePos() int {
+	return s.pos
+}
diff --git a/lib/textutil/utf8_test.go b/lib/textutil/utf8_test.go
new file mode 100644
index 0000000..7517bef
--- /dev/null
+++ b/lib/textutil/utf8_test.go
@@ -0,0 +1,110 @@
+package textutil
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestUTF8ChunkDecoder(t *testing.T) {
+	r2 := "Δ"
+	r3 := "王"
+	r4 := "\U0001F680"
+	tests := []struct {
+		Text string
+		Want []rune
+	}{
+		{"", nil},
+		{"a", []rune{'a'}},
+		{"abc", []rune{'a', 'b', 'c'}},
+		{"abc def ghi", []rune{'a', 'b', 'c', ' ', 'd', 'e', 'f', ' ', 'g', 'h', 'i'}},
+		// 2-byte runes.
+		{"ΔΘΠΣΦ", []rune{'Δ', 'Θ', 'Π', 'Σ', 'Φ'}},
+		// 3-byte runes.
+		{"王普澤世界", []rune{'王', '普', '澤', '世', '界'}},
+		// 4-byte runes.
+		{"\U0001F680\U0001F681\U0001F682\U0001F683", []rune{'\U0001F680', '\U0001F681', '\U0001F682', '\U0001F683'}},
+		// Mixed-bytes.
+		{"aΔ王\U0001F680普Θb", []rune{'a', 'Δ', '王', '\U0001F680', '普', 'Θ', 'b'}},
+		// Error runes translated to U+FFFD.
+		{"\uFFFD", []rune{'\uFFFD'}},
+		{"a\uFFFDb", []rune{'a', '\uFFFD', 'b'}},
+		{"\xFF", []rune{'\uFFFD'}},
+		{"a\xFFb", []rune{'a', '\uFFFD', 'b'}},
+		// Multi-byte full runes.
+		{r2, []rune{[]rune(r2)[0]}},
+		{r3, []rune{[]rune(r3)[0]}},
+		{r4, []rune{[]rune(r4)[0]}},
+		// Partial runes translated to U+FFFD.
+		{r2[:1], []rune{'\uFFFD'}},
+		{r3[:1], []rune{'\uFFFD'}},
+		{r3[:2], []rune{'\uFFFD', '\uFFFD'}},
+		{r4[:1], []rune{'\uFFFD'}},
+		{r4[:2], []rune{'\uFFFD', '\uFFFD'}},
+		{r4[:3], []rune{'\uFFFD', '\uFFFD', '\uFFFD'}},
+		// Leading partial runes translated to U+FFFD.
+		{r2[:1] + "b", []rune{'\uFFFD', 'b'}},
+		{r3[:1] + "b", []rune{'\uFFFD', 'b'}},
+		{r3[:2] + "b", []rune{'\uFFFD', '\uFFFD', 'b'}},
+		{r4[:1] + "b", []rune{'\uFFFD', 'b'}},
+		{r4[:2] + "b", []rune{'\uFFFD', '\uFFFD', 'b'}},
+		{r4[:3] + "b", []rune{'\uFFFD', '\uFFFD', '\uFFFD', 'b'}},
+		// Trailing partial runes translated to U+FFFD.
+		{"a" + r2[:1], []rune{'a', '\uFFFD'}},
+		{"a" + r3[:1], []rune{'a', '\uFFFD'}},
+		{"a" + r3[:2], []rune{'a', '\uFFFD', '\uFFFD'}},
+		{"a" + r4[:1], []rune{'a', '\uFFFD'}},
+		{"a" + r4[:2], []rune{'a', '\uFFFD', '\uFFFD'}},
+		{"a" + r4[:3], []rune{'a', '\uFFFD', '\uFFFD', '\uFFFD'}},
+		// Bracketed partial runes translated to U+FFFD.
+		{"a" + r2[:1] + "b", []rune{'a', '\uFFFD', 'b'}},
+		{"a" + r3[:1] + "b", []rune{'a', '\uFFFD', 'b'}},
+		{"a" + r3[:2] + "b", []rune{'a', '\uFFFD', '\uFFFD', 'b'}},
+		{"a" + r4[:1] + "b", []rune{'a', '\uFFFD', 'b'}},
+		{"a" + r4[:2] + "b", []rune{'a', '\uFFFD', '\uFFFD', 'b'}},
+		{"a" + r4[:3] + "b", []rune{'a', '\uFFFD', '\uFFFD', '\uFFFD', 'b'}},
+	}
+	for _, test := range tests {
+		// Run with a variety of chunk sizes.
+		for _, sizes := range [][]int{nil, {1}, {2}, {1, 2}, {2, 1}, {3}, {1, 2, 3}} {
+			got := runeChunkWriteFlush(t, test.Text, sizes)
+			if want := test.Want; !reflect.DeepEqual(got, want) {
+				t.Errorf("%q got %v, want %v", test.Text, got, want)
+			}
+		}
+	}
+}
+
+func runeChunkWriteFlush(t *testing.T, text string, sizes []int) []rune {
+	var dec UTF8ChunkDecoder
+	var runes []rune
+	addRune := func(r rune) error {
+		runes = append(runes, r)
+		return nil
+	}
+	// Write chunks of different sizes until we've exhausted the input text.
+	remain := text
+	for ix := 0; len(remain) > 0; ix++ {
+		var chunk []byte
+		chunk, remain = nextChunk(remain, sizes, ix)
+		got, err := RuneChunkWrite(&dec, addRune, chunk)
+		if want := len(chunk); got != want || err != nil {
+			t.Errorf("%q RuneChunkWrite(%q) got (%d,%v), want (%d,nil)", text, chunk, got, err, want)
+		}
+	}
+	// Flush the decoder.
+	if err := RuneChunkFlush(&dec, addRune); err != nil {
+		t.Errorf("%q RuneChunkFlush got %v, want nil", text, err)
+	}
+	return runes
+}
+
+func nextChunk(text string, sizes []int, index int) (chunk []byte, remain string) {
+	if len(sizes) == 0 {
+		return []byte(text), ""
+	}
+	size := sizes[index%len(sizes)]
+	if size >= len(text) {
+		return []byte(text), ""
+	}
+	return []byte(text[:size]), text[size:]
+}
diff --git a/lib/textutil/util.go b/lib/textutil/util.go
new file mode 100644
index 0000000..85402de
--- /dev/null
+++ b/lib/textutil/util.go
@@ -0,0 +1,40 @@
+package textutil
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+// TerminalSize returns the dimensions of the terminal, if it's available from
+// the OS, otherwise returns an error.
+func TerminalSize() (row, col int, _ error) {
+	// Try getting the terminal size from stdout, stderr and stdin respectively.
+	// We try each of these in turn because the mechanism we're using fails if any
+	// of the fds is redirected on the command line.  E.g. "tool | less" redirects
+	// the stdout of tool to the stdin of less, and will mean tool cannot retrieve
+	// the terminal size from stdout.
+	//
+	// TODO(toddw): This probably only works on some linux / unix variants; add
+	// build tags and support different platforms.
+	if row, col, err := terminalSize(syscall.Stdout); err == nil {
+		return row, col, err
+	}
+	if row, col, err := terminalSize(syscall.Stderr); err == nil {
+		return row, col, err
+	}
+	return terminalSize(syscall.Stdin)
+}
+
+func terminalSize(fd int) (int, int, error) {
+	var ws winsize
+	if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws))); err != 0 {
+		return 0, 0, err
+	}
+	return int(ws.row), int(ws.col), nil
+}
+
+// winsize must correspond to the struct defined in "sys/ioctl.h".  Do not
+// export this struct; it's a platform-specific implementation detail.
+type winsize struct {
+	row, col, xpixel, ypixel uint16
+}