├── example
├── togif
│ ├── demo.gif
│ ├── README.md
│ ├── go.mod
│ ├── main.go
│ └── go.sum
└── webplayer
│ ├── README.md
│ ├── go.mod
│ ├── web
│ ├── index.html
│ ├── xterm
│ │ ├── xterm-addon-fit.js
│ │ └── xterm.css
│ └── player.html
│ ├── termhandler.go
│ ├── main.go
│ ├── wsterm.go
│ └── go.sum
├── terminal_os_other.go
├── go.mod
├── .gitignore
├── go.sum
├── terminal_os_windows.go
├── .github
└── workflows
│ ├── dependabot.yml
│ └── testing.yml
├── terminal.go
├── options.go
├── LICENSE
├── framesource_stream_test.go
├── framesource.go
├── cmd
└── asciinema-player
│ └── main.go
├── framesource_stream.go
├── frame.go
├── README.md
├── terminal_os.go
├── testdata
├── terminal_out.bin
└── test.cast
├── player.go
├── player_test.go
└── app-demo.cast
/example/togif/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xakep666/asciinema-player/HEAD/example/togif/demo.gif
--------------------------------------------------------------------------------
/terminal_os_other.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package player
5 |
6 | func enableVT100(fd int) error { return nil }
7 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/xakep666/asciinema-player/v3
2 |
3 | go 1.17
4 |
5 | require (
6 | golang.org/x/sys v0.1.0
7 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
8 | )
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Go template
3 | # Binaries for programs and plugins
4 | *.exe
5 | *.exe~
6 | *.dll
7 | *.so
8 | *.dylib
9 |
10 | # Test binary, build with `go test -c`
11 | *.test
12 |
13 | # Output of the go coverage tool, specifically when used with LiteIDE
14 | *.out
15 |
16 | asciinema-player
17 | !asciinema-player/
18 | .idea
19 |
--------------------------------------------------------------------------------
/example/webplayer/README.md:
--------------------------------------------------------------------------------
1 | # webplayer
2 |
3 | A simple web player for casts stored on server.
4 |
5 | Usage:
6 | ```
7 | Usage of webplayer:
8 | -listen-addr string
9 | HTTP server listen address
10 | -rootdir string
11 | Root directory for casts (default ".")
12 | ```
13 |
14 | I.e. run with `go run -v . -rootdir=../.. -listen-addr=:8080` and go to http://localhost:8080/
15 | You will see some available casts, just click on it to start playback.
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
3 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
5 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
6 |
--------------------------------------------------------------------------------
/example/webplayer/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/xakep666/asciinema-player/example/webplayer
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/xakep666/asciinema-player/v3 v3.0.0
7 | nhooyr.io/websocket v1.8.7
8 | )
9 |
10 | require (
11 | github.com/klauspost/compress v1.10.3 // indirect
12 | golang.org/x/sys v0.1.0 // indirect
13 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
14 | )
15 |
16 | replace github.com/xakep666/asciinema-player/v3 v3.0.0 => ../../
17 |
--------------------------------------------------------------------------------
/example/togif/README.md:
--------------------------------------------------------------------------------
1 | # togif
2 |
3 | Renders asciicast to gif image using `fyne` gui toolkit.
4 |
5 | Usage:
6 | ```
7 | Usage of togif:
8 | -f string
9 | path to asciinema v2 file
10 | -maxWait duration
11 | maximum time between frames (default 2s)
12 | -o string
13 | path to output gif
14 | -speed float
15 | speed adjustment: <1 - increase, >1 - decrease (default 1)
16 |
17 | ```
18 |
19 | Required parameters are `-f` and `-o`.
20 |
21 | Example gif rendered from `app-demo.cast` with default settings:
22 | 
23 |
--------------------------------------------------------------------------------
/terminal_os_windows.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "errors"
5 |
6 | "golang.org/x/sys/windows"
7 | )
8 |
9 | func enableVT100(fd int) error {
10 | var consoleMode uint32
11 |
12 | err := windows.GetConsoleMode(windows.Handle(fd), &consoleMode)
13 | if err != nil {
14 | return err
15 | }
16 |
17 | consoleMode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
18 |
19 | err = windows.SetConsoleMode(windows.Handle(fd), consoleMode)
20 | switch {
21 | case errors.Is(err, nil):
22 | return nil
23 | case errors.Is(err, windows.ERROR_INVALID_PARAMETER): // vt100 not supported
24 | return nil
25 | default:
26 | return err
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-approve
2 | on: pull_request
3 |
4 | permissions:
5 | pull-requests: write
6 |
7 | jobs:
8 | dependabot:
9 | runs-on: ubuntu-latest
10 | if: ${{ github.actor == 'dependabot[bot]' }}
11 | steps:
12 | - name: Dependabot metadata
13 | id: metadata
14 | uses: dependabot/fetch-metadata@v1
15 | with:
16 | github-token: "${{ secrets.GITHUB_TOKEN }}"
17 | - name: Approve a PR
18 | run: gh pr review --approve "$PR_URL"
19 | env:
20 | PR_URL: ${{github.event.pull_request.html_url}}
21 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
22 |
--------------------------------------------------------------------------------
/example/webplayer/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | name: Go Test
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Set up Go
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.17
19 |
20 | - name: Download dependencies
21 | run: go mod download -x
22 |
23 | - name: Test
24 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
25 |
26 | - uses: codecov/codecov-action@v2
27 | with:
28 | token: ${{ secrets.CODECOV_TOKEN }}
29 | files: ./coverage.txt
30 | flags: unittests
31 | verbose: true
32 |
--------------------------------------------------------------------------------
/terminal.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "io"
5 | )
6 |
7 | // Terminal is interface for terminal interaction.
8 | type Terminal interface {
9 | io.WriteCloser
10 |
11 | // Dimensions returns terminal window size.
12 | Dimensions() (width, height int)
13 |
14 | // ToRaw puts terminal to raw mode. Implementation must store previous terminal state.
15 | ToRaw() error
16 |
17 | // Restore puts terminal into state stored in ToRaw.
18 | Restore() error
19 |
20 | // Control starts "event loop" where Terminal may call methods of PlaybackControl. Method blocks until Close.
21 | Control(PlaybackControl)
22 | }
23 |
24 | // PlaybackControl describes playback control methods for Terminal.
25 | type PlaybackControl interface {
26 | // Pause pauses playback. If playback already paused it will continue.
27 | Pause()
28 |
29 | // Stop interrupts playback. Must be called once.
30 | Stop()
31 |
32 | sealed()
33 | }
34 |
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type options struct {
8 | maxWait time.Duration
9 | speed float64
10 | ignoreSizeCheck bool
11 | }
12 |
13 | // Option for Player.
14 | type Option func(*options)
15 |
16 | // WithMaxWait sets minimal delay between frames. Zero or negative value are ignored.
17 | func WithMaxWait(t time.Duration) Option {
18 | return func(o *options) {
19 | if t > 0 {
20 | o.maxWait = t
21 | }
22 | }
23 | }
24 |
25 | // WithSpeed sets playback speed.
26 | // Values greater than 1 speeds up playback.
27 | // Values between 0 and 1 slows down playback.
28 | // Negative values are ignored.
29 | func WithSpeed(speed float64) Option {
30 | return func(o *options) {
31 | if speed > 0 {
32 | o.speed = speed
33 | }
34 | }
35 | }
36 |
37 | //WithIgnoreSizeCheck turns off check that terminal can fit frames.
38 | func WithIgnoreSizeCheck() Option {
39 | return func(o *options) {
40 | o.ignoreSizeCheck = true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Containerum
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/framesource_stream_test.go:
--------------------------------------------------------------------------------
1 | package player_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "os"
7 | "path/filepath"
8 | "testing"
9 |
10 | player "github.com/xakep666/asciinema-player/v3"
11 | )
12 |
13 | func TestStreamFrameSource(t *testing.T) {
14 | cast, err := os.ReadFile(filepath.Join("testdata", "test.cast"))
15 | if err != nil {
16 | t.Fatalf("File read failed: %s", err)
17 | }
18 |
19 | var buf bytes.Buffer
20 | enc := json.NewEncoder(&buf)
21 |
22 | source, err := player.NewStreamFrameSource(bytes.NewReader(cast))
23 | if err != nil {
24 | t.Fatalf("Source create failed: %s", err)
25 | }
26 |
27 | if err = enc.Encode(source.Header()); err != nil {
28 | t.Fatalf("Failed to encode header: %s", err)
29 | }
30 |
31 | for source.Next() {
32 | frame := source.Frame()
33 | if err = enc.Encode([]interface{}{frame.Time, frame.Type, string(frame.Data)}); err != nil {
34 | t.Fatalf("Failed to encode header: %s", err)
35 | }
36 | }
37 |
38 | if err = source.Err(); err != nil {
39 | t.Fatalf("Source error: %s", err)
40 | }
41 |
42 | if !bytes.Equal(cast, buf.Bytes()) {
43 | t.Fatalf("Output not equal to input. Output:\n%s", buf.String())
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/framesource.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | // FrameType is a type of Frame.
4 | type FrameType string
5 |
6 | const (
7 | // InputFrame contains data sent from stdin of recorded shell.
8 | InputFrame FrameType = "i"
9 |
10 | // OutputFrame contains data written to stdout of recorded shell.
11 | OutputFrame FrameType = "o"
12 | )
13 |
14 | const FormatVersion = 2
15 |
16 | // Header represents asciinema-v2 header (first line). It doesn't include unneeded fields.
17 | type Header struct {
18 | // Version is a format version. Must be 2.
19 | Version int `json:"version"`
20 |
21 | // With is a captured terminal width.
22 | Width int `json:"width"`
23 |
24 | // Height is a captured terminal height.
25 | Height int `json:"height"`
26 | }
27 |
28 | // FrameSource describes frames source.
29 | type FrameSource interface {
30 | // Header returns asciinema-v2 header.
31 | Header() Header
32 |
33 | // Next advances to next available frame. It must return false if error occurs or there is no more frames.
34 | Next() bool
35 |
36 | // Frame returns current frame. It becomes unusable after Next call.
37 | Frame() Frame
38 |
39 | // Err returns error if it happens during iteration.
40 | Err() error
41 | }
42 |
--------------------------------------------------------------------------------
/example/togif/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/xakep666/asciinema-player/v3/example/togif
2 |
3 | go 1.17
4 |
5 | require (
6 | fyne.io/fyne/v2 v2.1.1
7 | github.com/fyne-io/terminal v0.0.0-20220109064744-3fb0c336f262
8 | github.com/xakep666/asciinema-player/v3 v3.0.0
9 | )
10 |
11 | require (
12 | github.com/ActiveState/termtest/conpty v0.5.0 // indirect
13 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
14 | github.com/creack/pty v1.1.17 // indirect
15 | github.com/davecgh/go-spew v1.1.1 // indirect
16 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
17 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
18 | github.com/pmezard/go-difflib v1.0.0 // indirect
19 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
20 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
21 | github.com/stretchr/testify v1.6.1 // indirect
22 | github.com/yuin/goldmark v1.4.13 // indirect
23 | golang.org/x/image v0.5.0 // indirect
24 | golang.org/x/net v0.17.0 // indirect
25 | golang.org/x/sys v0.13.0 // indirect
26 | golang.org/x/term v0.13.0 // indirect
27 | golang.org/x/text v0.13.0 // indirect
28 | gopkg.in/yaml.v3 v3.0.0 // indirect
29 | )
30 |
31 | replace github.com/xakep666/asciinema-player/v3 v3.0.0 => ../../
32 |
--------------------------------------------------------------------------------
/cmd/asciinema-player/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | player "github.com/xakep666/asciinema-player/v3"
10 | )
11 |
12 | func errExit(err error) {
13 | if err != nil {
14 | fmt.Println(err)
15 | os.Exit(1)
16 | }
17 | }
18 |
19 | var (
20 | maxWait time.Duration
21 | speed float64
22 | filePath string
23 | )
24 |
25 | func init() {
26 | flag.DurationVar(&maxWait, "maxWait", 2*time.Second, "maximum time between frames")
27 | flag.Float64Var(&speed, "speed", 1, "speed adjustment: <1 - increase, >1 - decrease")
28 | flag.StringVar(&filePath, "f", "", "path to asciinema v2 file")
29 | flag.Parse()
30 | }
31 |
32 | func main() {
33 | if filePath == "" {
34 | fmt.Println("Please specify file\nUsage:")
35 | flag.PrintDefaults()
36 | os.Exit(1)
37 | }
38 |
39 | file, err := os.Open(filePath)
40 | errExit(err)
41 | defer file.Close()
42 |
43 | source, err := player.NewStreamFrameSource(file)
44 | errExit(err)
45 |
46 | term, err := player.NewOSTerminal()
47 | errExit(err)
48 | defer term.Close()
49 |
50 | p, err := player.NewPlayer(source, term, player.WithSpeed(speed), player.WithMaxWait(maxWait), player.WithIgnoreSizeCheck())
51 | errExit(err)
52 |
53 | err = p.Start()
54 | if err != nil {
55 | fmt.Println("Start failed:", err)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/framesource_stream.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | // StreamFrameSource reads frames from io.Reader.
10 | type StreamFrameSource struct {
11 | dec *json.Decoder
12 |
13 | hdr Header
14 | frame Frame
15 | err error
16 | }
17 |
18 | // NewStreamFrameSource constructs StreamFrameSource. It reads Header from input stream.
19 | func NewStreamFrameSource(reader io.Reader) (*StreamFrameSource, error) {
20 | dec := json.NewDecoder(reader)
21 |
22 | var hdr Header
23 | if err := dec.Decode(&hdr); err != nil {
24 | return nil, fmt.Errorf("read header failed: %w", err)
25 | }
26 |
27 | return &StreamFrameSource{
28 | dec: dec,
29 | hdr: hdr,
30 | }, nil
31 | }
32 |
33 | // Header returns asciinema-v2 header.
34 | func (s *StreamFrameSource) Header() Header { return s.hdr }
35 |
36 | // Next advances to next available frame. It must return false if error occurs or there is no more frames.
37 | func (s *StreamFrameSource) Next() bool {
38 | err := s.dec.Decode(&s.frame)
39 | switch err {
40 | case nil:
41 | return true
42 | case io.EOF: // all done
43 | return false
44 | default:
45 | s.err = err
46 | return false
47 | }
48 | }
49 |
50 | // Frame returns current frame. It becomes unusable after Next call.
51 | func (s *StreamFrameSource) Frame() Frame { return s.frame }
52 |
53 | // Err returns error if it happens during iteration.
54 | func (s *StreamFrameSource) Err() error { return s.err }
55 |
--------------------------------------------------------------------------------
/example/webplayer/web/xterm/xterm-addon-fit.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={775:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0;var r=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core;if(0!==e._renderService.dimensions.actualCellWidth&&0!==e._renderService.dimensions.actualCellHeight){var t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),i=Math.max(0,parseInt(t.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),o=r-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=i-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(o/e._renderService.dimensions.actualCellHeight))}}}},e}();t.FitAddon=r}},t={};return function r(i){if(t[i])return t[i].exports;var n=t[i]={exports:{}};return e[i](n,n.exports,r),n.exports}(775)})()}));
2 |
--------------------------------------------------------------------------------
/example/webplayer/termhandler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | player "github.com/xakep666/asciinema-player/v3"
5 | "io/fs"
6 | "net/http"
7 | "nhooyr.io/websocket"
8 | )
9 |
10 | type TermHandler struct {
11 | FS fs.FS
12 | FileSet map[string]struct{}
13 | }
14 |
15 | func (h *TermHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
16 | fileName := r.URL.Query().Get("file")
17 | if _, ok := h.FileSet[fileName]; !ok {
18 | http.Error(w, "Requested file was not found", http.StatusNotFound)
19 | return
20 | }
21 |
22 | conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{})
23 | if err != nil {
24 | return
25 | }
26 |
27 | go h.handleConn(fileName, conn)
28 | }
29 |
30 | func (h *TermHandler) handleConn(fileName string, conn *websocket.Conn) {
31 | term, err := NewWSTerm(conn)
32 | if err != nil {
33 | conn.Close(websocket.StatusProtocolError, "failed to initiate terminal:"+err.Error())
34 | return
35 | }
36 |
37 | defer term.Close()
38 |
39 | file, err := h.FS.Open(fileName)
40 | if err != nil {
41 | conn.Close(websocket.StatusProtocolError, "file open failed:"+err.Error())
42 | return
43 | }
44 |
45 | defer file.Close()
46 |
47 | src, err := player.NewStreamFrameSource(file)
48 | if err != nil {
49 | conn.Close(websocket.StatusProtocolError, "frame source create failed:"+err.Error())
50 | return
51 | }
52 |
53 | p, err := player.NewPlayer(src, term)
54 | if err != nil {
55 | conn.Close(websocket.StatusProtocolError, "player create failed:"+err.Error())
56 | return
57 | }
58 |
59 | if err = p.Start(); err != nil {
60 | conn.Close(websocket.StatusProtocolError, "playback error:"+err.Error())
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/example/webplayer/web/player.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 | Press "Space" for pause/play
14 | Press "Q" for stop
15 |
16 |
46 |
47 |
--------------------------------------------------------------------------------
/frame.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | // Frame represents asciinema-v2 frame.
9 | // This is JSON-array with fixed size of 3 elements:
10 | // [0]: frame delay in seconds (float64),
11 | // [1]: frame type,
12 | // [2]: frame data (escaped string).
13 | type Frame struct {
14 | // Time in seconds since record start.
15 | Time float64
16 |
17 | // Type of frame.
18 | Type FrameType
19 |
20 | // Data contains frame data.
21 | Data []byte
22 | }
23 |
24 | // FrameUnmarshalError returned if frame conversion to struct failed.
25 | type FrameUnmarshalError struct {
26 | Description string
27 | Index int
28 | }
29 |
30 | func (e *FrameUnmarshalError) Error() string {
31 | return fmt.Sprintf("frame[%d]: %s", e.Index, e.Description)
32 | }
33 |
34 | // UnmarshalJSON implements json.Unmarshaler.
35 | func (f *Frame) UnmarshalJSON(b []byte) error {
36 | var rawFrame [3]interface{}
37 | if err := json.Unmarshal(b, &rawFrame); err != nil {
38 | return err
39 | }
40 |
41 | switch t := rawFrame[0].(type) {
42 | case float64:
43 | f.Time = t
44 | default:
45 | return &FrameUnmarshalError{Description: fmt.Sprintf("invalid type %T", t), Index: 0}
46 | }
47 |
48 | switch frameTypeRaw := rawFrame[1].(type) {
49 | case string:
50 | switch FrameType(frameTypeRaw) {
51 | case InputFrame, OutputFrame:
52 | f.Type = FrameType(frameTypeRaw)
53 | default:
54 | return &FrameUnmarshalError{Description: fmt.Sprintf("invalid value %v", frameTypeRaw), Index: 1}
55 | }
56 | default:
57 | return &FrameUnmarshalError{Description: fmt.Sprintf("invalid type %T", frameTypeRaw), Index: 1}
58 | }
59 |
60 | switch text := rawFrame[2].(type) {
61 | case string:
62 | f.Data = []byte(text)
63 | default:
64 | return &FrameUnmarshalError{Description: fmt.Sprintf("invalid type %T", text), Index: 2}
65 | }
66 |
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/example/webplayer/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "encoding/json"
6 | "flag"
7 | "fmt"
8 | "io/fs"
9 | "log"
10 | "net"
11 | "net/http"
12 | "os"
13 | "path/filepath"
14 | )
15 |
16 | var (
17 | rootDir = flag.String("rootdir", ".", "Root directory for casts")
18 | listenAddr = flag.String("listen-addr", "", "HTTP server listen address")
19 | )
20 |
21 | //go:embed web
22 | var webpages embed.FS
23 |
24 | func main() {
25 | flag.Parse()
26 |
27 | fsys, fset, err := getFSWithSet(*rootDir)
28 | if err != nil {
29 | log.Fatalln("Open root directory failed:", err)
30 | }
31 |
32 | flist := make([]string, 0, len(fset))
33 | for fname := range fset {
34 | flist = append(flist, fname)
35 | }
36 |
37 | mux := http.NewServeMux()
38 | mux.Handle("/play", &TermHandler{FS: fsys, FileSet: fset})
39 | mux.HandleFunc("/files", func(w http.ResponseWriter, r *http.Request) {
40 | json.NewEncoder(w).Encode(map[string]interface{}{"files": flist})
41 | })
42 |
43 | sub, _ := fs.Sub(webpages, "web")
44 | mux.Handle("/", http.FileServer(http.FS(sub)))
45 |
46 | lis, err := net.Listen("tcp", *listenAddr)
47 | if err != nil {
48 | log.Fatalln("Listen failed:", err)
49 | }
50 |
51 | log.Println("Listening on ", lis.Addr())
52 |
53 | if err = http.Serve(lis, mux); err != http.ErrServerClosed {
54 | log.Fatalln("Serve failed:", err)
55 | }
56 | }
57 |
58 | func getFSWithSet(rootDir string) (fs.FS, map[string]struct{}, error) {
59 | abs, err := filepath.Abs(rootDir)
60 | if err != nil {
61 | return nil, nil, fmt.Errorf("resolve absoulete path failed: %s", err)
62 | }
63 |
64 | osfs := os.DirFS(abs)
65 | fset := make(map[string]struct{})
66 |
67 | err = fs.WalkDir(osfs, ".", func(path string, d fs.DirEntry, err error) error {
68 | if err != nil {
69 | return err
70 | }
71 |
72 | if filepath.Ext(path) == ".cast" && d.Type().IsRegular() {
73 | fset[path] = struct{}{}
74 | }
75 |
76 | return nil
77 | })
78 |
79 | return osfs, fset, err
80 | }
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # asciinema-player
2 | [](https://github.com/xakep666/asciinema-player/actions/workflows/testing.yml)
3 | [](https://codecov.io/gh/xakep666/asciinema-player)
4 | [](https://goreportcard.com/report/github.com/xakep666/asciinema-player)
5 | [](https://pkg.go.dev/github.com/xakep666/asciinema-player/v3)
6 | [](https://opensource.org/licenses/MIT)
7 |
8 | asciinema-player is a library and cli-app to play terminal sessions recorded by asciinema (http://github.com/asciinema/asciinema)
9 |
10 | ## Prerequisites
11 | * Golang >= 1.17
12 |
13 | ## Installation
14 | Library:
15 | ```bash
16 | go get -v -u github.com/xakep666/asciinema-player
17 | ```
18 |
19 | App:
20 | ```bash
21 | go get -v -u github.com/xakep666/asciinema-player/cmd/asciinema-player
22 | ```
23 |
24 | ## Usage
25 | ### App
26 | ```
27 | $ ./asciinema-player --help
28 | Usage of ./asciinema-player:
29 | -f string
30 | path to asciinema v2 file
31 | -maxWait duration
32 | maximum time between frames (default 2s)
33 | -speed float
34 | speed adjustment: <1 - increase, >1 - decrease (default 1)
35 | ```
36 | For example you can play test session `./asciinema-player -f test.cast`
37 |
38 | [](https://asciinema.org/a/189343)
39 |
40 | ### Library
41 | ```go
42 | frameSource, err := player.NewStreamFrameSource(reader)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | term, err := player.NewOSTerminal()
48 | if err != nil {
49 | return err
50 | }
51 |
52 | defer term.Close()
53 |
54 | player, err := player.NewPlayer(frameSource, terminal)
55 | if err != nil {
56 | return err
57 | }
58 |
59 | err = player.Play()
60 | if err != nil {
61 | return err
62 | }
63 | ```
64 | Library usage example is app, actually.
65 |
66 | ## Examples
67 | [Renderer to GIF](./example/togif)
68 | [Web-based player for server-stored casts](./example/webplayer)
69 |
70 | ## License
71 | Asciinema-player project is licensed under the terms of the MIT license. Please see LICENSE in this repository for more details.
72 |
--------------------------------------------------------------------------------
/example/webplayer/wsterm.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 |
9 | "nhooyr.io/websocket"
10 | "nhooyr.io/websocket/wsjson"
11 |
12 | player "github.com/xakep666/asciinema-player/v3"
13 | )
14 |
15 | type MessageType int
16 |
17 | const (
18 | DimensionsMessage MessageType = iota
19 | DataMessage
20 | PlayPauseMessage
21 | StopMessage
22 | )
23 |
24 | type Dimensions struct {
25 | Width int `json:"width"`
26 | Height int `json:"height"`
27 | }
28 |
29 | type Message struct {
30 | Type MessageType `json:"type"`
31 |
32 | Dimensions *Dimensions `json:"dimensions,omitempty"`
33 | Data string `json:"data,omitempty"`
34 | }
35 |
36 | type WSTerm struct {
37 | conn *websocket.Conn
38 | stop chan struct{}
39 |
40 | dimensions Dimensions
41 | }
42 |
43 | func NewWSTerm(conn *websocket.Conn) (*WSTerm, error) {
44 | var msg Message
45 | if err := wsjson.Read(context.Background(), conn, &msg); err != nil {
46 | return nil, fmt.Errorf("dimensions read error: %w", err)
47 | }
48 |
49 | if msg.Type != DimensionsMessage {
50 | return nil, fmt.Errorf("first message was not about dimensions")
51 | }
52 |
53 | return &WSTerm{
54 | conn: conn,
55 | stop: make(chan struct{}),
56 |
57 | dimensions: *msg.Dimensions,
58 | }, nil
59 | }
60 |
61 | func (t *WSTerm) Write(p []byte) (n int, err error) {
62 | return len(p), wsjson.Write(context.Background(), t.conn, Message{
63 | Type: DataMessage,
64 | Data: string(p),
65 | })
66 | }
67 |
68 | func (t *WSTerm) Close() error {
69 | close(t.stop)
70 | return t.conn.Close(websocket.StatusNormalClosure, "goodbye")
71 | }
72 |
73 | func (t *WSTerm) Dimensions() (width, height int) {
74 | return t.dimensions.Width, t.dimensions.Height
75 | }
76 |
77 | func (t *WSTerm) ToRaw() error { return nil }
78 |
79 | func (t *WSTerm) Restore() error { return nil }
80 |
81 | func (t *WSTerm) Control(control player.PlaybackControl) {
82 | ctx, cancel := context.WithCancel(context.Background())
83 | go func() {
84 | <-t.stop
85 | cancel()
86 | }()
87 |
88 | for {
89 | var msg Message
90 |
91 | err := wsjson.Read(ctx, t.conn, &msg)
92 | switch {
93 | case errors.Is(err, nil):
94 | case errors.Is(err, context.Canceled):
95 | return
96 | default:
97 | log.Printf("read error: %s", err)
98 | return
99 | }
100 |
101 | switch msg.Type {
102 | case PlayPauseMessage:
103 | control.Pause()
104 | case StopMessage:
105 | control.Stop()
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/terminal_os.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "golang.org/x/term"
8 | )
9 |
10 | const (
11 | space = 0x20
12 | ctrlC = 0x03
13 | )
14 |
15 | var ErrNotTerminal = fmt.Errorf("stdin is not terminal")
16 |
17 | // OSTerminal represents terminal on operating system.
18 | type OSTerminal struct {
19 | file *os.File
20 | stop chan struct{}
21 |
22 | width, height int
23 | state *term.State
24 | }
25 |
26 | // NewOSTerminal constructs OSTerminal from stdin.
27 | // It returns ErrNotTerminal if stdin is not terminal.
28 | func NewOSTerminal() (*OSTerminal, error) {
29 | return NewOSTerminalFromFile(os.Stdin)
30 | }
31 |
32 | // NewOSTerminalFromFile constructs OSTerminal from file.
33 | // It returns ErrNotTerminal if file is not terminal.
34 | func NewOSTerminalFromFile(file *os.File) (*OSTerminal, error) {
35 | if !term.IsTerminal(int(file.Fd())) {
36 | return nil, ErrNotTerminal
37 | }
38 |
39 | width, height, err := term.GetSize(int(file.Fd()))
40 | if err != nil {
41 | return nil, fmt.Errorf("get terminal size failed: %s", err)
42 | }
43 |
44 | return &OSTerminal{
45 | file: file,
46 | stop: make(chan struct{}),
47 | width: width,
48 | height: height,
49 | }, nil
50 | }
51 |
52 | func (t *OSTerminal) Write(p []byte) (n int, err error) { return t.file.Write(p) }
53 |
54 | // Close closes terminal (stop control loop). It doesn't close underlying file.
55 | func (t *OSTerminal) Close() error {
56 | close(t.stop)
57 | return nil
58 | }
59 |
60 | func (t *OSTerminal) Dimensions() (width, height int) { return t.width, t.height }
61 |
62 | func (t *OSTerminal) ToRaw() error {
63 | state, err := term.MakeRaw(int(t.file.Fd()))
64 | if err != nil {
65 | return fmt.Errorf("%s", err) // decouple from lib error
66 | }
67 |
68 | // attempt to enable vt100 capabilities to support ansi escape codes
69 | if err = enableVT100(int(t.file.Fd())); err != nil {
70 | return fmt.Errorf("%s", err)
71 | }
72 |
73 | t.state = state
74 |
75 | return nil
76 | }
77 |
78 | func (t *OSTerminal) Restore() error {
79 | if t.state == nil {
80 | return nil
81 | }
82 |
83 | // attempt to reset terminal to remove effects possibly set by player
84 | if _, err := fmt.Fprint(t.file, "\033c"); err != nil {
85 | return fmt.Errorf("reset terminal failed: %w", err)
86 | }
87 |
88 | if err := term.Restore(int(t.file.Fd()), t.state); err != nil {
89 | return fmt.Errorf("%s", err)
90 | }
91 |
92 | return nil
93 | }
94 |
95 | func (t *OSTerminal) Control(control PlaybackControl) {
96 | var buf [3]byte // for control sequences beginning with "ESC-["
97 | for {
98 | select {
99 | case <-t.stop:
100 | return
101 | default:
102 | }
103 |
104 | n, err := t.file.Read(buf[:])
105 | if err != nil {
106 | return
107 | }
108 |
109 | if n != 1 {
110 | continue
111 | }
112 |
113 | switch buf[0] {
114 | case space:
115 | control.Pause()
116 | case ctrlC:
117 | control.Stop()
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/testdata/terminal_out.bin:
--------------------------------------------------------------------------------
1 | ]0;fish /Users/sickill/code/asciinema/asciinema[30m(B[m[?2004h]0;fish /Users/sickill/code/asciinema/asciinema[30m(B[m[38;5;237m⏎(B[m
⏎
[2K[32m~/c/a/asciinema[30m(B[m (develop ↩☡=) [30m(B[m[Kv[38;2;0;95;215mv[30m(B[m[38;2;85;85;85mim tests/vim.cast [18D[30m(B[m[38;2;0;95;215mi[38;2;85;85;85mm tests/vim.cast [17D[30m(B[m[38;2;0;95;215mm[38;2;85;85;85m tests/vim.cast [16D[30m(B[m[K
2 | [30m(B[m[?2004l]0;vim /Users/sickill/code/asciinema/asciinema[30m(B[m
[?1000h[?2004h[?1049h[?1h=[?2004h[1;18r[?12h[?12l[27m[29m[m[38;5;231m[48;5;235m[H[2J[2;1H▽[6n[2;1H [1;1H[>c[?1000l[?1002h[?12$p[?25l[1;1H[93m1 [m[38;5;231m[48;5;235m
3 | [38;5;59m[48;5;236m~ [3;1H~ [4;1H~ [5;1H~ [6;1H~ [7;1H~ [8;1H~ [9;1H~ [10;1H~ [11;1H~ [12;1H~ [13;1H~ [14;1H~ [15;1H~ [16;1H~ [m[38;5;231m[48;5;235m[17;1H[1m[38;5;231m[48;5;236m[No Name] (unix/utf-8/) (line 0/1, col 000)[m[38;5;231m[48;5;235m[3;30HVIM - Vi IMproved[5;30Hversion 8.0.1171[6;26Hby Bram Moolenaar et al.[7;17HVim is open source and freely distributable[9;24HBecome a registered Vim user![10;15Htype :help register[38;5;59m[48;5;236m[m[38;5;231m[48;5;235m for information [12;15Htype :q[38;5;59m[48;5;236m[m[38;5;231m[48;5;235m to exit [13;15Htype :help[38;5;59m[48;5;236m[m[38;5;231m[48;5;235m or [38;5;59m[48;5;236m[m[38;5;231m[48;5;235m for on-line help[14;15Htype :help version8[38;5;59m[48;5;236m[m[38;5;231m[48;5;235m for version info[1;5H[?25h[?25l[18;65H:[1;5H[18;65H[K[18;1H:[?2004l[?2004h[?25hq[?25l[?25h
[?25l[?1002l[?2004l[18;1H[K[18;1H[?2004l[?1l>[?25h[?1049l[?2004h]0;fish /Users/sickill/code/asciinema/asciinema[30m(B[m[38;5;237m⏎(B[m
⏎
[2K[32m~/c/a/asciinema[30m(B[m (develop ↩☡=) [30m(B[m[K
4 | [30m(B[m[30m(B[m[?2004l
--------------------------------------------------------------------------------
/player.go:
--------------------------------------------------------------------------------
1 | package player
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | var (
9 | ErrUnexpectedVersion = fmt.Errorf("unexpected asciicast version")
10 | ErrSmallTerminal = fmt.Errorf("terminal too small for frames")
11 | )
12 |
13 | type Player struct {
14 | frameSource FrameSource
15 | terminal Terminal
16 | options options
17 |
18 | pause chan struct{}
19 | stop chan struct{}
20 | }
21 |
22 | func NewPlayer(frameSource FrameSource, terminal Terminal, opts ...Option) (*Player, error) {
23 | defaultOptions := options{
24 | maxWait: 0,
25 | speed: 1,
26 | }
27 |
28 | for _, o := range opts {
29 | o(&defaultOptions)
30 | }
31 |
32 | termWidth, termHeight := terminal.Dimensions()
33 | hdr := frameSource.Header()
34 |
35 | if hdr.Version != FormatVersion {
36 | return nil, ErrUnexpectedVersion
37 | }
38 |
39 | if !defaultOptions.ignoreSizeCheck && (hdr.Height > termHeight || hdr.Width > termWidth) {
40 | return nil, ErrSmallTerminal
41 | }
42 |
43 | p := &Player{
44 | frameSource: frameSource,
45 | terminal: terminal,
46 | options: defaultOptions,
47 | pause: make(chan struct{}),
48 | stop: make(chan struct{}),
49 | }
50 |
51 | go terminal.Control(p)
52 |
53 | return p, nil
54 | }
55 |
56 | // Start starts playback. Method blocks until Stop call.
57 | func (p *Player) Start() (err error) {
58 | if err = p.terminal.ToRaw(); err != nil {
59 | return fmt.Errorf("put terminal to raw mode failed: %w", err)
60 | }
61 |
62 | defer func() {
63 | if restoreErr := p.terminal.Restore(); restoreErr != nil {
64 | err = fmt.Errorf("restore terminal failed: %w", restoreErr)
65 | }
66 | }()
67 |
68 | timer := time.NewTimer(1)
69 | <-timer.C // wait for first tick
70 |
71 | prevFrameTime := 0.
72 | for {
73 | if !p.frameSource.Next() {
74 | return p.frameSource.Err()
75 | }
76 |
77 | frame := p.frameSource.Frame()
78 | if frame.Type != OutputFrame {
79 | continue
80 | }
81 |
82 | timer.Stop()
83 | timer.Reset(p.nextFrameDelay(frame, prevFrameTime))
84 | prevFrameTime = frame.Time
85 |
86 | select {
87 | case <-timer.C:
88 | // play
89 | case <-p.pause:
90 | select {
91 | case <-p.pause:
92 | // play
93 | case <-p.stop:
94 | return nil
95 | }
96 | case <-p.stop:
97 | return nil
98 | }
99 |
100 | if _, err = p.terminal.Write(frame.Data); err != nil {
101 | return fmt.Errorf("frame write failed: %w", err)
102 | }
103 | }
104 | }
105 |
106 | func (p *Player) nextFrameDelay(frame Frame, prevFrameTime float64) time.Duration {
107 | delay := time.Duration((frame.Time - prevFrameTime) / p.options.speed * float64(time.Second))
108 | if p.options.maxWait > 0 && delay > p.options.maxWait {
109 | return p.options.maxWait
110 | }
111 |
112 | return delay
113 | }
114 |
115 | // Pause pauses playback. If playback already paused it will continue.
116 | func (p *Player) Pause() {
117 | p.pause <- struct{}{}
118 | }
119 |
120 | // Stop interrupts playback. Must be called once.
121 | func (p *Player) Stop() {
122 | p.stop <- struct{}{}
123 | }
124 |
125 | func (p *Player) sealed() {}
126 |
--------------------------------------------------------------------------------
/example/togif/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "image"
7 | "image/color/palette"
8 | "image/draw"
9 | "image/gif"
10 | "io"
11 | "os"
12 | "time"
13 |
14 | "fyne.io/fyne/v2"
15 | "fyne.io/fyne/v2/driver/software"
16 | "github.com/fyne-io/terminal"
17 |
18 | player "github.com/xakep666/asciinema-player/v3"
19 | )
20 |
21 | var (
22 | filePath = flag.String("f", "", "path to asciinema v2 file")
23 | maxWait = flag.Duration("maxWait", 2*time.Second, "maximum time between frames")
24 | speed = flag.Float64("speed", 1, "speed adjustment: <1 - increase, >1 - decrease")
25 | outputPath = flag.String("o", "", "path to output gif")
26 | )
27 |
28 | func main() {
29 | flag.Parse()
30 |
31 | if *filePath == "" {
32 | fmt.Println("Please specify asciicast file\nUsage:")
33 | flag.PrintDefaults()
34 | os.Exit(1)
35 | }
36 |
37 | if *outputPath == "" {
38 | fmt.Println("Please specify output file\nUsage:")
39 | flag.PrintDefaults()
40 | os.Exit(1)
41 | }
42 |
43 | file, err := os.Open(*filePath)
44 | if err != nil {
45 | fmt.Println("Failed to open file", err)
46 | os.Exit(1)
47 | }
48 |
49 | src, err := player.NewStreamFrameSource(file)
50 | if err != nil {
51 | fmt.Println("Failed to create frame source", err)
52 | os.Exit(1)
53 | }
54 |
55 | cnv := software.NewCanvas()
56 | fyneTerm := terminal.New()
57 | pr, pw := io.Pipe()
58 |
59 | cnv.SetContent(fyneTerm)
60 | cnv.Resize(windowSize(src.Header()))
61 | go fyneTerm.RunWithConnection(nil, pr)
62 |
63 | var (
64 | images []*image.Paletted
65 | delays []int // 1/100 of second
66 | prevFrameTime = 0.
67 | )
68 |
69 | for src.Next() {
70 | frame := src.Frame()
71 | if frame.Type != player.OutputFrame {
72 | continue
73 | }
74 |
75 | _, err = pw.Write(frame.Data)
76 | if err != nil {
77 | fmt.Println("Failed to write to terminal", err)
78 | os.Exit(1)
79 | }
80 |
81 | img := cnv.Capture()
82 | paletted := image.NewPaletted(img.Bounds(), palette.WebSafe)
83 | draw.Draw(paletted, img.Bounds(), img, image.Point{}, draw.Src)
84 |
85 | images = append(images, paletted)
86 | delays = append(delays, calcDelay(frame, prevFrameTime, *maxWait, *speed))
87 | prevFrameTime = frame.Time
88 | }
89 |
90 | pw.Close()
91 |
92 | outFile, err := os.OpenFile("demo.gif", os.O_WRONLY|os.O_CREATE, os.ModePerm)
93 | if err != nil {
94 | fmt.Println("Output file create failed", err)
95 | os.Exit(1)
96 | }
97 |
98 | defer outFile.Close()
99 |
100 | err = gif.EncodeAll(outFile, &gif.GIF{
101 | Image: images,
102 | Delay: delays,
103 | })
104 | if err != nil {
105 | fmt.Println("Output gif encode failed", err)
106 | os.Exit(1)
107 | }
108 | }
109 |
110 | func windowSize(header player.Header) fyne.Size {
111 | width, height := float32(10), float32(10)
112 |
113 | return fyne.NewSize(width*float32(header.Width), height*float32(header.Height))
114 | }
115 |
116 | func calcDelay(frame player.Frame, prevFrameTime float64, maxWait time.Duration, speed float64) int {
117 | delay := frame.Time - prevFrameTime
118 | if speed > 0 {
119 | delay /= speed
120 | }
121 |
122 | if maxWait > 0 && delay > maxWait.Seconds() {
123 | return int(100 * maxWait.Seconds())
124 | }
125 |
126 | return int(100 * delay)
127 | }
128 |
--------------------------------------------------------------------------------
/example/webplayer/web/xterm/xterm.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4 | * https://github.com/chjj/term.js
5 | * @license MIT
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | * Originally forked from (with the author's permission):
26 | * Fabrice Bellard's javascript vt100 for jslinux:
27 | * http://bellard.org/jslinux/
28 | * Copyright (c) 2011 Fabrice Bellard
29 | * The original design remains. The terminal itself
30 | * has been extended to include xterm CSI codes, among
31 | * other features.
32 | */
33 |
34 | /**
35 | * Default styles for xterm.js
36 | */
37 |
38 | .xterm {
39 | position: relative;
40 | user-select: none;
41 | -ms-user-select: none;
42 | -webkit-user-select: none;
43 | }
44 |
45 | .xterm.focus,
46 | .xterm:focus {
47 | outline: none;
48 | }
49 |
50 | .xterm .xterm-helpers {
51 | position: absolute;
52 | top: 0;
53 | /**
54 | * The z-index of the helpers must be higher than the canvases in order for
55 | * IMEs to appear on top.
56 | */
57 | z-index: 5;
58 | }
59 |
60 | .xterm .xterm-helper-textarea {
61 | padding: 0;
62 | border: 0;
63 | margin: 0;
64 | /* Move textarea out of the screen to the far left, so that the cursor is not visible */
65 | position: absolute;
66 | opacity: 0;
67 | left: -9999em;
68 | top: 0;
69 | width: 0;
70 | height: 0;
71 | z-index: -5;
72 | /** Prevent wrapping so the IME appears against the textarea at the correct position */
73 | white-space: nowrap;
74 | overflow: hidden;
75 | resize: none;
76 | }
77 |
78 | .xterm .composition-view {
79 | /* TODO: Composition position got messed up somewhere */
80 | background: #000;
81 | color: #FFF;
82 | display: none;
83 | position: absolute;
84 | white-space: nowrap;
85 | z-index: 1;
86 | }
87 |
88 | .xterm .composition-view.active {
89 | display: block;
90 | }
91 |
92 | .xterm .xterm-viewport {
93 | /* On OS X this is required in order for the scroll bar to appear fully opaque */
94 | background-color: #000;
95 | overflow-y: scroll;
96 | cursor: default;
97 | position: absolute;
98 | right: 0;
99 | left: 0;
100 | top: 0;
101 | bottom: 0;
102 | }
103 |
104 | .xterm .xterm-screen {
105 | position: relative;
106 | }
107 |
108 | .xterm .xterm-screen canvas {
109 | position: absolute;
110 | left: 0;
111 | top: 0;
112 | }
113 |
114 | .xterm .xterm-scroll-area {
115 | visibility: hidden;
116 | }
117 |
118 | .xterm-char-measure-element {
119 | display: inline-block;
120 | visibility: hidden;
121 | position: absolute;
122 | top: 0;
123 | left: -9999em;
124 | line-height: normal;
125 | }
126 |
127 | .xterm {
128 | cursor: text;
129 | }
130 |
131 | .xterm.enable-mouse-events {
132 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
133 | cursor: default;
134 | }
135 |
136 | .xterm.xterm-cursor-pointer,
137 | .xterm .xterm-cursor-pointer {
138 | cursor: pointer;
139 | }
140 |
141 | .xterm.column-select.focus {
142 | /* Column selection mode */
143 | cursor: crosshair;
144 | }
145 |
146 | .xterm .xterm-accessibility,
147 | .xterm .xterm-message {
148 | position: absolute;
149 | left: 0;
150 | top: 0;
151 | bottom: 0;
152 | right: 0;
153 | z-index: 10;
154 | color: transparent;
155 | }
156 |
157 | .xterm .live-region {
158 | position: absolute;
159 | left: -9999px;
160 | width: 1px;
161 | height: 1px;
162 | overflow: hidden;
163 | }
164 |
165 | .xterm-dim {
166 | opacity: 0.5;
167 | }
168 |
169 | .xterm-underline {
170 | text-decoration: underline;
171 | }
172 |
173 | .xterm-strikethrough {
174 | text-decoration: line-through;
175 | }
176 |
--------------------------------------------------------------------------------
/testdata/test.cast:
--------------------------------------------------------------------------------
1 | {"version":2,"width":75,"height":18}
2 | [0.089436,"o","\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
3 | [0.100989,"o","\u001b[?2004h"]
4 | [0.164215,"o","\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
5 | [0.164513,"o","\u001b[38;5;237m⏎\u001b(B\u001b[m \r⏎ \r\u001b[2K"]
6 | [0.164709,"o","\u001b[32m~/c/a/asciinema\u001b[30m\u001b(B\u001b[m (develop ↩☡=) \u001b[30m\u001b(B\u001b[m\u001b[K"]
7 | [1.511526,"i","v"]
8 | [1.511937,"o","v"]
9 | [1.512148,"o","\u0008\u001b[38;2;0;95;215mv\u001b[30m\u001b(B\u001b[m"]
10 | [1.514564,"o","\u001b[38;2;85;85;85mim tests/vim.cast \u001b[18D\u001b[30m\u001b(B\u001b[m"]
11 | [1.615727,"i","i"]
12 | [1.616261,"o","\u001b[38;2;0;95;215mi\u001b[38;2;85;85;85mm tests/vim.cast \u001b[17D\u001b[30m\u001b(B\u001b[m"]
13 | [1.694908,"i","m"]
14 | [1.695262,"o","\u001b[38;2;0;95;215mm\u001b[38;2;85;85;85m tests/vim.cast \u001b[16D\u001b[30m\u001b(B\u001b[m"]
15 | [2.751713,"i","\r"]
16 | [2.752186,"o","\u001b[K\r\n\u001b[30m"]
17 | [2.752381,"o","\u001b(B\u001b[m\u001b[?2004l"]
18 | [2.752718,"o","\u001b]0;vim /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m\r"]
19 | [2.86619,"o","\u001b[?1000h\u001b[?2004h\u001b[?1049h\u001b[?1h\u001b=\u001b[?2004h"]
20 | [2.867669,"o","\u001b[1;18r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[H\u001b[2J\u001b[2;1H▽\u001b[6n\u001b[2;1H \u001b[1;1H\u001b[\u003ec"]
21 | [2.868169,"i","\u001b[2;2R\u001b[\u003e0;95;0c"]
22 | [2.869918,"o","\u001b[?1000l\u001b[?1002h\u001b[?12$p"]
23 | [2.870136,"o","\u001b[?25l\u001b[1;1H\u001b[93m1 \u001b[m\u001b[38;5;231m\u001b[48;5;235m\r\n\u001b[38;5;59m\u001b[48;5;236m~ \u001b[3;1H~ \u001b[4;1H~ \u001b[5;1H~ \u001b[6;1H~ \u001b[7;1H~ \u001b[8;1H~ \u001b[9;1H~ \u001b[10;1H~ \u001b[11;1H~ \u001b[12;1H~ \u001b[13;1H~ "]
24 | [2.870245,"o"," \u001b[14;1H~ \u001b[15;1H~ \u001b[16;1H~ \u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[17;1H\u001b[1m\u001b[38;5;231m\u001b[48;5;236m[No Name] (unix/utf-8/) (line 0/1, col 000)\u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[3;30HVIM - Vi IMproved\u001b[5;30Hversion 8.0.1171\u001b[6;26Hby Bram Moolenaar et al.\u001b[7;17HVim is open source and freely distributable\u001b[9;24HBecome a registered Vim user!\u001b[10;15Htype :help register\u001b[38;5;59m\u001b[48;5;236m\u003cEnter\u003e\u001b[m\u001b[38;5;231m\u001b[48;5;235m for information \u001b[12;15Htype :q\u001b[38;5;59m\u001b[48;5;236m\u003cEnter\u003e\u001b[m\u001b[38;5;231m\u001b[48;5;235m to exit \u001b[13;15Htype :help\u001b[38;5;59m\u001b[48;5;236m\u003cEnter\u003e\u001b[m\u001b[38;5;231m\u001b[48;5;235m or \u001b[38;5;59m\u001b[48;5;236m\u003cF1\u003e\u001b[m\u001b[38;5;231m\u001b[48;5;235m for on-line help\u001b[14;15Htype :help version8\u001b[38;5;59m\u001b[48;5;236m\u003cEnter\u003e\u001b[m\u001b[38;5;231m\u001b[48;5;235m for version"]
25 | [2.870302,"o"," info\u001b[1;5H\u001b[?25h"]
26 | [5.63147,"i",":"]
27 | [5.631755,"o","\u001b[?25l\u001b[18;65H:\u001b[1;5H"]
28 | [5.631934,"o","\u001b[18;65H\u001b[K\u001b[18;1H:\u001b[?2004l\u001b[?2004h\u001b[?25h"]
29 | [6.16692,"i","q"]
30 | [6.167137,"o","q\u001b[?25l\u001b[?25h"]
31 | [7.463349,"i","\r"]
32 | [7.463561,"o","\r"]
33 | [7.498922,"o","\u001b[?25l\u001b[?1002l\u001b[?2004l"]
34 | [7.604236,"o","\u001b[18;1H\u001b[K\u001b[18;1H\u001b[?2004l\u001b[?1l\u001b\u003e\u001b[?25h\u001b[?1049l"]
35 | [7.612576,"o","\u001b[?2004h"]
36 | [7.655999,"o","\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
37 | [7.656239,"o","\u001b[38;5;237m⏎\u001b(B\u001b[m \r⏎ \r\u001b[2K\u001b[32m~/c/a/asciinema\u001b[30m\u001b(B\u001b[m (develop ↩☡=) \u001b[30m\u001b(B\u001b[m\u001b[K"]
38 | [11.891762,"i","\u0004"]
39 | [11.893297,"o","\r\n\u001b[30m\u001b(B\u001b[m\u001b[30m\u001b(B\u001b[m"]
40 | [11.89348,"o","\u001b[?2004l"]
41 |
--------------------------------------------------------------------------------
/player_test.go:
--------------------------------------------------------------------------------
1 | package player_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "errors"
7 | "os"
8 | "path/filepath"
9 | "sync"
10 | "testing"
11 | "time"
12 |
13 | player "github.com/xakep666/asciinema-player/v3"
14 | )
15 |
16 | type bufferTerminal struct {
17 | bytes.Buffer
18 | Width, Height int
19 | }
20 |
21 | func (b *bufferTerminal) Close() error { return nil }
22 |
23 | func (b *bufferTerminal) Dimensions() (width, height int) { return b.Width, b.Height }
24 |
25 | func (b *bufferTerminal) ToRaw() error { return nil }
26 |
27 | func (b *bufferTerminal) Restore() error { return nil }
28 |
29 | func (b *bufferTerminal) Control(control player.PlaybackControl) {}
30 |
31 | func TestPlayer(t *testing.T) {
32 | cast, err := os.ReadFile(filepath.Join("testdata", "test.cast"))
33 | if err != nil {
34 | t.Fatalf("Cast read failed: %s", err)
35 | }
36 |
37 | source, err := player.NewStreamFrameSource(bytes.NewReader(cast))
38 | if err != nil {
39 | t.Fatalf("Source create failed: %s", err)
40 | }
41 |
42 | term := &bufferTerminal{Width: 100, Height: 100}
43 |
44 | p, err := player.NewPlayer(source, term, player.WithMaxWait(1))
45 | if err != nil {
46 | t.Fatalf("Player setup failed: %s", err)
47 | }
48 |
49 | if err = p.Start(); err != nil {
50 | t.Fatalf("Play failed: %s", err)
51 | }
52 |
53 | out, err := os.ReadFile(filepath.Join("testdata", "terminal_out.bin"))
54 | if err != nil {
55 | t.Fatalf("Terminal out read failed: %s", err)
56 | }
57 |
58 | if !bytes.Equal(out, term.Bytes()) {
59 | t.Fatalf("Output mismatch. Run `echo \"%s\" | base64 -d > %s` to update expected output.",
60 | base64.StdEncoding.EncodeToString(term.Bytes()), filepath.Join("testdata", "terminal_out.bin"),
61 | )
62 | }
63 | }
64 |
65 | func TestPlayer_PausePlay(t *testing.T) {
66 | cast, err := os.ReadFile(filepath.Join("testdata", "test.cast"))
67 | if err != nil {
68 | t.Fatalf("Cast read failed: %s", err)
69 | }
70 |
71 | source, err := player.NewStreamFrameSource(bytes.NewReader(cast))
72 | if err != nil {
73 | t.Fatalf("Source create failed: %s", err)
74 | }
75 |
76 | term := &bufferTerminal{Width: 100, Height: 100}
77 |
78 | p, err := player.NewPlayer(source, term, player.WithMaxWait(100*time.Millisecond))
79 | if err != nil {
80 | t.Fatalf("Player setup failed: %s", err)
81 | }
82 |
83 | var wg sync.WaitGroup
84 |
85 | go func() {
86 | wg.Add(1)
87 | defer wg.Done()
88 |
89 | p.Pause()
90 | p.Pause()
91 | }()
92 |
93 | if err = p.Start(); err != nil {
94 | t.Fatalf("Play failed: %s", err)
95 | }
96 |
97 | wg.Wait()
98 |
99 | out, err := os.ReadFile(filepath.Join("testdata", "terminal_out.bin"))
100 | if err != nil {
101 | t.Fatalf("Terminal out read failed: %s", err)
102 | }
103 |
104 | if !bytes.Equal(out, term.Bytes()) {
105 | t.Fatalf("Output mismatch. Run `echo \"%s\" | base64 -d > %s` to update expected output.",
106 | base64.StdEncoding.EncodeToString(term.Bytes()), filepath.Join("testdata", "terminal_out.bin"),
107 | )
108 | }
109 | }
110 |
111 | func TestPlayer_Stop(t *testing.T) {
112 | cast, err := os.ReadFile(filepath.Join("testdata", "test.cast"))
113 | if err != nil {
114 | t.Fatalf("Cast read failed: %s", err)
115 | }
116 |
117 | source, err := player.NewStreamFrameSource(bytes.NewReader(cast))
118 | if err != nil {
119 | t.Fatalf("Source create failed: %s", err)
120 | }
121 |
122 | term := &bufferTerminal{Width: 100, Height: 100}
123 |
124 | p, err := player.NewPlayer(source, term, player.WithMaxWait(100*time.Millisecond))
125 | if err != nil {
126 | t.Fatalf("Player setup failed: %s", err)
127 | }
128 |
129 | var wg sync.WaitGroup
130 |
131 | go func() {
132 | wg.Add(1)
133 | defer wg.Done()
134 |
135 | time.Sleep(300 * time.Millisecond)
136 | p.Stop()
137 | }()
138 |
139 | if err = p.Start(); err != nil {
140 | t.Fatalf("Play failed: %s", err)
141 | }
142 |
143 | out, err := os.ReadFile(filepath.Join("testdata", "terminal_out.bin"))
144 | if err != nil {
145 | t.Fatalf("Terminal out read failed: %s", err)
146 | }
147 |
148 | if !bytes.HasPrefix(out, term.Bytes()) || bytes.Equal(out, term.Bytes()) {
149 | t.Fatalf("Terminal out must be prefix of expected output but not equal to it")
150 | }
151 | }
152 |
153 | func TestPlayer_DisableSizeCheck(t *testing.T) {
154 | cast, err := os.ReadFile(filepath.Join("testdata", "test.cast"))
155 | if err != nil {
156 | t.Fatalf("Cast read failed: %s", err)
157 | }
158 |
159 | source, err := player.NewStreamFrameSource(bytes.NewReader(cast))
160 | if err != nil {
161 | t.Fatalf("Source create failed: %s", err)
162 | }
163 |
164 | term := &bufferTerminal{Width: 1, Height: 1}
165 |
166 | _, err = player.NewPlayer(source, term)
167 | if !errors.Is(err, player.ErrSmallTerminal) {
168 | t.Fatalf("Unexpected error returned: %s, expected ErrSmallTerminal", err)
169 | }
170 |
171 | _, err = player.NewPlayer(source, term, player.WithIgnoreSizeCheck())
172 | if err != nil {
173 | t.Fatalf("Unexpected error returned: %s, expected nil", err)
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/example/webplayer/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
4 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
5 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
6 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
7 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
8 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
9 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
10 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
11 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
12 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
13 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
14 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
15 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
16 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
17 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
18 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
19 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
20 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
21 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
22 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
23 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
24 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
25 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
26 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
27 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
28 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
29 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
30 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
31 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
32 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
33 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
34 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
35 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
36 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
37 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
38 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
39 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
41 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
42 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
43 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
44 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
45 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
46 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
47 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
48 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
49 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
50 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
51 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
52 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
53 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
54 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
55 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
57 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
61 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
62 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
63 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
64 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
65 |
--------------------------------------------------------------------------------
/app-demo.cast:
--------------------------------------------------------------------------------
1 | {"version": 2, "width": 100, "height": 25, "timestamp": 1530264003, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}}
2 | [0.142074, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
3 | [0.143135, "o", "\u001b]2;xakep666@xakep666-INSPIRON-5756: ~/sources/go/src/github.com/xakep666/asciinema-player\u0007\u001b]1;..iinema-player\u0007"]
4 | [0.282059, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[39m\u001b[0m\u001b[49m\u001b[44m\u001b[30m ~/sources/go/src/github.com/xakep666/asciinema-player \u001b[43m\u001b[34m\u001b[30m master \u001b[49m\u001b[33m\u001b[39m \u001b[K"]
5 | [0.282345, "o", "\u001b[?1h\u001b="]
6 | [0.282779, "o", "\u001b[?2004h"]
7 | [1.62972, "o", "\u001b[32m.\u001b[39m\u001b[90m/asciinema-player -f test.cast\u001b[39m\u001b[30D"]
8 | [1.729243, "o", "\b\u001b[39m\u001b[4m.\u001b[39m\u001b[4m/\u001b[24m"]
9 | [3.26842, "o", "\b\b\u001b[24m\u001b[32m.\u001b[24m\u001b[32m/\u001b[32ma\u001b[32ms\u001b[32mc\u001b[32mi\u001b[32mi\u001b[32mn\u001b[32me\u001b[32mm\u001b[32ma\u001b[32m-\u001b[32mp\u001b[32ml\u001b[32ma\u001b[32my\u001b[32me\u001b[32mr\u001b[39m\u001b[39m \u001b[39m-\u001b[39mf\u001b[39m \u001b[39m\u001b[4mt\u001b[39m\u001b[4me\u001b[39m\u001b[4ms\u001b[39m\u001b[4mt\u001b[39m\u001b[4m.\u001b[39m\u001b[4mc\u001b[39m\u001b[4ma\u001b[39m\u001b[4ms\u001b[39m\u001b[4mt\u001b[24m"]
10 | [6.32266, "o", "\u001b[?1l\u001b>"]
11 | [6.329327, "o", "\u001b[?2004l\r\r\n"]
12 | [6.332234, "o", "\u001b]2;./asciinema-player -f test.cast\u0007\u001b]1;./asciinema-player\u0007"]
13 | [6.340052, "o", "\u001b[?1049h\u001b[22;0;0t\u001b[?1h\u001b=\u001b[?25l\u001b[H\u001b[2J"]
14 | [6.431142, "o", "\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
15 | [6.442671, "o", "\u001b[?2004h"]
16 | [6.505781, "o", "\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
17 | [6.506504, "o", "\u001b[38;5;237m⏎\u001b(B\u001b[m \r⏎ \r\u001b[2K\u001b[32m~/c/a/asciinema\u001b[30m\u001b(B\u001b[m (develop ↩☡=) \u001b[30m\u001b(B\u001b[m\u001b[K"]
18 | [7.853664, "o", "v\b\u001b[38;2;0;95;215mv\u001b[30m\u001b(B\u001b[m"]
19 | [7.856209, "o", "\u001b[38;2;85;85;85mim tests/vim.cast \u001b[18D\u001b[30m\u001b(B\u001b[m"]
20 | [7.957954, "o", "\u001b[38;2;0;95;215mi\u001b[38;2;85;85;85mm tests/vim.cast \u001b[17D\u001b[30m\u001b(B\u001b[m"]
21 | [8.036953, "o", "\u001b[38;2;0;95;215mm\u001b[38;2;85;85;85m tests/vim.cast \u001b[16D\u001b[30m\u001b(B\u001b[m"]
22 | [9.093961, "o", "\u001b[K\r\n\u001b[30m\u001b(B\u001b[m\u001b[?2004l"]
23 | [9.094659, "o", "\u001b]0;vim /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m\r"]
24 | [9.207871, "o", "\u001b[?1000h\u001b[?2004h\u001b[?1049h\u001b[?1h\u001b=\u001b[?2004h"]
25 | [9.209273, "o", "\u001b[1;18r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[H\u001b[2J\u001b[2;1H▽\u001b[6n\u001b[2;1H \u001b[1;1H\u001b[>c"]
26 | [9.21147, "o", "\u001b[?1000l\u001b[?1002h\u001b[?12$p"]
27 | [9.21203, "o", "\u001b[?25l\u001b[1;1H\u001b[93m1 \u001b[m\u001b[38;5;231m\u001b[48;5;235m\r\n\u001b[38;5;59m\u001b[48;5;236m~ \u001b[3;1H~ \u001b[4;1H~ \u001b[5;1H~ \u001b[6;1H~ \u001b[7;1H~ \u001b[8;1H~ \u001b[9;1H~ \u001b[10;1H~ \u001b[11;1H~ \u001b[12;1H~ \u001b[13;1H~ "]
28 | [9.212584, "o", " \u001b[14;1H~ \u001b[15;1H~ \u001b[16;1H~ \u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[17;1H\u001b[1m\u001b[38;5;231m\u001b[48;5;236m[No Name] (unix/utf-8/) (line 0/1, col 000)\u001b[m\u001b[38;5;231m\u001b[48;5;235m\u001b[3;30HVIM - Vi IMproved\u001b[5;30Hversion 8.0.1171\u001b[6;26Hby Bram Moolenaar et al.\u001b[7;17HVim is open source and freely distributable\u001b[9;24HBecome a registered Vim user!\u001b[10;15Htype :help register\u001b[38;5;59m\u001b[48;5;236m\u001b[m\u001b[38;5;231m\u001b[48;5;235m for information \u001b[12;15Htype :q\u001b[38;5;59m\u001b[48;5;236m\u001b[m\u001b[38;5;231m\u001b[48;5;235m to exit \u001b[13;15Htype :help\u001b[38;5;59m\u001b[48;5;236m\u001b[m\u001b[38;5;231m\u001b[48;5;235m or \u001b[38;5;59m\u001b[48;5;236m\u001b[m\u001b[38;5;231m\u001b[48;5;235m for on-line help\u001b[14;15Htype :help version8\u001b[38;5;59m\u001b[48;5;236m\u001b[m\u001b[38;5;231m\u001b[48;5;235m for version"]
29 | [9.212964, "o", " info\u001b[1;5H\u001b[?25h"]
30 | [11.216472, "o", "\u001b[?25l\u001b[18;65H:\u001b[1;5H\u001b[18;65H\u001b[K\u001b[18;1H:\u001b[?2004l\u001b[?2004h\u001b[?25h"]
31 | [11.747416, "o", "q\u001b[?25l\u001b[?25h"]
32 | [13.043781, "o", "\r"]
33 | [13.079059, "o", "\u001b[?25l\u001b[?1002l\u001b[?2004l"]
34 | [13.184524, "o", "\u001b[18;1H\u001b[K\u001b[18;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l"]
35 | [13.192765, "o", "\u001b[?2004h"]
36 | [13.235964, "o", "\u001b]0;fish /Users/sickill/code/asciinema/asciinema\u0007\u001b[30m\u001b(B\u001b[m"]
37 | [13.236362, "o", "\u001b[38;5;237m⏎\u001b(B\u001b[m \r⏎ \r\u001b[2K\u001b[32m~/c/a/asciinema\u001b[30m\u001b(B\u001b[m (develop ↩☡=) \u001b[30m\u001b(B\u001b[m\u001b[K"]
38 | [15.236421, "o", "\r\n\u001b[30m\u001b(B\u001b[m\u001b[30m\u001b(B\u001b[m\u001b[?2004l"]
39 | [15.238987, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
40 | [15.239523, "o", "\u001b]2;xakep666@xakep666-INSPIRON-5756: ~/sources/go/src/github.com/xakep666/asciinema-player\u0007"]
41 | [15.24048, "o", "\u001b]1;..iinema-player\u0007"]
42 | [15.412617, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[39m\u001b[0m\u001b[49m\u001b[44m\u001b[30m ~/sources/go/src/github.com/xakep666/asciinema-player \u001b[43m\u001b[34m\u001b[30m master \u001b[49m\u001b[33m\u001b[39m \u001b[K"]
43 | [15.412786, "o", "\u001b[?1h\u001b="]
44 | [15.413154, "o", "\u001b[?2004h"]
45 | [16.841557, "o", "\u001b[?2004l\r\r\n"]
46 |
--------------------------------------------------------------------------------
/example/togif/go.sum:
--------------------------------------------------------------------------------
1 | fyne.io/fyne/v2 v2.1.1 h1:3p39SwQ/rBiYODVYI4ggTuwMufWYmqaRMJvXTFg7jSw=
2 | fyne.io/fyne/v2 v2.1.1/go.mod h1:c1vwI38Ebd0dAdxVa6H1Pj6/+cK1xtDy61+I31g+s14=
3 | github.com/ActiveState/termtest/conpty v0.5.0 h1:JLUe6YDs4Jw4xNPCU+8VwTpniYOGeKzQg4SM2YHQNA8=
4 | github.com/ActiveState/termtest/conpty v0.5.0/go.mod h1:LO4208FLsxw6DcNZ1UtuGUMW+ga9PFtX4ntv8Ymg9og=
5 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
6 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
7 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
8 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
9 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
10 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
11 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
12 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
13 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
14 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
19 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
20 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
21 | github.com/fyne-io/terminal v0.0.0-20220109064744-3fb0c336f262 h1:rf6QARFv3anXjc//QZidtOR1oxTXFjz9bAanjHQt5GY=
22 | github.com/fyne-io/terminal v0.0.0-20220109064744-3fb0c336f262/go.mod h1:zkqKA2a0sUIR0dxwSE+Pzon9IEuuS1DVo4AyihLROfo=
23 | github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
24 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
25 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
26 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
27 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
28 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
29 | github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
30 | github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
31 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
32 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
33 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
34 | github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
35 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
36 | github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
37 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
38 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
39 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
40 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
43 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
44 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
45 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
46 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
47 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM=
48 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
49 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM=
50 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
52 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
53 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
54 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
55 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
56 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
57 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
58 | github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
59 | github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
60 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
61 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
62 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
63 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
64 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
65 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
66 | golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
67 | golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
68 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
69 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
70 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
71 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
72 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
73 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
74 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
75 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
76 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
77 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
78 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
79 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
80 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
81 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
82 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
83 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
84 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
85 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
86 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
87 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
88 | golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
89 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
90 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
91 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
92 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
93 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
94 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
95 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
100 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
102 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
103 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
104 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
105 | golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
106 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
107 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
108 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
109 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
110 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
111 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
112 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
113 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
114 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
115 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
116 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
117 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
118 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
119 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
120 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
121 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
122 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
123 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
124 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
125 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
126 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
127 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
128 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
129 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
130 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
131 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
132 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
133 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
134 |
--------------------------------------------------------------------------------