Sfoglia il codice sorgente

website: Make ttyshots look like transcripts.

This further simplifies the op structure - only code input and tmux commands are
necessary now; waiting for prompts is now implicit.
Qi Xiao 1 anno fa
parent
commit
82a72e31e3

+ 1 - 1
website/Makefile

@@ -31,7 +31,7 @@ clean:
 
 ifdef TTYSHOT
 %.ttyshot.html: %.ttyshot tools/ttyshot.bin
-	tools/ttyshot.bin -o $@ $<
+	tools/ttyshot.bin $(if $(findstring verbose,$(TTYSHOT)),-v,) -o $@ $<
 else
 %.ttyshot.html:
 	@: ttyshot generation disabled by default

+ 44 - 15
website/README.md

@@ -37,6 +37,35 @@ Building the docset requires the following additional dependencies:
 To build the docset, run `make docset`. The generated docset is in
 `Elvish.docset`.
 
+# Transcripts
+
+Documents can contain **transcripts** of Elvish sessions, identified by the
+language tag `elvish-transcript`. A simple example:
+
+````markdown
+```elvish-transcript
+~> echo foo |
+   str:to-upper (one)
+▶ FOO
+```
+````
+
+When the website is built, the toolchain will highlight the
+`echo foo | str:to-upper (one)` part correctly as Elvish code.
+
+To be exact, the toolchain uses the following heuristic to determine the range
+of Elvish code:
+
+-   It looks for what looks like a prompt, which starts with either `~` or `/`,
+    ends with `>` and a space, with no spaces in between.
+
+-   It then extends the range downwards, as long as the line starts with N
+    whitespaces, where N is the length of the prompt (including the trailing
+    space).
+
+As long as you use Elvish's default prompt, you should be able to rely on this
+heuristic.
+
 # Ttyshots
 
 Some of the pages include "ttyshots" that show the content of Elvish sessions.
@@ -50,24 +79,23 @@ built `elvish` in `PATH`. Windows is not supported.
 
 ## Instruction syntax
 
-Each line in a ttyshot instruction file is one of the following:
+Ttyshot instruction files look like Elvish transcripts, with the following
+differences:
 
--   `#prompt` instructs waiting for a new shell prompt.
+-   It should not contain the output of commands. Anything that is not part of
+    an input at a prompt causes a parse error.
 
--   `#`_`command`_, where `command` is a string that does **not** start with a
-    space and is not `prompt`, is a command sent to `tmux`. The most useful one
-    (and only one being used now) is `send-keys`.
+-   If the Elvish code starts with `#` followed immediately by a letter, it is
+    treated instead as a command to sent to `tmux`.
 
--   Anything else is treated as text that should be sent directly to the Elvish
-    prompt.
+    The most useful one (and only one being used now) is `send-keys`.
 
-For example, the following instructions runs `cd /tmp`, waits for the next
-prompt, and sends Ctrl-N to trigger navigation mode:
+For example, the following instructions runs `cd /tmp`, and sends Ctrl-N to
+trigger navigation mode at the next prompt:
 
 ```
-cd /tmp
-#prompt
-#send-keys ctrl-L
+~> cd /tmp
+~> #send-keys ctrl-L
 ```
 
 ## Generating ttyshots
@@ -77,9 +105,10 @@ the repository, and the `Makefile` rule to generate them is disabled by default.
 This is because the process to generate ttyshots is relatively slow and may have
 network dependencies.
 
-To turn on ttyshot generation, pass `TTYSHOT=1` to `make`. For example, to
-generate a single ttyshot, run `make TTYSHOT=1 foo.ttyshot.html`. To build the
-website with ttyshot generation enabled, run `make TTYSHOT=1`.
+To turn on ttyshot generation, pass `TTYSHOT=1` to `make` (where `1` can be
+replaced by any non-empty string). For example, to generate a single ttyshot,
+run `make TTYSHOT=1 foo.ttyshot.html`. To build the website with ttyshot
+generation enabled, run `make TTYSHOT=1`.
 
 The first time you generate ttyshots, `make` will build the `ttyshot` tool, and
 regenerate all ttyshots. Subsequent runs will only regenerate ttyshots whose

