Browse Source

Make ttyshots scripted and reproducible

The main benefits of this change are:

1) It uses a hermetic "home" directory with a known command and location
   history. Which means it no longer depends on the interactive history
   and directory layout of the person creating the ttyshot. Which also
   means it no longer leaks the private history of anyone creating a
   ttyshot. This produces reproducible results when updating ttyshots.

2) The user no longer has to augment the ttyshot by manually adding the
   output of the commands to the generated HTML file. A process that is
   error prone. The output of the commands that generate the ttyshot is
   now captured and automatically included in the resulting HTML.

3) It makes it trivial to recreate every ttyshot. Simply execute these
   commands:

   ```
   make ttyshot
   for f [website/ttyshot/**.spec] { put $f; ./ttyshot $f }
   ```

4) It makes it easy to introduce new "ttyshot" images by creating a
   shell session "spec" file. This makes it easy to replace the existing
   "```elvish-transcript...```" examples with ttyshots in order to
   ensure a consistent representation and visual consistency with the
   transcripts that are currently generated as ttyshots.

The downside of this change is the introduction of a dependency on the
Tmux application. But that seems reasonable since Tmux is a mature
application available on Linux, macOS, BSD, and probably every other
UNIX like OS we care about. Note that generating the Elvish
documentation already depends on similar apps such as Pandoc.

Related #1459
Kurtis Rader 2 years ago
parent
commit
4b4726b9a6
56 changed files with 1235 additions and 215 deletions
  1. 2 0
      .gitignore
  2. 5 1
      Makefile
  3. 2 2
      pkg/edit/key_binding.go
  4. 3 2
      pkg/ui/color.go
  5. 1 1
      pkg/ui/key.go
  6. 4 0
      pkg/ui/parse_sgr.go
  7. 7 3
      pkg/ui/style.go
  8. 6 4
      pkg/ui/styling.go
  9. 3 3
      pkg/ui/styling_test.go
  10. 546 0
      website/cmd/ttyshot/ttyshot.go
  11. 1 0
      website/go.mod
  12. 2 0
      website/go.sum
  13. 8 14
      website/learn/tour.md
  14. 1 1
      website/ref/edit.md
  15. 94 33
      website/style.css
  16. 18 0
      website/tools/cp-elvish.sh
  17. 87 20
      website/ttyshot/README.md
  18. 0 5
      website/ttyshot/completion-mode.html
  19. 16 15
      website/ttyshot/control-structures.html
  20. 11 0
      website/ttyshot/control-structures.spec
  21. 2 2
      website/ttyshot/fundamentals/history-1.html
  22. 4 0
      website/ttyshot/fundamentals/history-1.spec
  23. 2 2
      website/ttyshot/fundamentals/history-2.html
  24. 5 0
      website/ttyshot/fundamentals/history-2.spec
  25. 16 15
      website/ttyshot/histlist-mode.html
  26. 4 0
      website/ttyshot/histlist-mode.spec
  27. 16 15
      website/ttyshot/location-mode.html
  28. 2 0
      website/ttyshot/location-mode.spec
  29. 16 15
      website/ttyshot/navigation-mode.html
  30. 4 0
      website/ttyshot/navigation-mode.spec
  31. 16 14
      website/ttyshot/pipelines.html
  32. 25 0
      website/ttyshot/pipelines.spec
  33. 6 0
      website/ttyshot/ref/edit/completion-mode.html
  34. 12 0
      website/ttyshot/ref/edit/completion-mode.spec
  35. 4 0
      website/ttyshot/tmux.rc
  36. 5 4
      website/ttyshot/tour/completion-filter.html
  37. 7 0
      website/ttyshot/tour/completion-filter.spec
  38. 12 6
      website/ttyshot/tour/completion.html
  39. 6 0
      website/ttyshot/tour/completion.spec
  40. 16 7
      website/ttyshot/tour/history-list.html
  41. 3 0
      website/ttyshot/tour/history-list.spec
  42. 2 2
      website/ttyshot/tour/history-walk-prefix.html
  43. 6 0
      website/ttyshot/tour/history-walk-prefix.spec
  44. 2 2
      website/ttyshot/tour/history-walk.html
  45. 3 0
      website/ttyshot/tour/history-walk.spec
  46. 8 5
      website/ttyshot/tour/lastcmd.html
  47. 6 0
      website/ttyshot/tour/lastcmd.spec
  48. 6 7
      website/ttyshot/tour/location-filter.html
  49. 4 0
      website/ttyshot/tour/location-filter.spec
  50. 11 7
      website/ttyshot/tour/location.html
  51. 3 0
      website/ttyshot/tour/location.spec
  52. 16 7
      website/ttyshot/tour/navigation.html
  53. 5 0
      website/ttyshot/tour/navigation.spec
  54. 7 1
      website/ttyshot/tour/unicode-prompts.html
  55. 11 0
      website/ttyshot/tour/unicode-prompts.spec
  56. 145 0
      website/ttyshot/ttyshot.rc

+ 2 - 0
.gitignore

@@ -25,3 +25,5 @@ _testmain.go
 cover
 /_bin/
 /elvish
+/ttyshot
+/website/ttyshot/**/*.raw

+ 5 - 1
Makefile

@@ -45,8 +45,12 @@ lint:
 codespell:
 	codespell --skip .git
 
+ttyshot: website/tools/ttyshot.go
+	make -C website tools/ttyshot.bin
+
+
 check-content:
 	./tools/check-content.sh
 
 .SILENT: checkstyle-go checkstyle-md lint
-.PHONY: default get generate test cover style checkstyle checkstyle-go checkstyle-md lint codespell check-content
+.PHONY: default get generate test cover style checkstyle checkstyle-go checkstyle-md lint codespell check-content ttyshot

+ 2 - 2
pkg/edit/key_binding.go

