From 82a72e31e342e52d0579dbf7e49e4e71c90d2cf4 Mon Sep 17 00:00:00 2001 From: Qi Xiao Date: Tue, 9 Aug 2022 13:43:21 +0100 Subject: [PATCH] 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. --- website/Makefile | 2 +- website/README.md | 59 ++++++++++++----- website/cmd/ttyshot/interp.go | 59 +++++++++++------ website/cmd/ttyshot/main.go | 5 +- website/cmd/ttyshot/parse.go | 64 +++++++++---------- website/home/control-structures.ttyshot | 24 ++++--- website/home/histlist-mode.ttyshot | 2 +- website/home/location-mode.ttyshot | 2 +- website/home/navigation-mode.ttyshot | 5 +- website/home/pipelines.ttyshot | 8 +-- website/learn/fundamentals/history-1.ttyshot | 8 +-- website/learn/fundamentals/history-2.ttyshot | 11 ++-- website/learn/tour/completion-filter.ttyshot | 5 +- website/learn/tour/completion.ttyshot | 5 +- website/learn/tour/history-list.ttyshot | 2 +- .../learn/tour/history-walk-prefix.ttyshot | 2 +- website/learn/tour/history-walk.ttyshot | 2 +- website/learn/tour/lastcmd.ttyshot | 5 +- website/learn/tour/location-filter.ttyshot | 2 +- website/learn/tour/location.ttyshot | 2 +- website/learn/tour/navigation.ttyshot | 5 +- website/learn/tour/unicode-prompts.ttyshot | 16 ++--- website/ref/edit/completion-mode.ttyshot | 5 +- 23 files changed, 167 insertions(+), 133 deletions(-) diff --git a/website/Makefile b/website/Makefile index cd99fb2d..c92f3244 100644 --- a/website/Makefile +++ b/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 diff --git a/website/README.md b/website/README.md index cf1c67ac..aca26d35 100644 --- a/website/README.md +++ b/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 diff --git a/website/cmd/ttyshot/interp.go b/website/cmd/ttyshot/interp.go index b3e0c4f9..e96bc30c 100644 --- a/website/cmd/ttyshot/interp.go +++ b/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 { diff --git a/website/cmd/ttyshot/main.go b/website/cmd/ttyshot/main.go index fef1f42f..f7317bab 100644 --- a/website/cmd/ttyshot/main.go +++ b/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 { diff --git a/website/cmd/ttyshot/parse.go b/website/cmd/ttyshot/parse.go index 65d148a8..54b0fadb 100644 --- a/website/cmd/ttyshot/parse.go +++ b/website/cmd/ttyshot/parse.go @@ -3,48 +3,48 @@ package main import ( + "fmt" + "regexp" "strings" ) -type opType int +type op struct { + codeLines []string + tmuxCommand []string +} -// 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 +var ( + ps1Pattern = regexp.MustCompile(`^[~/][^ ]*> `) + tmuxPattern = regexp.MustCompile(`^#[a-z]`) ) -type op struct { - typ opType - val any -} - -func parseSpec(content string) []op { +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) } - ops = append(ops, newOp) + content := line[len(ps1):] + if tmuxPattern.MatchString(content) { + ops = append(ops, op{tmuxCommand: strings.Fields(content[1:])}) + continue + } + + 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}) } - 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}) - } - - return ops + return ops, nil } diff --git a/website/home/control-structures.ttyshot b/website/home/control-structures.ttyshot index ebb792ff..a5e893af 100644 --- a/website/home/control-structures.ttyshot +++ b/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 + } diff --git a/website/home/histlist-mode.ttyshot b/website/home/histlist-mode.ttyshot index 33f3cd68..2328ba4e 100644 --- a/website/home/histlist-mode.ttyshot +++ b/website/home/histlist-mode.ttyshot @@ -1 +1 @@ -#send-keys C-R Up Up +~> #send-keys C-R Up Up diff --git a/website/home/location-mode.ttyshot b/website/home/location-mode.ttyshot index 1a05179a..c3374b15 100644 --- a/website/home/location-mode.ttyshot +++ b/website/home/location-mode.ttyshot @@ -1 +1 @@ -#send-keys C-L +~> #send-keys C-L diff --git a/website/home/navigation-mode.ttyshot b/website/home/navigation-mode.ttyshot index e15342f5..13f9fe7f 100644 --- a/website/home/navigation-mode.ttyshot +++ b/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 diff --git a/website/home/pipelines.ttyshot b/website/home/pipelines.ttyshot index 6266a75f..a189d496 100644 --- a/website/home/pipelines.ttyshot +++ b/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 diff --git a/website/learn/fundamentals/history-1.ttyshot b/website/learn/fundamentals/history-1.ttyshot index 8843a0cb..48eb0fee 100644 --- a/website/learn/fundamentals/history-1.ttyshot +++ b/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 diff --git a/website/learn/fundamentals/history-2.ttyshot b/website/learn/fundamentals/history-2.ttyshot index ac25be4f..51c7a14e 100644 --- a/website/learn/fundamentals/history-2.ttyshot +++ b/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 diff --git a/website/learn/tour/completion-filter.ttyshot b/website/learn/tour/completion-filter.ttyshot index 22c2bf96..660c7d27 100644 --- a/website/learn/tour/completion-filter.ttyshot +++ b/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 diff --git a/website/learn/tour/completion.ttyshot b/website/learn/tour/completion.ttyshot index 2507aa30..a478c2f8 100644 --- a/website/learn/tour/completion.ttyshot +++ b/website/learn/tour/completion.ttyshot @@ -1,3 +1,2 @@ -cd elvish -#prompt -#send-keys echo Space Tab +~> cd elvish +~> #send-keys echo Space Tab diff --git a/website/learn/tour/history-list.ttyshot b/website/learn/tour/history-list.ttyshot index cf5cf47f..c63ec4fa 100644 --- a/website/learn/tour/history-list.ttyshot +++ b/website/learn/tour/history-list.ttyshot @@ -1 +1 @@ -#send-keys C-R +~> #send-keys C-R diff --git a/website/learn/tour/history-walk-prefix.ttyshot b/website/learn/tour/history-walk-prefix.ttyshot index 0f5cae1d..cc629966 100644 --- a/website/learn/tour/history-walk-prefix.ttyshot +++ b/website/learn/tour/history-walk-prefix.ttyshot @@ -1 +1 @@ -#send-keys echo Up Up Up +~> #send-keys echo Up Up Up diff --git a/website/learn/tour/history-walk.ttyshot b/website/learn/tour/history-walk.ttyshot index 60eefc95..bb516dfb 100644 --- a/website/learn/tour/history-walk.ttyshot +++ b/website/learn/tour/history-walk.ttyshot @@ -1 +1 @@ -#send-keys Up +~> #send-keys Up diff --git a/website/learn/tour/lastcmd.ttyshot b/website/learn/tour/lastcmd.ttyshot index 057d97ca..d1a45641 100644 --- a/website/learn/tour/lastcmd.ttyshot +++ b/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-, diff --git a/website/learn/tour/location-filter.ttyshot b/website/learn/tour/location-filter.ttyshot index 56648730..5f61fdff 100644 --- a/website/learn/tour/location-filter.ttyshot +++ b/website/learn/tour/location-filter.ttyshot @@ -1 +1 @@ -#send-keys C-L local +~> #send-keys C-L local diff --git a/website/learn/tour/location.ttyshot b/website/learn/tour/location.ttyshot index 1a05179a..c3374b15 100644 --- a/website/learn/tour/location.ttyshot +++ b/website/learn/tour/location.ttyshot @@ -1 +1 @@ -#send-keys C-L +~> #send-keys C-L diff --git a/website/learn/tour/navigation.ttyshot b/website/learn/tour/navigation.ttyshot index e15342f5..13f9fe7f 100644 --- a/website/learn/tour/navigation.ttyshot +++ b/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 diff --git a/website/learn/tour/unicode-prompts.ttyshot b/website/learn/tour/unicode-prompts.ttyshot index bcb9e343..86b6ca8e 100644 --- a/website/learn/tour/unicode-prompts.ttyshot +++ b/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! diff --git a/website/ref/edit/completion-mode.ttyshot b/website/ref/edit/completion-mode.ttyshot index fad8b24d..46deca5e 100644 --- a/website/ref/edit/completion-mode.ttyshot +++ b/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