+ 38 - 21
website/cmd/ttyshot/interp.go

@@ -107,7 +107,7 @@ func createTtyshot(homePath string, script []op, saveRaw string) ([]byte, error)
 	if err != nil {
 		return nil, err
 	}
-	executeScript(script, ctrl, homePath)
+	finalEmptyPrompt := executeScript(script, ctrl, homePath)
 	log.Println("executed script, waiting for tmux to exit")
 
 	// Drain outputs from the terminal. This is needed so that tmux can exit
@@ -129,11 +129,9 @@ func createTtyshot(homePath string, script []op, saveRaw string) ([]byte, error)
 	segments := strings.Split(ttyshot, cutMarker+"\n")
 	ttyshot = segments[len(segments)-1]
 
-	// Strip all the prompt markers, and the content after the last prompt
-	// marker if the last instruction was #prompt (in which case the content
-	// will just be an empty prompt).
+	// Strip all the prompt markers and the final empty prompt.
 	segments = strings.Split(ttyshot, promptMarker+"\n")
-	if len(script) > 0 && script[len(script)-1].typ == opPrompt {
+	if finalEmptyPrompt {
 		segments = segments[:len(segments)-1]
 	}
 	ttyshot = strings.Join(segments, "")
@@ -186,39 +184,58 @@ func spawnElvish(homePath string, tty *os.File) (<-chan error, error) {
 	return doneCh, nil
 }
 
-func executeScript(script []op, ctrl *os.File, homePath string) {
+func executeScript(script []op, ctrl *os.File, homePath string) (finalEmptyPrompt bool) {
 	for _, op := range script {
+		log.Println("waiting for prompt")
+		err := waitForPrompt(ctrl)
+		if err != nil {
+			// TODO: Handle the error properly
+			panic(err)
+		}
+
 		log.Println("executing", op)
-		switch op.typ {
-		case opText:
-			text := op.val.(string)
-			ctrl.WriteString(text)
-			ctrl.WriteString("\r")
-		case opPrompt:
-			err := waitForOutput(ctrl, promptMarker,
-				func(bs []byte) bool { return bytes.HasSuffix(bs, []byte(promptMarker)) })
-			if err != nil {
-				// TODO: Handle the error properly
-				panic(err)
+		if op.codeLines != nil {
+			for i, line := range op.codeLines {
+				if i > 0 {
+					// Use Alt-Enter to avoid committing the code
+					ctrl.WriteString("\033\r")
+				}
+				ctrl.WriteString(line)
 			}
-		case opTmux:
+			ctrl.WriteString("\r")
+		} else {
 			tmuxSock := filepath.Join(homePath, ".tmp", "tmux.sock")
 			tmuxCmd := exec.Command("tmux",
-				append([]string{"-S", tmuxSock}, op.val.([]string)...)...)
+				append([]string{"-S", tmuxSock}, op.tmuxCommand...)...)
 			tmuxCmd.Env = []string{}
 			err := tmuxCmd.Run()
 			if err != nil {
 				// TODO: Handle the error properly
 				panic(err)
 			}
-		default:
-			panic("unhandled op")
 		}
 	}
+
+	if len(script) > 0 && script[len(script)-1].codeLines != nil {
+		log.Println("waiting for final empty prompt")
+		finalEmptyPrompt = true
+		err := waitForPrompt(ctrl)
+		if err != nil {
+			// TODO: Handle the error properly
+			panic(err)
+		}
+	}
+
 	log.Println("sending Alt-q")
 	// Alt-q is bound to a function that captures the content of the pane and
 	// exits
 	ctrl.Write([]byte{'\033', 'q'})
+	return finalEmptyPrompt
+}
+
+func waitForPrompt(f *os.File) error {
+	return waitForOutput(f, promptMarker,
+		func(bs []byte) bool { return bytes.HasSuffix(bs, []byte(promptMarker)) })
 }
 
 func waitForOutput(f *os.File, expected string, matcher func([]byte) bool) error {

+ 4 - 1
website/cmd/ttyshot/main.go

@@ -53,7 +53,10 @@ func run(args []string) error {
 	if err != nil {
 		return err
 	}
-	spec := parseSpec(string(content))
+	spec, err := parseSpec(string(content))
+	if err != nil {
+		return err
+	}
 
 	homePath, err := setupHome()
 	if err != nil {

+ 31 - 31
website/cmd/ttyshot/parse.go

@@ -3,48 +3,48 @@
 package main
 
 import (
+	"fmt"
+	"regexp"
 	"strings"
 )
 
-type opType int
-
-// Operations for driving a demo ttyshot.
-const (
-	opText   opType = iota // send the provided text, optionally followed by Enter
-	opPrompt               // wait for prompt marker
-	opTmux                 // run tmux command
-)
-
 type op struct {
-	typ opType
-	val any
+	codeLines   []string
+	tmuxCommand []string
 }
 
-func parseSpec(content string) []op {
+var (
+	ps1Pattern  = regexp.MustCompile(`^[~/][^ ]*> `)
+	tmuxPattern = regexp.MustCompile(`^#[a-z]`)
+)
+
+func parseSpec(content string) ([]op, error) {
 	lines := strings.Split(content, "\n")
-	ops := make([]op, 1, len(lines)+2)
-	ops[0] = op{opPrompt, nil}
 
-	for _, line := range lines {
-		if len(line) == 0 {
-			continue // ignore empty lines
+	var ops []op
+	for i := 0; i < len(lines); i++ {
+		line := lines[i]
+		if line == "" {
+			continue
 		}
-		var newOp op
-		if line == "#prompt" {
-			newOp = op{opPrompt, nil}
-		} else if strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "# ") {
-			newOp = op{opTmux, strings.Fields(line[1:])}
-		} else {
-			newOp = op{opText, line}
+		ps1 := ps1Pattern.FindString(line)
+		if ps1 == "" {
+			return nil, fmt.Errorf("invalid line %v", i+1)
+		}
+		content := line[len(ps1):]
+		if tmuxPattern.MatchString(content) {
+			ops = append(ops, op{tmuxCommand: strings.Fields(content[1:])})
+			continue
 		}
-		ops = append(ops, newOp)
-	}
 
-	if len(ops) > 0 && ops[len(ops)-1].typ == opText {
-		// The termination of the ttyshot process relies on the editor to be
-		// active, so add an implicit #prompt.
-		ops = append(ops, op{opPrompt, nil})
+		codeLines := []string{content}
+		ps2 := strings.Repeat(" ", len(ps1))
+		for i++; i < len(lines) && strings.HasPrefix(lines[i], ps2); i++ {
+			codeLines = append(codeLines, lines[i][len(ps2):])
+		}
+		i--
+		ops = append(ops, op{codeLines: codeLines})
 	}
 
-	return ops
+	return ops, nil
 }

+ 11 - 13
website/home/control-structures.ttyshot

@@ -1,13 +1,11 @@
-if $true { echo good } else { echo bad }
-#prompt
-for x [lorem ipsum] {
-  echo $x.pdf
-}
-#prompt
-try {
-  fail 'bad error'
-} catch e {
-  echo error $e
-} else {
-  echo ok
-}
+~> if $true { echo good } else { echo bad }
+~> for x [lorem ipsum] {
+     echo $x.pdf
+   }
+~> try {
+     fail 'bad error'
+   } catch e {
+     echo error $e
+   } else {
+     echo ok
+   }

+ 1 - 1
website/home/histlist-mode.ttyshot

@@ -1 +1 @@
-#send-keys C-R Up Up
+~> #send-keys C-R Up Up

+ 1 - 1
website/home/location-mode.ttyshot

@@ -1 +1 @@
-#send-keys C-L
+~> #send-keys C-L

+ 2 - 3
website/home/navigation-mode.ttyshot

@@ -1,3 +1,2 @@
-cd elvish; echo '[CUT]'
-#prompt
-#send-keys C-N
+~> cd elvish; echo '[CUT]'
+~> #send-keys C-N

+ 4 - 4
website/home/pipelines.ttyshot

@@ -1,4 +1,4 @@
-curl -sL api.github.com/repos/elves/elvish/issues |
-  all (from-json) |
-  each {|x| echo (exact-num $x[number]): $x[title] } |
-  head -n 10
+~> curl -sL api.github.com/repos/elves/elvish/issues |
+     all (from-json) |
+     each {|x| echo (exact-num $x[number]): $x[title] } |
+     head -n 10

+ 3 - 5
website/learn/fundamentals/history-1.ttyshot

@@ -1,5 +1,3 @@
- -randseed 1; echo '[CUT]'
-#prompt
-randint 1 7
-#prompt
-#send-keys Up
+~>  -randseed 1; echo '[CUT]'
+~> randint 1 7
+~> #send-keys Up

+ 4 - 7
website/learn/fundamentals/history-2.ttyshot

@@ -1,7 +1,4 @@
- -randseed 2; echo '[CUT]'
-#prompt
-randint 1 7
-#prompt
-# more commands ...
-#prompt
-#send-keys ra Up
+~>  -randseed 2; echo '[CUT]'
+~> randint 1 7
+~> # more commands ...
+~> #send-keys ra Up

+ 2 - 3
website/learn/tour/completion-filter.ttyshot

@@ -1,3 +1,2 @@
-cd elvish
-#prompt
-#send-keys echo Space Tab .md
+~> cd elvish
+~> #send-keys echo Space Tab .md

+ 2 - 3
website/learn/tour/completion.ttyshot

@@ -1,3 +1,2 @@
-cd elvish
-#prompt
-#send-keys echo Space Tab
+~> cd elvish
+~> #send-keys echo Space Tab

+ 1 - 1
website/learn/tour/history-list.ttyshot

@@ -1 +1 @@
-#send-keys C-R
+~> #send-keys C-R

+ 1 - 1
website/learn/tour/history-walk-prefix.ttyshot

@@ -1 +1 @@
-#send-keys echo Up Up Up
+~> #send-keys echo Up Up Up

+ 1 - 1
website/learn/tour/history-walk.ttyshot

@@ -1 +1 @@
-#send-keys Up
+~> #send-keys Up

+ 2 - 3
website/learn/tour/lastcmd.ttyshot

@@ -1,3 +1,2 @@
-echo abc def
-#prompt
-#send-keys vim Space M-,
+~> echo abc def
+~> #send-keys vim Space M-,

+ 1 - 1
website/learn/tour/location-filter.ttyshot

@@ -1 +1 @@
-#send-keys C-L local
+~> #send-keys C-L local

+ 1 - 1
website/learn/tour/location.ttyshot

@@ -1 +1 @@
-#send-keys C-L
+~> #send-keys C-L

+ 2 - 3
website/learn/tour/navigation.ttyshot

@@ -1,3 +1,2 @@
-cd elvish; echo '[CUT]'
-#prompt
-#send-keys C-N
+~> cd elvish; echo '[CUT]'
+~> #send-keys C-N

+ 7 - 9
website/learn/tour/unicode-prompts.ttyshot

@@ -1,9 +1,7 @@
-set edit:rprompt = (constantly ^
-  (styled (whoami)✸(hostname) inverse))
-#prompt
-set edit:prompt = {
-  tilde-abbr $pwd
-  styled '❱ ' bright-red
-}
-#prompt
-#send-keys # Space Fancy Space unicode Space prompts!
+~> set edit:rprompt = (constantly ^
+     (styled (whoami)✸(hostname) inverse))
+~> set edit:prompt = {
+     tilde-abbr $pwd
+     styled '❱ ' bright-red
+   }
+~> #send-keys # Space Fancy Space unicode Space prompts!

+ 2 - 3
website/ref/edit/completion-mode.ttyshot

@@ -1,3 +1,2 @@
-cd elvish; echo '[CUT]'
-#prompt
-#send-keys vim Space Tab
+~> cd elvish; echo '[CUT]'
+~> #send-keys vim Space Tab