mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-11-28 07:21:21 +08:00
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.
This commit is contained in:
parent
07791a4631
commit
82a72e31e3
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 @@
|
|||
#send-keys C-R Up Up
|
||||
~> #send-keys C-R Up Up
|
||||
|
|
|
@ -1 +1 @@
|
|||
#send-keys C-L
|
||||
~> #send-keys C-L
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
cd elvish; echo '[CUT]'
|
||||
#prompt
|
||||
#send-keys C-N
|
||||
~> cd elvish; echo '[CUT]'
|
||||
~> #send-keys C-N
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
cd elvish
|
||||
#prompt
|
||||
#send-keys echo Space Tab .md
|
||||
~> cd elvish
|
||||
~> #send-keys echo Space Tab .md
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
cd elvish
|
||||
#prompt
|
||||
#send-keys echo Space Tab
|
||||
~> cd elvish
|
||||
~> #send-keys echo Space Tab
|
||||
|
|
|
@ -1 +1 @@
|
|||
#send-keys C-R
|
||||
~> #send-keys C-R
|
||||
|
|
|
@ -1 +1 @@
|
|||
#send-keys echo Up Up Up
|
||||
~> #send-keys echo Up Up Up
|
||||
|
|
|
@ -1 +1 @@
|
|||
#send-keys Up
|
||||
~> #send-keys Up
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
echo abc def
|
||||
#prompt
|
||||
#send-keys vim Space M-,
|
||||
~> echo abc def
|
||||
~> #send-keys vim Space M-,
|
||||
|
|
|
@ -1 +1 @@
|
|||
#send-keys C-L local
|
||||
~> #send-keys C-L local
|
||||
|
|
|
@ -1 +1 @@
|
|||
#send-keys C-L
|
||||
~> #send-keys C-L
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
cd elvish; echo '[CUT]'
|
||||
#prompt
|
||||
#send-keys C-N
|
||||
~> cd elvish; echo '[CUT]'
|
||||
~> #send-keys C-N
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
cd elvish; echo '[CUT]'
|
||||
#prompt
|
||||
#send-keys vim Space Tab
|
||||
~> cd elvish; echo '[CUT]'
|
||||
~> #send-keys vim Space Tab
|
||||
|
|
Loading…
Reference in New Issue
Block a user