@@ -51,8 +51,8 @@ func indexLayeredBindings(k ui.Key, maps ...bindingsMap) eval.Callable {
 		}
 	}
 	for _, m := range maps {
-		if m.HasKey(ui.Default) {
-			return m.GetKey(ui.Default)
+		if m.HasKey(ui.DefaultKey) {
+			return m.GetKey(ui.DefaultKey)
 		}
 	}
 	return nil

+ 3 - 2
pkg/ui/color.go

@@ -23,6 +23,7 @@ var (
 	Magenta Color = ansiColor(5)
 	Cyan    Color = ansiColor(6)
 	White   Color = ansiColor(7)
+	Default Color = ansiColor(9)
 
 	BrightBlack   Color = ansiBrightColor(0)
 	BrightRed     Color = ansiBrightColor(1)
@@ -41,8 +42,7 @@ func XTerm256Color(i uint8) Color { return xterm256Color(i) }
 func TrueColor(r, g, b uint8) Color { return trueColor{r, g, b} }
 
 var colorNames = []string{
-	"black", "red", "green", "yellow",
-	"blue", "magenta", "cyan", "white",
+	"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "unknown", "default",
 }
 
 var colorByName = map[string]Color{
@@ -54,6 +54,7 @@ var colorByName = map[string]Color{
 	"magenta": Magenta,
 	"cyan":    Cyan,
 	"white":   White,
+	"default": Default,
 
 	"bright-black":   BrightBlack,
 	"bright-red":     BrightRed,

+ 1 - 1
pkg/ui/key.go

@@ -26,7 +26,7 @@ func K(r rune, mods ...Mod) Key {
 }
 
 // Default is used in the key binding table to indicate a default binding.
-var Default = Key{DefaultBindingRune, 0}
+var DefaultKey = Key{DefaultBindingRune, 0}
 
 // Mod represents a modifier key.
 type Mod byte

+ 4 - 0
pkg/ui/parse_sgr.go

@@ -131,6 +131,10 @@ func StylingFromSGR(s string) Styling {
 			moreStyling = Bg(trueColor{
 				uint8(codes[2]), uint8(codes[3]), uint8(codes[4])})
 			consume = 5
+		case code == 39:
+			moreStyling = ExplicitFgDefault
+		case code == 49:
+			moreStyling = ExplicitBgDefault
 		default:
 			// Do nothing; skip this code
 		}

+ 7 - 3
pkg/ui/style.go

@@ -17,8 +17,8 @@ type Style struct {
 	Inverse    bool
 }
 
-// SGR returns SGR sequence for the style.
-func (s Style) SGR() string {
+// SGRValues returns an array of the individual SGR values for the style.
+func (s Style) SGRValues() []string {
 	var sgr []string
 
 	addIf := func(b bool, code string) {
@@ -38,8 +38,12 @@ func (s Style) SGR() string {
 	if s.Background != nil {
 		sgr = append(sgr, s.Background.bgSGR())
 	}
+	return sgr
+}
 
-	return strings.Join(sgr, ";")
+// SGR returns, for the Style, a string that can be included in an ANSI X3.64 SGR sequence.
+func (s Style) SGR() string {
+	return strings.Join(s.SGRValues(), ";")
 }
 
 // MergeFromOptions merges all recognized values from a map to the current

+ 6 - 4
pkg/ui/styling.go

@@ -41,7 +41,8 @@ func Stylings(ts ...Styling) Styling { return jointStyling(ts) }
 var (
 	Reset Styling = reset{}
 
-	FgDefault Styling = setForeground{nil}
+	ImplicitFgDefault Styling = setForeground{nil}
+	ExplicitFgDefault Styling = setForeground{Default}
 
 	FgBlack   Styling = setForeground{Black}
 	FgRed     Styling = setForeground{Red}
@@ -61,7 +62,8 @@ var (
 	FgBrightCyan    Styling = setForeground{BrightCyan}
 	FgBrightWhite   Styling = setForeground{BrightWhite}
 
-	BgDefault Styling = setBackground{nil}
+	ImplicitBgDefault Styling = setBackground{nil}
+	ExplicitBgDefault Styling = setBackground{Default}
 
 	BgBlack   Styling = setBackground{Black}
 	BgRed     Styling = setBackground{Red}
@@ -182,13 +184,13 @@ var boolFields = map[string]boolField{
 func parseOneStyling(name string) Styling {
 	switch {
 	case name == "default" || name == "fg-default":
-		return FgDefault
+		return ExplicitFgDefault
 	case strings.HasPrefix(name, "fg-"):
 		if color := parseColor(name[len("fg-"):]); color != nil {
 			return setForeground{color}
 		}
 	case name == "bg-default":
-		return BgDefault
+		return ExplicitBgDefault
 	case strings.HasPrefix(name, "bg-"):
 		if color := parseColor(name[len("bg-"):]); color != nil {
 			return setBackground{color}

+ 3 - 3
pkg/ui/styling_test.go

@@ -68,12 +68,12 @@ var parseStylingTests = []struct {
 	s           string
 	wantStyling Styling
 }{
-	{"default", FgDefault},
+	{"default", ExplicitFgDefault},
 	{"red", FgRed},
-	{"fg-default", FgDefault},
+	{"fg-default", ExplicitFgDefault},
 	{"fg-red", FgRed},
 
-	{"bg-default", BgDefault},
+	{"bg-default", ExplicitBgDefault},
 	{"bg-red", BgRed},
 
 	{"bold", Bold},

+ 546 - 0
website/cmd/ttyshot/ttyshot.go

@@ -0,0 +1,546 @@
+// Generate a ttyshot HTML image from a ttyshot specification.
+//
+// Usage: ./ttyshot website/ttyshot/*.spec
+//
+// You can recreate all the ttyshots by running the following from the project top-level directory:
+//
+//   make ttyshot
+//   for f [website/ttyshot/**.spec] { put $f; ./ttyshot $f >/dev/tty 2>&1 }
+//
+// This assumes working `elvish` and `tmux` programs in $E:PATH.
+//
+package main
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"html"
+	"log"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/creack/pty"
+	"src.elv.sh/pkg/env"
+	"src.elv.sh/pkg/ui"
+)
+
+// Note: This depends on a custom tmux.rc that disables the status line. Otherwise the simulated tty
+// would need to have 17 rows to achieve the desired snapshot dimensions.
+const (
+	terminalRows = 16
+	terminalCols = 60
+)
+
+// Operations for driving a demo ttyshot.
+const (
+	opEnter          = iota // enable implicit Enter key and send an Enter key
+	opNoEnter               // inhibit implicit Enter key
+	opTrimEmptyLines        // trim trailing empty lines -- can occur anywhere in the spec
+	opUp                    // send Up arrow sequence
+	opDown                  // send Down arrow sequence
+	opRight                 // send Right arrow sequence
+	opLeft                  // send Left arrow sequence
+	opText                  // send the provided text, optionally followed by Enter
+	opAlt                   // send an alt sequence
+	opCtrl                  // send a control character
+	opSleep                 // sleep for the specified duration
+	opWaitForPrompt         // wait for the expected "<n>" (command number) in the next prompt
+	opWaitForRegexp         // wait for sequence of bytes matching the regexp
+	opWaitForString         // wait for the literal sequence of bytes
+)
+
+type demoOp struct {
+	what int
+	val  any
+}
+
+var promptRe = regexp.MustCompile(`^\[\d+\]$`)
+var promptFmt = "[%d]"
+
+func parseDirective(directive []byte) (demoOp, error) {
+	if bytes.HasPrefix(directive, []byte("sleep ")) {
+		duration, err := time.ParseDuration(string(directive[6:]) + "s")
+		if err != nil {
+			return demoOp{}, err
+		}
+		return demoOp{opSleep, duration}, nil
+	}
+
+	if bytes.Equal(directive, []byte("no-enter")) {
+		return demoOp{opNoEnter, nil}, nil
+	}
+
+	if bytes.Equal(directive, []byte("trim-empty")) {
+		return demoOp{opTrimEmptyLines, nil}, nil
+	}
+
+	if bytes.Equal(directive, []byte("enter")) {
+		return demoOp{opEnter, nil}, nil
+	}
+
+	// Tab is frequently used so it's useful to support it as a directive rather than requiring
+	// `//ctrl I`.
+	if bytes.Equal(directive, []byte("tab")) {
+		return demoOp{opCtrl, byte('I')}, nil
+	}
+
+	if bytes.HasPrefix(directive, []byte("ctrl ")) {
+		if len(directive) != 6 {
+			return demoOp{}, errors.New("invalid ctrl directive: " + string(directive))
+		}
+		return demoOp{opCtrl, directive[5]}, nil
+	}
+
+	if bytes.HasPrefix(directive, []byte("alt ")) {
+		if len(directive) != 5 {
+			return demoOp{}, errors.New("invalid alt directive: " + string(directive))
+		}
+		return demoOp{opAlt, directive[4]}, nil
+	}
+
+	if bytes.Equal(directive, []byte("prompt")) {
+		return demoOp{opWaitForPrompt, nil}, nil
+	}
+
+	if bytes.Equal(directive, []byte("up")) {
+		return demoOp{opUp, nil}, nil
+	}
+
+	if bytes.Equal(directive, []byte("down")) {
+		return demoOp{opDown, nil}, nil
+	}
+
+	if bytes.Equal(directive, []byte("right")) {
+		return demoOp{opRight, nil}, nil
+	}
+
+	if bytes.Equal(directive, []byte("left")) {
+		return demoOp{opLeft, nil}, nil
+	}
+
+	if bytes.HasPrefix(directive, []byte("wait-for-re ")) {
+		re, err := regexp.Compile(string(directive[12:]))
+		if err != nil {
+			return demoOp{}, errors.New("invalid wait-for-re value: " + string(directive[12:]))
+		}
+		return demoOp{opWaitForRegexp, re}, nil
+	}
+
+	if bytes.HasPrefix(directive, []byte("wait-for-str ")) {
+		return demoOp{opWaitForString, directive[13:]}, nil
+	}
+
+	return demoOp{}, errors.New("unrecognized directive: " + string(directive))
+}
+
+func parseSpec(content []byte) ([]demoOp, error) {
+	lines := bytes.Split(content, []byte{'\n'})
+	ops := make([]demoOp, 1, len(lines)+2)
+	ops[0] = demoOp{opWaitForPrompt, nil}
+
+	for _, line := range lines {
+		if len(line) == 0 {
+			continue // ignore empty lines
+		}
+		if bytes.HasPrefix(line, []byte("//")) {
+			directive, err := parseDirective(line[2:])
+			if err != nil {
+				return ops, err
+			}
+			ops = append(ops, directive)
+		} else {
+			ops = append(ops, demoOp{opText, line})
+		}
+	}
+
+	return ops, nil
+}
+
+func sgrTextToHTML(ttyshot string) string {
+	t := ui.ParseSGREscapedText(ttyshot)
+
+	var sb strings.Builder
+	for _, c := range t {
+		var classes []string
+		for _, c := range c.Style.SGRValues() {
+			classes = append(classes, "sgr-"+c)
+		}
+		text, newline := c.Text, false
+		if c.Text[len(c.Text)-1] == '\n' {
+			newline = true
+			text = c.Text[:len(c.Text)-1]
+		}
+		// This "undoes" the ugly hack in website/ttyshot/ttyshot.rc that requires we gratuitously
+		// modify the style of the prompt to make it practical to recognize a prompt when executing
+		// a ttyshot script.
+		if promptRe.Match([]byte(text)) {
+			// It looks like the text might be a shell prompt. Check if the styling matches the case
+			// that needs to be fixed.
+			if len(classes) >= 1 && classes[0] == "sgr-90" {
+				classes[0] = "sgr-30" // fg-bright-black => fg-black
+			}
+		}
+
+		fmt.Fprintf(&sb,
+			`<span class="%s">%s</span>`, strings.Join(classes, " "), html.EscapeString(text))
+		if newline {
+			sb.Write([]byte{'\n'})
+		}
+	}
+
+	return sb.String()
+}
+
+func waitForOutput(ttyOutput chan byte, expected string, matcher func([]byte) bool) []byte {
+	text := make([]byte, 0, 4096)
+	// It shouldn't take more than a couple of seconds to see the expected output so use a timeout
+	// an order of magnitude longer to allow for overloaded systems.
+	timeout := time.After(30 * time.Second)
+	for {
+		var newByte byte
+		select {
+		case newByte = <-ttyOutput:
+		case <-timeout:
+			fmt.Fprintf(os.Stderr, "Timeout waiting for text matching: %q\n", expected)
+			fmt.Fprintf(os.Stderr, "This is what we've captured so far:\n%q\n", text)
+			os.Exit(3)
+		}
+		text = append(text, newByte)
+		if matcher(text) {
+			break
+		}
+	}
+	return text
+}
+
+var cmdNum int = 0
+
+func executeScript(script []demoOp, master *os.File, ttyOutput chan byte) (bool, error) {
+	trimEmptyLines := false
+	implicitEnter := true
+	for _, op := range script {
+		switch op.what {
+		case opText:
+			text := op.val.([]byte)
+			master.Write(text)
+			if implicitEnter {
+				master.Write([]byte{'\r'})
+			}
+		case opAlt:
+			master.Write([]byte{'\033', op.val.(byte)})
+		case opCtrl:
+			master.Write([]byte{op.val.(byte) & 0x1F})
+		case opEnter:
+			master.Write([]byte{'\r'})
+			implicitEnter = true
+		case opUp:
+			master.Write([]byte{'\033', '[', 'A'})
+		case opDown:
+			master.Write([]byte{'\033', '[', 'B'})
+		case opRight:
+			master.Write([]byte{'\033', '[', 'C'})
+		case opLeft:
+			master.Write([]byte{'\033', '[', 'D'})
+		case opNoEnter:
+			implicitEnter = false
+		case opSleep:
+			time.Sleep(op.val.(time.Duration))
+		case opWaitForPrompt:
+			cmdNum++
+			expected := fmt.Sprintf(promptFmt, cmdNum)
+			waitForOutput(ttyOutput, expected,
+				func(content []byte) bool { return bytes.Contains(content, []byte(expected)) })
+		case opWaitForString:
+			expected := op.val.([]byte)
+			waitForOutput(ttyOutput, string(expected),
+				func(content []byte) bool { return bytes.Contains(content, expected) })
+		case opWaitForRegexp:
+			expected := op.val.(*regexp.Regexp)
+			waitForOutput(ttyOutput, expected.String(),
+				func(content []byte) bool { return expected.Match(content) })
+		case opTrimEmptyLines:
+			trimEmptyLines = true
+		default:
+			panic("unhandled op")
+		}
+	}
+	return trimEmptyLines, nil
+}
+
+func spawnElvish(homePath, dbPath string, slave *os.File,
+	ttyImage *bytes.Buffer) (chan bool, chan bool) {
+	cwd, err := os.Getwd()
+	if err != nil {
+		log.Fatal("unable to determine the CWD: " + err.Error())
+	}
+
+	triggerTtyCapture := make(chan bool)
+	ttyCaptureDone := make(chan bool)
+
+	// Construct a file name for the tmux and Elvish daemon socket files in the temp home path.
+	tmuxSock := filepath.Join(homePath, "tmp", "tmux.sock")
+	elvSock := filepath.Join(homePath, "tmp", "elv.sock")
+
+	elvishPath, err := exec.LookPath("elvish")
+	if err != nil {
+		log.Fatal("unable to find elvish: " + err.Error())
+	}
+	tmuxPath, err := exec.LookPath("tmux")
+	if err != nil {
+		log.Fatal("unable to find tmux: " + err.Error())
+	}
+
+	devnul, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
+	if err != nil {
+		log.Fatal("unable to open os.DevNull: " + err.Error())
+	}
+
+	daemonCmd := exec.Cmd{
+		Path:   elvishPath,
+		Args:   []string{"elvish", "-daemon", "-sock", elvSock, "-db", dbPath},
+		Stdin:  devnul,
+		Stdout: devnul,
+		Stderr: devnul,
+	}
+	// Run the Elvish daemon using the hermetic home.
+	go func() {
+		if err := daemonCmd.Start(); err != nil {
+			log.Fatal(err)
+		}
+		// The daemon will exit when the Elvish shell we're intereacting with exits. So we simply
+		// need to wait for the daemon to terminate.
+		if err := daemonCmd.Wait(); err != nil {
+			log.Fatal(err)
+		}
+	}()
+	// Wait for the Elvish daemon to create the socket file before we start the Elvish shell.
+	// This isn't strictly speaking necessary but helps avoid race conditions and makes it more
+	// likely we'll abort with a useful diagnostic if the daemon fails to start.
+	launchTime := time.Now()
+	time.Sleep(10 * time.Millisecond)
+	for {
+		if _, err := os.Stat(elvSock); err == nil {
+			break
+		}
+		if time.Now().Sub(launchTime) > time.Duration(5*time.Second) {
+			log.Fatal("Elvish daemon failed to create socket in a reasonable interval")
+		}
+		time.Sleep(10 * time.Millisecond)
+	}
+
+	// Start tmux and have it start the hermetic Elvish shell.
+	elvRcPath := filepath.Join(cwd, "website/ttyshot/ttyshot.rc")
+	tmuxCmd := exec.Cmd{
+		Path: tmuxPath,
+		Args: []string{
+			"tmux", "-S", tmuxSock, "-f", "website/ttyshot/tmux.rc", "new-session", "-s", "ttyshot",
+			"-c", homePath, elvishPath, "-rc", elvRcPath, "-sock", elvSock},
+		Stdin:  slave,
+		Stdout: slave,
+		Stderr: slave,
+	}
+	go func() {
+		// We ignore the Run() error return value because it will normally tell us the tmux exit
+		// status was one. We could explicitly test for that error and only call log.Fatal if it was
+		// some other error but there really isn't a good reason to do so.
+		tmuxCmd.Run()
+	}()
+
+	// Capture the output of the Elvish shell.
+	captureCmd := exec.Cmd{
+		Path:   tmuxPath,
+		Args:   []string{"tmux", "-S", tmuxSock, "capture-pane", "-t", "ttyshot", "-p", "-e"},
+		Stdin:  devnul,
+		Stdout: ttyImage,
+		Stderr: os.Stderr,
+	}
+	go func() {
+		<-triggerTtyCapture
+		if err := captureCmd.Run(); err != nil {
+			log.Fatal(err)
+		}
+		killTmuxCmd := exec.Cmd{
+			Path:   tmuxPath,
+			Args:   []string{"tmux", "-S", tmuxSock, "kill-server"},
+			Stdin:  devnul,
+			Stdout: devnul,
+			Stderr: devnul,
+		}
+		if err := killTmuxCmd.Run(); err != nil {
+			fmt.Fprintf(os.Stderr, "Killing tmux returned error: %v\n", err)
+		}
+		ttyCaptureDone <- true
+	}()
+
+	return triggerTtyCapture, ttyCaptureDone
+}
+
+// Create a hermetic environment for generating a ttyshot. We want to ensure we don't use the real
+// home directory, or interactive history, of the person running this tool.
+func initEnv() (string, string, func()) {
+	// There are systems, such as macOs, which generate a temp dir that includes symlinks in the
+	// path. For example, `/var/` => `/private/var`. Expand those symlinks so that Elvish command
+	// `tilde-abbr` will behave as expected.
+	homePath, err := os.MkdirTemp("", "ttyshot-*")
+	if err != nil {
+		log.Fatal("unable to create temp home: " + err.Error())
+	}
+	homePath, err = filepath.EvalSymlinks(homePath)
+	if err != nil {
+		log.Fatal("unable to resolve symlinks in homePath: " + err.Error())
+	}
+	// We'll put the Elvish and Tmux socket files in this directory. This makes the "navigation"
+	// mode ttyshots a trifle less confusing.
+	os.Mkdir(filepath.Join(homePath, "tmp"), 0o700)
+
+	// We don't pass any XDG env vars to the Elvish programs we spawn. We want them to rely solely
+	// on HOME in order to force using our hermetic home.
+	os.Setenv("HOME", homePath)
+	os.Unsetenv(env.XDG_CONFIG_HOME)
+	os.Unsetenv(env.XDG_DATA_DIRS)
+	os.Unsetenv(env.XDG_DATA_HOME)
+	os.Unsetenv(env.XDG_STATE_HOME)
+	os.Unsetenv(env.XDG_RUNTIME_DIR)
+
+	// Create the Elvish local state directory in the hermetic home.
+	dotLocalStateElvish := filepath.Join(homePath, ".local", "state", "elvish")
+	if err := os.MkdirAll(dotLocalStateElvish, 0o700); err != nil {
+		log.Fatal("mkdir -p " + dotLocalStateElvish + ": " + err.Error())
+	}
+
+	// Copy the Elvish source code to the hermetic home for use in demos of things like Elvish's
+	// "navigation" mode.
+	copySrcCmd := exec.Cmd{
+		Path: "website/tools/cp-elvish.sh",
+		Args: []string{"cp-elvish.sh", homePath},
+	}
+	if err := copySrcCmd.Run(); err != nil {
+		log.Fatal(err)
+	}
+
+	// Create a couple of other directories to make demos of "navigation" mode more interesting.
+	os.Mkdir(filepath.Join(homePath, "bash"), 0o700)
+	os.Mkdir(filepath.Join(homePath, "zsh"), 0o700)
+
+	// Ensure the terminal type seen by tmux is a widely recognized terminal definition. This makes
+	// it possible to generate ttyshots in a continuous deployment environment. It's also good to
+	// decouple invocations from an environment we don't control if this is run by hand from an
+	// interactive shell whose TERM value we can't predict.
+	_ = os.Setenv("TERM", "xterm-256color")
+
+	cleanup := func() {
+		if err := os.RemoveAll(homePath); err != nil {
+			log.Fatal("Unable to remove temp HOME: " + err.Error())
+		}
+	}
+
+	dbPath := filepath.Join(dotLocalStateElvish, "db.bolt")
+	return homePath, dbPath, cleanup
+}
+
+func createTtyshot(homePath, dbPath string, script []demoOp, outFile, rawFile *os.File) error {
+	master, slave, err := pty.Open()
+	if err != nil {
+		return err
+	}
+	winsize := pty.Winsize{Rows: terminalRows, Cols: terminalCols}
+	pty.Setsize(master, &winsize)
+
+	// Relay the output of the ttyshot Elvish session to the channel that will capture and evaluate
+	// the output; e.g., to detect whether a prompt was seen.
+	ttyOutput := make(chan byte, 32*1024)
+	go func() {
+		for {
+			content := make([]byte, 1024)
+			n, err := master.Read(content)
+			if n == 0 {
+				close(ttyOutput)
+				return
+			}
+			if err != nil {
+				log.Fatal(err)
+			}
+			for i := 0; i < n; i++ {
+				ttyOutput <- content[i]
+			}
+		}
+	}()
+
+	var ttyImage bytes.Buffer
+	triggerTtyCapture, ttyCaptureDone := spawnElvish(homePath, dbPath, slave, &ttyImage)
+	trimEmptyLines, err := executeScript(script, master, ttyOutput)
+	if err != nil {
+		return err
+	}
+
+	// Give the ttyshot image a chance to stabilize. Yes, this is not guaranteed to work, but in
+	// practice it's rarely needed and even pausing a handful of milliseconds will usually suffice.
+	time.Sleep(100 * time.Millisecond)
+	triggerTtyCapture <- true
+	<-ttyCaptureDone
+	// Close the pty master to signal EOF to the processes running inside the simulated terminal.
+	// This helps ensure processes running inside the simulated terminal will terminate once we're
+	// done capturing the "ttyshot".
+	master.Close()
+
+	ttyshot := ttyImage.String()
+	rawFile.WriteString(ttyshot)
+	// Trim the last, or all, trailing newlines in order to eliminate from the generated HTML
+	// unwanted empty lines at the bottom of the ttyshot. The latter behavior occurs if the ttyshot
+	// specification includes the `trim-empty` directive.
+	if !trimEmptyLines {
+		ttyshot = strings.TrimSuffix(ttyshot, "\n")
+	} else {
+		ttyshot = strings.TrimRight(ttyshot, "\n")
+	}
+	outFile.WriteString(sgrTextToHTML(ttyshot))
+	outFile.WriteString("\n")
+	return nil
+}
+
+func main() {
+	if len(os.Args) != 2 {
+		fmt.Fprintf(os.Stderr, "Expected one argument, got %d\n", len(os.Args)-1)
+		os.Exit(1)
+	}
+	specPath := os.Args[1]
+	if !strings.HasSuffix(specPath, ".spec") {
+		fmt.Fprintf(os.Stderr, "Expected extension \".spec\", found %q\n", path.Ext(specPath))
+		os.Exit(2)
+	}
+	basePath := specPath[:len(specPath)-len(".spec")]
+	htmlPath := basePath + ".html"
+	rawPath := basePath + ".raw"
+
+	content, err := os.ReadFile(specPath)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	script, err := parseSpec(content)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	outFile, err := os.OpenFile(htmlPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	rawFile, err := os.OpenFile(rawPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	homePath, dbPath, cleanup := initEnv()
+	defer cleanup()
+	if err := createTtyshot(homePath, dbPath, script, outFile, rawFile); err != nil {
+		log.Fatal(err)
+	}
+}

+ 1 - 0
website/go.mod

@@ -4,6 +4,7 @@ go 1.18
 
 require (
 	github.com/BurntSushi/toml v1.0.0
+	github.com/creack/pty v1.1.15
 	src.elv.sh v0.17.0
 )
 

+ 2 - 0
website/go.sum

@@ -1,3 +1,5 @@
+github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
+github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
 github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=

+ 8 - 14
website/learn/tour.md

@@ -930,8 +930,7 @@ Type to filter:
 
 ## Navigation mode
 
-Press <kbd>Ctrl-N</kbd> to start the builtin filesystem navigator, or
-**navigation mode**.
+Press <kbd>Ctrl-N</kbd> to start the builtin filesystem navigator.
 
 @ttyshot tour/navigation
 
@@ -961,18 +960,13 @@ will be called. Otherwise this definition will result in infinite recursion.
 ### Prompt customization
 
 The left and right prompts can be customized by assigning functions to
-`edit:prompt` and `edit:rprompt`. The following configuration simulates the
-default prompts, but uses fancy Unicode:
-
-```elvish
-# "tilde-abbr" abbreviates home directory to a tilde.
-set edit:prompt = { tilde-abbr $pwd; put '❱ ' }
-# "constantly" returns a function that always writes the same value(s) to
-# output; "styled" writes styled output.
-set edit:rprompt = (constantly (styled (whoami)✸(hostname) inverse))
-```
-
-This is how it looks:
+[`edit:prompt`](../ref/edit.html#edit:prompt) and
+[`edit:rprompt`](../ref/edit.html#edit:rprompt). The following example defines
+prompts similar to the default, but uses fancy Unicode. The
+[`tilde-abbr`](../ref/builtin.html#tilde-abbr) command abbreviates home
+directory to a tilde. The [`constantly`](../ref/builtin.html#constantly) command
+returns a function that always writes the same value(s) to the value output. The
+[`styled`](../ref/builtin.html#styled) command writes styled output.
 
 @ttyshot tour/unicode-prompts
 

+ 1 - 1
website/ref/edit.md

@@ -19,7 +19,7 @@ same time. Each mode has its own UI and keybindings. For instance, the default
 (triggered by <kbd>Tab</kbd> by default) shows you all candidates for
 completion, and you can use arrow keys to navigate those candidates.
 
-@ttyshot completion-mode
+@ttyshot ref/edit/completion-mode
 
 Each mode has its own submodule under `edit:`. For instance, builtin functions
 and configuration variables for the completion mode can be found in the

+ 94 - 33
website/style.css

@@ -337,6 +337,7 @@ pre.ttyshot {
 }
 
 pre.ttyshot, pre.ttyshot code {
+    color: black;
     background-color: white;
 }
 
@@ -346,59 +347,119 @@ pre.ttyshot, pre.ttyshot code {
     }
 }
 
-/* SGR classes, used in ttyshots. */
+/* SGR classes used in ttyshots. */
 .sgr-1 {
     font-weight: bold;
 }
 .sgr-4 {
     text-decoration: underline;
 }
-.sgr-7 {
-    color: white;
-    background-color: black;
+.sgr-7 { /* inverse */
+    color: white !important;
+    background-color: black !important;
+}
+.sgr-39 { /* default foreground color */
+    color: black;
+}
+.sgr-49 { /* default background color */
+    background-color: white;
+}
+
+.sgr-30 { /* fg black */
+    color: #000000;
+}
+.sgr-40 { /* bg black */
+    background-color: #000000;
+}
+.sgr-31 { /* fg dark red */
+    color: #8B0000;
+}
+.sgr-41 { /* bg dark red */
+    background-color: #8B0000;
+}
+.sgr-32 { /* fg dark green */
+    color: #008000;
+}
+.sgr-42 { /* bg dark green */
+    background-color: #008000;
+}
+.sgr-33 { /* fg dark yellow */
+    color: #DAA520;
+}
+.sgr-43 { /* bg dark yellow */
+    background-color: #DAA520;
+}
+.sgr-34 { /* fg dark blue */
+    color: #00008B;
+}
+.sgr-44 { /* bg dark blue */
+    background-color: #00008B;
+}
+.sgr-35 { /* fg dark magenta */
+    color: #FF00FF;
+}
+.sgr-45 { /* bg dark magenta */
+    background-color: #FF00FF;
+}
+.sgr-36 { /* fg dark cyan */
+    color: #008B8B;
+}
+.sgr-46 { /* bg dark cyan */
+    background-color: #008B8B;
+}
+.sgr-37 { /* fg white */
+    color: #E5E5E5;
+}
+.sgr-47 { /* bg white */
+    background-color: #E5E5E5;
+}
+.sgr-90 { /* fg bright black (grey) */
+    color: #7F7F7F;
+}
+.sgr-100 { /* bg bright black (grey) */
+    background-color: #7F7F7F;
 }
-.sgr-31 {
-    color: darkred; /* red in tty */
+.sgr-91 { /* fg bright red */
+    color: #FF0000;
 }
-.sgr-41 {
-    background-color: darkred; /* red in tty */
+.sgr-101 { /* bg bright red */
+    background-color: #FF0000;
 }
-.sgr-32 {
-    color: green; /* green in tty */
+.sgr-92 { /* fg bright green */
+    color: #00FF00;
 }
-.sgr-42, .sgr-7.sgr-32 {
-    background-color: green; /* green in tty */
+.sgr-102 { /* bg bright green */
+    background-color: #00FF00;
 }
-.sgr-33 {
-    color: goldenrod; /* yellow in tty */
+.sgr-93 { /* fg bright yellow */
+    color: #FFFF00;
 }
-.sgr-43, .sgr-7.sgr-33 {
-    background-color: goldenrod; /* yellow in tty */
+.sgr-103 { /* bg bright yellow */
+    background-color: #FFFF00;
 }
-.sgr-34 {
-    color: blue;
+.sgr-94 { /* fg bright blue */
+    color: #0000FF;
 }
-.sgr-44, .sgr-7.sgr-34 {
-    color: white; /* Hacky hacky, just to make the nav ttyshot work */
-    background-color: blue;
+.sgr-104 { /* bg bright blue */
+    background-color: #0000FF;
 }
-.sgr-35 {
-    color: darkorchid; /* magenta in tty */
+.sgr-95 { /* fg bright magenta */
+    color: #F984E5;
 }
-.sgr-45, .sgr-7.sgr-35 {
-    background-color: darkorchid; /* magenta in tty */
+.sgr-105 { /* bg bright magenta */
+    background-color: #F984E5;
 }
-.sgr-36 {
-    color: darkcyan; /* cyan in tty */
+.sgr-96 { /* fg bright cyan */
+    color: #00FFFF;
 }
-.sgr-46, .sgr-7.sgr-36 {
-    background-color: darkcyan; /* cyan in tty */
+.sgr-106 { /* bg bright cyan */
+    background-color: #00FFFF;
 }
-.sgr-37 {
-    color: lightgray;
+.sgr-97 { /* fg bright white */
+    color: #FFFFFF;
 }
-.sgr-47, .sgr-7.sgr-37 {
-    background-color: gray;
+.sgr-107 { /* bg bright white */
+    background-color: #FFFFFF;
 }
 
 /** Header anchors. */

+ 18 - 0
website/tools/cp-elvish.sh

@@ -0,0 +1,18 @@
+#!/bin/sh
+#
+# This is a helper script used by the ttyshot generation program to copy just
+# the non-hidden files in the top-level Elvish source directory to the ttyshot
+# hermetic home directory. This creates a predictable directory for
+# demonstrating things like "navigation" mode.
+#
+home="$1"
+mkdir "$home/elvish"
+for f in *
+do
+    if [ -d "$f" ]
+    then
+        mkdir "$home/elvish/$f"
+    else
+        cp "$f" "$home/elvish/$f"
+    fi
+done

+ 87 - 20
website/ttyshot/README.md

@@ -1,27 +1,94 @@
-This directory contains "ttyshots" -- they are like screenshots, but taken on
-terminals. They are taken with Elvish's `edit:-dump-buf` function. To take one,
-use the following procedure:
+# What this directory contains
 
-1.  Modify `edit:rprompt` to pretend that the username is `elf` and the hostname
-    is `host`:
+This directory contains "ttyshots" that represent the final state of a set of
+Elvish interactive shell interactions. You will need to have the
+[`tmux`](https://github.com/tmux/tmux) command installed to create these images.
+The process for generating a ttyshot does not require a real terminal unless you
+want to examine the "raw" image. This means ttyshots can be generated in a CI/CD
+workflow using nothing more than the "spec" files.
 
-    ```elvish
-    set edit:rprompt = (constantly (styled 'elf@host' inverse))
-    ```
+# How to create a ttyshot
 
-2.  Add a keybinding for taking ttyshots:
+To create a ttyshot use the following procedure from the project root dir:
 
-    ```elvish
-    var header = '<!-- Follow website/ttyshot/README.md to regenerate -->'
-    edit:global-binding[Alt-x] = { print $header(edit:-dump-buf) > ~/ttyshot.html }
-    ```
+1.  Create the ttyshot program: `make -C website tools/ttyshot.bin`
 
-3.  Make sure that the terminal width is 58, to be consistent with existing
-    ttyshots.
+1.  Create or modify a ttyshot specification (a ".spec" file) in the
+    `website/ttyshot` directory or subdirectory. See below for what can appear
+    in a ".spec" file.
 
-4.  Put Elvish in the state you want, and press Alt-X. The ttyshot is saved at
-    `~/ttyshot.html`.
+1.  Create the ttyshot; e.g.,
+    `website/tools/ttyshot.bin website/ttyshot/pipelines.spec`.
 
-Some of the ttyshots also show the output of commands. Since `edit:-dump-buf`
-only captures the Elvish UI, you need to manually append the command output when
-updating such ttyshots.
+1.  Review the results; e.g., `cat website/ttyshot/pipelines.raw`.
+
+1.  Add a `@ttyshot` directive to the appropriate document (e.g.,
+    `website/home.md` or `website/learn/fundamentals.md`) if adding a new
+    ttyshot.
+
+You can easily refresh all the ttyshots by running this:
+
+```
+for f [website/ttyshot/**.spec] { put $f; website/tools/ttyshot.bin $f }
+```
+
+# Content of a ttyshot specification
+
+A ttyshot specification consists of two types of lines: plain text to be sent to
+the Elvish shell as if a human had typed the text, and directives that begin
+with `//`. Empty lines are ignored. The available directives are:
+
+-   `//prompt`: Wait for a new shell prompt. The process of converting a
+    sequence of commands to a ttyshot implicitly waits for the first prompt to
+    appear so don't begin your specification with this directive.
+
+-   `//no-enter`: Disable the implicit Enter normally sent after each line of
+    plain text.
+
+-   `//enter`: Enable an implicit Enter after each line of plain text and send
+    an Enter.
+
+-   `//sleep d`: Pause for the specified duration in seconds. For example:
+    `//sleep 1`. No unit suffix should be present since only (fractional)
+    seconds are allowed. The use of this directive shouldn't be necessary. There
+    is an implicit sleep at the end of a ttyshot specification before capturing
+    the ttyshot image to give the Elvish shell time to stabilize its output;
+    e.g., when displaying a navigation view.
+
+-   `//alt x`: Simulate an Alt sequence. That is, send an Escape followed by the
+    specified text.
+
+-   `//ctrl n`: Send a Ctrl char version of `n`; e.g., `//ctrl L`.
+
+-   `//tab`: Send a Tab; i.e., `//ctrl I`.
+
+-   `//up`: Send an Up-arrow key sequence.
+
+-   `//down`: Send an Down-arrow key sequence.
+
+-   `//left`: Send an Left-arrow key sequence.
+
+-   `//right`: Send an Right-arrow key sequence.
+
+-   `//wait-for-str string`: Wait for the literal string `string` to appear in
+    the output.
+
+-   `//wait-for-re regexp`: Wait for text matching the regexp to appear in the
+    output.
+
+## Example ttyshot specification
+
+The following specification simulates a user pressing Ctrl-N to enter navigation
+mode. Followed by Ctrl-F and the text `pkg` to select that directory. Followed
+by navigating into and out of that directory.
+
+```
+//no-enter
+//ctrl N
+//down
+//ctrl F
+pkg
+//right
+sys
+//right
+```

+ 0 - 5
website/ttyshot/completion-mode.html

@@ -1,5 +0,0 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/go/src/github.com/elves/elvish&gt; <span class="sgr-32">vim</span> <span class="sgr-4">CONTRIBUTING.md </span>
-<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span> 
-<span class="sgr-7">CONTRIBUTING.md</span>  LICENSE   NEXT-RELEASE.md  <span class="sgr-1 sgr-34">cmd/  </span>  go.su
-Dockerfile       Makefile  README.md        go.mod  main.
-<span class="sgr-7 sgr-35">                                            </span><span class="sgr-35">━━━━━━━━━━━━━</span>

+ 16 - 15
website/ttyshot/control-structures.html

@@ -1,15 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~&gt; <span class="sgr-32">if</span><span class="sgr-"> </span><span class="sgr-35">$true</span><span class="sgr-"> </span><span class="sgr-1">{</span><span class="sgr-"> </span><span class="sgr-32">echo</span><span class="sgr-"> good </span><span class="sgr-1">}</span><span class="sgr-"> </span><span class="sgr-33">else</span><span class="sgr-"> </span><span class="sgr-1">{</span><span class="sgr-"> </span><span class="sgr-32">echo</span><span class="sgr-"> bad </span><span class="sgr-1">}</span>               
-good
-~&gt; <span class="sgr-32">for</span><span class="sgr-"> </span><span class="sgr-35">x</span><span class="sgr-"> </span><span class="sgr-1">[</span><span class="sgr-">lorem ipsum</span><span class="sgr-1">]</span><span class="sgr-"> </span><span class="sgr-1">{</span>
-     <span class="sgr-32">echo</span><span class="sgr-"> </span><span class="sgr-35">$x</span><span class="sgr-">.pdf</span>
-   <span class="sgr-1">}</span>
-lorem.pdf
-ipsum.pdf
-~&gt; <span class="sgr-32">try</span><span class="sgr-"> </span><span class="sgr-1">{</span>
-     <span class="sgr-32">fail</span><span class="sgr-"> </span><span class="sgr-33">&#39;bad error&#39;</span>
-   <span class="sgr-1">}</span><span class="sgr-"> </span><span class="sgr-33">except</span><span class="sgr-"> </span><span class="sgr-35">e</span><span class="sgr-"> </span><span class="sgr-1">{</span>
-     <span class="sgr-32">echo</span><span class="sgr-"> error </span><span class="sgr-35">$e</span>
-   <span class="sgr-1">}</span><span class="sgr-"> </span><span class="sgr-33">else</span><span class="sgr-"> </span><span class="sgr-1">{</span>
-     <span class="sgr-32">echo</span><span class="sgr-"> ok</span>
-   <span class="sgr-1">}</span>
-error ?(fail 'bad error')
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">if</span><span class="sgr-39"> </span><span class="sgr-35">$true</span><span class="sgr-39"> </span><span class="sgr-1 sgr-39">{</span><span class="sgr-39 sgr-49"> </span><span class="sgr-32 sgr-49">echo</span><span class="sgr-39 sgr-49"> good </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">else</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-39 sgr-49"> </span><span class="sgr-32 sgr-49">echo</span><span class="sgr-39 sgr-49"> bad </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49">
+good</span>
+<span class="sgr-30 sgr-49">[2]</span><span class="sgr-39 sgr-49"> ~&gt; </span><span class="sgr-32 sgr-49">for</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">x</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">[</span><span class="sgr-39 sgr-49">lorem ipsum</span><span class="sgr-1 sgr-39 sgr-49">]</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-39 sgr-49"> </span><span class="sgr-32 sgr-49">put</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$x</span><span class="sgr-39 sgr-49">.pdf </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-95 sgr-49">▶ </span><span class="sgr-39 sgr-49">lorem.pdf</span>
+<span class="sgr-95 sgr-49">▶ </span><span class="sgr-39 sgr-49">ipsum.pdf</span>
+<span class="sgr-30 sgr-49">[3]</span><span class="sgr-39 sgr-49"> ~&gt; </span><span class="sgr-32 sgr-49">try</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-39 sgr-49">
+            </span><span class="sgr-32 sgr-49">fail</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">&#39;bad error&#39;</span><span class="sgr-39 sgr-49">
+       </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49"> catch e </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">put</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$e</span><span class="sgr-39 sgr-49">
+       </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49"> finally </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">put</span><span class="sgr-39 sgr-49"> done
+       </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-95 sgr-49">▶ </span><span class="sgr-39 sgr-49">[&amp;reason=[&amp;content=&#39;bad error&#39; &amp;type=fail]]</span>
+<span class="sgr-95 sgr-49">▶ </span><span class="sgr-39 sgr-49">done</span>
+<span class="sgr-30 sgr-49">[4]</span><span class="sgr-39 sgr-49"> ~&gt;                                              </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+

+ 11 - 0
website/ttyshot/control-structures.spec

@@ -0,0 +1,11 @@
+if $true { echo good } else { echo bad }
+//prompt
+for x [lorem ipsum] { put $x.pdf }
+//prompt
+try {
+     fail 'bad error'
+} catch e {
+    put $e
+} finally {
+    put done
+}

+ 2 - 2
website/ttyshot/fundamentals/history-1.html

@@ -1,2 +1,2 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~&gt; <span class="sgr-4">randint 1 7</span><span class="sgr-">                                    </span><span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> HISTORY #59321 </span><span class="sgr-"> </span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-4 sgr-32">math:min</span><span class="sgr-4 sgr-39"> 3 1 30</span><span class="sgr-39 sgr-49">                              </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> HISTORY #155</span>

+ 4 - 0
website/ttyshot/fundamentals/history-1.spec

@@ -0,0 +1,4 @@
+//trim-empty
+//no-enter
+//up
+//wait-for-str HISTORY #

+ 2 - 2
website/ttyshot/fundamentals/history-2.html

@@ -1,2 +1,2 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~&gt; <span class="sgr-31">ra</span><span class="sgr-4">ndint 1 7</span><span class="sgr-">                                    </span><span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> HISTORY #59321 </span><span class="sgr-"> </span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">ra</span><span class="sgr-4 sgr-32">ndint</span><span class="sgr-4 sgr-39"> 1 10</span><span class="sgr-39 sgr-49">                                 </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> HISTORY #125</span>

+ 5 - 0
website/ttyshot/fundamentals/history-2.spec

@@ -0,0 +1,5 @@
+//trim-empty
+//no-enter
+ra
+//up
+//wait-for-str HISTORY #

+ 16 - 15
website/ttyshot/histlist-mode.html

@@ -1,15 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~&gt;                                           <span class="sgr-7">xiaq@xiaqsmbp</span>
-<span class="sgr-1 sgr-37 sgr-45"> HISTORY </span><span class="sgr-"> </span>
-13345 make tools/ttyshot                                 <span class="sgr-35">│</span>
-13346 make                                               <span class="sgr-35">│</span>
-13347 ./assets/                                          <span class="sgr-35">│</span>
-13348 ls                                                 <span class="sgr-35">│</span>
-13349 ls                                                 <span class="sgr-35">│</span>
-13350 rm *.png                                           <span class="sgr-35">│</span>
-<span class="sgr-7">13351 git st                                             </span><span class="sgr-35">│</span>
-13352 ..                                                 <span class="sgr-35">│</span>
-13353 git st                                             <span class="sgr-35">│</span>
-13354 git add .                                          <span class="sgr-35">│</span>
-13355 git st                                             <span class="sgr-35">│</span>
-13356 git commit                                         <span class="sgr-35">│</span>
-13357 git push                                           <span class="sgr-35">│</span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt;                                              </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span><span class="sgr-39 sgr-49">
+ 126 echo (styled warning: red) bumpy road                 </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49"> 127 echo &#34;hello\nbye&#34; &gt; /tmp/x                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 128 from-lines &lt; /tmp/x                                   </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 129 cd /tmp                                               </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 130 cd ~/elvish                                           </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 131 git branch                                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 132 git checkout .                                        </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 133 git commit                                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 142 git status                                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 143 cd /usr/local/bin                                     </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 144 echo $pwd                                             </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-7 sgr-39 sgr-49"> 145 * (+ 3 4) (- 100 94)                                  </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 154 make                                                  </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 155 math:min 3 1 30                                       </span>

+ 4 - 0
website/ttyshot/histlist-mode.spec

@@ -0,0 +1,4 @@
+//no-enter
+//ctrl R
+//up
+//up

+ 16 - 15
website/ttyshot/location-mode.html

@@ -1,15 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~&gt;                                           <span class="sgr-7">xiaq@xiaqsmbp</span>
-<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span><span class="sgr-"> </span>
-<span class="sgr-7">  * ~                                                    </span><span class="sgr-35 sgr-7"> </span>
-  * ~/go/src/github.com/elves/elvish                     <span class="sgr-35">│</span>
-110 ~/on/elvish-site/code                                <span class="sgr-35">│</span>
- 62 ~/on/elvish-site/code/src                            <span class="sgr-35">│</span>
- 52 ~/go/src/github.com/elves/elvish/edit                <span class="sgr-35">│</span>
- 34 ~/on/elvish-site/code/tty                            <span class="sgr-35">│</span>
- 33 ~/on/elvish-site/code/assets                         <span class="sgr-35">│</span>
- 32 ~/go/src/github.com/elves/elvish/eval                <span class="sgr-35">│</span>
- 26 ~/on/chat-app/code                                   <span class="sgr-35">│</span>
- 24 ~/on/elvish-site/code/dst                            <span class="sgr-35">│</span>
- 20 ~/go/src/github.com/elves/md-highlighter             <span class="sgr-35">│</span>
- 14 ~/on/chat-app/code/public                            <span class="sgr-35">│</span>
- 13 ~/.elvish                                            <span class="sgr-35">│</span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt;                                              </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-7 sgr-39 sgr-49"> 51 /tmp</span>
+<span class="sgr-39 sgr-49"> 51 ~/elvish
+ 50 ~/.config/elvish
+ 47 ~/.local/share/elvish
+ 42 /usr
+ 36 /usr/local/bin
+ 28 /usr/local/share
+ 20 /usr/local
+ 10 /opt
+
+
+
+</span>
+

+ 2 - 0
website/ttyshot/location-mode.spec

@@ -0,0 +1,2 @@
+//no-enter
+//ctrl L

+ 16 - 15
website/ttyshot/navigation-mode.html

@@ -1,15 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/go/src/github.com/elves/elvish&gt;            <span class="sgr-7">xiaq@xiaqsmbp</span>
-<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span><span class="sgr-"> </span>
-<span class="sgr-01 sgr-34 sgr-7"> elvish         </span><span class="sgr-">  CONTRIBUTING.md </span><span class="sgr-35 sgr-7"> </span><span class="sgr-">  FROM golang:onbuild </span>
-<span class="sgr-01 sgr-34"> fix-for-0.7    </span><span class="sgr-"> </span><span class="sgr- sgr-7"> Dockerfile      </span><span class="sgr-35 sgr-7"> </span><span class="sgr-">                      </span>
-<span class="sgr-01 sgr-34"> images         </span><span class="sgr-">  Gopkg.lock      </span><span class="sgr-35 sgr-7"> </span>
-<span class="sgr-01 sgr-34"> md-highlighter </span><span class="sgr-">  Gopkg.toml      </span><span class="sgr-35 sgr-7"> </span>
-                  LICENSE         <span class="sgr-35 sgr-7"> </span>
-                  Makefile        <span class="sgr-35 sgr-7"> </span>
-                  README.md       <span class="sgr-35 sgr-7"> </span>
-                 <span class="sgr-01 sgr-34"> cover           </span><span class="sgr-35">│</span>
-                 <span class="sgr-01 sgr-34"> daemon          </span><span class="sgr-35">│</span>
-                 <span class="sgr-01 sgr-34"> edit            </span><span class="sgr-35">│</span>
-                 <span class="sgr-01 sgr-34"> errors          </span><span class="sgr-35">│</span>
-                 <span class="sgr-01 sgr-34"> eval            </span><span class="sgr-35">│</span>
-                 <span class="sgr-01 sgr-34"> getopt          </span><span class="sgr-35">│</span>
+<span class="sgr-30">[2]</span><span class="sgr-39"> ~/elvish&gt;                                       </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-34 sgr-49"> bash  </span><span class="sgr-39 sgr-49"> </span><span class="sgr-7 sgr-39 sgr-49"> 0.19.0-release-not </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> This is the draft release not</span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-7 sgr-34 sgr-49"> elvis </span><span class="sgr-39 sgr-49">  CONTRIBUTING.md    </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> 2022-07-01.                  </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-34 sgr-49"> tmp   </span><span class="sgr-39 sgr-49">  Dockerfile         </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-34 sgr-49"> zsh   </span><span class="sgr-39 sgr-49">  LICENSE            </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> # Breaking changes           </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49">         Makefile           </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49">         PACKAGING.md       </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> # Deprecated features        </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49">         README.md          </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">         SECURITY.md        </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> Deprecated features will be r</span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-34 sgr-49"> cmd                </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-31 sgr-49"> elvish             </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> The following deprecated feat</span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">         go.mod             </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> and compiled, even if it is n</span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">         go.sum             </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-34 sgr-49"> pkg                </span><span class="sgr-35 sgr-49">│</span><span class="sgr-39 sgr-49"> -   The `float64` command is </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-34 sgr-49"> syntaxes           </span><span class="sgr-35 sgr-49">│</span><span class="sgr-39 sgr-49">     number, or `inexact-num` </span><span class="sgr-35 sgr-49">│</span>

+ 4 - 0
website/ttyshot/navigation-mode.spec

@@ -0,0 +1,4 @@
+cd elvish
+//prompt
+//no-enter
+//ctrl N

+ 16 - 14
website/ttyshot/pipelines.html

@@ -1,14 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~&gt; <span class="sgr-32">curl</span> https://api.github.com/repos/elves/elvish/issues <span class="sgr-32">|</span>
-     <span class="sgr-32">from-json</span> <span class="sgr-32">|</span> <span class="sgr-32">all</span> <span class="sgr-1">(</span><span class="sgr-32">one</span><span class="sgr-1">)</span> <span class="sgr-32">|</span>
-     <span class="sgr-32">each</span> <span class="sgr-1">{</span><span class="sgr-32">|</span>x<span class="sgr-32">|</span> <span class="sgr-32">echo</span> <span class="sgr-1">(</span><span class="sgr-32">exact-num</span> <span class="sgr-35">$x</span><span class="sgr-1">[</span>number<span class="sgr-1">])</span>: <span class="sgr-35">$x</span><span class="sgr-1">[</span>title<span class="sgr-1">]</span> <span class="sgr-1">}</span> <span class="sgr-32">|</span>
-     <span class="sgr-32">head</span> -n 7
-1465: Pipelines.html still using legacy syntax.
-1464: Ability to change the initial filter for completion
-1463: No filtering when constructing the candidate list du
-ring completion
-1462: Add language.html and command.html to docset search
-index
-1460: Symbols Clobbered by Elvish?
-1453: Add a `&uniq` option to the `builtin:order` command
-and a `builtin:uniq` command
-1449: Hard crash with an Alias module alias with closure
+<span class="sgr-30">[7]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">var</span><span class="sgr-39"> </span><span class="sgr-35">url</span><span class="sgr-39"> </span><span class="sgr-33">=</span><span class="sgr-39"> </span><span class="sgr-33">&#34;https://api.github.com/repos/elves/elvish/</span>
+<span class="sgr-39">       </span><span class="sgr-33">issues?state=all&amp;sort=updated&amp;per_page=5&#34;</span><span class="sgr-39"></span>
+<span class="sgr-30">[8]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">curl</span><span class="sgr-39"> -s </span><span class="sgr-35">$url</span><span class="sgr-39"> </span><span class="sgr-32">|</span><span class="sgr-39"> </span><span class="sgr-32">from-json</span><span class="sgr-39"> </span><span class="sgr-32">|</span><span class="sgr-39"> </span><span class="sgr-32">all</span><span class="sgr-39"> </span><span class="sgr-1 sgr-39">(</span><span class="sgr-32 sgr-49">one</span><span class="sgr-1 sgr-39 sgr-49">)</span><span class="sgr-39 sgr-49"> </span><span class="sgr-32 sgr-49">|</span><span class="sgr-39 sgr-49">
+       </span><span class="sgr-32 sgr-49">each</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-32 sgr-49">|</span><span class="sgr-39 sgr-49">issue</span><span class="sgr-32 sgr-49">|</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">var</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">id</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">=</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">exact-num</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$issue</span><span class="sgr-1 sgr-39 sgr-49">[</span><span class="sgr-39 sgr-49">number</span><span class="sgr-1 sgr-39 sgr-49">])</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">var</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">t</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">=</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$issue</span><span class="sgr-1 sgr-39 sgr-49">[</span><span class="sgr-39 sgr-49">title</span><span class="sgr-1 sgr-39 sgr-49">]</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">var</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">title</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">=</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$t</span><span class="sgr-1 sgr-39 sgr-49">[</span><span class="sgr-39 sgr-49">..</span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">math:min</span><span class="sgr-39 sgr-49"> 45 </span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">count</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$t</span><span class="sgr-1 sgr-39 sgr-49">))]</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">var</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">state</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">=</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$issue</span><span class="sgr-1 sgr-39 sgr-49">[</span><span class="sgr-39 sgr-49">state</span><span class="sgr-1 sgr-39 sgr-49">]</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">echo</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">colored</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$state</span><span class="sgr-1 sgr-39 sgr-49">)</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$id</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$title</span><span class="sgr-39 sgr-49">
+       </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-92 sgr-49">open  </span><span class="sgr-39 sgr-49"> 1541 Make ttyshots scripted and reproducible</span>
+<span class="sgr-31 sgr-49">closed</span><span class="sgr-39 sgr-49"> 1460 Symbols Clobbered by Elvish?</span>
+<span class="sgr-92 sgr-49">open  </span><span class="sgr-39 sgr-49"> 1372 Octal Format Specifier</span>
+<span class="sgr-92 sgr-49">open  </span><span class="sgr-39 sgr-49"> 1374 Feed stdin to all code blocks in run-parallel</span>
+<span class="sgr-92 sgr-49">open  </span><span class="sgr-39 sgr-49"> 1406 Adjust behavior of `use` statement to improve</span>
+<span class="sgr-30 sgr-49">[9]</span><span class="sgr-39 sgr-49"> ~&gt;                                              </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>

+ 25 - 0
website/ttyshot/pipelines.spec

@@ -0,0 +1,25 @@
+use math
+//prompt
+var stateColors = [&open=fg-bright-green &closed=fg-red]
+//prompt
+curl -s $url > /tmp/x
+//prompt
+head -n 5 /tmp/x
+//prompt
+rm /tmp/x
+//prompt
+fn colored {|state|
+    put (styled {$state"  "}[..6] $stateColors[$state])
+}
+//prompt
+var url = "https://api.github.com/repos/elves/elvish/issues?state=all&sort=updated&per_page=5"
+//prompt
+curl -s $url | from-json | all (one) |
+each {|issue|
+    var id = (exact-num $issue[number])
+    var t = $issue[title]
+    var title = $t[..(math:min 45 (count $t))]
+    var state = $issue[state]
+    echo (colored $state) $id $title
+}
+//prompt

+ 6 - 0
website/ttyshot/ref/edit/completion-mode.html

@@ -0,0 +1,6 @@
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">cd</span><span class="sgr-39"> elvish</span>
+<span class="sgr-30">[2]</span><span class="sgr-39"> ~/elvish&gt; </span><span class="sgr-32">vim</span><span class="sgr-39"> CONTRIBUTING.md </span><span class="sgr-4 sgr-39">README.md </span><span class="sgr-39 sgr-49">        </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span><span class="sgr-39 sgr-49"> .md
+0.19.0-release-notes.md  </span><span class="sgr-7 sgr-39 sgr-49">README.md</span>
+<span class="sgr-39 sgr-49">CONTRIBUTING.md          SECURITY.md
+PACKAGING.md</span>

+ 12 - 0
website/ttyshot/ref/edit/completion-mode.spec

@@ -0,0 +1,12 @@
+//trim-empty
+cd elvish
+//prompt
+//no-enter
+vim 
+//tab
+//down
+//enter
+//no-enter
+//tab
+.md
+//right

+ 4 - 0
website/ttyshot/tmux.rc

@@ -0,0 +1,4 @@
+# Disable the tmux status line.
+set-option -g status off
+# Ensure 256 and RGB support is enabled.
+set-option -g terminal-features *:256:RGB

+ 5 - 4
website/ttyshot/tour/completion-filter.html

@@ -1,5 +1,6 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt; <span class="sgr-32">vim</span> <span class="sgr-4">0.18.0-release-notes.md </span>         <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span> .md
-<span class="sgr-7">0.18.0-release-notes.md</span>  README.md  
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">cd</span><span class="sgr-39"> elvish</span>
+<span class="sgr-30">[2]</span><span class="sgr-39"> ~/elvish&gt; </span><span class="sgr-32">echo</span><span class="sgr-39"> </span><span class="sgr-4 sgr-39">0.19.0-release-notes.md </span><span class="sgr-39 sgr-49">         </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span><span class="sgr-39 sgr-49"> .md</span>
+<span class="sgr-7 sgr-39 sgr-49">0.19.0-release-notes.md</span><span class="sgr-39 sgr-49">  README.md
 CONTRIBUTING.md          SECURITY.md
-PACKAGING.md           
+PACKAGING.md</span>

+ 7 - 0
website/ttyshot/tour/completion-filter.spec

@@ -0,0 +1,7 @@
+//trim-empty
+cd elvish
+//prompt
+//no-enter
+echo 
+//tab
+.md

+ 12 - 6
website/ttyshot/tour/completion.html

@@ -1,6 +1,12 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt; <span class="sgr-32">vim</span> <span class="sgr-4">0.18.0-release-notes.md </span>         <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span> 
-<span class="sgr-7">0.18.0-release-notes.md</span>  Makefile      <span class="sgr-1 sgr-34">cmd/  </span>  <span class="sgr-1 sgr-34">pkg/    </span>
-CONTRIBUTING.md          PACKAGING.md  cover   <span class="sgr-1 sgr-34">tools/  </span>
-Dockerfile               README.md     go.mod  <span class="sgr-1 sgr-34">website/</span>
-LICENSE                  SECURITY.md   go.sum
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">cd</span><span class="sgr-39"> elvish</span>
+<span class="sgr-30">[2]</span><span class="sgr-39"> ~/elvish&gt; </span><span class="sgr-32">echo</span><span class="sgr-39"> </span><span class="sgr-4 sgr-39">0.19.0-release-notes.md </span><span class="sgr-39 sgr-49">         </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-7 sgr-39 sgr-49">0.19.0-release-notes.md</span><span class="sgr-39 sgr-49">  </span><span class="sgr-31 sgr-49">elvish</span>
+<span class="sgr-39 sgr-49">CONTRIBUTING.md          go.mod
+Dockerfile               go.sum
+LICENSE                  </span><span class="sgr-34 sgr-49">pkg/</span>
+<span class="sgr-39 sgr-49">Makefile                 </span><span class="sgr-34 sgr-49">syntaxes/</span>
+<span class="sgr-39 sgr-49">PACKAGING.md             </span><span class="sgr-34 sgr-49">tools/</span>
+<span class="sgr-39 sgr-49">README.md                </span><span class="sgr-34 sgr-49">vscode/</span>
+<span class="sgr-39 sgr-49">SECURITY.md              </span><span class="sgr-34 sgr-49">website/
+cmd/</span>

+ 6 - 0
website/ttyshot/tour/completion.spec

@@ -0,0 +1,6 @@
+//trim-empty
+cd elvish
+//prompt
+//no-enter
+echo 
+//tab

+ 16 - 7
website/ttyshot/tour/history-list.html

@@ -1,7 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt;                                      <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span> 
-20286 demo                                               <span class="sgr-35">│</span>
-20288 cd ~                                               <span class="sgr-35">│</span>
-20289 ls                                                 <span class="sgr-35">│</span>
-20290 echo foo bar                                       <span class="sgr-35">│</span>
-<span class="sgr-7">20291 vim README.md                                      </span><span class="sgr-7 sgr-35"> </span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt;                                              </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span><span class="sgr-39 sgr-49">
+ 126 echo (styled warning: red) bumpy road                 </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49"> 127 echo &#34;hello\nbye&#34; &gt; /tmp/x                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 128 from-lines &lt; /tmp/x                                   </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 129 cd /tmp                                               </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 130 cd ~/elvish                                           </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 131 git branch                                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 132 git checkout .                                        </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 133 git commit                                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 142 git status                                            </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 143 cd /usr/local/bin                                     </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 144 echo $pwd                                             </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 145 * (+ 3 4) (- 100 94)                                  </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49"> 154 make                                                  </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-7 sgr-39 sgr-49"> 155 math:min 3 1 30                                       </span>

+ 3 - 0
website/ttyshot/tour/history-list.spec

@@ -0,0 +1,3 @@
+//trim-empty
+//no-enter
+//ctrl R

+ 2 - 2
website/ttyshot/tour/history-walk-prefix.html

@@ -1,2 +1,2 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt; <span class="sgr-32">echo</span> <span class="sgr-4">foo bar</span>                         <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> HISTORY #20279 </span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">echo</span><span class="sgr-4 sgr-39"> </span><span class="sgr-1 sgr-4 sgr-39">(</span><span class="sgr-4 sgr-32 sgr-49">styled</span><span class="sgr-4 sgr-39 sgr-49"> warning: red</span><span class="sgr-1 sgr-4 sgr-39 sgr-49">)</span><span class="sgr-4 sgr-39 sgr-49"> bumpy road</span><span class="sgr-39 sgr-49">        </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> HISTORY #126</span>

+ 6 - 0
website/ttyshot/tour/history-walk-prefix.spec

@@ -0,0 +1,6 @@
+//trim-empty
+//no-enter
+echo
+//up
+//up
+//up

+ 2 - 2
website/ttyshot/tour/history-walk.html

@@ -1,2 +1,2 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt; <span class="sgr-4 sgr-32">vim</span><span class="sgr-4"> README.md </span>                       <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> HISTORY #20280 </span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-4 sgr-32">math:min</span><span class="sgr-4 sgr-39"> 3 1 30</span><span class="sgr-39 sgr-49">                              </span><span class="sgr-7 sgr-39 sgr-49">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> HISTORY #155</span>

+ 3 - 0
website/ttyshot/tour/history-walk.spec

@@ -0,0 +1,3 @@
+//trim-empty
+//no-enter
+//up

+ 8 - 5
website/ttyshot/tour/lastcmd.html

@@ -1,5 +1,8 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt;                                      <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> LASTCMD </span> 
-<span class="sgr-7">    vim README.md                                         </span>
-  0 vim
-  1 README.md
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">echo</span><span class="sgr-39"> abc def
+abc def</span>
+<span class="sgr-30">[2]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">vim</span><span class="sgr-39">                                          </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> LASTCMD </span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-7 sgr-39 sgr-49">    echo abc def</span>
+<span class="sgr-39 sgr-49">  0 echo
+  1 abc
+  2 def</span>

+ 6 - 0
website/ttyshot/tour/lastcmd.spec

@@ -0,0 +1,6 @@
+//trim-empty
+echo abc def
+//prompt
+//no-enter
+vim 
+//alt ,

+ 6 - 7
website/ttyshot/tour/location-filter.html

@@ -1,7 +1,6 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt;                                      <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span> ev
-<span class="sgr-7"> 52 ~/on/elvish/pkg/eval                                 </span><span class="sgr-7 sgr-35"> </span>
- 11 ~/on/elvish/pkg/eval/vals                            <span class="sgr-35">│</span>
-  2 ~/on/elvish/pkg/eval/errs                            <span class="sgr-35">│</span>
-  1 ~/on/elvish/pkg/eval/vars                            <span class="sgr-35">│</span>
-  0 ~/on/elvish/pkg/eval/evaltest                        <span class="sgr-35">│</span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt;                                              </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span><span class="sgr-39 sgr-49"> local</span>
+<span class="sgr-7 sgr-39 sgr-49"> 47 ~/.local/share/elvish</span>
+<span class="sgr-39 sgr-49"> 36 /usr/local/bin
+ 28 /usr/local/share
+ 20 /usr/local</span>

+ 4 - 0
website/ttyshot/tour/location-filter.spec

@@ -0,0 +1,4 @@
+//trim-empty
+//no-enter
+//ctrl L
+local

+ 11 - 7
website/ttyshot/tour/location.html

@@ -1,7 +1,11 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt;                                      <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span> 
-<span class="sgr-7">148 ~                                                    </span><span class="sgr-7 sgr-35"> </span>
- 52 ~/on/elvish/pkg/eval                                 <span class="sgr-35">│</span>
- 49 ~/on/elvish/pkg                                      <span class="sgr-35">│</span>
- 34 ~/.config/kak/plugins                                <span class="sgr-35">│</span>
- 27 ~/on/elvish/pkg/edit                                 <span class="sgr-35">│</span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt;                                              </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-7 sgr-39 sgr-49"> 51 /tmp</span>
+<span class="sgr-39 sgr-49"> 51 ~/elvish
+ 50 ~/.config/elvish
+ 47 ~/.local/share/elvish
+ 42 /usr
+ 36 /usr/local/bin
+ 28 /usr/local/share
+ 20 /usr/local
+ 10 /opt</span>

+ 3 - 0
website/ttyshot/tour/location.spec

@@ -0,0 +1,3 @@
+//trim-empty
+//no-enter
+//ctrl L

+ 16 - 7
website/ttyshot/tour/navigation.html

@@ -1,7 +1,16 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~/on/elvish&gt;                                      <span class="sgr-7">elf@host</span>
-<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span> 
-<span class="sgr-1 sgr-34"> cmdg </span><span class="sgr-35">│</span> <span class="sgr-7"> 0.18.0-release-not </span><span class="sgr-7 sgr-35"> </span> This is the draft release n<span class="sgr-7 sgr-35"> </span>
-<span class="sgr-1 sgr-34"> duan </span><span class="sgr-7 sgr-35"> </span>  CONTRIBUTING.md    <span class="sgr-7 sgr-35"> </span> 2022-03-01.                <span class="sgr-7 sgr-35"> </span>
-<span class="sgr-1 sgr-7 sgr-34"> elvi </span><span class="sgr-35">│</span>  Dockerfile         <span class="sgr-35">│</span>                            <span class="sgr-35">│</span>
-<span class="sgr-1 sgr-34"> elvi </span><span class="sgr-35">│</span>  LICENSE            <span class="sgr-35">│</span> # Notable bugfixes         <span class="sgr-35">│</span>
-<span class="sgr-1 sgr-34"> elvi </span><span class="sgr-35">│</span>  Makefile           <span class="sgr-35">│</span>                            <span class="sgr-35">│</span>
+<span class="sgr-30">[2]</span><span class="sgr-39"> ~/elvish&gt;                                       </span><span class="sgr-7 sgr-39">elf@host</span>
+<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-34 sgr-49"> bash  </span><span class="sgr-39 sgr-49"> </span><span class="sgr-7 sgr-39 sgr-49"> 0.19.0-release-not </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> This is the draft release not</span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-7 sgr-34 sgr-49"> elvis </span><span class="sgr-39 sgr-49">  CONTRIBUTING.md    </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> 2022-07-01.                  </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-34 sgr-49"> tmp   </span><span class="sgr-39 sgr-49">  Dockerfile         </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-34 sgr-49"> zsh   </span><span class="sgr-39 sgr-49">  LICENSE            </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> # Breaking changes           </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49">         Makefile           </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49">         PACKAGING.md       </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> # Deprecated features        </span><span class="sgr-7 sgr-35 sgr-49"></span>
+<span class="sgr-39 sgr-49">         README.md          </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">         SECURITY.md        </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> Deprecated features will be r</span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-34 sgr-49"> cmd                </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-31 sgr-49"> elvish             </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> The following deprecated feat</span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">         go.mod             </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49"> and compiled, even if it is n</span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">         go.sum             </span><span class="sgr-7 sgr-35 sgr-49"> </span><span class="sgr-39 sgr-49">                              </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-34 sgr-49"> pkg                </span><span class="sgr-35 sgr-49">│</span><span class="sgr-39 sgr-49"> -   The `float64` command is </span><span class="sgr-35 sgr-49">│</span>
+<span class="sgr-39 sgr-49">        </span><span class="sgr-34 sgr-49"> syntaxes           </span><span class="sgr-35 sgr-49">│</span><span class="sgr-39 sgr-49">     number, or `inexact-num` </span><span class="sgr-35 sgr-49">│</span>

+ 5 - 0
website/ttyshot/tour/navigation.spec

@@ -0,0 +1,5 @@
+//trim-empty
+cd elvish
+//prompt
+//no-enter
+//ctrl N

+ 7 - 1
website/ttyshot/tour/unicode-prompts.html

@@ -1 +1,7 @@
-<!-- Follow website/ttyshot/README.md to regenerate -->~❱ <span class="sgr-36"># Fancy unicode prompts!</span>                       <span class="sgr-7">elv✸host</span>
+<span class="sgr-30">[1]</span><span class="sgr-39"> ~&gt; </span><span class="sgr-32">set</span><span class="sgr-39"> </span><span class="sgr-35">edit:rprompt</span><span class="sgr-39"> </span><span class="sgr-33">=</span><span class="sgr-39"> </span><span class="sgr-1 sgr-39">(</span><span class="sgr-32 sgr-49">constantly</span><span class="sgr-39 sgr-49"> ^
+           </span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">styled</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">whoami</span><span class="sgr-1 sgr-39 sgr-49">)</span><span class="sgr-33 sgr-49">&#34;\u00A7&#34;</span><span class="sgr-1 sgr-39 sgr-49">(</span><span class="sgr-32 sgr-49">hostname</span><span class="sgr-1 sgr-39 sgr-49">)</span><span class="sgr-39 sgr-49"> inverse</span><span class="sgr-1 sgr-39 sgr-49">))</span><span class="sgr-39 sgr-49"></span>
+<span class="sgr-30 sgr-49">[2]</span><span class="sgr-39 sgr-49"> ~&gt; </span><span class="sgr-32 sgr-49">set</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">edit:prompt</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">=</span><span class="sgr-39 sgr-49"> </span><span class="sgr-1 sgr-39 sgr-49">{</span><span class="sgr-32 sgr-49">||</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">tilde-abbr</span><span class="sgr-39 sgr-49"> </span><span class="sgr-35 sgr-49">$pwd</span><span class="sgr-39 sgr-49">
+           </span><span class="sgr-32 sgr-49">styled</span><span class="sgr-39 sgr-49"> </span><span class="sgr-33 sgr-49">&#34; \u00BB\u00BB &#34;</span><span class="sgr-39 sgr-49"> bright-red
+       </span><span class="sgr-1 sgr-39 sgr-49">}</span><span class="sgr-39 sgr-49">
+~</span><span class="sgr-91 sgr-49"> »» </span><span class="sgr-36 sgr-49"># Fancy unicode prompts!</span><span class="sgr-39 sgr-49">           </span><span class="sgr-7 sgr-39 sgr-49">elf§host.example.com</span>

+ 11 - 0
website/ttyshot/tour/unicode-prompts.spec

@@ -0,0 +1,11 @@
+//trim-empty
+set edit:rprompt = (constantly ^
+    (styled (whoami)"\u00A7"(hostname) inverse))
+//prompt
+set edit:prompt = {||
+    tilde-abbr $pwd
+    styled " \u00BB\u00BB " bright-red
+}
+//wait-for-str »»
+//no-enter
+# Fancy unicode prompts!

+ 145 - 0
website/ttyshot/ttyshot.rc

@@ -0,0 +1,145 @@
+# This is the interactive configuration for generating Elvish "ttyshots".
+
+# Use all the embedded modules so that a ttyshot that depends on one of them
+# doesn't need to explicitly include a `use` command. Note that we
+# unconditionally `use unix` because we don't support generating ttyshots on
+# non-UNIX systems.
+#
+# Note: We explicitly do not `use readline-binding` because it changes the
+# default key bindings. We want the ttyshot specifications to be able to
+# depend on the default key bindings. But this list should otherwise include
+# all embedded modules -- even if a ttyshot doesn't currently rely on it.
+use builtin
+use epm
+use file
+use flag
+use math
+use path
+use platform
+use re
+use store
+use str
+use unix
+
+# Populate the interactive location history.
+range 9 | each {|_| store:add-dir $E:HOME }
+range 9 | each {|_| store:add-dir $E:HOME/elvish }
+range 8 | each {|_| store:add-dir /tmp }
+range 7 | each {|_| store:add-dir $E:HOME/.config/elvish }
+range 6 | each {|_| store:add-dir $E:HOME/.local/share/elvish }
+range 5 | each {|_| store:add-dir /usr }
+range 4 | each {|_| store:add-dir /usr/local/bin }
+range 3 | each {|_| store:add-dir /usr/local/share }
+range 2 | each {|_| store:add-dir /usr/local }
+range 1 | each {|_| store:add-dir /opt }
+
+# Populate the interactive command history.
+set @_ = (range 5 | each {|_|
+    store:add-cmd 'randint 1 10'
+    store:add-cmd 'echo (styled warning: red) bumpy road'
+    store:add-cmd 'echo "hello\nbye" > /tmp/x'
+    store:add-cmd 'from-lines < /tmp/x'
+    store:add-cmd 'cd /tmp'
+    store:add-cmd 'cd ~/elvish'
+    store:add-cmd 'git branch'
+    store:add-cmd 'git checkout .'
+    store:add-cmd 'git commit'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'git status'
+    store:add-cmd 'cd /usr/local/bin'
+    store:add-cmd 'echo $pwd'
+    store:add-cmd '* (+ 3 4) (- 100 94)'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'make'
+    store:add-cmd 'math:min 3 1 30'
+})
+
+# Sync the history we just manufactured with this elvish process.
+edit:history:fast-forward
+
+# Change the appearance of the indicator that prefaces values output by `put`
+# (or any other command that writes to the value stream) to be styled so it
+# is colorful and stands out.
+set value-out-indicator = (print (styled '▶ ' bright-magenta))
+
+# Warning: Ugly hack ahead.
+#
+# The left-hand prompt is complicated because we need to circumvent the
+# optimizations done by Elvish when rendering the prompt. If we don't change
+# the style of the "<n>" component (the command number) of the prompt then the
+# emitted sequence of bytes won't always have the fixed portion ("<", ">") and
+# variable portion ("n") adjacent in the emitted byte stream. Which makes it
+# really hard for the ttyshot program to reliably recognize when a new prompt
+# is written. So force Elvish to rewrite the entire command number sequence by
+# changing the style of that text whenever a new prompt, not just an update to
+# an existing prompt, is written.
+var cmd-num = (num 0)
+set edit:before-readline = [$@edit:before-readline {
+    set cmd-num = (+ 1 $cmd-num)
+}]
+var cmd-num-style = ["fg-black" "fg-bright-black"]
+set edit:prompt = {
+    # The inclusion of bg-default is critical to ensure that the ttyshot code
+    # can reliably detect the prompt and correct the foreground color.
+    styled '['$cmd-num']' bg-default $cmd-num-style[(% $cmd-num 2)]
+    put ' '(tilde-abbr $pwd)'> '
+}
+
+set edit:rprompt = (constantly (styled 'elf@host' inverse))
+set edit:rprompt-persistent = $false
+
+# These functions are used in some of the ttyshot scripts to ensure consistent
+# output that doesn't leak info about the machine used to create the ttyshot.
+fn whoami {
+    put elf
+}
+
+fn hostname {
+    put host.example.com
+}
+
+# This command is useful for verifying the display of basic text styles.
+# Specifically: bold, underline, and the 16 legacy tty colors. Start a
+# "ttyshot" session and run `styles`. Then view the results in a web browser.
+fn styles {||
+    var colors = [black red green yellow blue magenta cyan white]
+    print (styled ' under ' underlined)
+    print (styled ' bold ' bold)
+    print (styled ' bold+under ' bold underlined)
+    echo
+
+    for c $colors {
+        print (styled ' '$c' ' bg-$c)
+    }
+    echo
+    for c $colors {
+        print (styled ' '$c' ' bg-bright-$c)
+    }
+    echo
+    for c $colors {
+        print (styled ' '$c' ' fg-$c)
+    }
+    echo
+    for c $colors {
+        print (styled ' '$c' ' fg-bright-$c)
+    }
+    echo
+    for c $colors {
+        print (styled ' '$c' ' fg-bright-$c underlined bold)
+    }
+    echo
+}