├── .github └── workflows │ ├── dependabot.yml │ └── testing.yml ├── .gitignore ├── LICENSE ├── README.md ├── app-demo.cast ├── cmd └── asciinema-player │ └── main.go ├── example ├── togif │ ├── README.md │ ├── demo.gif │ ├── go.mod │ ├── go.sum │ └── main.go └── webplayer │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── termhandler.go │ ├── web │ ├── index.html │ ├── player.html │ └── xterm │ │ ├── xterm-addon-fit.js │ │ ├── xterm.css │ │ └── xterm.js │ └── wsterm.go ├── frame.go ├── framesource.go ├── framesource_stream.go ├── framesource_stream_test.go ├── go.mod ├── go.sum ├── options.go ├── player.go ├── player_test.go ├── terminal.go ├── terminal_os.go ├── terminal_os_other.go ├── terminal_os_windows.go └── testdata ├── terminal_out.bin └── test.cast /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asciinema-player 2 | [![Build Status](https://github.com/xakep666/asciinema-player/actions/workflows/testing.yml/badge.svg)](https://github.com/xakep666/asciinema-player/actions/workflows/testing.yml) 3 | [![codecov](https://codecov.io/gh/xakep666/asciinema-player/branch/master/graph/badge.svg)](https://codecov.io/gh/xakep666/asciinema-player) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/xakep666/asciinema-player)](https://goreportcard.com/report/github.com/xakep666/asciinema-player) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/xakep666/asciinema-player/v3.svg)](https://pkg.go.dev/github.com/xakep666/asciinema-player/v3) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | [![asciicast](https://asciinema.org/a/189343.png)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![app-demo](demo.gif) 23 | -------------------------------------------------------------------------------- /example/togif/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xakep666/asciinema-player/fee6c7b01033ab897a93e5ff8794ef26a8fa3682/example/togif/demo.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /testdata/terminal_out.bin: -------------------------------------------------------------------------------- 1 | ]0;fish /Users/sickill/code/asciinema/asciinema(B[?2004h]0;fish /Users/sickill/code/asciinema/asciinema(B⏎(B ⏎ ~/c/a/asciinema(B (develop ↩☡=) (Bvv(Bim tests/vim.cast (Bim tests/vim.cast (Bm tests/vim.cast (B 2 | (B[?2004l]0;vim /Users/sickill/code/asciinema/asciinema(B [?1000h[?2004h[?1049h[?1h=[?2004h[?12h[?12l▽ [>c[?1000l[?1002h[?12$p[?25l1  3 | ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ [No Name] (unix/utf-8/) (line 0/1, col 000)VIM - Vi IMprovedversion 8.0.1171by Bram Moolenaar et al.Vim is open source and freely distributableBecome a registered Vim user!type :help register for information type :q to exit type :help or  for on-line helptype :help version8 for version info[?25h[?25l::[?2004l[?2004h[?25hq[?25l[?25h [?25l[?1002l[?2004l[?2004l[?1l>[?25h[?1049l[?2004h]0;fish /Users/sickill/code/asciinema/asciinema(B⏎(B ⏎ ~/c/a/asciinema(B (develop ↩☡=) (B 4 | (B(B[?2004l -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------