├── .Rbuildignore ├── .github ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── SUPPORT.md └── workflows │ ├── R-CMD-check.yaml │ ├── pkgdown.yaml │ ├── pr-commands.yaml │ └── test-coverage.yaml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── aaa-rstudio-detect.R ├── asciicast.R ├── debug.R ├── embed.R ├── errors.R ├── frames.R ├── gif.R ├── html.R ├── htmlwidget.R ├── install-phantomjs.R ├── knitr.R ├── merge.R ├── onload.R ├── parameters.R ├── read.R ├── record-output.R ├── rstudio.R ├── svg.R ├── test.R ├── utils.R └── write-json.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── air.toml ├── asciicast.Rproj ├── codecov.yml ├── configure ├── configure.win ├── inst ├── browserify.js ├── examples │ ├── Makefile │ ├── asciicast.R │ ├── asciicast.cast │ ├── dplyr.R │ ├── dplyr.cast │ ├── errors.R │ ├── errors.cast │ ├── github-readme.Rmd │ ├── github-readme.md │ ├── hello.R │ ├── hello.cast │ └── man │ │ └── figures │ │ └── README- │ │ ├── unnamed-chunk-4.svg │ │ ├── unnamed-chunk-5.svg │ │ ├── unnamed-chunk-6.svg │ │ ├── unnamed-chunk-7.svg │ │ └── unnamed-chunk-8.svg ├── htmlwidgets │ ├── asciinema_player.js │ ├── asciinema_player.yaml │ └── lib │ │ ├── asciinema-player.css │ │ ├── asciinema-player.js │ │ └── jquery-3.2.1.min.js ├── load-cast.js ├── page │ ├── asciicast2gif.html │ ├── asciinema-player.css │ ├── fontfaceonload.js │ └── page.js ├── renderer.js └── svg-term.js.gz ├── man ├── asciicast-package.Rd ├── asciicast_options.Rd ├── asciicast_start_process.Rd ├── asciinema_player.Rd ├── default_theme.Rd ├── expect_snapshot_r_process.Rd ├── figures │ ├── README- │ │ ├── unnamed-chunk-3-dark.svg │ │ ├── unnamed-chunk-3.svg │ │ ├── unnamed-chunk-5-dark.svg │ │ ├── unnamed-chunk-5.svg │ │ ├── unnamed-chunk-7-dark.svg │ │ └── unnamed-chunk-7.svg │ └── lifecycle-deprecated.svg ├── get_locales.Rd ├── init_knitr_engine.Rd ├── install_phantomjs.Rd ├── load_frames.Rd ├── merge_casts.Rd ├── play.Rd ├── read_cast.Rd ├── record.Rd ├── record_output.Rd ├── write_gif.Rd ├── write_html.Rd ├── write_json.Rd └── write_svg.Rd ├── src ├── Makevars.in ├── Makevars.win ├── asciicast.h ├── client.c ├── common.c ├── install.libs.R ├── psignal.h ├── r.c └── rwin.c ├── tests ├── testthat.R └── testthat │ ├── _snaps │ ├── ascii │ │ └── html │ │ │ └── html-truecolor.html │ ├── asciicast.md │ ├── darwin-aarch64 │ │ └── embed.md │ ├── darwin-x86_64 │ │ └── embed.md │ ├── embed.md │ ├── frames.md │ ├── gif.md │ ├── html.md │ ├── html │ │ ├── html-256-colors.html │ │ ├── html-8-colors.html │ │ ├── html-hyperlink.html │ │ ├── html-no-markup.html │ │ ├── html-prefix.html │ │ └── html-styles.html │ ├── knitr.md │ ├── knitr │ │ ├── 1-cpp11-mean.svg │ │ ├── 1-letters.svg │ │ ├── 3-echo.svg │ │ ├── 3-header.svg │ │ ├── 5-process.svg │ │ ├── test-1.md │ │ ├── test-4.md │ │ ├── test-5.md │ │ └── test-6.md │ ├── linux-aarch64 │ │ └── embed.md │ ├── linux-x86_64 │ │ └── embed.md │ ├── merge.md │ ├── merge │ │ ├── merge-1.svg │ │ ├── merge-2.svg │ │ └── merge-3.svg │ ├── read.md │ ├── record-output.md │ ├── svg.md │ ├── svg │ │ ├── hello.svg │ │ ├── hello1.svg │ │ └── hello2.svg │ ├── test.md │ ├── unix │ │ └── rstudio │ │ │ └── out.html │ ├── utf8 │ │ └── html │ │ │ └── html-truecolor.html │ ├── utils.md │ └── windows-x86_64 │ │ └── embed.md │ ├── fixtures │ ├── bad.json │ ├── badver.json │ ├── test-1.R │ ├── test-1.Rmd │ ├── test-2.Rmd │ ├── test-3.Rmd │ ├── test-4.Rmd │ ├── test-5.Rmd │ ├── test-6.Rmd │ └── v1.json │ ├── helper-mock.R │ ├── helper.R │ ├── setup.R │ ├── test-asciicast.R │ ├── test-embed.R │ ├── test-frames.R │ ├── test-gif.R │ ├── test-html.R │ ├── test-htmlwidget.R │ ├── test-install-phantomjs.R │ ├── test-knitr.R │ ├── test-merge.R │ ├── test-read.R │ ├── test-record-output.R │ ├── test-rstudio.R │ ├── test-src-c.R │ ├── test-svg.R │ ├── test-test.R │ └── test-utils.R └── vignettes └── asciicast-demo.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^asciicast\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^README\.html$ 6 | ^cran-comments\.md$ 7 | ^\.github$ 8 | ^\.Rprofile$ 9 | ^r-packages$ 10 | ^\.travis\.yml$ 11 | ^doc$ 12 | ^Meta$ 13 | ^vignettes$ 14 | ^vignettes/.*_cache$ 15 | ^vignettes/.*_files$ 16 | ^README_cache$ 17 | ^inst/examples/github-readme_cache$ 18 | ^appveyor\.yml$ 19 | ^src/Makevars$ 20 | ^src/rem$ 21 | ^src/rem[.]exe$ 22 | ^src/r[.]o$ 23 | ^codecov\.yml$ 24 | ^dev-lib$ 25 | ^src/.*[.]gcda$ 26 | ^src/.*[.]gcno$ 27 | ^_pkgdown\.yml$ 28 | ^docs$ 29 | ^pkgdown$ 30 | ^[\.]?air\.toml$ 31 | ^\.vscode$ 32 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to asciicast 2 | 3 | This outlines how to propose a change to asciicast. For more detailed 4 | info about contributing to this, and other tidyverse packages, please see the 5 | [**development contributing guide**](https://rstd.io/tidy-contrib). 6 | 7 | ### Fixing typos 8 | 9 | Small typos or grammatical errors in documentation may be edited directly using 10 | the GitHub web interface, so long as the changes are made in the _source_ file. 11 | 12 | * YES: you edit a roxygen comment in a `.R` file below `R/`. 13 | * NO: you edit an `.Rd` file below `man/`. 14 | 15 | ### Prerequisites 16 | 17 | Before you make a substantial pull request, you should always file an issue and 18 | make sure someone from the team agrees that it’s a problem. If you’ve found a 19 | bug, create an associated issue and illustrate the bug with a minimal 20 | [reprex](https://www.tidyverse.org/help/#reprex). 21 | 22 | ### Pull request process 23 | 24 | * We recommend that you create a Git branch for each pull request (PR). 25 | * Look at the Travis and AppVeyor build status before and after making changes. 26 | The `README` should contain badges for any continuous integration services used 27 | by the package. 28 | * New code should follow the tidyverse [style guide](http://style.tidyverse.org). 29 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to 30 | apply these styles, but please don't restyle code that has nothing to do with 31 | your PR. 32 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2), with 33 | [Markdown syntax](https://roxygen2.r-lib.org/articles/rd-formatting.html), 34 | for documentation. 35 | * We use [testthat](https://cran.r-project.org/package=testthat). Contributions 36 | with test cases included are easier to accept. 37 | * For user-facing changes, add a bullet to the top of `NEWS.md` below the 38 | current development version header describing the changes made followed by your 39 | GitHub username, and links to relevant issue(s)/PR(s). 40 | 41 | ### Code of Conduct 42 | 43 | Please note that the asciicast project is released with a 44 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this 45 | project you agree to abide by its terms. 46 | 47 | ### See tidyverse [development contributing guide](https://rstd.io/tidy-contrib) 48 | for further details. 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please briefly describe your problem and what output you expect. If you have a question, please don't use this form. Instead, ask on or . 2 | 3 | Please include a minimal reproducible example (AKA a reprex). If you've never heard of a [reprex](https://reprex.tidyverse.org/) before, start by reading . 4 | 5 | --- 6 | 7 | Brief description of the problem 8 | 9 | ```r 10 | # insert reprex here 11 | ``` 12 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting help with asciicast 2 | 3 | Thanks for using asciicast. Before filing an issue, there are a few places 4 | to explore and pieces to put together to make the process as smooth as possible. 5 | 6 | Start by making a minimal **repr**oducible **ex**ample using the 7 | [reprex](https://reprex.tidyverse.org/) package. If you haven't heard of or used 8 | reprex before, you're in for a treat! Seriously, reprex will make all of your 9 | R-question-asking endeavors easier (which is a pretty insane ROI for the five to 10 | ten minutes it'll take you to learn what it's all about). For additional reprex 11 | pointers, check out the [Get help!](https://www.tidyverse.org/help/) section of 12 | the tidyverse site. 13 | 14 | Armed with your reprex, the next step is to figure out [where to ask](https://www.tidyverse.org/help/#where-to-ask). 15 | 16 | * If it's a question: start with [community.rstudio.com](https://community.rstudio.com/), 17 | and/or StackOverflow. There are more people there to answer questions. 18 | * If it's a bug: you're in the right place, file an issue. 19 | * If you're not sure: let the community help you figure it out! If your 20 | problem _is_ a bug or a feature request, you can easily return here and 21 | report it. 22 | 23 | Before opening a new issue, be sure to [search issues and pull requests](https://github.com/tidyverse/asciicast/issues) to make sure the 24 | bug hasn't been reported and/or already fixed in the development version. By 25 | default, the search will be pre-populated with `is:issue is:open`. You can 26 | [edit the qualifiers](https://help.github.com/articles/searching-issues-and-pull-requests/) 27 | (e.g. `is:pr`, `is:closed`) as needed. For example, you'd simply 28 | remove `is:open` to search _all_ issues in the repo, open or closed. 29 | 30 | 31 | If you _are_ in the right place, and need to file an issue, please review the 32 | ["File issues"](https://www.tidyverse.org/contribute/#issues) paragraph from 33 | the tidyverse contributing guidelines. 34 | 35 | Thanks for your help! 36 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | 12 | name: R-CMD-check.yaml 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | R-CMD-check: 18 | runs-on: ${{ matrix.config.os }} 19 | 20 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | config: 26 | - {os: macos-latest, r: 'release'} 27 | 28 | - {os: windows-latest, r: 'release'} 29 | # use 4.0 or 4.1 to check with rtools40's older compiler 30 | - {os: windows-latest, r: 'oldrel-4'} 31 | 32 | - {os: ubuntu-22.04, r: 'devel', http-user-agent: 'release'} 33 | - {os: ubuntu-22.04, r: 'release'} 34 | - {os: ubuntu-22.04, r: 'oldrel-1'} 35 | - {os: ubuntu-22.04, r: 'oldrel-2'} 36 | - {os: ubuntu-22.04, r: 'oldrel-3'} 37 | - {os: ubuntu-22.04, r: 'oldrel-4'} 38 | 39 | env: 40 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 41 | R_KEEP_PKG_SOURCE: yes 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - uses: r-lib/actions/setup-pandoc@v2 47 | 48 | - uses: r-lib/actions/setup-r@v2 49 | with: 50 | r-version: ${{ matrix.config.r }} 51 | http-user-agent: ${{ matrix.config.http-user-agent }} 52 | use-public-rspm: true 53 | 54 | - uses: r-lib/actions/setup-r-dependencies@v2 55 | with: 56 | extra-packages: any::rcmdcheck 57 | needs: check 58 | 59 | - uses: r-lib/actions/check-r-package@v2 60 | with: 61 | upload-snapshots: true 62 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 63 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | name: pkgdown.yaml 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | pkgdown: 17 | runs-on: ubuntu-22.04 18 | # Only restrict concurrency for non-PR jobs 19 | concurrency: 20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | permissions: 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::pkgdown, local::. 37 | needs: website 38 | 39 | - name: Build site 40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 41 | shell: Rscript {0} 42 | 43 | - name: Deploy to GitHub pages 🚀 44 | if: github.event_name != 'pull_request' 45 | uses: JamesIves/github-pages-deploy-action@v4.5.0 46 | with: 47 | clean: false 48 | branch: gh-pages 49 | folder: docs 50 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: pr-commands.yaml 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | document: 13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 14 | name: document 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | permissions: 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: r-lib/actions/pr-fetch@v2 24 | with: 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::roxygen2 34 | needs: pr-document 35 | 36 | - name: Document 37 | run: roxygen2::roxygenise() 38 | shell: Rscript {0} 39 | 40 | - name: commit 41 | run: | 42 | git config --local user.name "$GITHUB_ACTOR" 43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 44 | git add man/\* NAMESPACE 45 | git commit -m 'Document' 46 | 47 | - uses: r-lib/actions/pr-push@v2 48 | with: 49 | repo-token: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | style: 52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 53 | name: style 54 | runs-on: ubuntu-latest 55 | env: 56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 57 | permissions: 58 | contents: write 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - uses: r-lib/actions/pr-fetch@v2 63 | with: 64 | repo-token: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | - uses: r-lib/actions/setup-r@v2 67 | 68 | - name: Install dependencies 69 | run: install.packages("styler") 70 | shell: Rscript {0} 71 | 72 | - name: Style 73 | run: styler::style_pkg() 74 | shell: Rscript {0} 75 | 76 | - name: commit 77 | run: | 78 | git config --local user.name "$GITHUB_ACTOR" 79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 80 | git add \*.R 81 | git commit -m 'Style' 82 | 83 | - uses: r-lib/actions/pr-push@v2 84 | with: 85 | repo-token: ${{ secrets.GITHUB_TOKEN }} 86 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: test-coverage.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test-coverage: 14 | runs-on: ubuntu-22.04 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: r-lib/actions/setup-r@v2 22 | with: 23 | use-public-rspm: true 24 | 25 | - uses: r-lib/actions/setup-r-dependencies@v2 26 | with: 27 | extra-packages: any::covr, any::xml2 28 | needs: coverage 29 | 30 | - name: Test coverage 31 | run: | 32 | cov <- covr::package_coverage( 33 | quiet = FALSE, 34 | clean = FALSE, 35 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 36 | ) 37 | print(cov) 38 | covr::to_cobertura(cov) 39 | shell: Rscript {0} 40 | 41 | - uses: codecov/codecov-action@v5 42 | with: 43 | # Fail if error if not on PR, or if on PR and token is given 44 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} 45 | files: ./cobertura.xml 46 | plugins: noop 47 | disable_search: true 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | 50 | - name: Show testthat output 51 | if: always() 52 | run: | 53 | ## -------------------------------------------------------------------- 54 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 55 | shell: bash 56 | 57 | - name: Upload test results 58 | if: failure() 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: coverage-test-failures 62 | path: ${{ runner.temp }}/package 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | /r-packages 5 | /README.html 6 | inst/doc 7 | doc 8 | Meta 9 | /vignettes/*_cache 10 | /README_cache 11 | /inst/examples/github-readme_cache 12 | /vignettes/asciicast-demo_files 13 | /src/*.o 14 | /src/rem 15 | /src/rem.exe 16 | /src/*.gcda 17 | /src/*.gcno 18 | /docs 19 | /vignettes/asciicast-demo.html 20 | /dev-lib 21 | docs 22 | /src/asciicastclient.so 23 | /src/asciicastclient.dll 24 | /src/Makevars 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Posit.air-vscode" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[r]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "Posit.air-vscode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: asciicast 2 | Title: Create 'Ascii' Screen Casts from R Scripts 3 | Version: 2.3.1.9000 4 | Authors@R: c( 5 | person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre")), 6 | person("Romain", "Francois", role = "aut"), 7 | person("Mario", "Nebl", role = "aut", 8 | comment = "https://github.com/marionebl/svg-term author"), 9 | person("Marcin", "Kulik", role = "aut", 10 | comment = "https://github.com/asciinema/asciinema-player author"), 11 | person("Posit Software, PBC", role = c("cph", "fnd"), 12 | comment = c(ROR = "03wc8by49")) 13 | ) 14 | Description: Record 'asciicast' screen casts from R scripts. Convert them 15 | to animated SVG images, to be used in 'README' files, or blog posts. 16 | Includes 'asciinema-player' as an 'HTML' widget, and an 'asciicast' 17 | 'knitr' engine, to embed 'ascii' screen casts in 'Rmarkdown' 18 | documents. 19 | License: MIT + file LICENSE 20 | URL: https://asciicast.r-lib.org/, https://github.com/r-lib/asciicast 21 | BugReports: https://github.com/r-lib/asciicast/issues 22 | Imports: 23 | cli (>= 3.3.0.9000), 24 | curl, 25 | jsonlite, 26 | lifecycle, 27 | magick (>= 2.2.9002), 28 | processx (>= 3.7.0), 29 | tibble, 30 | utils, 31 | V8, 32 | withr 33 | Suggests: 34 | callr, 35 | covr, 36 | cpp11, 37 | decor, 38 | htmlwidgets, 39 | knitr, 40 | rmarkdown, 41 | rstudioapi, 42 | testthat (>= 3.2.0) 43 | LinkingTo: 44 | processx 45 | Config/Needs/website: r-lib/downlit, tidyverse/tidytemplate 46 | Config/testthat/edition: 3 47 | Config/usethis/last-upkeep: 2025-04-29 48 | Encoding: UTF-8 49 | Roxygen: list(markdown = TRUE) 50 | RoxygenNote: 7.3.2.9000 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2025 2 | COPYRIGHT HOLDER: asciicast authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 asciicast authors 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 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,asciicast) 4 | export(asciicast_options) 5 | export(asciicast_start_process) 6 | export(asciinema_player) 7 | export(clear_screen) 8 | export(default_theme) 9 | export(expect_snapshot_r_process) 10 | export(get_locales) 11 | export(init_knitr_engine) 12 | export(install_phantomjs) 13 | export(merge_casts) 14 | export(pause) 15 | export(play) 16 | export(read_cast) 17 | export(record) 18 | export(record_output) 19 | export(write_gif) 20 | export(write_html) 21 | export(write_json) 22 | export(write_svg) 23 | importFrom(V8,JS) 24 | importFrom(V8,v8) 25 | importFrom(cli,cli_process_done) 26 | importFrom(cli,cli_process_start) 27 | importFrom(cli,cli_status) 28 | importFrom(cli,cli_status_clear) 29 | importFrom(cli,cli_status_update) 30 | importFrom(jsonlite,fromJSON) 31 | importFrom(jsonlite,toJSON) 32 | importFrom(magick,image_animate) 33 | importFrom(magick,image_display) 34 | importFrom(magick,image_quantize) 35 | importFrom(magick,image_read) 36 | importFrom(magick,image_write) 37 | importFrom(processx,process) 38 | importFrom(stats,runif) 39 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # asciicast (development version) 2 | 3 | # asciicast 2.3.1 4 | 5 | * New `show` argument in `write_svg()` to show the SVG on the screen. 6 | It is `TRUE` by default in RStudio. 7 | 8 | * `play()` now show the output in the viewer pane in RStudio. 9 | 10 | # asciicast 2.3.0 11 | 12 | * `record()` has a new argument: `show_output`, to show the output from the 13 | subprocess in real time. 14 | 15 | * New SVG theme: `readme`, to be used in `README.Rmd` (and similar) files on 16 | GitHub. It generates two SVG files, one for light and one for dark mode, 17 | and creates a `README.md` file that automatically uses the correct one 18 | when viewed on GitHub. 19 | 20 | * New SVG theme: `github-dark`. 21 | 22 | # asciicast 2.2.1 23 | 24 | * New `expect_snapshot_r_process()` function to run a testthat snapshot 25 | test in an asciicast subprocess. 26 | 27 | * On Windows asciicast does not error if the patch version of the current 28 | R version is different from the one asciiast was built with. This happens 29 | often when installing asciicast from binaries, and these R versions should 30 | be fully compatible. 31 | 32 | # asciicast 2.2.0 33 | 34 | * `write_html()` now supports ANSI hyperlinks. 35 | 36 | * Much better parameterization of asciicast in knitr. You can now set 37 | asciicast parameters via chunk options. Relatedly, the 38 | `init_knitr_engine()` does not have an `options` argument any more. 39 | Set knitr chunk options instead. See the `README.Rmd` file of the package 40 | or the included `github-readme.Rmd` for examples. 41 | 42 | * HTML output now works better in roxygen2/Rd. 43 | 44 | * `init_knitr_engine()` has an `interactive` argument now, to allow 45 | non-interactive mode. 46 | 47 | * asciicast in knitr now caches HTML output much better. 48 | 49 | * asciicast now supports true color ANSI escapes in HTML output (#46). 50 | 51 | * If the `asciicast_html_details` option is set, then asciicast puts the 52 | HTML output in a `
` tag. 53 | 54 | * New `interactive` parameter of `init_knitr_engine()` allows recording in 55 | non-interactive R sessions. The default is (still) `interactive = TRUE`. 56 | 57 | # asciicast 2.1.0 58 | 59 | * The new `write_html()` function can create a themed HTML snapshot of an 60 | ascii cast. HTML otuput is now the default in pkgdown, for snapshots. 61 | For animations we still need to use SVG files. 62 | 63 | * asciicast can now record casts on R installations that do not have an R 64 | shared or static library. 65 | 66 | * SVG output now looks correct in Firefox with large fonts (#42). 67 | 68 | # asciicast 2.0.0 69 | 70 | * Completely new `record()` implementation that uses an embedded R 71 | interpreter in a subprocess. This means that asciicast can now record 72 | on Windows, and the recordings are of much faster and of higher quiality. 73 | 74 | Another benefit is that simulated typing does not happen in real time, 75 | but recordings are as fast as the script itself, and the simulated typing 76 | (if requested) is added after the recording. 77 | 78 | It is also possible to record R scripts that are not valid R code. 79 | (These will fail of course, but sometimes that's what you want to show.) 80 | 81 | `record()` does not set the `error` option in the asciicast subprocess 82 | any more, but the output of errors is simply recorded. 83 | 84 | * New `record_output()` which returns the recorded output as a character 85 | vector instead of an asciic cast. 86 | 87 | * Adding a `#'` to a line omits the simulated typing for that line and the 88 | line will appear in one step. 89 | 90 | * New `write_gif()` function to save a cast as a GIF. It needs phantom.js 91 | currently. 92 | 93 | * SVG output can have themes now. The package comes with a bunch of 94 | predefined themes, or you can create your own. See `default_theme()`. 95 | 96 | * knitr options are set temporarily now, and they are restored after the 97 | knitr run. 98 | 99 | * The `asciinema_player()` HTML widget now properly sets the height and 100 | width of the player window, using the `height` and `width` parameters 101 | of the cast. 102 | 103 | * `record()` now works with script files that end with a comment line (#13). 104 | 105 | * The initial R prompt is now not left out when recording a cast. 106 | 107 | * `asciinema_player()` now creates an HTML widget that works well on 108 | Firefox as well. 109 | 110 | * asciicast is properly tested now, so it should be much more stable. 111 | 112 | # asciicast 1.0.0 113 | 114 | First release on CRAN. 115 | -------------------------------------------------------------------------------- /R/debug.R: -------------------------------------------------------------------------------- 1 | debug_levels <- c( 2 | fatal = 0, 3 | error = 1, 4 | warn = 2, 5 | info = 3, 6 | debug = 4, 7 | trace = 5 8 | ) 9 | 10 | get_debug_level <- function() { 11 | lvl <- tolower(Sys.getenv("ASCIICAST_DEBUG_LEVEL", "fatal")) 12 | if (!lvl %in% names(debug_levels)) { 13 | throw(cli::format_error(c( 14 | "Invalid asciicast debug level in {.env ASCIICAST_DEBUG_LEVEL env 15 | var: {.code lvl}.", 16 | i = "It must be one of {.code {names(debug_levels)}}." 17 | ))) 18 | } 19 | } 20 | 21 | debug <- function(level, ..., .envir = parent.frame()) { 22 | current <- get_debug_level() 23 | if (debug_levels[current] >= debug_levels[level]) { 24 | cli::cli_alert(..., .envir = .envir) 25 | } 26 | } 27 | 28 | show_line <- function(line) { 29 | pl <- parse_line(line) 30 | if (pl$type == "o") { 31 | cat(pl$value) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /R/frames.R: -------------------------------------------------------------------------------- 1 | load_svg_term <- function() { 2 | check_svg_support() 3 | 4 | ct <- v8(c("global", "window", "document")) 5 | ct$assign("setTimeout", JS("function(callback, after) { callback(); }")) 6 | ct$assign("clearTimeout", JS("function(timer) { }")) 7 | jsfile <- gzfile(system.file("svg-term.js.gz", package = "asciicast")) 8 | on.exit(close(jsfile), add = TRUE) 9 | ct$source(jsfile) 10 | 11 | ct 12 | } 13 | 14 | #' Extract / create complete screen frames from an ascii cast 15 | #' 16 | #' @param cast aciicast object 17 | #' @param height Number of rows. (`NA` for default.) 18 | #' @param width Number of columns. (`NA` for default.) 19 | #' @return `asciicast_frames` object. 20 | #' 21 | #' @keywords internal 22 | 23 | load_frames <- function(cast, height = NA, width = NA) { 24 | # Start with this, because it is faster 25 | json <- paste(as_json(cast), collapse = "\n") 26 | 27 | ct <- load_svg_term() 28 | ldjs <- system.file("load-cast.js", package = "asciicast") 29 | if (ldjs == "") { 30 | throw(cli::format_error( 31 | "Internal error, cannot find {.file load-cast.js}." 32 | )) 33 | } 34 | ct$source(ldjs) 35 | 36 | ct$assign("json", json) 37 | options <- list(fps = 15, height = height, width = width) 38 | structure( 39 | ct$call("get_cast", json, options), 40 | class = "aciicast_frames" 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /R/htmlwidget.R: -------------------------------------------------------------------------------- 1 | #' asciinema player HTML widget 2 | #' 3 | #' You can use this widget in Rmd files or Shiny applications, the 4 | #' same way as [other HTML widgets](http://www.htmlwidgets.org/). 5 | #' 6 | #' @param cast `asciicast` object. 7 | #' @param start_at Where to start the playback from, in seconds. 8 | #' @param rows Number of rows, defaults to the number of rows in the 9 | #' recording, or 24 if not specified in the cast. 10 | #' @param cols Number of columns, defaults to the number columns in the 11 | #' recording, or 80 if not specified in the cast. 12 | #' @param autoplay Whether to start playing the cast automatically. 13 | #' @param loop Whether to loop the playback. 14 | #' @param speed Whether to play slower or faster. 1 is normal speed. 15 | #' @param title If specified, it overrides the title in the recording. 16 | #' @param author Author, displayed in the titlebar in fullscreen mode. 17 | #' @param author_url URL of the author's homepage/profile. Author name 18 | #' (author above) is linked to this URL. 19 | #' @param author_img_url URL of the author's image, displayed in the 20 | #' titlebar in fullscreen mode. 21 | #' @param poster_text if not `NULL`, used as the text of the poster 22 | #' (preview). 23 | #' @param poster_frame Which frame to use for the preview. A number means 24 | #' seconds. Defaults to the last frame. This is only used if `poster_text` 25 | #' is `NULL`. 26 | #' @param font_size Size of terminal font. Possible values: small, medium, 27 | #' big, any css `font-size` value (e.g. 15px). 28 | #' @param theme Theme. 29 | #' @param idle_time_limit Time limit for the cast not printing anything, 30 | #' in seconds. By default there is no limit. 31 | #' @param html_height HTML height of the widget. 32 | #' @param html_width HTML width of the widget. 33 | #' @param element_id HTML id of the widget's element. If `NULL`, then the 34 | #' id is generated randomly. 35 | #' 36 | #' @export 37 | #' @examplesIf interactive() 38 | #' cast <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 39 | #' asciinema_player(cast) 40 | asciinema_player <- function( 41 | cast, 42 | start_at = 0, 43 | rows = NULL, 44 | cols = NULL, 45 | autoplay = NULL, 46 | loop = NULL, 47 | speed = NULL, 48 | title = NULL, 49 | author = NULL, 50 | author_url = NULL, 51 | author_img_url = NULL, 52 | poster_text = NULL, 53 | poster_frame = NULL, 54 | font_size = NULL, 55 | theme = NULL, 56 | idle_time_limit = NULL, 57 | html_height = NULL, 58 | html_width = NULL, 59 | element_id = NULL 60 | ) { 61 | rows <- rows %||% cast$config$rows %||% cast$config$height %||% 24 62 | cols <- cols %||% cast$config$cols %||% cast$config$width %||% 80 63 | title <- title %||% cast$config$title %||% "" 64 | poster_frame <- poster_frame %||% cast$config$poster_frame %||% "" 65 | poster_text <- poster_text %||% cast$config$poster_text %||% title 66 | theme <- theme %||% cast$config$theme %||% "asciinema" 67 | idle_time_limit <- idle_time_limit %||% cast$config$idle_time_limit 68 | 69 | tmp <- tempfile() 70 | on.exit(unlink(tmp), add = TRUE) 71 | write_json(cast, tmp) 72 | src <- paste0( 73 | "data:application/json;base64,", 74 | jsonlite::base64_enc(readBin(tmp, what = "raw", n = file.size(tmp))) 75 | ) 76 | 77 | last_frame <- utils::tail(c(0, cast$output$time), 1) 78 | htmlwidgets::createWidget( 79 | name = "asciinema_player", 80 | list( 81 | src = src, 82 | cols = cols, 83 | rows = rows, 84 | autoplay = autoplay %||% cast$config$autoplay, 85 | loop = loop %||% cast$config$loop, 86 | start_at = start_at, 87 | speed = speed %||% cast$config$speed %||% 1, 88 | poster = poster(poster_text, poster_frame, last_frame), 89 | theme = theme, 90 | font_size = font_size %||% cast$config$font_size %||% NULL, 91 | title = title, 92 | author = author %||% "", 93 | author_url = author_url %||% "", 94 | author_img_url = author_img_url %||% "" 95 | ), 96 | width = html_width, 97 | height = html_height, 98 | package = "asciicast", 99 | elementId = element_id, 100 | sizingPolicy = htmlwidgets::sizingPolicy( 101 | viewer.suppress = TRUE, 102 | knitr.figure = FALSE, 103 | browser.fill = TRUE, 104 | browser.padding = 20, 105 | knitr.defaultWidth = "100%", 106 | knitr.defaultHeight = "100%" 107 | ) 108 | ) 109 | } 110 | 111 | poster <- function(poster_text = NULL, poster_frame = NULL, secs = 0) { 112 | seconds <- if (identical(poster_frame, "")) { 113 | secs 114 | } else { 115 | poster_frame 116 | } 117 | 118 | if (!identical(poster_text, "")) { 119 | sprintf("data:text/plain,%s", poster_text) 120 | } else { 121 | sprintf("npt:%f", seconds) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /R/merge.R: -------------------------------------------------------------------------------- 1 | #' Merge multiple ASCII casts into one 2 | #' 3 | #' The new cast will inherit its options (screen size, etc.) from the 4 | #' first cast in the argument list. The options of the rest of the casts 5 | #' are ignored. 6 | #' 7 | #' `pause()` inserts a pause of the specified seconds between the casts. 8 | #' 9 | #' `clear_screen()` clears the screen between two casts. 10 | #' 11 | #' @param ... Ascii casts to merge or merge commands. Merge commands 12 | #' provide a way to insert pause, clear the screen, etc., between casts. 13 | #' @return An `asciicast` object. 14 | #' 15 | #' @export 16 | #' @examplesIf interactive() 17 | #' # merge two casts, with a pause, and clear screen between them 18 | #' cast1 <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 19 | #' cast2 <- read_cast(system.file("examples", "dplyr.cast", package = "asciicast")) 20 | #' cast <- merge_casts(cast1, pause(3), clear_screen(), cast2) 21 | #' play(cast) 22 | merge_casts <- function(...) { 23 | casts <- lapply(list(...), handle_merge_cast) 24 | types <- map_chr(casts, "[[", "type") 25 | if (!"cast" %in% types) { 26 | throw(cli::format_error( 27 | "You need to include at least one cast in {.fn merge_casts}." 28 | )) 29 | } 30 | wconf <- which(types == "cast")[[1]] 31 | new_cast <- structure( 32 | list( 33 | config = casts[[wconf]]$output$config, 34 | output = shift_output(lapply(casts, function(x) x$output$output)) 35 | ), 36 | class = "asciicast" 37 | ) 38 | } 39 | 40 | handle_merge_cast <- function(x) { 41 | if (inherits(x, "merge_cast_command")) { 42 | list(type = "command", output = x$output) 43 | } else if (inherits(x, "asciicast")) { 44 | list(type = "cast", output = x) 45 | } else { 46 | list(type = "cast", output = read_cast(x)) 47 | } 48 | } 49 | 50 | shift_output <- function(recs) { 51 | last <- map_dbl(recs, function(x) utils::tail(x$time, 1)) + 1 / 10000 52 | shift <- utils::head(cumsum(c(0, last)), -1) 53 | for (i in seq_along(recs)) { 54 | recs[[i]]$time <- recs[[i]]$time + shift[[i]] 55 | } 56 | do.call(rbind, recs) 57 | } 58 | 59 | #' @rdname merge_casts 60 | #' @export 61 | 62 | clear_screen <- function() { 63 | output <- tibble::tibble( 64 | time = 0, 65 | type = "o", 66 | data = "\u001b[H\u001b[2J" 67 | ) 68 | new_merge_cast_command("clear_screen", output = output) 69 | } 70 | 71 | #' @rdname merge_casts 72 | #' @param secs Number of seconds to wait. 73 | #' @export 74 | 75 | pause <- function(secs) { 76 | output <- tibble::tibble( 77 | time = as.numeric(secs), 78 | type = "o", 79 | data = "" 80 | ) 81 | new_merge_cast_command("pause", output = output) 82 | } 83 | 84 | new_merge_cast_command <- function(command, output) { 85 | structure( 86 | list(command = command, output = list(output = output)), 87 | class = "merge_cast_command" 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /R/onload.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(libname, pkgname) { 2 | err$onload_hook() 3 | invisible() 4 | } 5 | -------------------------------------------------------------------------------- /R/parameters.R: -------------------------------------------------------------------------------- 1 | #' asciicast parameters 2 | #' 3 | #' You can set asciicast parameters in the header of the recorded R script. 4 | #' The header is in DCF format (see [read.dcf()]), but all lines are prefixed 5 | #' with `#'` comments. 6 | #' 7 | #' The DCF header may specify arbitrary parameters. We list here the 8 | #' parameters that are interpreted by the asciicast functions. 9 | #' 10 | #' Recording parameters: 11 | #' 12 | #' * `allow_errors`: Whether to cast errors properly. If this is set to 13 | #' `TRUE`, then asciicast overwrites the `"error"` option. Only change 14 | #' this if you know what you are doing. 15 | #' * `cols`: Width of the terminal, in number of characters. 16 | #' * `empty_wait`: How long to wait for empty lines in the script file, 17 | #' in seconds. 18 | #' * `end_wait`: Delay at the very end, in seconds. 19 | #' * `env`: Environment variables to include in the case JSON file. 20 | #' Defaults to `list(TERM = "xterm-256color", SHELL = "/bin/zsh")`. 21 | #' * `idle_time_limit`: Time limit for the cast not printing anything, 22 | #' in seconds. By default there is no limit. 23 | #' * `record_env`: Environment variables to set for the R subprocess. 24 | #' * `rows`: Height of the terminal, in number of characters. 25 | #' * `start_wait`: Delay at the beginning, in seconds. 26 | #' * `timeout`: Idle timeout, in seconds If the R subprocess running 27 | #' the recording does not answer within this limit, it is killed and the 28 | #' recording stops. Update this for slow running code, that produces no 29 | #' output as it runs. 30 | #' * `timestamp`: Time stamp of the recording, defaults to `Sys.time()`, 31 | #' this is included in the cast JSON file. 32 | #' * `title`: Title of the cast, this is included in the cast JSON file. 33 | #' * `typing_speed`: Average typing speed, per keypress, in seconds. 34 | #' 35 | #' Asciinema player parameters: 36 | #' 37 | #' * `author`: Author, displayed in the titlebar in fullscreen mode. 38 | #' * `author_img_url`: URL of the author's image, displayed in the 39 | #' titlebar in fullscreen mode. 40 | #' * `author_url`: URL of the author's homepage/profile. Author name 41 | #' (author above) is linked to this URL. 42 | #' * `autoplay`: Whether to start playing the cast automatically. 43 | #' * `cols`: Width of the terminal, in number of characters. 44 | #' * `font_size`: Size of terminal font. Possible values: small, medium, 45 | #' big, any css `font-size` value (e.g. 15px). 46 | #' * `idle_time_limit`: Time limit for the cast not printing anything, 47 | #' in seconds. By default there is no limit. 48 | #' * `loop`: Whether to loop the playback. 49 | #' * `poster_frame`: Which frame to use (in seconds) as the preview picture. 50 | #' * `poster_text`: Text to use as the preview picture. Defaults to the 51 | #' title. 52 | #' * `rows`: Height of the terminal, in number of characters. 53 | #' * `speed`: Whether to play slower or faster. 1 is normal speed. 54 | #' * `start_at`: Where to start the playback from, in seconds. 55 | #' * `theme`: Theme to use, currently it has to be a string, one of 56 | #' `"asciinema"`, `"tango"`, `"solarized-dark"`, `"solarized-light"`, 57 | #' `"monokai"`. The first one is the default. 58 | #' * `title`: Title of the cast. 59 | #' 60 | #' Parameters for SVG files: 61 | #' * `at`: Timestamp of single frame to render, in seconds. 62 | #' * `cols`: Width of the terminal, in number of characters. 63 | #' * `cursor`: Enable cursor rendering. 64 | #' * `end_at`: Upper range of timeline to render in seconds. 65 | #' * `padding`: Distance between text and image bounds. 66 | #' * `padding_x`: Distance between text and image bounds on x axis. 67 | #' * `padding_y`: Distance between text and image bounds on y axis. 68 | #' * `rows`: Height of the terminal, in number of characters. 69 | #' * `start_at`: Where to start the playback from, in seconds. 70 | #' * `window`: Render with window decorations. 71 | #' * `theme`: Theme to use, currently it has to be a string referring to 72 | #' a build-in theme, or a named list of theme properties, 73 | #' see [default_theme()]. 74 | #' The built-in themes are `"asciinema"`, `"tango"`, `"solarized-dark"`, 75 | #' `"solarized-light"`, `"seti"`, `"monokai"`, `"github-light"`, 76 | #' `"github-dark"`, `"pkgdown"` ,`"readme"`. 77 | #' `"readme"` is a special theme the switches between light and dark 78 | #' mode automatically in `README.md` files on GitHub. 79 | #' 80 | #' @name asciicast-package 81 | #' @family asciicast functions 82 | NULL 83 | -------------------------------------------------------------------------------- /R/read.R: -------------------------------------------------------------------------------- 1 | #' Import an asciicast from an asciicast JSON file 2 | #' 3 | #' @param json Path to JSON asciicast file, version 2: 4 | #' . 5 | #' If a numeric id, then it is taken as a public 6 | #' recording id, that is downloaded. It can also be a URL of private 7 | #' link. 8 | #' @return `asciicast` object. 9 | #' 10 | #' @export 11 | #' @importFrom jsonlite fromJSON 12 | #' @family asciicast functions 13 | #' @examplesIf interactive() 14 | #' c1 <- read_cast("https://asciinema.org/a/uHQwIVpiZvu0Ioio8KYx6Uwlj.cast?dl=1") 15 | #' play(c1) 16 | #' 17 | #' c2 <- read_cast(258660) 18 | #' play(c2) 19 | #' 20 | #' @examplesIf interactive() 21 | #' c3 <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 22 | #' play(c3) 23 | read_cast <- function(json) { 24 | if (is.numeric(json)) { 25 | anurl <- sprintf("https://asciinema.org/a/%d.cast?dl=1", json) 26 | con <- curl::curl(anurl) 27 | on.exit(close(con), add = TRUE) 28 | lines <- readLines(con) 29 | } else if (grepl("^https?://", json)) { 30 | con <- curl::curl(json) 31 | on.exit(close(con), add = TRUE) 32 | lines <- readLines(con) 33 | } else { 34 | lines <- readLines(json) 35 | } 36 | 37 | config <- chain_error(fromJSON(lines[1]), new_parse_error(json)) 38 | if (!identical(as.integer(config$version), 2L)) { 39 | throw(new_parse_error(json)) 40 | } 41 | 42 | nrec <- length(lines) - 1L 43 | output <- tibble::tibble( 44 | time = double(nrec), 45 | type = character(nrec), 46 | data = character(nrec) 47 | ) 48 | 49 | for (i in seq_along(lines)[-1]) { 50 | l <- chain_error( 51 | fromJSON(lines[i], simplifyVector = FALSE), 52 | new_parse_error(json, line = i) 53 | ) 54 | if (!is.numeric(l[[1]]) || !is.character(l[[2]]) || !is.character(l[[3]])) { 55 | throw(new_parse_error(json, line = i)) 56 | } 57 | output[i - 1L, ] <- l 58 | } 59 | 60 | new_cast(config, output) 61 | } 62 | 63 | new_parse_error <- function(file, line = 1L) { 64 | msg <- paste0( 65 | "Parse error in ", 66 | file, 67 | ":", 68 | line, 69 | ".", 70 | if (line == 1L) " Only version 2 asciicast files are supported" 71 | ) 72 | cnd <- new_error(msg) 73 | class(cnd) <- c("asciicast_parse_error", class(cnd)) 74 | cnd$file <- file 75 | cnd$line <- line 76 | cnd 77 | } 78 | -------------------------------------------------------------------------------- /R/record-output.R: -------------------------------------------------------------------------------- 1 | #' Record output of an R script and return it as a character vector 2 | #' 3 | #' This function uses [record()] internally, but instead of creating 4 | #' an ascii cast, it just returns the output of the code in a character 5 | #' vector. 6 | #' 7 | #' @param script The code to record, passed to [record()]. 8 | #' @param echo Whether to include the input in the return value. 9 | #' @param prompt Whether to include the R prompt in the return value. 10 | #' @param stdout Whether to include the standard output in the return 11 | #' value. 12 | #' @param stderr Whether to include the standard error in the return 13 | #' value. 14 | #' @param ... Addiitonal arguments are passed to [record()]. (You cannot 15 | #' use `typing_speed` and `echo`, though, because these are used 16 | #' internally by `record_output()`. 17 | #' @return Character vector of output (plus input if `echo`, plus 18 | #' prompt if `prompt`), as it would appear on a terminal. 19 | #' 20 | #' See [record()] for additional options. 21 | #' 22 | #' @export 23 | 24 | record_output <- function( 25 | script, 26 | echo = FALSE, 27 | prompt = echo, 28 | stdout = TRUE, 29 | stderr = TRUE, 30 | ... 31 | ) { 32 | cast <- record( 33 | script, 34 | typing_speed = 0, 35 | echo = TRUE, 36 | ... 37 | ) 38 | 39 | data <- cast$output 40 | 41 | keep_echo <- if (echo) { 42 | which(data$type == "rlib" & data$data == "type: input") + 1L 43 | } 44 | keep_prompt <- if (prompt) { 45 | which(data$type == "rlib" & data$data == "type: prompt") + 1L 46 | } 47 | keep_stdout <- if (stdout) { 48 | which(data$type == "rlib" & data$data == "type: stdout") + 1L 49 | } 50 | keep_stderr <- if (stderr) { 51 | which(data$type == "rlib" & data$data == "type: stderr") + 1L 52 | } 53 | 54 | keep <- sort(c(keep_echo, keep_prompt, keep_stdout, keep_stderr)) 55 | 56 | strsplit( 57 | paste(data$data[keep], collapse = ""), 58 | "\r?\n" 59 | )[[1]] 60 | } 61 | -------------------------------------------------------------------------------- /R/rstudio.R: -------------------------------------------------------------------------------- 1 | is_rstudio <- function() { 2 | "rstudioapi" %in% loadedNamespaces() && rstudioapi::isAvailable() 3 | } 4 | 5 | view_image_in_rstudio <- function(path) { 6 | html <- tempfile("asciicast-preview-", fileext = ".html") 7 | img <- paste0( 8 | tempfile("asciicast-preview-", fileext = ""), 9 | file_ext(path) 10 | ) 11 | file.copy(path, img) 12 | 13 | cat( 14 | file = html, 15 | sprintf( 16 | "", 17 | basename(img) 18 | ) 19 | ) 20 | 21 | rstudioapi::viewer(html) 22 | } 23 | -------------------------------------------------------------------------------- /R/test.R: -------------------------------------------------------------------------------- 1 | #' testthat snapshot test with asciicast 2 | #' 3 | #' This function is very similar to [testthat::expect_snapshot_output()], 4 | #' but it runs the code in an asciciast subprocess, using [record_output()]. 5 | #' 6 | #' THe `Code` part of the snapshot is always the same, but the 7 | #' `Output` part shows the code, assuming `echo = TRUE` (the default). 8 | #' 9 | #' @param ... Code to run (unnamed arguments) and arguments to pass to 10 | #' [record_output()] (named arguments). The code is evaluated in a new 11 | #' asciicast subprocess. Their output is returned and used in a testthat 12 | #' snapshot test. 13 | #' @param interactive Whether to use an interactive R process to evaluate 14 | #' the code. 15 | #' @param echo Whether to echo the code in the subprocess before running 16 | #' it. 17 | #' @param startup Expression to evaluate in the subprocess before 18 | #' recording the snapshot. By default it loads and attaches the calling 19 | #' package, including its internal functions. 20 | #' @param transform Passed to [testthat::expect_snapshot()]. 21 | #' @param variant Passed to [testthat::expect_snapshot()]. 22 | #' 23 | #' @export 24 | #' @examplesIf !asciicast:::is_rcmd_check() 25 | #' Sys.getpid() 26 | #' testthat::local_edition(3) 27 | #' expect_snapshot_r_process(Sys.getpid()) 28 | expect_snapshot_r_process <- function( 29 | ..., 30 | interactive = TRUE, 31 | echo = TRUE, 32 | startup = NULL, 33 | transform = NULL, 34 | variant = NULL 35 | ) { 36 | # errors.R assumes non-interactive in testthat, but we don't want that 37 | withr::local_envvar(TESTTHAT = NA_character_) 38 | dots <- eval(substitute(alist(...))) 39 | nms <- names(dots) 40 | if (all(nms == "")) { 41 | code_pos <- rep(TRUE, length(dots)) 42 | } else { 43 | code_pos <- nms == "" 44 | } 45 | code <- unlist(lapply(dots[code_pos], deparse)) 46 | args <- dots[!code_pos] 47 | 48 | if (is.null(startup)) { 49 | startup <- substitute( 50 | do.call("attach", list(asNamespace(pkg), name = paste0("package:", pkg))), 51 | list(pkg = environmentName(topenv(parent.frame()))) 52 | ) 53 | } 54 | rec_args <- list( 55 | interactive = interactive, 56 | echo = echo, 57 | startup = startup, 58 | record_env = c("R_CLI_HIDE_CURSOR" = "false") 59 | ) 60 | 61 | record_output <- record_output 62 | output <- do.call( 63 | "record_output", 64 | c(list(code), args, rec_args), 65 | quote = TRUE 66 | ) 67 | 68 | r_process <- function() writeLines(output) 69 | 70 | testthat::expect_snapshot( 71 | r_process(), 72 | transform = transform, 73 | variant = variant 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | `%||%` <- function(l, r) if (is.null(l)) r else l 2 | 3 | not_null <- function(x) { 4 | x[!vapply(x, is.null, logical(1))] 5 | } 6 | 7 | encode_str <- function(x) { 8 | vapply(x, jsonlite::toJSON, character(1), auto_unbox = TRUE) 9 | } 10 | 11 | mkdirp <- function(x) { 12 | dir.create(x, showWarnings = FALSE, recursive = TRUE) 13 | } 14 | 15 | get_param <- function(x, default = NULL, config = parent.frame()$cast$config) { 16 | x <- tolower(x) 17 | env <- parent.frame() 18 | env[[x]] %||% 19 | config[[x]] %||% 20 | getOption(paste0("asciicast_", x)) %||% 21 | default 22 | } 23 | 24 | modify_list <- function(l, upd) { 25 | l[names(upd)] <- upd 26 | l 27 | } 28 | 29 | map_chr <- function(.x, .f, ...) { 30 | vapply(.x, .f, FUN.VALUE = character(1), ...) 31 | } 32 | 33 | map_dbl <- function(.x, .f, ...) { 34 | vapply(.x, .f, FUN.VALUE = double(1), ...) 35 | } 36 | 37 | is_windows <- function() { 38 | .Platform$OS.type == "windows" 39 | } 40 | 41 | is_macos <- function() { 42 | Sys.info()[["sysname"]] == "Darwin" 43 | } 44 | 45 | is_linux <- function() { 46 | Sys.info()[["sysname"]] == "Linux" 47 | } 48 | 49 | dir_exists <- function(path) { 50 | utils::file_test("-d", path) 51 | } 52 | 53 | try_silently <- function(expr) { 54 | try(expr, silent = TRUE) 55 | } 56 | 57 | #' @importFrom cli cli_process_start cli_process_done 58 | 59 | with_cli_process <- function(msg, expr, ...) { 60 | proc <- cli_process_start(msg, ...) 61 | ret <- withVisible(expr) 62 | cli_process_done(proc) 63 | if (ret$visible) ret$value else invisible(ret$value) 64 | } 65 | 66 | file_ext <- function(x) { 67 | pos <- regexpr("(\\.[[:alnum:]]+)$", x) 68 | ifelse(pos > -1L, substring(x, pos + 1L), "") 69 | } 70 | 71 | na_omit <- function(x) { 72 | x[!is.na(x)] 73 | } 74 | 75 | if (getRversion() < "3.6.0") { 76 | str2lang <- function(x) { 77 | parse(text = x, keep.source = FALSE)[[1]] 78 | } 79 | } 80 | 81 | is_rcmd_check <- function() { 82 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 83 | FALSE 84 | } else { 85 | Sys.getenv("_R_CHECK_PACKAGE_NAME_", "") != "" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /R/write-json.R: -------------------------------------------------------------------------------- 1 | as_json <- function(cast) { 2 | if (!inherits(cast, "asciicast")) { 3 | throw(cli::format_error(c( 4 | "{.var cast} must be an {.cls asciicast}, object.", 5 | i = "It is a {.type {cast}}." 6 | ))) 7 | } 8 | con <- textConnection(NULL, "w", local = TRUE) 9 | on.exit(close(con), add = TRUE) 10 | write_json_con(cast, con) 11 | textConnectionValue(con) 12 | } 13 | 14 | write_json_con <- function(cast, con) { 15 | # Header 16 | cat(jsonlite::toJSON(cast$config, auto_unbox = TRUE), file = con) 17 | cat("\n", file = con) 18 | 19 | # data 20 | cat( 21 | sep = "", 22 | file = con, 23 | paste0( 24 | "[", 25 | cast$output$time, 26 | ", ", 27 | encode_str(cast$output$type), 28 | ", ", 29 | encode_str(cast$output$data), 30 | "]", 31 | "\n" 32 | ) 33 | ) 34 | } 35 | 36 | #' Write an ascii cast to file 37 | #' 38 | #' The file uses the asciinema file format, version 2: 39 | #' . 40 | #' 41 | #' @param cast `asciicast` object. 42 | #' @param path Path to write to. 43 | #' 44 | #' @export 45 | #' @family asciicast functions 46 | #' @examplesIf !asciicast:::is_rcmd_check() 47 | #' script <- system.file("examples", "hello.R", package = "asciicast") 48 | #' cast <- record(script) 49 | #' json <- tempfile(fileext = ".json") 50 | #' write_json(cast, json) 51 | #' \dontshow{ 52 | #' unlink(json, recursive = TRUE) 53 | #' } 54 | write_json <- function(cast, path) { 55 | con <- file(path, open = "wb") 56 | on.exit(close(con), add = TRUE) 57 | write_json_con(cast, con) 58 | invisible() 59 | } 60 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: yes 4 | --- 5 | 6 | 7 | 8 | ```{r} 9 | #| include: false 10 | #| cache: false 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | fig.path = "man/figures/README-", 15 | out.width = "100%", 16 | asciicast_at = "all", 17 | asciicast_theme = if (Sys.getenv("IN_PKGDOWN") == "true") "pkgdown" else "readme" 18 | ) 19 | asciicast::init_knitr_engine() 20 | ``` 21 | 22 | # asciicast 23 | 24 | > Turn R scripts into terminal screencasts 25 | 26 | 27 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 28 | [![CRAN status](https://www.r-pkg.org/badges/version/asciicast)](https://cran.r-project.org/package=asciicast) 29 | [![R-CMD-check](https://github.com/r-lib/asciicast/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/asciicast/actions/workflows/R-CMD-check.yaml) 30 | [![Codecov test coverage](https://codecov.io/gh/r-lib/asciicast/graph/badge.svg)](https://app.codecov.io/gh/r-lib/asciicast) 31 | 32 | 33 | asciicast takes an R script and turns it into an 34 | [asciinema](https://asciinema.org/) cast. It can simulate typing, and 35 | records all terminal output in real time as it happens. 36 | 37 | ## Features 38 | 39 | * Input is an R script, output is a 40 | [v2 asciicast recording](https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md). 41 | * Record all terminal output in real time, as it happens. 42 | * Simulate typing in the commands, with a configurable, randomized speed. 43 | * Alternatively, whole comment blocks or expressions can just appear on 44 | the screen. 45 | * Convert casts to SVG images using 46 | [svg-term](https://github.com/marionebl/svg-term). The package comes with 47 | its own svg-term bundle, no external dependencies are needed. 48 | * Render a single frame of a cast as an SVG image. 49 | * Configurable delay at the beginning, at the end and between paragraphs. 50 | * [HTML widget](http://www.htmlwidgets.org), to be used in Rmarkdown 51 | documents, e.g. in vignettes. 52 | * Read casts from asciinema JSON files (version 2), or from 53 | directly. 54 | * Special knitr engine to create R markdown files with ascii casts. See 55 | the `asciicast-demo` vignette. 56 | * Create ascii casts in GitHub READMEs via animated SVG files. See an 57 | example in `inst/examples` or the `README.Rmd` source of the README 58 | file you are reading. 59 | 60 | ## Limitations 61 | 62 | * asciicast works best in an UTF-8 locale. It also works well if all output 63 | is ASCII, but non-ASCII output might cause problems 64 | (https://github.com/r-lib/asciicast/issues/36). 65 | 66 | ## Installation 67 | 68 | You can install the released version of asciicast from 69 | [CRAN](https://CRAN.R-project.org): 70 | 71 | ``` r 72 | install.packages("asciicast") 73 | ``` 74 | 75 | ## Examples 76 | 77 | See the [`inst/examples` directory](https://github.com/r-lib/asciicast/tree/main/inst/examples) 78 | for these examples. 79 | 80 | ### Hello world 81 | 82 | The input script: 83 | 84 | ```{r} 85 | #| code: !expr readLines("inst/examples/hello.R") 86 | #| eval: false 87 | ``` 88 | 89 | The result: 90 | 91 | ```{asciicast} 92 | #| code: !expr readLines("inst/examples/hello.R") 93 | #| cache: true 94 | ``` 95 | 96 | ### Asciicast demo in asciicast 97 | 98 | Input script that uses asciicast itself: 99 | 100 | ```{r} 101 | #| code: !expr readLines("inst/examples/asciicast.R") 102 | #| eval: false 103 | ``` 104 | 105 | ```{asciicast} 106 | #| code: !expr readLines("inst/examples/asciicast.R") 107 | #| cache: true 108 | ``` 109 | 110 | ### Errors are recorded 111 | 112 | Input script with errors: 113 | 114 | ```{r} 115 | #| code: !expr readLines("inst/examples/errors.R") 116 | #| eval: false 117 | ``` 118 | 119 | ```{asciicast} 120 | #| code: !expr readLines("inst/examples/errors.R") 121 | #| cache: true 122 | ``` 123 | 124 | ## Related tools 125 | 126 | * asciinema: https://asciinema.org/ 127 | * The original terminal session recorder: 128 | https://github.com/asciinema/asciinema 129 | * svg-term: https://github.com/marionebl/svg-term, 130 | https://github.com/marionebl/svg-term-cli 131 | 132 | ## License 133 | 134 | MIT @ [RStudio](https://github.com/rstudio) 135 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://asciicast.r-lib.org/ 2 | 3 | template: 4 | package: tidytemplate 5 | bootstrap: 5 6 | includes: 7 | in_header: | 8 | 9 | 10 | reference: 11 | - title: Basics 12 | contents: 13 | - record 14 | - play 15 | - merge_casts 16 | - record_output 17 | 18 | - title: Sharing 19 | contents: 20 | - write_svg 21 | - write_html 22 | - write_gif 23 | - write_json 24 | - read_cast 25 | 26 | - title: Theming and options 27 | contents: 28 | - default_theme 29 | - asciicast_options 30 | - '`asciicast-package`' 31 | 32 | - title: Other functions 33 | contents: 34 | - get_locales 35 | - init_knitr_engine 36 | - asciicast_start_process 37 | - asciinema_player 38 | - expect_snapshot_r_process 39 | - install_phantomjs 40 | 41 | development: 42 | mode: auto 43 | -------------------------------------------------------------------------------- /air.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/asciicast/18f96f36b12b7ea5879b86cba7cde46786ddf141/air.toml -------------------------------------------------------------------------------- /asciicast.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | ProjectId: 6d3a84a0-b1f6-4617-b66c-a4cd26058153 3 | 4 | RestoreWorkspace: No 5 | SaveWorkspace: No 6 | AlwaysSaveHistory: Default 7 | 8 | EnableCodeIndexing: Yes 9 | UseSpacesForTab: Yes 10 | NumSpacesForTab: 2 11 | Encoding: UTF-8 12 | 13 | RnwWeave: Sweave 14 | LaTeX: pdfLaTeX 15 | 16 | AutoAppendNewline: Yes 17 | StripTrailingWhitespace: Yes 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Check that this is not just ./configure. We need to run this 4 | # from R CMD INSTALL, to have the R env vars set. 5 | 6 | if [ -z "$R_HOME" ]; then 7 | echo >&2 R_HOME is not set, are you running R CMD INSTALL? 8 | exit 1 9 | fi 10 | 11 | # Find the R binary we need to use. This is a bit trickier on 12 | # Windows, because it has two architectures. On windows R_ARCH_BIN 13 | # is set, so this should work everywhere. 14 | RBIN="${R_HOME}/bin${R_ARCH_BIN}/R" 15 | 16 | CHKLIBR=`${RBIN} CMD config --ldflags 2>/dev/null` 17 | have_libR=no 18 | if test -n "$CHKLIBR"; then 19 | have_libR=yes 20 | fi 21 | 22 | if test "${have_libR}" = yes; then 23 | REM="rem" 24 | else 25 | REM="" 26 | fi 27 | 28 | cat src/Makevars.in | \ 29 | sed "s/@REM@/${REM}/" > src/Makevars 30 | -------------------------------------------------------------------------------- /configure.win: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/asciicast/18f96f36b12b7ea5879b86cba7cde46786ddf141/configure.win -------------------------------------------------------------------------------- /inst/browserify.js: -------------------------------------------------------------------------------- 1 | 2 | var JSDOM = require('jsdom').JSDOM; 3 | 4 | global.dom = new JSDOM(''); 5 | global.window = dom.window; 6 | global.document = dom.window.document; 7 | global.navigator = global.window.navigator; 8 | 9 | global.svgterm = require('.'); 10 | global.loadcast = require('load-asciicast').load; 11 | -------------------------------------------------------------------------------- /inst/examples/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: asciicast.cast dplyr.cast errors.cast hello.cast github-readme.md 3 | 4 | %.cast: %.R 5 | R -q -e 'asciicast::write_json(asciicast::record("$<"), "$@")' 6 | 7 | %.md: %.Rmd 8 | R -q -e 'rmarkdown::render("$<")' 9 | -------------------------------------------------------------------------------- /inst/examples/asciicast.R: -------------------------------------------------------------------------------- 1 | #' Title: asciicast example recorded in asciicast 2 | #' Empty_wait: 3 3 | #' End_wait: 20 4 | 5 | # An example for using asciicast, recorded in asciicast itself! #! 6 | 7 | # First, save the R code you want to run, in a script file. #! 8 | # The file can contain any code, including interactive code, #! 9 | # as long as it is a syntactically valid R file. #! 10 | 11 | # Second, perform the recording with the `record()` function. #! 12 | # We are recording an example file now, that comes with the package. #! 13 | 14 | src <- system.file("examples", "hello.R", package = "asciicast") 15 | cast <- asciicast::record(src) 16 | 17 | # `cast` is an `asciicast` object, which has some metadata and the #! 18 | # recording itself: #! 19 | 20 | cast 21 | 22 | # You can write `cast` to a JSON file that can be played by any #! 23 | # asciinema player. Or you can write it to an SVG file that can #! 24 | # be embedded into a web page, or a GitHub README. #! 25 | 26 | svg <- tempfile(fileext = ".svg") 27 | asciicast::write_svg(cast, svg, window = TRUE) 28 | -------------------------------------------------------------------------------- /inst/examples/dplyr.R: -------------------------------------------------------------------------------- 1 | #' Title: Simple asciicast example 2 | #' Depends: processx (>= 3.4.0), dplyr 3 | 4 | # Comments are simply "typed in" 5 | # Commands are typed in as well. They are executed and their output 6 | # is printed: 7 | 8 | library(dplyr) 9 | 10 | iris |> 11 | dplyr::group_by(Species) |> 12 | dplyr::summarise_all(mean) 13 | 14 | # Here is another command: 15 | 16 | starwars |> 17 | group_by(species) |> 18 | summarise( 19 | n = n(), 20 | mass = mean(mass, na.rm = TRUE) 21 | ) |> 22 | filter(n > 1, mass > 50) 23 | -------------------------------------------------------------------------------- /inst/examples/errors.R: -------------------------------------------------------------------------------- 1 | #' End_wait: 20 2 | # Demonstrate that errors are handled well 3 | 4 | # Base R error 5 | library("not-this-really") 6 | traceback() 7 | 8 | # callr errors are saved to `.Last.error`, including a stack trace 9 | library(cli) 10 | callr::r(function() library("another-failure")) 11 | .Last.error 12 | -------------------------------------------------------------------------------- /inst/examples/github-readme.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: yes 4 | --- 5 | 6 | 7 | 8 | ```{r, include = FALSE} 9 | knitr::opts_chunk$set( 10 | collapse = TRUE, 11 | comment = "#>", 12 | fig.path = "man/figures/README-", 13 | out.width = "100%" 14 | ) 15 | ``` 16 | 17 | # Using ascii casts in GitHub README files 18 | 19 | GitHub READMEs do not allow custom JavaScript code, so we cannot use 20 | HTML widgets in them. But they do allow SVG images, which can also be 21 | animated. This is file is an example README. See the source Rmd file. 22 | 23 | First we need to initialize the asciicast engine, as usual: 24 | 25 | ```` 26 | ```{r echo = FALSE, results = "hide"}`r ''` 27 | asciicast::init_knitr_engine() 28 | ``` 29 | ```` 30 | 31 | ```{r echo = FALSE, results = "hide"} 32 | options(asciicast_theme = "asciinema") 33 | asciicast::init_knitr_engine() 34 | ``` 35 | 36 | Now we are ready to include casts. The current default is to create a 37 | snapshot of the screen after the code has run: 38 | 39 | ## Still screenshots 40 | 41 | To include a snapshot instead of an animation, the `at` option must be 42 | set to `"end"`, but that is the default currently: 43 | 44 | ```` 45 | ```{asciicast, cache = TRUE}`r ''` 46 | # This is an asciicast example 47 | loadedNamespaces() 48 | ``` 49 | ```` 50 | 51 | ```{asciicast, cache = TRUE} 52 | # This is an asciicast example 53 | loadedNamespaces() 54 | ``` 55 | 56 | ## Proper ASCII casts 57 | 58 | To use animated casts instead of screen shots, we need to set the 59 | `at` option to `all`. We also set `end_wait` to wait five second before 60 | restarting the animation. By default asciicast creates animated SVG files: 61 | 62 | ```` 63 | ```{asciicast, cache = TRUE}`r ''` 64 | #' Rows: 10 65 | #' End_wait: 5 66 | #' At: all 67 | # This is an asciicast example 68 | loadedNamespaces() 69 | ``` 70 | ```` 71 | 72 | ```{asciicast, cache = TRUE} 73 | #' At: all 74 | #' End_wait: 5 75 | # This is an asciicast example 76 | loadedNamespaces() 77 | ``` 78 | 79 | ## ANSI colors 80 | 81 | asciicast supports 256 ANSI colors, and ANSI support is automatically 82 | enabled in the asciicast subprocess: 83 | 84 | ```` 85 | ```{asciicast, cache = TRUE}`r ''` 86 | cli::ansi_palette_show() 87 | ``` 88 | ```` 89 | 90 | ```{asciicast, cache = TRUE} 91 | cli::ansi_palette_show() 92 | ``` 93 | -------------------------------------------------------------------------------- /inst/examples/github-readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Using ascii casts in GitHub README files 5 | 6 | GitHub READMEs do not allow custom JavaScript code, so we cannot use 7 | HTML widgets in them. But they do allow SVG images, which can also be 8 | animated. This is file is an example README. See the source Rmd file. 9 | 10 | First we need to initialize the asciicast engine, as usual: 11 | 12 | ```{r echo = FALSE, results = "hide"} 13 | asciicast::init_knitr_engine() 14 | ``` 15 | 16 | Now we are ready to include casts. The current default is to create a 17 | snapshot of the screen after the code has run: 18 | 19 | ## Still screenshots 20 | 21 | To include a snapshot instead of an animation, the `at` option must be 22 | set to `"end"`, but that is the default currently: 23 | 24 | ```{asciicast, cache = TRUE} 25 | # This is an asciicast example 26 | loadedNamespaces() 27 | ``` 28 | 29 | 30 | 31 | ## Proper ASCII casts 32 | 33 | To use animated casts instead of screen shots, we need to set the `at` 34 | option to `all`. We also set `end_wait` to wait five second before 35 | restarting the animation. By default asciicast creates animated SVG 36 | files: 37 | 38 | ```{asciicast, cache = TRUE} 39 | #' Rows: 10 40 | #' End_wait: 5 41 | #' At: all 42 | # This is an asciicast example 43 | loadedNamespaces() 44 | ``` 45 | 46 | 47 | 48 | ## ANSI colors 49 | 50 | asciicast supports 256 ANSI colors, and ANSI support is automatically 51 | enabled in the asciicast subprocess: 52 | 53 | ```{asciicast, cache = TRUE} 54 | cli::ansi_palette_show() 55 | ``` 56 | 57 | 58 | -------------------------------------------------------------------------------- /inst/examples/hello.R: -------------------------------------------------------------------------------- 1 | print("Hello world!") 2 | -------------------------------------------------------------------------------- /inst/examples/hello.cast: -------------------------------------------------------------------------------- 1 | {"version":2,"command":"R -q","timestamp":1655733730,"env":{"TERM":"xterm-256color","SHELL":"/bin/zsh"},"height":24,"rows":24,"width":80,"cols":80} 2 | [0, "rlib", "type: prompt"] 3 | [0, "o", "> "] 4 | [0, "i", "print(\"Hello world!\")\r\n"] 5 | [0, "rlib", "type: input"] 6 | [0.0677882328047417, "o", "p"] 7 | [0.140848970436491, "o", "r"] 8 | [0.168955978692975, "o", "i"] 9 | [0.239333532238379, "o", "n"] 10 | [0.297064860211685, "o", "t"] 11 | [0.356196804635692, "o", "("] 12 | [0.408004034636542, "o", "\""] 13 | [0.458546201442368, "o", "H"] 14 | [0.484744597319514, "o", "e"] 15 | [0.526930806983728, "o", "l"] 16 | [0.559034877666272, "o", "l"] 17 | [0.628078044881113, "o", "o"] 18 | [0.694658056157641, "o", " "] 19 | [0.721490461518988, "o", "w"] 20 | [0.774800527922344, "o", "o"] 21 | [0.847071109351236, "o", "r"] 22 | [0.897263726114761, "o", "l"] 23 | [0.951062322838698, "o", "d"] 24 | [0.994743338751141, "o", "!"] 25 | [1.04142752870685, "o", "\""] 26 | [1.08166222495493, "o", ")"] 27 | [1.08166222495493, "o", "\r\n"] 28 | [1.08168922495493, "rlib", "busy: 1"] 29 | [1.08178622495493, "rlib", "type: stdout"] 30 | [1.08178622495493, "o", "[1]"] 31 | [1.08179022495493, "rlib", "type: stdout"] 32 | [1.08179022495493, "o", " \"Hello world!\""] 33 | [1.08179222495493, "rlib", "type: stdout"] 34 | [1.08179222495493, "o", "\r\n"] 35 | [1.08179522495493, "rlib", "busy: 0"] 36 | [1.08179622495493, "rlib", "busy: 0"] 37 | [6.08179622495493, "rlib", "type: wait"] 38 | [6.08179622495493, "o", ""] 39 | -------------------------------------------------------------------------------- /inst/examples/man/figures/README-/unnamed-chunk-5.svg: -------------------------------------------------------------------------------- 1 | ████████████████████████████████████████████████████████████████>cli::ansi_palette_show()brightvariantsblckredgrnyllwbluemgntcyanwhteblckredgrnyllwbluemgntcyanwhte -------------------------------------------------------------------------------- /inst/examples/man/figures/README-/unnamed-chunk-6.svg: -------------------------------------------------------------------------------- 1 | ████████████████████████████████████████████████████████████████>cli::ansi_palette_show()brightvariantsblckredgrnyllwbluemgntcyanwhteblckredgrnyllwbluemgntcyanwhte -------------------------------------------------------------------------------- /inst/examples/man/figures/README-/unnamed-chunk-7.svg: -------------------------------------------------------------------------------- 1 | ████████████████████████████████████████████████████████████████>cli::ansi_palette_show()brightvariantsblckredgrnyllwbluemgntcyanwhteblckredgrnyllwbluemgntcyanwhte -------------------------------------------------------------------------------- /inst/examples/man/figures/README-/unnamed-chunk-8.svg: -------------------------------------------------------------------------------- 1 | >Sys.getlocale()[1]"C" -------------------------------------------------------------------------------- /inst/htmlwidgets/asciinema_player.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | HTMLWidgets.widget({ 4 | 5 | name: 'asciinema_player', 6 | 7 | type: 'output', 8 | 9 | factory: function(el, width, height) { 10 | 11 | function guidGenerator() { 12 | var S4 = function() { 13 | return (((1+Math.random())*0x10000)|0).toString(16).substring(1); 14 | }; 15 | return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); 16 | } 17 | 18 | var $player; 19 | var $id = guidGenerator(); 20 | 21 | return { 22 | 23 | renderValue: function(x) { 24 | $(el).empty(); 25 | 26 | $player = $(""); 27 | $player.attr("cols", x.cols); 28 | $player.attr("rows", x.rows); 29 | if (x.autoplay) { $player.attr("autoplay", true); } 30 | if (x.loop) { $player.attr("loop", true); } 31 | $player.attr("start-at", x.start_at); 32 | $player.attr("speed", x.speed); 33 | $player.attr("poster", x.poster); 34 | $player.attr("font-size", x.font_size); 35 | $player.attr("theme", x.theme); 36 | $player.attr("id", $id); 37 | 38 | if (x.title !== "") $player.attr("title", x.title); 39 | if (x.author !== "") $player.attr("author", x.author); 40 | if (x.author_url !== "") $player.attr("author-url", x.author_url); 41 | if (x.author_img_url !== "") $player.attr("author-img-url", x.author_img_url); 42 | 43 | $(el).append($player); 44 | 45 | var term = $($player).find(".asciinema-terminal")[0]; 46 | term.style.height = Number(x.rows) + 3 + "ch"; 47 | term.style.width = x.cols + "ch"; 48 | }, 49 | 50 | resize: function(width, height) {} 51 | }; 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /inst/htmlwidgets/asciinema_player.yaml: -------------------------------------------------------------------------------- 1 | 2 | dependencies: 3 | - name: asciinema_player 4 | version: 2.6.1 5 | src: htmlwidgets/lib 6 | script: 7 | - asciinema-player.js 8 | stylesheet: 9 | - asciinema-player.css 10 | - name: jquery 11 | version: 3.2.1 12 | src: htmlwidgets/lib 13 | script: jquery-3.2.1.min.js 14 | -------------------------------------------------------------------------------- /inst/load-cast.js: -------------------------------------------------------------------------------- 1 | 2 | // This runs after svg-term is loaded into the global namespace 3 | 4 | function get_cast(json, options) { 5 | var cast = loadcast(json, options); 6 | for (var i = 0; i < cast.frames.length; i++) { 7 | cast.frames[i][1].screen.lines = 8 | simplify_lines(cast.frames[i][1].screen.lines); 9 | } 10 | return cast; 11 | } 12 | 13 | function simplify_lines(lines) { 14 | return lines.map(simplify_line); 15 | } 16 | 17 | function simplify_line(line) { 18 | var out = []; 19 | var str = ""; 20 | var att = line[0][1]; 21 | for (var i = 0; i < line.length; i++) { 22 | var p = String.fromCodePoint(line[i][0]); 23 | if (are_arrays_equal(line[i][1], att)) { 24 | str = str + p; 25 | } else { 26 | out.push([str, att]); 27 | str = p; 28 | att = line[i][1]; 29 | } 30 | } 31 | out.push([str, att]); 32 | return out; 33 | } 34 | 35 | function are_arrays_equal(a1, a2) { 36 | return JSON.stringify(a1) == JSON.stringify(a2); 37 | } 38 | -------------------------------------------------------------------------------- /inst/page/asciicast2gif.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /inst/renderer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env phantomjs 2 | 3 | var system = require('system'); 4 | 5 | var pageUrl = system.args[1]; 6 | var width = parseInt(system.args[2], 10); 7 | var height = parseInt(system.args[3], 10); 8 | var theme = system.args[4]; 9 | var scale = parseInt(system.args[5], 10); 10 | 11 | var page = require('webpage').create(); 12 | page.viewportSize = { width: 9999, height: 9999 }; 13 | page.zoomFactor = scale; 14 | 15 | function logDebug(message) { 16 | if (system.env['PHJS_DEBUG'] === '1') { 17 | console.log(message); 18 | } 19 | } 20 | 21 | function logError(message) { 22 | console.log("\u001b[31m==> \u001b[0m" + message); 23 | } 24 | 25 | function exit(code) { 26 | phantom.exit(code === undefined ? 0 : code); 27 | } 28 | 29 | page.onConsoleMessage = function(msg) { 30 | logDebug('console.log: ' + msg); 31 | }; 32 | 33 | page.onError = function(msg, trace) { 34 | logError('Script error: ' + msg); 35 | exit(1); 36 | }; 37 | 38 | page.onResourceError = function(resourceError) { 39 | logError('Unable to load resource (#' + resourceError.id + ', URL:' + resourceError.url + ')'); 40 | logError('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); 41 | exit(1); 42 | }; 43 | 44 | page.onCallback = function(data) { 45 | var rect = data.rect; 46 | 47 | if (!rect) { 48 | logError("Couldn't get geometry of requested DOM element"); 49 | exit(1); 50 | return; 51 | } 52 | 53 | logDebug('Setting clipRect...'); 54 | page.clipRect = { 55 | left: rect.left * scale, 56 | top: rect.top * scale, 57 | width: rect.width * scale, 58 | height: rect.height * scale 59 | }; 60 | 61 | logDebug('Reading update from stdin...'); 62 | var line = system.stdin.readLine(); 63 | var num = 1; 64 | 65 | while (line !== '') { 66 | logDebug("Starting frame " + num); 67 | var screen = JSON.parse(line); 68 | var imagePath = system.stdin.readLine(); 69 | if (imagePath == '') { 70 | logError('Error: imagePath empty'); 71 | exit(1); 72 | } 73 | 74 | logDebug('Calling updateTerminal...'); 75 | page.evaluate(function(screen) { 76 | window.updateTerminal(screen); 77 | }, screen); 78 | 79 | logDebug('Saving screenshot to ' + imagePath + '...'); 80 | page.render(imagePath); 81 | 82 | logDebug('Reading update from stdin...'); 83 | line = system.stdin.readLine(); 84 | num = num + 1; 85 | } 86 | 87 | logDebug('Rendering success'); 88 | 89 | exit(0); 90 | }; 91 | 92 | logDebug('Loading page...'); 93 | 94 | page.open(pageUrl, function(status) { 95 | if (status !== "success") { 96 | logError("Failed to load " + url); 97 | exit(1); 98 | } 99 | 100 | page.evaluate(function(width, height, theme) { 101 | function initTerminal() { 102 | var opts = { 103 | width: width, 104 | height: height, 105 | theme: theme 106 | }; 107 | 108 | window.updateTerminal = asciinema.gif.page.InitTerminal('player', opts); 109 | 110 | setTimeout(function() { // let Powerline font render 111 | var elements = document.querySelectorAll('.asciinema-player'); 112 | 113 | if (elements.length > 0) { 114 | window.callPhantom({ rect: elements[0].getBoundingClientRect() }); 115 | } else { 116 | window.callPhantom({ rect: undefined }); 117 | } 118 | }, 10); 119 | } 120 | 121 | FontFaceOnload("Powerline Symbols", { 122 | success: initTerminal, 123 | error: function() { 124 | console.log('Failed to pre-load Powerline Symbols font'); 125 | initTerminal(); 126 | }, 127 | timeout: 1000 128 | }); 129 | }, width, height, theme); 130 | }); 131 | 132 | // vim: ft=javascript 133 | -------------------------------------------------------------------------------- /inst/svg-term.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/asciicast/18f96f36b12b7ea5879b86cba7cde46786ddf141/inst/svg-term.js.gz -------------------------------------------------------------------------------- /man/asciicast_options.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/embed.R 3 | \name{asciicast_options} 4 | \alias{asciicast_options} 5 | \title{Default options to set in the asciicast subprocess.} 6 | \usage{ 7 | asciicast_options() 8 | } 9 | \value{ 10 | Named list. 11 | } 12 | \description{ 13 | Default options to set in the asciicast subprocess. 14 | } 15 | \examples{ 16 | asciicast_options() 17 | } 18 | -------------------------------------------------------------------------------- /man/asciicast_start_process.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/embed.R 3 | \name{asciicast_start_process} 4 | \alias{asciicast_start_process} 5 | \title{Start an asciicast background process} 6 | \usage{ 7 | asciicast_start_process( 8 | startup = NULL, 9 | timeout = 10, 10 | record_env = NULL, 11 | interactive = TRUE, 12 | locales = get_locales(), 13 | options = NULL, 14 | show_output = FALSE 15 | ) 16 | } 17 | \arguments{ 18 | \item{startup}{Quoted language object to run in the subprocess before 19 | starting the recording.} 20 | 21 | \item{timeout}{Idle timeout, in seconds If the R subprocess running 22 | the recording does not answer within this limit, it is killed and the 23 | recording stops.} 24 | 25 | \item{record_env}{Environment variables to set for the R subprocess.} 26 | 27 | \item{interactive}{Whether to run R in interactive mode. Note that in 28 | interactive mode R might ask for terminal input.} 29 | 30 | \item{locales}{Locales to set in the asciicast subprocess. Defaults 31 | to the current locales in the main R process. Specify a named character 32 | vector here to override some of the defaults. See also \code{\link[=get_locales]{get_locales()}}.} 33 | 34 | \item{options}{Options to set in the subprocess, a named list. 35 | They are deparsed to code, and then the code setting them is 36 | executed in the subprocess. See \code{\link[=asciicast_options]{asciicast_options()}} for the 37 | defaults. Supply a named list here to override the defaults or set 38 | additionsl ones. Passing large and/or complicated options here might 39 | not work, or might be slow.} 40 | 41 | \item{show_output}{Whether to show the output of the subprocess in 42 | real time.} 43 | } 44 | \value{ 45 | The R process, a \link[processx:process]{processx::process} object. 46 | } 47 | \description{ 48 | This is for expert use, if you want to run multiple recordings in the 49 | same process. 50 | } 51 | \examples{ 52 | \dontshow{if (!asciicast:::is_rcmd_check()) withAutoprint(\{ # examplesIf} 53 | # Use the same R process to record multiple casts 54 | process <- asciicast_start_process() 55 | script1 <- "a <- runif(10)\n" 56 | script2 <- "a\n" 57 | cast1 <- record(textConnection(script1), process = process) 58 | cast2 <- record(textConnection(script2), process = process) 59 | cast1 60 | cast2 61 | \dontshow{\}) # examplesIf} 62 | } 63 | \seealso{ 64 | Other asciicast functions: 65 | \code{\link{asciicast-package}}, 66 | \code{\link{read_cast}()}, 67 | \code{\link{record}()}, 68 | \code{\link{write_json}()} 69 | } 70 | \concept{asciicast functions} 71 | -------------------------------------------------------------------------------- /man/asciinema_player.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/htmlwidget.R 3 | \name{asciinema_player} 4 | \alias{asciinema_player} 5 | \title{asciinema player HTML widget} 6 | \usage{ 7 | asciinema_player( 8 | cast, 9 | start_at = 0, 10 | rows = NULL, 11 | cols = NULL, 12 | autoplay = NULL, 13 | loop = NULL, 14 | speed = NULL, 15 | title = NULL, 16 | author = NULL, 17 | author_url = NULL, 18 | author_img_url = NULL, 19 | poster_text = NULL, 20 | poster_frame = NULL, 21 | font_size = NULL, 22 | theme = NULL, 23 | idle_time_limit = NULL, 24 | html_height = NULL, 25 | html_width = NULL, 26 | element_id = NULL 27 | ) 28 | } 29 | \arguments{ 30 | \item{cast}{\code{asciicast} object.} 31 | 32 | \item{start_at}{Where to start the playback from, in seconds.} 33 | 34 | \item{rows}{Number of rows, defaults to the number of rows in the 35 | recording, or 24 if not specified in the cast.} 36 | 37 | \item{cols}{Number of columns, defaults to the number columns in the 38 | recording, or 80 if not specified in the cast.} 39 | 40 | \item{autoplay}{Whether to start playing the cast automatically.} 41 | 42 | \item{loop}{Whether to loop the playback.} 43 | 44 | \item{speed}{Whether to play slower or faster. 1 is normal speed.} 45 | 46 | \item{title}{If specified, it overrides the title in the recording.} 47 | 48 | \item{author}{Author, displayed in the titlebar in fullscreen mode.} 49 | 50 | \item{author_url}{URL of the author's homepage/profile. Author name 51 | (author above) is linked to this URL.} 52 | 53 | \item{author_img_url}{URL of the author's image, displayed in the 54 | titlebar in fullscreen mode.} 55 | 56 | \item{poster_text}{if not \code{NULL}, used as the text of the poster 57 | (preview).} 58 | 59 | \item{poster_frame}{Which frame to use for the preview. A number means 60 | seconds. Defaults to the last frame. This is only used if \code{poster_text} 61 | is \code{NULL}.} 62 | 63 | \item{font_size}{Size of terminal font. Possible values: small, medium, 64 | big, any css \code{font-size} value (e.g. 15px).} 65 | 66 | \item{theme}{Theme.} 67 | 68 | \item{idle_time_limit}{Time limit for the cast not printing anything, 69 | in seconds. By default there is no limit.} 70 | 71 | \item{html_height}{HTML height of the widget.} 72 | 73 | \item{html_width}{HTML width of the widget.} 74 | 75 | \item{element_id}{HTML id of the widget's element. If \code{NULL}, then the 76 | id is generated randomly.} 77 | } 78 | \description{ 79 | You can use this widget in Rmd files or Shiny applications, the 80 | same way as \href{http://www.htmlwidgets.org/}{other HTML widgets}. 81 | } 82 | \examples{ 83 | \dontshow{if (interactive()) withAutoprint(\{ # examplesIf} 84 | cast <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 85 | asciinema_player(cast) 86 | \dontshow{\}) # examplesIf} 87 | } 88 | -------------------------------------------------------------------------------- /man/default_theme.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/svg.R 3 | \name{default_theme} 4 | \alias{default_theme} 5 | \title{The default asciicast theme} 6 | \usage{ 7 | default_theme() 8 | } 9 | \value{ 10 | A named list. 11 | } 12 | \description{ 13 | Currently only used for \code{\link[=write_svg]{write_svg()}} 14 | } 15 | \examples{ 16 | \dontshow{if (!asciicast:::is_rcmd_check()) withAutoprint(\{ # examplesIf} 17 | cast <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 18 | svg_file <- tempfile(fileext = ".svg") 19 | mytheme <- modifyList(default_theme(), list(cursor = c(255, 0, 0))) 20 | write_svg(cast, svg_file, theme = mytheme) 21 | \dontshow{ 22 | unlink(svg_file, recursive = TRUE) 23 | } 24 | \dontshow{\}) # examplesIf} 25 | } 26 | \seealso{ 27 | Other SVG functions: 28 | \code{\link{play}()}, 29 | \code{\link{write_svg}()} 30 | } 31 | \concept{SVG functions} 32 | -------------------------------------------------------------------------------- /man/expect_snapshot_r_process.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/test.R 3 | \name{expect_snapshot_r_process} 4 | \alias{expect_snapshot_r_process} 5 | \title{testthat snapshot test with asciicast} 6 | \usage{ 7 | expect_snapshot_r_process( 8 | ..., 9 | interactive = TRUE, 10 | echo = TRUE, 11 | startup = NULL, 12 | transform = NULL, 13 | variant = NULL 14 | ) 15 | } 16 | \arguments{ 17 | \item{...}{Code to run (unnamed arguments) and arguments to pass to 18 | \code{\link[=record_output]{record_output()}} (named arguments). The code is evaluated in a new 19 | asciicast subprocess. Their output is returned and used in a testthat 20 | snapshot test.} 21 | 22 | \item{interactive}{Whether to use an interactive R process to evaluate 23 | the code.} 24 | 25 | \item{echo}{Whether to echo the code in the subprocess before running 26 | it.} 27 | 28 | \item{startup}{Expression to evaluate in the subprocess before 29 | recording the snapshot. By default it loads and attaches the calling 30 | package, including its internal functions.} 31 | 32 | \item{transform}{Passed to \code{\link[testthat:expect_snapshot]{testthat::expect_snapshot()}}.} 33 | 34 | \item{variant}{Passed to \code{\link[testthat:expect_snapshot]{testthat::expect_snapshot()}}.} 35 | } 36 | \description{ 37 | This function is very similar to \code{\link[testthat:expect_snapshot_output]{testthat::expect_snapshot_output()}}, 38 | but it runs the code in an asciciast subprocess, using \code{\link[=record_output]{record_output()}}. 39 | } 40 | \details{ 41 | THe \code{Code} part of the snapshot is always the same, but the 42 | \code{Output} part shows the code, assuming \code{echo = TRUE} (the default). 43 | } 44 | \examples{ 45 | \dontshow{if (!asciicast:::is_rcmd_check()) withAutoprint(\{ # examplesIf} 46 | Sys.getpid() 47 | testthat::local_edition(3) 48 | expect_snapshot_r_process(Sys.getpid()) 49 | \dontshow{\}) # examplesIf} 50 | } 51 | -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/get_locales.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/embed.R 3 | \name{get_locales} 4 | \alias{get_locales} 5 | \title{Helper function to query locales as a named character vector.} 6 | \usage{ 7 | get_locales() 8 | } 9 | \value{ 10 | Named character vector with entries: 11 | \itemize{ 12 | \item \code{LC_COLLATE}, \code{LC_CTYPE}, \code{LC_MONETARY}, \code{LC_NUMERIC} and \code{LC_TIME}. 13 | } 14 | } 15 | \description{ 16 | Helper function to query locales as a named character vector. 17 | } 18 | -------------------------------------------------------------------------------- /man/init_knitr_engine.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/knitr.R 3 | \name{init_knitr_engine} 4 | \alias{init_knitr_engine} 5 | \title{Initialize the asciicast knitr engine} 6 | \usage{ 7 | init_knitr_engine( 8 | echo = FALSE, 9 | same_process = TRUE, 10 | timeout = 10, 11 | startup = NULL, 12 | record_env = NULL, 13 | echo_input = TRUE, 14 | interactive = TRUE 15 | ) 16 | } 17 | \arguments{ 18 | \item{echo}{Whether to print the code of asciicast chunks.} 19 | 20 | \item{same_process}{Whether to run all asciicast chunks \emph{in the same} 21 | R process. To restart this R process, call \code{init_knitr_engine()} 22 | again.} 23 | 24 | \item{timeout}{Idle timeout, in seconds If the R subprocess running 25 | the recording does not answer within this limit, it is killed and the 26 | recording stops.} 27 | 28 | \item{startup}{Quoted language object to run in the subprocess before 29 | starting the recording.} 30 | 31 | \item{record_env}{Environment variables to set for the R subprocess.} 32 | 33 | \item{echo_input}{Whether to echo the input in the asciicast recording.} 34 | 35 | \item{interactive}{Whether to run R in interactive mode. Note that in 36 | interactive mode R might ask for terminal input.} 37 | } 38 | \description{ 39 | Call this function in your Rmd file to enable creating asciinema 40 | casts from code chunks. 41 | } 42 | \details{ 43 | \subsection{Limitations}{ 44 | \itemize{ 45 | \item \code{purl()} or setting the \code{purl = TRUE} chunk option, does not work 46 | properly, in that knitr thinks that asciicast chunks are not R code, 47 | so they will appear as comments. If you know how to fix this, please 48 | contact us. 49 | } 50 | } 51 | } 52 | \section{Examples}{ 53 | 54 | Call this function from an Rmd chunk and then you can use the asciicast 55 | knitr engine: 56 | 57 | \if{html}{\out{
}}\preformatted{```\{r setup, include = FALSE\} 58 | asciicast::init_knitr_engine() 59 | ``` 60 | }\if{html}{\out{
}} 61 | 62 | \if{html}{\out{
}}\preformatted{```\{asciicast, cache = TRUE\}` 63 | #' Rows: 10 64 | # This is an asciicast example 65 | loadedNamespaces() 66 | ``` 67 | }\if{html}{\out{
}} 68 | } 69 | 70 | \concept{asciicast in Rmd} 71 | -------------------------------------------------------------------------------- /man/install_phantomjs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/install-phantomjs.R 3 | \name{install_phantomjs} 4 | \alias{install_phantomjs} 5 | \title{Install PhantomJS} 6 | \usage{ 7 | install_phantomjs( 8 | version = "2.1.1", 9 | baseURL = "https://github.com/wch/webshot/releases/download/v0.3.1/", 10 | quiet = FALSE 11 | ) 12 | } 13 | \arguments{ 14 | \item{version}{The version number of PhantomJS.} 15 | 16 | \item{baseURL}{The base URL for the location of PhantomJS binaries for 17 | download. If the default download site is unavailable, you may specify an 18 | alternative mirror, such as 19 | \code{"https://bitbucket.org/ariya/phantomjs/downloads/"}.} 20 | 21 | \item{quiet}{If \code{TRUE} suppress status messages and progress bar.} 22 | } 23 | \value{ 24 | \code{NULL} (the executable is written to a system directory). 25 | } 26 | \description{ 27 | Download the zip package, unzip it, and copy the executable to a system 28 | directory in which \pkg{asciicast} can look for the PhantomJS executable. 29 | } 30 | \details{ 31 | This function was designed primarily to help Windows users since it is 32 | cumbersome to modify the \code{PATH} variable. Mac OS X users may install 33 | PhantomJS via Homebrew. If you download the package from the PhantomJS 34 | website instead, please make sure the executable can be found via the 35 | \code{PATH} variable. 36 | 37 | On Windows, the directory specified by the environment variable 38 | \code{APPDATA} is used to store \file{phantomjs.exe}. On OS X, the directory 39 | \file{~/Library/Application Support} is used. On other platforms (such as 40 | Linux), the directory \file{~/bin} is used. If these directories are not 41 | writable, the directory \file{PhantomJS} under the installation directory of 42 | the \pkg{asciicast} package will be tried. If this directory still fails, you 43 | will have to install PhantomJS by yourself. 44 | } 45 | -------------------------------------------------------------------------------- /man/load_frames.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/frames.R 3 | \name{load_frames} 4 | \alias{load_frames} 5 | \title{Extract / create complete screen frames from an ascii cast} 6 | \usage{ 7 | load_frames(cast, height = NA, width = NA) 8 | } 9 | \arguments{ 10 | \item{cast}{aciicast object} 11 | 12 | \item{height}{Number of rows. (\code{NA} for default.)} 13 | 14 | \item{width}{Number of columns. (\code{NA} for default.)} 15 | } 16 | \value{ 17 | \code{asciicast_frames} object. 18 | } 19 | \description{ 20 | Extract / create complete screen frames from an ascii cast 21 | } 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /man/merge_casts.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/merge.R 3 | \name{merge_casts} 4 | \alias{merge_casts} 5 | \alias{clear_screen} 6 | \alias{pause} 7 | \title{Merge multiple ASCII casts into one} 8 | \usage{ 9 | merge_casts(...) 10 | 11 | clear_screen() 12 | 13 | pause(secs) 14 | } 15 | \arguments{ 16 | \item{...}{Ascii casts to merge or merge commands. Merge commands 17 | provide a way to insert pause, clear the screen, etc., between casts.} 18 | 19 | \item{secs}{Number of seconds to wait.} 20 | } 21 | \value{ 22 | An \code{asciicast} object. 23 | } 24 | \description{ 25 | The new cast will inherit its options (screen size, etc.) from the 26 | first cast in the argument list. The options of the rest of the casts 27 | are ignored. 28 | } 29 | \details{ 30 | \code{pause()} inserts a pause of the specified seconds between the casts. 31 | 32 | \code{clear_screen()} clears the screen between two casts. 33 | } 34 | \examples{ 35 | \dontshow{if (interactive()) withAutoprint(\{ # examplesIf} 36 | # merge two casts, with a pause, and clear screen between them 37 | cast1 <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 38 | cast2 <- read_cast(system.file("examples", "dplyr.cast", package = "asciicast")) 39 | cast <- merge_casts(cast1, pause(3), clear_screen(), cast2) 40 | play(cast) 41 | \dontshow{\}) # examplesIf} 42 | } 43 | -------------------------------------------------------------------------------- /man/play.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/svg.R 3 | \name{play} 4 | \alias{play} 5 | \title{Play asciinema cast as an SVG image in the default browser} 6 | \usage{ 7 | play(cast, ...) 8 | } 9 | \arguments{ 10 | \item{cast}{\code{asciicast} object} 11 | 12 | \item{...}{Additional arguments are passed to \code{\link[=write_svg]{write_svg()}}.} 13 | } 14 | \value{ 15 | The path of the temporary SVG file, invisibly. 16 | } 17 | \description{ 18 | Uses \code{\link[=write_svg]{write_svg()}} to create an SVG image for a cast, in a temporary 19 | file, and then previews a minimal HTML file with the SVG image, 20 | in the default browser. 21 | } 22 | \examples{ 23 | \dontshow{if (interactive()) withAutoprint(\{ # examplesIf} 24 | cast <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 25 | play(cast) 26 | \dontshow{\}) # examplesIf} 27 | } 28 | \seealso{ 29 | Other SVG functions: 30 | \code{\link{default_theme}()}, 31 | \code{\link{write_svg}()} 32 | } 33 | \concept{SVG functions} 34 | -------------------------------------------------------------------------------- /man/read_cast.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/read.R 3 | \name{read_cast} 4 | \alias{read_cast} 5 | \title{Import an asciicast from an asciicast JSON file} 6 | \usage{ 7 | read_cast(json) 8 | } 9 | \arguments{ 10 | \item{json}{Path to JSON asciicast file, version 2: 11 | \url{https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v2.md}. 12 | If a numeric id, then it is taken as a public \url{https://asciinema.org} 13 | recording id, that is downloaded. It can also be a URL of private 14 | \url{https://asciinema.org} link.} 15 | } 16 | \value{ 17 | \code{asciicast} object. 18 | } 19 | \description{ 20 | Import an asciicast from an asciicast JSON file 21 | } 22 | \examples{ 23 | \dontshow{if (interactive()) withAutoprint(\{ # examplesIf} 24 | c1 <- read_cast("https://asciinema.org/a/uHQwIVpiZvu0Ioio8KYx6Uwlj.cast?dl=1") 25 | play(c1) 26 | 27 | c2 <- read_cast(258660) 28 | play(c2) 29 | \dontshow{\}) # examplesIf} 30 | \dontshow{if (interactive()) withAutoprint(\{ # examplesIf} 31 | c3 <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 32 | play(c3) 33 | \dontshow{\}) # examplesIf} 34 | } 35 | \seealso{ 36 | Other asciicast functions: 37 | \code{\link{asciicast-package}}, 38 | \code{\link{asciicast_start_process}()}, 39 | \code{\link{record}()}, 40 | \code{\link{write_json}()} 41 | } 42 | \concept{asciicast functions} 43 | -------------------------------------------------------------------------------- /man/record_output.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/record-output.R 3 | \name{record_output} 4 | \alias{record_output} 5 | \title{Record output of an R script and return it as a character vector} 6 | \usage{ 7 | record_output( 8 | script, 9 | echo = FALSE, 10 | prompt = echo, 11 | stdout = TRUE, 12 | stderr = TRUE, 13 | ... 14 | ) 15 | } 16 | \arguments{ 17 | \item{script}{The code to record, passed to \code{\link[=record]{record()}}.} 18 | 19 | \item{echo}{Whether to include the input in the return value.} 20 | 21 | \item{prompt}{Whether to include the R prompt in the return value.} 22 | 23 | \item{stdout}{Whether to include the standard output in the return 24 | value.} 25 | 26 | \item{stderr}{Whether to include the standard error in the return 27 | value.} 28 | 29 | \item{...}{Addiitonal arguments are passed to \code{\link[=record]{record()}}. (You cannot 30 | use \code{typing_speed} and \code{echo}, though, because these are used 31 | internally by \code{record_output()}.} 32 | } 33 | \value{ 34 | Character vector of output (plus input if \code{echo}, plus 35 | prompt if \code{prompt}), as it would appear on a terminal. 36 | 37 | See \code{\link[=record]{record()}} for additional options. 38 | } 39 | \description{ 40 | This function uses \code{\link[=record]{record()}} internally, but instead of creating 41 | an ascii cast, it just returns the output of the code in a character 42 | vector. 43 | } 44 | -------------------------------------------------------------------------------- /man/write_gif.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gif.R 3 | \name{write_gif} 4 | \alias{write_gif} 5 | \title{Export ascii screencast to animated GIF file} 6 | \usage{ 7 | write_gif( 8 | cast, 9 | path, 10 | show = NULL, 11 | cols = NULL, 12 | rows = NULL, 13 | theme = NULL, 14 | scale = 2, 15 | speed = 1, 16 | max_colors = 256, 17 | loop = 0, 18 | end_wait = 10, 19 | optimize = TRUE 20 | ) 21 | } 22 | \arguments{ 23 | \item{cast}{\code{asciicast} object.} 24 | 25 | \item{path}{Path to GIF file to create.} 26 | 27 | \item{show}{Whether to show the GIF on the screen, in the viewer pane 28 | in RStudio, or using the image viewer in the magick package. 29 | By default it only show the image in RStudio.} 30 | 31 | \item{cols}{If not \code{NULL}, \emph{clip} terminal width to this number of 32 | columns.} 33 | 34 | \item{rows}{If not \code{NULL}, \emph{clip} terminal height to this number of 35 | rows.} 36 | 37 | \item{theme}{Theme. Currently supported themes: asciinema, tango, 38 | solarized-dark, solarized-light, monokai. Defaults to the theme 39 | specified in the cast, or asciiname if not specified.} 40 | 41 | \item{scale}{Image scale / pixel density.} 42 | 43 | \item{speed}{Playback speed. Higher number means faster.} 44 | 45 | \item{max_colors}{Maximum number of colors in the GIF. This is 46 | currently per frame.} 47 | 48 | \item{loop}{How many times to loop the animation. Zero means infinite 49 | loop.} 50 | 51 | \item{end_wait}{Number of seconds to wait at the end, before looping.} 52 | 53 | \item{optimize}{Whether to try to create smaller GIF files. This might 54 | be slow for casts with many frames.} 55 | } 56 | \value{ 57 | \code{path}, invisibly. 58 | } 59 | \description{ 60 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} 61 | } 62 | -------------------------------------------------------------------------------- /man/write_html.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/html.R 3 | \name{write_html} 4 | \alias{write_html} 5 | \title{Create a HTML snapshot of an asciicast} 6 | \usage{ 7 | write_html( 8 | cast, 9 | path, 10 | at = "end", 11 | omit_last_line = NULL, 12 | prefix = "", 13 | theme = NULL, 14 | details = FALSE, 15 | summary = "See output" 16 | ) 17 | } 18 | \arguments{ 19 | \item{cast}{\code{asciicast} object.} 20 | 21 | \item{path}{Path to the HTML file to create.} 22 | 23 | \item{at}{When to take the snapshot, defaults to the end of the cast 24 | (\code{"end"}). Can also be a number, in seconds.} 25 | 26 | \item{omit_last_line}{Whether to omit the last line of the cast. This 27 | often just the prompt, and sometimes it is not worth showing.} 28 | 29 | \item{prefix}{Prefix to add to the beginning to every line. E.g. 30 | \verb{#> } is usually added to knitr output.} 31 | 32 | \item{theme}{A theme name to use, or a a named list to override the 33 | default theme (see \code{\link[=default_theme]{default_theme()}}).} 34 | 35 | \item{details}{Whether to put the output in a \verb{
} tag.} 36 | 37 | \item{summary}{Summary of the \verb{
} tag, ignored if \code{details} is 38 | FALSE.} 39 | } 40 | \description{ 41 | Create a HTML snapshot of an asciicast 42 | } 43 | -------------------------------------------------------------------------------- /man/write_json.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/write-json.R 3 | \name{write_json} 4 | \alias{write_json} 5 | \title{Write an ascii cast to file} 6 | \usage{ 7 | write_json(cast, path) 8 | } 9 | \arguments{ 10 | \item{cast}{\code{asciicast} object.} 11 | 12 | \item{path}{Path to write to.} 13 | } 14 | \description{ 15 | The file uses the asciinema file format, version 2: 16 | \url{https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v2.md}. 17 | } 18 | \examples{ 19 | \dontshow{if (!asciicast:::is_rcmd_check()) withAutoprint(\{ # examplesIf} 20 | script <- system.file("examples", "hello.R", package = "asciicast") 21 | cast <- record(script) 22 | json <- tempfile(fileext = ".json") 23 | write_json(cast, json) 24 | \dontshow{ 25 | unlink(json, recursive = TRUE) 26 | } 27 | \dontshow{\}) # examplesIf} 28 | } 29 | \seealso{ 30 | Other asciicast functions: 31 | \code{\link{asciicast-package}}, 32 | \code{\link{asciicast_start_process}()}, 33 | \code{\link{read_cast}()}, 34 | \code{\link{record}()} 35 | } 36 | \concept{asciicast functions} 37 | -------------------------------------------------------------------------------- /man/write_svg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/svg.R 3 | \name{write_svg} 4 | \alias{write_svg} 5 | \title{Create animated SVG from an asciicast} 6 | \usage{ 7 | write_svg( 8 | cast, 9 | path, 10 | window = NULL, 11 | start_at = NULL, 12 | end_at = NULL, 13 | at = NULL, 14 | cursor = NULL, 15 | rows = NULL, 16 | cols = NULL, 17 | padding = NULL, 18 | padding_x = NULL, 19 | padding_y = NULL, 20 | omit_last_line = NULL, 21 | theme = NULL, 22 | show = NULL 23 | ) 24 | } 25 | \arguments{ 26 | \item{cast}{\code{asciicast} object.} 27 | 28 | \item{path}{Path to the SVG file to create.} 29 | 30 | \item{window}{Render with window decorations.} 31 | 32 | \item{start_at}{Lower range of timeline to render in seconds.} 33 | 34 | \item{end_at}{Upper range of timeline to render in seconds.} 35 | 36 | \item{at}{Timestamp of single frame to render, in seconds. Alternatively 37 | it can be \code{"end"}, to take a snapshot at the end of the cast, after 38 | all output is done.} 39 | 40 | \item{cursor}{Enable cursor rendering.} 41 | 42 | \item{rows}{Height in lines.} 43 | 44 | \item{cols}{Width in columns.} 45 | 46 | \item{padding}{Distance between text and image bounds.} 47 | 48 | \item{padding_x}{Distance between text and image bounds on x axis.} 49 | 50 | \item{padding_y}{Distance between text and image bounds on y axis.} 51 | 52 | \item{omit_last_line}{Whether to omit the last line of the cast. This 53 | often just the prompt, and sometimes it is not worth showing.} 54 | 55 | \item{theme}{A named list to override the default theme 56 | (see \code{\link[=default_theme]{default_theme()}}).} 57 | 58 | \item{show}{Whether to show the SVG file on the screen, in the viewer 59 | pane in RStudio, or in the web browser.} 60 | } 61 | \description{ 62 | Create animated SVG from an asciicast 63 | } 64 | \examples{ 65 | \dontshow{if (!asciicast:::is_rcmd_check()) withAutoprint(\{ # examplesIf} 66 | cast <- read_cast(system.file("examples", "hello.cast", package = "asciicast")) 67 | svg_file <- tempfile(fileext = ".svg") 68 | write_svg(cast, svg_file) 69 | \dontshow{ 70 | unlink(svg_file, recursive = TRUE) 71 | } 72 | \dontshow{\}) # examplesIf} 73 | } 74 | \seealso{ 75 | Other SVG functions: 76 | \code{\link{default_theme}()}, 77 | \code{\link{play}()} 78 | } 79 | \concept{SVG functions} 80 | -------------------------------------------------------------------------------- /src/Makevars.in: -------------------------------------------------------------------------------- 1 | 2 | tools: @REM@ asciicastclient$(SHLIB_EXT) 3 | 4 | rem: r.o common.o 5 | $(MAIN_LINK) -o $@ r.o common.o $(LIBR) $(LIBS) 6 | 7 | asciicastclient$(SHLIB_EXT): client.o common.o 8 | $(SHLIB_LINK) -o asciicastclient$(SHLIB_EXT) \ 9 | client.o common.o $(PKG_LIBS) \ 10 | $(SHLIB_LIBADD) $(LIBR) 11 | 12 | clean: 13 | @rm -f Makevars rem r.o client.o asciicastclient$(SHLIB_EXT) 14 | -------------------------------------------------------------------------------- /src/Makevars.win: -------------------------------------------------------------------------------- 1 | 2 | rem.exe: rwin.o 3 | $(CC) -O3 -o $@ rwin.o -I. $(LIBR) $(LIBS) 4 | 5 | clean: 6 | @rm -f rem.exe rwin.o 7 | -------------------------------------------------------------------------------- /src/asciicast.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef ASCIICAST_H 3 | #define ASCIICAST_H 4 | 5 | #include 6 | 7 | int rem_clock_gettime(int clk_id, struct timespec *t); 8 | double get_time(void); 9 | const char *escape_len(const char *str, size_t len); 10 | const char *escape(const char *str); 11 | 12 | void rem_show_message(const char *message); 13 | void rem_clean_up(SA_TYPE saveact, int status, int run_last); 14 | void rem_suicide(const char *message); 15 | void rem_busy(int which); 16 | void rem_write_console_ex(const char *buf, int buflen, int which); 17 | void rem_write_console(const char *buf, int buflen); 18 | int rem_read_console(const char *prompt, 19 | unsigned char *buf, 20 | int buflen, 21 | int hist); 22 | 23 | // TODO: time stamp 24 | const char *cast_header = 25 | "{" 26 | "\"version\":2," 27 | "\"command\":\"R -q\"," 28 | "\"env\":{\"TERM\":\"xterm-256color\",\"SHELL\":\"/bin/zsh\"}," 29 | "\"height\":24," 30 | "\"rows\":24," 31 | "\"width\":80," 32 | "\"cols\":80" 33 | "}\n"; 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/client.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #define R_INTERFACE_PTRS 1 9 | #include 10 | #include 11 | 12 | #include 13 | #include "asciicast.h" 14 | 15 | processx_socket_t sock = -1; 16 | FILE *sock_file = NULL; 17 | char *output_buffer = NULL; 18 | 19 | void R_init_asciicastclient(DllInfo *dll) { 20 | fprintf(stderr, "Starting up asciicast client\n"); 21 | 22 | const char *name = getenv("R_ASCIICAST_SOCKET"); 23 | if (!name) { 24 | Rf_error("Restart R and set the `R_ASCIICAST_SOCKET` env var"); 25 | } 26 | unsetenv("R_ASCIICSAT_SOCKET"); 27 | 28 | int ret = processx_socket_connect(name, &sock); 29 | if (ret == -1) { 30 | fprintf( // __NO_COVERAGE__ 31 | stderr, // __NO_COVERAGE__ 32 | "Failed to connect to socket at '%s': %s\n", // __NO_COVERAGE__ 33 | name, // __NO_COVERAGE__ 34 | processx_socket_error_message() // __NO_COVERAGE__ 35 | ); // __NO_COVERAGE__ 36 | exit(6); // __NO_COVERAGE__ 37 | } 38 | 39 | sock_file = fdopen(sock, "r+"); 40 | if (sock_file == NULL) { 41 | fprintf( // __NO_COVERAGE__ 42 | stderr, // __NO_COVERAGE__ 43 | "Cannot open socket at '%s' as file: %s\n", // __NO_COVERAGE__ 44 | name, // __NO_COVERAGE__ 45 | processx_socket_error_message() // __NO_COVERAGE__ 46 | ); // __NO_COVERAGE__ 47 | exit(7); // __NO_COVERAGE__ 48 | } 49 | setbuf(sock_file, NULL); 50 | 51 | size_t header_len = strlen(cast_header); 52 | size_t written = processx_socket_write(&sock, (void*) cast_header, header_len); 53 | if (written != header_len) { 54 | fprintf( // __NO_COVERAGE__ 55 | stderr, // __NO_COVERAGE__ 56 | "Failed to write header to server socket: %s\n", // __NO_COVERAGE__ 57 | processx_socket_error_message() // __NO_COVERAGE__ 58 | ); // __NO_COVERAGE__ 59 | exit(8); // __NO_COVERAGE__ 60 | } 61 | 62 | int interactive = 0; 63 | const char *ev_interactive = getenv("R_ASCIICAST_INTERACTIVE"); 64 | if (!ev_interactive || !strcmp(ev_interactive, "true")) { 65 | interactive = 1; 66 | } 67 | 68 | R_Interactive = interactive; 69 | R_Outputfile = NULL; 70 | R_Consolefile = NULL; 71 | // ptr_R_ShowMessage = rem_show_message; 72 | ptr_R_Busy = rem_busy; 73 | ptr_R_WriteConsole = NULL; 74 | ptr_R_WriteConsoleEx = rem_write_console_ex; 75 | ptr_R_ReadConsole = rem_read_console; 76 | ptr_R_Suicide = rem_suicide; 77 | ptr_R_CleanUp = rem_clean_up; 78 | } 79 | -------------------------------------------------------------------------------- /src/install.libs.R: -------------------------------------------------------------------------------- 1 | progs <- if (WINDOWS) { 2 | c("rem.exe", "asciicastclient.dll") 3 | } else { 4 | c("rem", "asciicastclient.so") 5 | } 6 | 7 | dest <- file.path(R_PACKAGE_DIR, paste0("bin", R_ARCH)) 8 | dir.create(dest, recursive = TRUE, showWarnings = FALSE) 9 | suppressWarnings(file.copy(progs, dest, overwrite = TRUE)) 10 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/tests.html 7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files 8 | 9 | library(testthat) 10 | library(asciicast) 11 | 12 | if (Sys.getenv("NOT_CRAN") == "true") { 13 | test_check("asciicast", reporter = "summary") 14 | if (.Platform$OS.type != "windows" && asciicast:::has_embedded()) { 15 | Sys.setenv(R_ASCIICAST_EMBEDDED = "false") 16 | test_check("asciicast", reporter = "summary") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/ascii/html/html-truecolor.html: -------------------------------------------------------------------------------- 1 |
 2 | > withr::with_options(list(cli.num_colors = cli::truecolor), cli::ansi_palette_show("dichro",                                                                                                           
 3 | +     colors = cli::truecolor))                                                                                                                                                                         
 4 |                                                      bright variants                                                                                                                                    
 5 | blck red  grn  yllw blue mgnt cyan whte  blck red  grn  yllw blue mgnt cyan whte                                                                                                                        
 6 |                                                                                                                                                                                                         
 7 | #### #### #### #### #### #### #### ####  #### #### #### #### #### #### #### ####                                                                                                                        
 8 | #### #### #### #### #### #### #### ####  #### #### #### #### #### #### #### ####                                                                                                                        
 9 | #### #### #### #### #### #### #### ####  #### #### #### #### #### #### #### ####                                                                                                                        
10 | #### #### #### #### #### #### #### ####  #### #### #### #### #### #### #### ####                                                                                                                        
11 | 
12 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/asciicast.md: -------------------------------------------------------------------------------- 1 | # record 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" "> " 7 | [3] "print(\"Hello world!\")\r\n" "type: input" 8 | [5] "print(\"Hello world!\")\r\n" "busy: 1" 9 | [7] "type: stdout" "[1]" 10 | [9] "type: stdout" " \"Hello world!\"" 11 | [11] "type: stdout" "\r\n" 12 | [13] "busy: 0" "type: read" 13 | [15] "type: wait" "" 14 | 15 | # env vars in header 16 | 17 | Code 18 | cast$output$data 19 | Output 20 | [1] "Sys.getenv('FOO')\r\n" 21 | [2] "busy: 1" 22 | [3] "type: stdout" 23 | [4] "[1]" 24 | [5] "type: stdout" 25 | [6] " \"bar\"" 26 | [7] "type: stdout" 27 | [8] "\r\n" 28 | [9] "busy: 0" 29 | [10] "type: read" 30 | [11] "Sys.getenv('SHELL', NA_character_)\r\n" 31 | [12] "busy: 1" 32 | [13] "type: stdout" 33 | [14] "[1]" 34 | [15] "type: stdout" 35 | [16] " NA" 36 | [17] "type: stdout" 37 | [18] "\r\n" 38 | [19] "busy: 0" 39 | [20] "type: read" 40 | [21] "type: wait" 41 | [22] "" 42 | 43 | # startup option 44 | 45 | Code 46 | cast$output$data 47 | Output 48 | [1] "foo\r\n" "busy: 1" "type: stdout" "[1]" "type: stdout" 49 | [6] " 112" "type: stdout" "\r\n" "busy: 0" "type: read" 50 | [11] "type: wait" "" 51 | 52 | # startup in header 53 | 54 | Code 55 | cast$output$data 56 | Output 57 | [1] "foo\r\n" "busy: 1" "type: stdout" "[1]" "type: stdout" 58 | [6] " 112" "type: stdout" "\r\n" "busy: 0" "type: read" 59 | [11] "type: wait" "" 60 | 61 | # print.asciicast 62 | 63 | Code 64 | cast 65 | Output 66 | 67 | 68 | "version": 2, 69 | "command": "R -q", 70 | "timestamp": 1656941462, 71 | "env": { 72 | "TERM": "xterm-256color", 73 | "SHELL": "/bin/zsh" 74 | }, 75 | "height": 24, 76 | "rows": 24, 77 | "width": 80, 78 | "cols": 80 79 | 80 | 81 | # A tibble: 35 x 3 82 | time type data 83 | 84 | 1 0.01 rlib "type: prompt" 85 | 2 0.02 o "> " 86 | 3 0.03 i "# comment\r\n" 87 | 4 0.04 rlib "type: input" 88 | 5 0.05 o "#" 89 | 6 0.06 o " " 90 | 7 0.07 o "c" 91 | 8 0.08 o "o" 92 | 9 0.09 o "m" 93 | 10 0.1 o "m" 94 | # i 25 more rows 95 | 96 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/darwin-aarch64/embed.md: -------------------------------------------------------------------------------- 1 | # R crashes 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" 7 | [2] "> " 8 | [3] "callr:::crash()\r\n" 9 | [4] "type: input" 10 | [5] "callr:::crash()\r\n" 11 | [6] "busy: 1" 12 | [7] "type: stderr" 13 | [8] "\r\n *** caught segfault ***\r\n" 14 | [9] "type: stderr" 15 | [10] "address 0x50, cause 'invalid permissions'\r\n" 16 | [11] "type: stderr" 17 | [12] "\r\nTraceback:\r\n" 18 | [13] "type: stderr" 19 | [14] " 1: " 20 | [15] "type: stderr" 21 | [16] "get(\"attach\")(structure(list(), class = \"UserDefinedDatabase\"))" 22 | [17] "type: stderr" 23 | [18] "\r\n" 24 | [19] "type: stderr" 25 | [20] " 2: " 26 | [21] "type: stderr" 27 | [22] "callr:::crash()" 28 | [23] "type: stderr" 29 | [24] "\r\n" 30 | [25] "type: stderr" 31 | [26] "An irrecoverable exception occurred. R is aborting now ...\r\n" 32 | [27] "type: wait" 33 | [28] "" 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/darwin-x86_64/embed.md: -------------------------------------------------------------------------------- 1 | # R crashes 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" 7 | [2] "> " 8 | [3] "callr:::crash()\r\n" 9 | [4] "type: input" 10 | [5] "callr:::crash()\r\n" 11 | [6] "busy: 1" 12 | [7] "type: stderr" 13 | [8] "\r\n *** caught segfault ***\r\n" 14 | [9] "type: stderr" 15 | [10] "address 0x50, cause 'memory not mapped'\r\n" 16 | [11] "type: stderr" 17 | [12] "\r\nTraceback:\r\n" 18 | [13] "type: stderr" 19 | [14] " 1: " 20 | [15] "type: stderr" 21 | [16] "get(\"attach\")(structure(list(), class = \"UserDefinedDatabase\"))" 22 | [17] "type: stderr" 23 | [18] "\r\n" 24 | [19] "type: stderr" 25 | [20] " 2: " 26 | [21] "type: stderr" 27 | [22] "callr:::crash()" 28 | [23] "type: stderr" 29 | [24] "\r\n" 30 | [25] "type: stderr" 31 | [26] "An irrecoverable exception occurred. R is aborting now ...\r\n" 32 | [27] "type: wait" 33 | [28] "" 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/embed.md: -------------------------------------------------------------------------------- 1 | # record 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" "> " 7 | [3] "print(\"Hello world!\")\r\n" "type: input" 8 | [5] "print(\"Hello world!\")\r\n" "busy: 1" 9 | [7] "type: stdout" "[1]" 10 | [9] "type: stdout" " \"Hello world!\"" 11 | [11] "type: stdout" "\r\n" 12 | [13] "busy: 0" "type: read" 13 | [15] "type: wait" "" 14 | 15 | # errors 16 | 17 | Code 18 | cast1$output$data 19 | Output 20 | [1] "type: prompt" 21 | [2] "> " 22 | [3] "foo12313\r\n" 23 | [4] "type: input" 24 | [5] "foo12313\r\n" 25 | [6] "busy: 1" 26 | [7] "type: stderr" 27 | [8] "Error: object 'foo12313' not found\r\n" 28 | [9] "busy: 0" 29 | [10] "type: read" 30 | [11] "type: prompt" 31 | [12] "> " 32 | [13] "barsdsdfsdf\r\n" 33 | [14] "type: input" 34 | [15] "barsdsdfsdf\r\n" 35 | [16] "busy: 1" 36 | [17] "type: stderr" 37 | [18] "Error: object 'barsdsdfsdf' not found\r\n" 38 | [19] "busy: 0" 39 | [20] "type: read" 40 | [21] "type: wait" 41 | [22] "" 42 | 43 | # R quits 44 | 45 | Code 46 | cast$output$data 47 | Output 48 | [1] "type: prompt" "> " "quit('no')\r\n" "type: input" 49 | [5] "quit('no')\r\n" "busy: 1" "type: wait" "" 50 | 51 | # incomplete expression 52 | 53 | Code 54 | record(textConnection("1 + (\n")) 55 | Condition 56 | Error: 57 | ! Incomplete asciicast expression 58 | 59 | # echo = FALSE 60 | 61 | Code 62 | cast$output$data 63 | Output 64 | [1] "print(\"Hello world!\")\r\n" "busy: 1" 65 | [3] "type: stdout" "[1]" 66 | [5] "type: stdout" " \"Hello world!\"" 67 | [7] "type: stdout" "\r\n" 68 | [9] "busy: 0" "type: read" 69 | [11] "type: wait" "" 70 | 71 | # subprocess fails 72 | 73 | Code 74 | asciicast_start_process() 75 | Condition 76 | Error: 77 | ! R subprocess did not connect back 78 | 79 | # startup crashes 80 | 81 | Code 82 | asciicast_start_process(startup = quote(callr:::crash()), interactive = FALSE) 83 | Condition 84 | Error: 85 | ! asciicast process exited while running `startup` 86 | 87 | # cannot send input, buffer is full 88 | 89 | Code 90 | record(textConnection(strrep("1 + ", 1e+05))) 91 | Condition 92 | Error: 93 | ! Cannot send input, buffer is full, line too long? 94 | 95 | # find_rem error 96 | 97 | Code 98 | find_rem() 99 | Condition 100 | Error: 101 | ! Cannot find embedded R executable rem 102 | 103 | # forced pause 104 | 105 | Code 106 | cmds 107 | Output 108 | [1] "type: prompt" "type: input" "type: stdout" "type: stdout" "type: stdout" 109 | [6] "type: read" "type: prompt" "type: wait" "type: input" "type: stdout" 110 | [11] "type: stdout" "type: stdout" "type: read" "type: wait" 111 | 112 | # edge case with no wait 113 | 114 | Code 115 | cmds 116 | Output 117 | [1] "type: prompt" "type: input" "type: stdout" "type: stdout" "type: stdout" 118 | [6] "type: read" 119 | 120 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/frames.md: -------------------------------------------------------------------------------- 1 | # load_frames 2 | 3 | Code 4 | sapply(frames$frames[[14]][[2]]$screen$lines, function(x) x[[1]][[1]]) 5 | Output 6 | [1] "> letters " 7 | [2] " [1] \"a\" \"b\" \"c\" \"d\" \"e\" \"f\" \"g\" \"h\" \"i\" \"j\" \"k\" \"l\" \"m\" \"n\" \"o\" \"p\" \"q\" \"r\" \"s\"" 8 | [3] "[20] \"t\" \"u\" \"v\" \"w\" \"x\" \"y\" \"z\" " 9 | [4] "> message('hi there') " 10 | [5] "hi there " 11 | [6] " " 12 | 13 | --- 14 | 15 | Code 16 | load_frames(cast) 17 | Condition 18 | Error: 19 | ! Internal error, cannot find 'load-cast.js'. 20 | 21 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/gif.md: -------------------------------------------------------------------------------- 1 | # write_gif 2 | 3 | Code 4 | write_gif(cast, gif, show = FALSE, rows = "auto") 5 | Condition 6 | Warning: 7 | `write_gif()` was deprecated in asciicast 3.0.0. 8 | i Please use `write_svg()` instead. 9 | i Convert the SVG to GIF. 10 | Message 11 | i Finding phantom.js 12 | v Finding phantom.js ... done 13 | 14 | i Creating 39 snapshots 15 | i Creating snapshot 1 16 | i Creating snapshot 2 17 | i Creating snapshot 3 18 | i Creating snapshot 4 19 | i Creating snapshot 5 20 | i Creating snapshot 6 21 | i Creating snapshot 7 22 | i Creating snapshot 8 23 | i Creating snapshot 9 24 | i Creating snapshot 10 25 | i Creating snapshot 11 26 | i Creating snapshot 12 27 | i Creating snapshot 13 28 | i Creating snapshot 14 29 | i Creating snapshot 15 30 | i Creating snapshot 16 31 | i Creating snapshot 17 32 | i Creating snapshot 18 33 | i Creating snapshot 19 34 | i Creating snapshot 20 35 | i Creating snapshot 21 36 | i Creating snapshot 22 37 | i Creating snapshot 23 38 | i Creating snapshot 24 39 | i Creating snapshot 25 40 | i Creating snapshot 26 41 | i Creating snapshot 27 42 | i Creating snapshot 28 43 | i Creating snapshot 29 44 | i Creating snapshot 30 45 | i Creating snapshot 31 46 | i Creating snapshot 32 47 | i Creating snapshot 33 48 | i Creating snapshot 34 49 | i Creating snapshot 35 50 | i Creating snapshot 36 51 | i Creating snapshot 37 52 | i Creating snapshot 38 53 | i Creating snapshot 39 54 | v Creating 39 snapshots ... done 55 | 56 | i Optimizing GIF frames 57 | v Optimizing GIF frames ... done 58 | 59 | i Optimizing GIF colors 60 | v Optimizing GIF colors ... done 61 | 62 | i Writing GIF output 63 | v Writing GIF output ... done 64 | 65 | 66 | # write_gif errors 67 | 68 | Code 69 | suppressMessages(write_gif()) 70 | Condition 71 | Warning: 72 | `write_gif()` was deprecated in asciicast 3.0.0. 73 | i Please use `write_svg()` instead. 74 | i Convert the SVG to GIF. 75 | Error: 76 | ! No phantom.js, exiting. 77 | 78 | --- 79 | 80 | Code 81 | write_gif(cast, gif) 82 | Condition 83 | Warning: 84 | `write_gif()` was deprecated in asciicast 3.0.0. 85 | i Please use `write_svg()` instead. 86 | i Convert the SVG to GIF. 87 | Message 88 | i Finding phantom.js 89 | v Finding phantom.js ... done 90 | 91 | i Creating 3 snapshots 92 | Condition 93 | Error: 94 | ! phantom.js failed, see `$stderr` for standard error 95 | 96 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html.md: -------------------------------------------------------------------------------- 1 | # unknown theme 2 | 3 | Code 4 | write_html(cast, tempfile(), theme = "file1868d5a82f56e") 5 | Condition 6 | Error: 7 | ! Unknown theme: file1868d5a82f56e 8 | 9 | # create_markup_{fg,bg} 10 | 11 | Code 12 | create_markup_fg("4", theme = theme) 13 | Output 14 | [1] "color: #71BEF2;" 15 | Code 16 | create_markup_fg("12", theme = theme) 17 | Output 18 | [1] "color: #73BEF3;" 19 | Code 20 | create_markup_fg("#010203", theme = theme) 21 | Output 22 | [1] "color:#010203;" 23 | 24 | --- 25 | 26 | Code 27 | create_markup_bg("4", theme = theme) 28 | Output 29 | [1] "background-color: #71BEF2;" 30 | Code 31 | create_markup_bg("12", theme = theme) 32 | Output 33 | [1] "background-color: #73BEF3;" 34 | Code 35 | create_markup_bg("#010203", theme = thene) 36 | Output 37 | [1] "color:#010203;" 38 | 39 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html/html-256-colors.html: -------------------------------------------------------------------------------- 1 |
2 | pre orange post                                                                 
3 |                                                                                 
4 | 
5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html/html-8-colors.html: -------------------------------------------------------------------------------- 1 |
2 | pre yellow  yellow post                                                         
3 |                                                                                 
4 | 
5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html/html-hyperlink.html: -------------------------------------------------------------------------------- 1 |
2 | pre text post                                                                   
3 |                                                                                 
4 | 
5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html/html-no-markup.html: -------------------------------------------------------------------------------- 1 |
2 | pre nothing post                                                                
3 |                                                                                 
4 | 
5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html/html-prefix.html: -------------------------------------------------------------------------------- 1 |
2 | #> foobar                                                                          
3 | #>                                                                                 
4 | 
5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/html/html-styles.html: -------------------------------------------------------------------------------- 1 |
2 | pre bold italic underline post                                                  
3 |                                                                                 
4 | 
5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr.md: -------------------------------------------------------------------------------- 1 | # crash 2 | 3 | Code 4 | suppressMessages(rmarkdown::render(tpath, quiet = TRUE)) 5 | Condition 6 | Error: 7 | ! asciicast subprocess crashed 8 | 9 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/1-cpp11-mean.svg: -------------------------------------------------------------------------------- 1 | >mean_cpp(1:10000*1.0)[1]5000.5 -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/1-letters.svg: -------------------------------------------------------------------------------- 1 | >letters[1]"a""b""c""d""e""f""g""h""i""j""k""l""m""n""o""p""q""r""s"[20]"t""u""v""w""x""y""z" -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/3-echo.svg: -------------------------------------------------------------------------------- 1 | >1:10[1]12345678910 -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/5-process.svg: -------------------------------------------------------------------------------- 1 | >cli::cli_text("processed!")processed! -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/test-1.md: -------------------------------------------------------------------------------- 1 | 2 | Simple SVG image at end 3 | 4 | ![](test-1_files/figure-gfm//1-letters.svg) 5 | 6 | `eval = FALSE` works. Need to `echo = TRUE` explicitly, the default is 7 | no echo. 8 | 9 | ``` r 10 | stop("do not run me") 11 | ``` 12 | 13 | Config in header is OK. 14 | 15 | ![](test-1_files/figure-gfm//3-header.svg) 16 | 17 | `eval = TRUE` + `echo = TRUE` 18 | 19 | ``` r 20 | 1:10 21 | ``` 22 | 23 | ![](test-1_files/figure-gfm//3-echo.svg) 24 | 25 | `include = FALSE` 26 | 27 | `fig.process` option. 28 | 29 | ![](test-1_files/figure-gfm//5-process.svg) 30 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/test-4.md: -------------------------------------------------------------------------------- 1 | 2 | Test cpp11 integration. 3 | 4 | ``` cpp 5 | double mean_cpp(doubles x) { 6 | int n = x.size(); 7 | double total = 0; 8 | for (double value : x) { 9 | total += value; 10 | } 11 | return total / n; 12 | } 13 | ``` 14 | 15 | ![](test-4_files/figure-gfm//1-cpp11-mean.svg) 16 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/test-5.md: -------------------------------------------------------------------------------- 1 | 2 | Simple SVG image at end 3 | 4 | ![](test-5_files/figure-gfm//1-letters.svg) 5 | 6 | `eval = FALSE` works. Need to `echo = TRUE` explicitly, the default is 7 | no echo. 8 | 9 | ``` r 10 | stop("do not run me") 11 | ``` 12 | 13 | Config in header is OK. 14 | 15 | ![](test-5_files/figure-gfm//3-header.svg) 16 | 17 | `eval = TRUE` + `echo = TRUE` 18 | 19 | ``` r 20 | 1:10 21 | ``` 22 | 23 | ![](test-5_files/figure-gfm//3-echo.svg) 24 | 25 | `include = FALSE` 26 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr/test-6.md: -------------------------------------------------------------------------------- 1 | 2 | Simple SVG image at end 3 | 4 | ![](test-6_files/figure-gfm//1-letters.svg) 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/linux-aarch64/embed.md: -------------------------------------------------------------------------------- 1 | # R crashes 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" 7 | [2] "> " 8 | [3] "callr:::crash()\r\n" 9 | [4] "type: input" 10 | [5] "callr:::crash()\r\n" 11 | [6] "busy: 1" 12 | [7] "type: stderr" 13 | [8] "\r\n *** caught segfault ***\r\n" 14 | [9] "type: stderr" 15 | [10] "address 0x50, cause 'memory not mapped'\r\n" 16 | [11] "type: stderr" 17 | [12] "\r\nTraceback:\r\n" 18 | [13] "type: stderr" 19 | [14] " 1: " 20 | [15] "type: stderr" 21 | [16] "get(\"attach\")(structure(list(), class = \"UserDefinedDatabase\"))" 22 | [17] "type: stderr" 23 | [18] "\r\n" 24 | [19] "type: stderr" 25 | [20] " 2: " 26 | [21] "type: stderr" 27 | [22] "callr:::crash()" 28 | [23] "type: stderr" 29 | [24] "\r\n" 30 | [25] "type: stderr" 31 | [26] "An irrecoverable exception occurred. R is aborting now ...\r\n" 32 | [27] "type: wait" 33 | [28] "" 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/linux-x86_64/embed.md: -------------------------------------------------------------------------------- 1 | # R crashes 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" 7 | [2] "> " 8 | [3] "callr:::crash()\r\n" 9 | [4] "type: input" 10 | [5] "callr:::crash()\r\n" 11 | [6] "busy: 1" 12 | [7] "type: stderr" 13 | [8] "\r\n *** caught segfault ***\r\n" 14 | [9] "type: stderr" 15 | [10] "address 0x50, cause 'memory not mapped'\r\n" 16 | [11] "type: stderr" 17 | [12] "\r\nTraceback:\r\n" 18 | [13] "type: stderr" 19 | [14] " 1: " 20 | [15] "type: stderr" 21 | [16] "get(\"attach\")(structure(list(), class = \"UserDefinedDatabase\"))" 22 | [17] "type: stderr" 23 | [18] "\r\n" 24 | [19] "type: stderr" 25 | [20] " 2: " 26 | [21] "type: stderr" 27 | [22] "callr:::crash()" 28 | [23] "type: stderr" 29 | [24] "\r\n" 30 | [25] "type: stderr" 31 | [26] "An irrecoverable exception occurred. R is aborting now ...\r\n" 32 | [27] "type: wait" 33 | [28] "" 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/merge.md: -------------------------------------------------------------------------------- 1 | # merge error 2 | 3 | Code 4 | merge_casts(pause(5), clear_screen()) 5 | Condition 6 | Error: 7 | ! You need to include at least one cast in `merge_casts()`. 8 | 9 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/read.md: -------------------------------------------------------------------------------- 1 | # errors 2 | 3 | Code 4 | read_cast(v1) 5 | Condition 6 | Error: 7 | ! Parse error in fixtures/v1.json:1. Only version 2 asciicast files are supported 8 | Caused by error: 9 | ! parse error: premature EOF 10 | { 11 | (right here) ------^ 12 | 13 | --- 14 | 15 | Code 16 | read_cast(badver) 17 | Condition 18 | Error: 19 | ! Parse error in fixtures/badver.json:1. Only version 2 asciicast files are supported 20 | 21 | --- 22 | 23 | Code 24 | read_cast(bad) 25 | Condition 26 | Error: 27 | ! Parse error in fixtures/bad.json:5. 28 | 29 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/record-output.md: -------------------------------------------------------------------------------- 1 | # record_output 2 | 3 | Code 4 | out1 5 | Output 6 | [1] " [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25" 7 | [2] "[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50" 8 | [3] "hi there!" 9 | 10 | --- 11 | 12 | Code 13 | out2 14 | Output 15 | [1] "hi there!" 16 | 17 | --- 18 | 19 | Code 20 | out3 21 | Output 22 | [1] " [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25" 23 | [2] "[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50" 24 | 25 | --- 26 | 27 | Code 28 | out4 29 | Output 30 | [1] "> 1:50" 31 | [2] " [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25" 32 | [3] "[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50" 33 | [4] "> message('hi there!')" 34 | [5] "hi there!" 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/svg.md: -------------------------------------------------------------------------------- 1 | # write_svg errors 2 | 3 | Code 4 | write_svg(cast, svg, theme = "foobarxx") 5 | Condition 6 | Error: 7 | ! Unknown theme: foobarxx 8 | 9 | --- 10 | 11 | Code 12 | check_svg_support() 13 | Condition 14 | Error: 15 | ! Writing SVG files needs a more recent Node library. 16 | i See the documentation of the V8 package: . 17 | 18 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/svg/hello.svg: -------------------------------------------------------------------------------- 1 | >print("Helloworld!")[1]"Helloworld!" -------------------------------------------------------------------------------- /tests/testthat/_snaps/svg/hello1.svg: -------------------------------------------------------------------------------- 1 | >print("Helloworld!")[1]"Helloworld!">[1] -------------------------------------------------------------------------------- /tests/testthat/_snaps/svg/hello2.svg: -------------------------------------------------------------------------------- 1 | >print("Helloworld!")[1]"Helloworld!" -------------------------------------------------------------------------------- /tests/testthat/_snaps/test.md: -------------------------------------------------------------------------------- 1 | # expect_snapshot_r_process 2 | 3 | Code 4 | r_process() 5 | Output 6 | > cat("'4.2.2'") 7 | '4.2.2' 8 | 9 | --- 10 | 11 | Code 12 | r_process() 13 | Output 14 | > 1 + "" 15 | Error in 1 + "" : non-numeric argument to binary operator 16 | 17 | --- 18 | 19 | Code 20 | r_process() 21 | Output 22 | > cat("\033[31m\033[1mboldred\033[22m\033[39m") 23 | boldred 24 | 25 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/unix/rstudio/out.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/utf8/html/html-truecolor.html: -------------------------------------------------------------------------------- 1 |
 2 | > withr::with_options(list(cli.num_colors = cli::truecolor), cli::ansi_palette_show("dichro",                                                                                                           
 3 | +     colors = cli::truecolor))                                                                                                                                                                         
 4 |                                                      bright variants                                                                                                                                    
 5 | blck red  grn  yllw blue mgnt cyan whte  blck red  grn  yllw blue mgnt cyan whte                                                                                                                        
 6 |                                                                                                                                                                                                         
 7 | ████ ████ ████ ████ ████ ████ ████ ████  ████ ████ ████ ████ ████ ████ ████ ████                                                                                                                        
 8 | ████ ████ ████ ████ ████ ████ ████ ████  ████ ████ ████ ████ ████ ████ ████ ████                                                                                                                        
 9 | ████ ████ ████ ████ ████ ████ ████ ████  ████ ████ ████ ████ ████ ████ ████ ████                                                                                                                        
10 | ████ ████ ████ ████ ████ ████ ████ ████  ████ ████ ████ ████ ████ ████ ████ ████                                                                                                                        
11 | 
12 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/utils.md: -------------------------------------------------------------------------------- 1 | # with_cli_process 2 | 3 | Code 4 | with_cli_process("I am practicing, mom!", 1:10) 5 | Message 6 | i I am practicing, mom! 7 | v I am practicing, mom! ... done 8 | 9 | Output 10 | [1] 1 2 3 4 5 6 7 8 9 10 11 | 12 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/windows-x86_64/embed.md: -------------------------------------------------------------------------------- 1 | # R crashes 2 | 3 | Code 4 | cast$output$data 5 | Output 6 | [1] "type: prompt" "> " "callr:::crash()\r\n" 7 | [4] "type: input" "callr:::crash()\r\n" "busy: 1" 8 | [7] "type: wait" "" 9 | 10 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/bad.json: -------------------------------------------------------------------------------- 1 | {"version":2,"command":"R -q","timestamp":1655733730,"env":{"TERM":"xterm-256color","SHELL":"/bin/zsh"},"height":24,"rows":24,"width":80,"cols":80} 2 | [0, "rlib", "type: prompt"] 3 | [0, "o", "> "] 4 | [0, "i", "print(\"Hello world!\")\r\n"] 5 | ["0", "rlib", "type: input"] 6 | [0.0677882328047417, "o", "p"] 7 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/badver.json: -------------------------------------------------------------------------------- 1 | {"version":3,"command":"R -q","timestamp":1655733730,"env":{"TERM":"xterm-256color","SHELL":"/bin/zsh"},"height":24,"rows":24,"width":80,"cols":80} 2 | [0, "rlib", "type: prompt"] 3 | [0, "o", "> "] 4 | [0, "i", "print(\"Hello world!\")\r\n"] 5 | ["0", "rlib", "type: input"] 6 | [0.0677882328047417, "o", "p"] 7 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-1.R: -------------------------------------------------------------------------------- 1 | ## ---- include = FALSE--------------------------------------------------------- 2 | asciicast::init_knitr_engine( 3 | options = list( 4 | asciicast_end_wait = 3 5 | ) 6 | ) 7 | 8 | ## letters 9 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-1.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: true 4 | --- 5 | 6 | ```{r, include = FALSE} 7 | knitr::opts_chunk$set( 8 | cache = FALSE 9 | ) 10 | asciicast::init_knitr_engine() 11 | ``` 12 | 13 | Simple SVG image at end 14 | 15 | ```{asciicast 1-letters} 16 | letters 17 | ``` 18 | 19 | `eval = FALSE` works. Need to `echo = TRUE` explicitly, the default is no 20 | echo. 21 | 22 | ```{asciicast 2-no-eval, eval = FALSE, echo = TRUE} 23 | stop("do not run me") 24 | ``` 25 | 26 | Config in header is OK. 27 | 28 | ```{asciicast 3-header} 29 | #' cols: 40 30 | options(width = 40) 31 | 1:100 32 | ``` 33 | 34 | `eval = TRUE` + `echo = TRUE` 35 | 36 | ```{asciicast 3-echo, echo = TRUE} 37 | 1:10 38 | ``` 39 | 40 | `include = FALSE` 41 | 42 | ```{asciicast 4-no-include, include = FALSE} 43 | "Now you don't see me" 44 | ``` 45 | 46 | `fig.process` option. 47 | 48 | ```{asciicast 5-process, fig.process = function(x) { file.create(paste0(x, "-processed")); x }} 49 | cli::cli_text("processed!") 50 | ``` 51 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-2.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: true 4 | --- 5 | 6 | ```{r, include = FALSE} 7 | knitr::opts_chunk$set( 8 | cache = FALSE 9 | ) 10 | asciicast::init_knitr_engine() 11 | ``` 12 | 13 | ```{r} 14 | .GlobalEnv$.knitr_asciicast_process$kill() 15 | ``` 16 | 17 | ```{asciicast 1-letters} 18 | letters 19 | ``` 20 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-3.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: true 4 | --- 5 | 6 | ```{r, include = FALSE} 7 | knitr::opts_chunk$set( 8 | cache = TRUE 9 | ) 10 | asciicast::init_knitr_engine() 11 | ``` 12 | 13 | SVG is cached. 14 | 15 | ```{asciicast 1-cached} 16 | cli::cli_alert_success("I am cached at {Sys.time()}") 17 | ``` 18 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-4.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: true 4 | --- 5 | 6 | ```{r, include = FALSE} 7 | knitr::opts_chunk$set( 8 | cache = FALSE 9 | ) 10 | asciicast::init_knitr_engine() 11 | ``` 12 | 13 | Test cpp11 integration. 14 | 15 | ```{asciicastcpp11, echo = TRUE} 16 | double mean_cpp(doubles x) { 17 | int n = x.size(); 18 | double total = 0; 19 | for (double value : x) { 20 | total += value; 21 | } 22 | return total / n; 23 | } 24 | ``` 25 | 26 | ```{asciicast 1-cpp11-mean} 27 | mean_cpp(1:10000*1.0) 28 | ``` 29 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-5.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: true 4 | --- 5 | 6 | ```{r, include = FALSE} 7 | knitr::opts_chunk$set( 8 | cache = FALSE, 9 | asciicast_theme = list(), 10 | asciicast_include_style = FALSE 11 | ) 12 | asciicast::init_knitr_engine() 13 | ``` 14 | 15 | Simple SVG image at end 16 | 17 | ```{asciicast 1-letters} 18 | letters 19 | ``` 20 | 21 | `eval = FALSE` works. Need to `echo = TRUE` explicitly, the default is no 22 | echo. 23 | 24 | ```{asciicast 2-no-eval, eval = FALSE, echo = TRUE} 25 | stop("do not run me") 26 | ``` 27 | 28 | Config in header is OK. 29 | 30 | ```{asciicast 3-header} 31 | #' cols: 40 32 | 1:100 33 | ``` 34 | 35 | `eval = TRUE` + `echo = TRUE` 36 | 37 | ```{asciicast 3-echo, echo = TRUE} 38 | 1:10 39 | ``` 40 | 41 | `include = FALSE` 42 | 43 | ```{asciicast 4-no-include, include = FALSE} 44 | "Now you don't see me" 45 | ``` 46 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/test-6.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | always_allow_html: true 4 | --- 5 | 6 | ```{r, include = FALSE} 7 | knitr::opts_chunk$set( 8 | cache = TRUE 9 | ) 10 | asciicast::init_knitr_engine() 11 | ``` 12 | 13 | Simple SVG image at end 14 | 15 | ```{asciicast 1-letters} 16 | cat(cli::col_red("this is red")) 17 | ``` 18 | -------------------------------------------------------------------------------- /tests/testthat/fixtures/v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "width": 80, 4 | "height": 24, 5 | "duration": 1.515658, 6 | "command": "/bin/zsh", 7 | "title": "", 8 | "env": { 9 | "TERM": "xterm-256color", 10 | "SHELL": "/bin/zsh" 11 | }, 12 | "stdout": [ 13 | [ 14 | 0.248848, 15 | "\u001b[1;31mHello \u001b[32mWorld!\u001b[0m\n" 16 | ], 17 | [ 18 | 1.001376, 19 | "I am \rThis is on the next line." 20 | ] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat/helper-mock.R: -------------------------------------------------------------------------------- 1 | fake <- local({ 2 | fake_through_tree <- function(tree, what, how) { 3 | for (d in tree) { 4 | for (parent in d) { 5 | parent_env <- parent[["parent_env"]] 6 | func_dict <- parent[["funcs"]] 7 | for (func_name in ls(func_dict, all.names = TRUE)) { 8 | func <- func_dict[[func_name]] 9 | func_env <- new.env(parent = environment(func)) 10 | 11 | what <- override_seperators(what, func_env) 12 | where_name <- override_seperators(func_name, parent_env) 13 | 14 | if (!is.function(how)) { 15 | assign(what, function(...) how, func_env) 16 | } else { 17 | assign(what, how, func_env) 18 | } 19 | 20 | environment(func) <- func_env 21 | locked <- exists(where_name, parent_env, inherits = FALSE) && 22 | bindingIsLocked(where_name, parent_env) 23 | if (locked) { 24 | baseenv()$unlockBinding(where_name, parent_env) 25 | } 26 | assign(where_name, func, parent_env) 27 | if (locked) { 28 | lockBinding(where_name, parent_env) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | override_seperators <- function(name, env) { 36 | mangled_name <- NULL 37 | for (sep in c("::", "$")) { 38 | if (grepl(sep, name, fixed = TRUE)) { 39 | elements <- strsplit(name, sep, fixed = TRUE) 40 | mangled_name <- paste( 41 | elements[[1L]][1L], 42 | elements[[1L]][2L], 43 | sep = "XXX" 44 | ) 45 | 46 | stub_list <- c(mangled_name) 47 | if ("stub_list" %in% names(attributes(get(sep, env)))) { 48 | stub_list <- c(stub_list, attributes(get(sep, env))[["stub_list"]]) 49 | } 50 | 51 | create_new_name <- create_create_new_name_function( 52 | stub_list, 53 | env, 54 | sep 55 | ) 56 | assign(sep, create_new_name, env) 57 | } 58 | } 59 | mangled_name %||% name 60 | } 61 | 62 | backtick <- function(x) { 63 | encodeString(x, quote = "`", na.encode = FALSE) 64 | } 65 | 66 | create_create_new_name_function <- function(stub_list, env, sep) { 67 | force(stub_list) 68 | force(env) 69 | force(sep) 70 | 71 | create_new_name <- function(pkg, func) { 72 | pkg_name <- deparse(substitute(pkg)) 73 | func_name <- deparse(substitute(func)) 74 | for (stub in stub_list) { 75 | if (paste(pkg_name, func_name, sep = "XXX") == stub) { 76 | return(eval(parse(text = backtick(stub)), env)) 77 | } 78 | } 79 | 80 | # used to avoid recursively calling the replacement function 81 | eval_env <- new.env(parent = parent.frame()) 82 | assign(sep, eval(parse(text = paste0("`", sep, "`"))), eval_env) 83 | 84 | code <- paste(pkg_name, backtick(func_name), sep = sep) 85 | return(eval(parse(text = code), eval_env)) 86 | } 87 | attributes(create_new_name) <- list(stub_list = stub_list) 88 | create_new_name 89 | } 90 | 91 | build_function_tree <- function(test_env, where, where_name) { 92 | func_dict <- new.env() 93 | func_dict[[where_name]] <- where 94 | tree <- list( 95 | list( 96 | list(parent_env = test_env, funcs = func_dict) 97 | ) 98 | ) 99 | 100 | tree 101 | } 102 | 103 | fake <- function(where, what, how) { 104 | where_name <- deparse(substitute(where)) 105 | stopifnot(is.character(what), length(what) == 1) 106 | test_env <- parent.frame() 107 | tree <- build_function_tree(test_env, where, where_name) 108 | fake_through_tree(tree, what, how) 109 | } 110 | }) 111 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | os_type <- function() { 2 | tolower(.Platform$OS.type) 3 | } 4 | 5 | os_name <- function() { 6 | tolower(Sys.info()[["sysname"]]) 7 | } 8 | 9 | os_arch <- function() { 10 | tolower(paste0(os_name(), "-", R.Version()$arch)) 11 | } 12 | -------------------------------------------------------------------------------- /tests/testthat/setup.R: -------------------------------------------------------------------------------- 1 | withr::defer( 2 | { 3 | try_silently(close(attr(.knitr_asciicast_process, "sock"))) 4 | try_silently(.knitr_asciicast_process$kill()) 5 | }, 6 | teardown_env() 7 | ) 8 | -------------------------------------------------------------------------------- /tests/testthat/test-asciicast.R: -------------------------------------------------------------------------------- 1 | test_that("record", { 2 | withr::local_options(asciicast_typing_speed = 0) 3 | hello <- system.file(package = "asciicast", "examples", "hello.R") 4 | cast <- record(hello, interactive = FALSE) 5 | expect_snapshot(cast$output$data) 6 | }) 7 | 8 | test_that("env vars in header", { 9 | code <- paste( 10 | sep = "\n", 11 | "#' record_env: c(FOO = 'bar', SHELL = NA_character_)", 12 | "#' typing_speed: 0", 13 | "Sys.getenv('FOO')", 14 | "Sys.getenv('SHELL', NA_character_)", 15 | "" 16 | ) 17 | 18 | cast <- record(textConnection(code), echo = FALSE) 19 | expect_snapshot(cast$output$data) 20 | }) 21 | 22 | test_that("startup option", { 23 | cast <- record( 24 | quote(foo), 25 | echo = FALSE, 26 | startup = quote(foo <- 112) 27 | ) 28 | expect_snapshot(cast$output$data) 29 | }) 30 | 31 | test_that("startup in header", { 32 | code <- paste( 33 | sep = "\n", 34 | "#' startup: foo <- 112", 35 | "foo", 36 | "" 37 | ) 38 | cast <- record(code, echo = FALSE) 39 | expect_snapshot(cast$output$data) 40 | }) 41 | 42 | test_that("automatic row numbers", { 43 | cast <- record(textConnection("# comment\n1+1\n"), rows = "auto") 44 | expect_equal(cast$config$rows, 3L) 45 | 46 | cast <- record(textConnection("cat('foobar')"), rows = "auto") 47 | expect_equal(cast$config$rows, 2L) 48 | }) 49 | 50 | test_that("print.asciicast", { 51 | cast <- record(textConnection("# comment\n1+1\n")) 52 | cast$output$time <- seq_along(cast$output$time) / 100 53 | cast$config$timestamp <- 1656941462 54 | # We filter out the # i Use `print(n = ...)` to see more rows" line 55 | expect_snapshot(cast, transform = function(x) { 56 | grep("to see more rows", x, value = TRUE, invert = TRUE) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/testthat/test-embed.R: -------------------------------------------------------------------------------- 1 | test_that("record", { 2 | withr::local_options(asciicast_typing_speed = 0) 3 | hello <- system.file(package = "asciicast", "examples", "hello.R") 4 | cast <- record(hello, interactive = FALSE) 5 | expect_snapshot(cast$output$data) 6 | }) 7 | 8 | test_that("errors", { 9 | withr::local_options(asciicast_typing_speed = 0) 10 | cast1 <- record(textConnection("foo12313\nbarsdsdfsdf\n")) 11 | expect_snapshot(cast1$output$data) 12 | }) 13 | 14 | test_that("R quits", { 15 | withr::local_options(asciicast_typing_speed = 0) 16 | cast <- record(textConnection("quit('no')\n")) 17 | expect_snapshot(cast$output$data) 18 | }) 19 | 20 | test_that("R crashes", { 21 | # TODO: needs callr release to fix callr:::crash() 22 | if (getRversion() >= "4.5.0") skip("needs newer callr") 23 | # TODO: why does this fail? 24 | if (!is_embedded()) { 25 | skip("Fails on non-embedded R") 26 | } 27 | withr::local_options(asciicast_typing_speed = 0) 28 | cast <- record(textConnection("callr:::crash()\n"), interactive = FALSE) 29 | expect_snapshot(cast$output$data, variant = os_arch()) 30 | }) 31 | 32 | test_that("incomplete expression", { 33 | withr::local_options(asciicast_typing_speed = 0) 34 | expect_snapshot(error = TRUE, record(textConnection("1 + (\n"))) 35 | }) 36 | 37 | test_that("incomplete expression allowed", { 38 | withr::local_options(asciicast_typing_speed = 0) 39 | expect_silent( 40 | record(textConnection("1 + (\n"), incomplete_error = FALSE) 41 | ) 42 | }) 43 | 44 | test_that("timeout", { 45 | withr::local_options(asciicast_typing_speed = 0) 46 | # does not work well a snapshot, output changes 47 | expect_error( 48 | record(textConnection("Sys.sleep(1)\n"), timeout = 0.1) 49 | ) 50 | }) 51 | 52 | test_that("echo = FALSE", { 53 | withr::local_options(asciicast_typing_speed = 0) 54 | hello <- system.file(package = "asciicast", "examples", "hello.R") 55 | cast <- record(hello, interactive = FALSE, echo = FALSE) 56 | expect_snapshot(cast$output$data) 57 | }) 58 | 59 | test_that("speed", { 60 | hello <- system.file(package = "asciicast", "examples", "hello.R") 61 | cast1 <- record(hello) 62 | cast2 <- record(hello, speed = 10) 63 | expect_true( 64 | utils::tail(cast2$output$time, 1) < utils::tail(cast1$output$time, 1) / 2 65 | ) 66 | }) 67 | 68 | test_that("subprocess fails", { 69 | fake(asciicast_start_process, "processx::poll", list("timeout")) 70 | expect_snapshot(error = TRUE, asciicast_start_process()) 71 | }) 72 | 73 | test_that("startup crashes", { 74 | # TODO: why does this fail? 75 | if (!is_embedded()) { 76 | skip("Fails on non-embedded R") 77 | } 78 | expect_snapshot( 79 | error = TRUE, 80 | asciicast_start_process( 81 | startup = quote(callr:::crash()), 82 | interactive = FALSE 83 | ) 84 | ) 85 | }) 86 | 87 | test_that("cannot send input, buffer is full", { 88 | skip_on_os("windows") # TODO 89 | expect_snapshot( 90 | error = TRUE, 91 | record(textConnection(strrep("1 + ", 100000))) 92 | ) 93 | }) 94 | 95 | test_that("shift", { 96 | expect_equal(shift(character()), character()) 97 | expect_equal(shift("a"), "") 98 | expect_equal(shift(letters), c(letters[-1], "")) 99 | }) 100 | 101 | test_that("add_empty_wait", { 102 | withr::local_options(asciicast_typing_speed = 0) 103 | cast1 <- record(textConnection("1+1\n\n2+2\n"), empty_wait = 0) 104 | cast2 <- record(textConnection("1+1\n\n2+2\n"), empty_wait = 5) 105 | expect_true( 106 | utils::tail(cast1$output$time, 1) < utils::tail(cast2$output$time, 1) - 3 107 | ) 108 | }) 109 | 110 | test_that("adjust_typing_speed", { 111 | withr::local_options(asciicast_typing_speed = 0) 112 | cast1 <- record(textConnection("1+1\n\n2+2\n"), empty_wait = 0) 113 | data <- cast1$output 114 | 115 | data1 <- adjust_typing_speed(data, 0.05) 116 | data2 <- adjust_typing_speed(data, 0.5) 117 | expect_true( 118 | utils::tail(data1$time, 1) < utils::tail(data2$time, 1) - 1 119 | ) 120 | 121 | empty <- data[integer(), ] 122 | expect_equal(adjust_typing_speed(empty, 0.05), empty) 123 | }) 124 | 125 | test_that("find_rem error", { 126 | fake(find_rem, "get_embedded", "") 127 | expect_snapshot( 128 | error = TRUE, 129 | find_rem(), 130 | transform = function(x) sub("rem.exe", "rem", fixed = TRUE, x) 131 | ) 132 | }) 133 | 134 | test_that("forced pause", { 135 | cast <- record(c( 136 | "#! --", 137 | "1 + 1", 138 | "#! --", 139 | "2 + 2" 140 | )) 141 | cmds <- grep("^type:", cast$output$data, value = TRUE) 142 | expect_snapshot(cmds) 143 | }) 144 | 145 | test_that("edge case with no wait", { 146 | cast <- record( 147 | c( 148 | "#! --", 149 | "1 + 1" 150 | ), 151 | end_wait = 0 152 | ) 153 | cmds <- grep("^type:", cast$output$data, value = TRUE) 154 | expect_snapshot(cmds) 155 | }) 156 | -------------------------------------------------------------------------------- /tests/testthat/test-frames.R: -------------------------------------------------------------------------------- 1 | test_that("load_svg_term", { 2 | ct <- load_svg_term() 3 | expect_s3_class(ct, "V8") 4 | }) 5 | 6 | test_that("load_frames", { 7 | code <- paste( 8 | sep = "\n", 9 | "# comment", 10 | "letters", 11 | "message('hi there')", 12 | "" 13 | ) 14 | 15 | cast <- record(textConnection(code), typing_speed = 0, rows = "auto") 16 | cast$output$time <- seq_along(cast$output$time) / 100 17 | cast$config$timestamp <- 1656941462 18 | frames <- load_frames(cast) 19 | 20 | expect_snapshot( 21 | sapply(frames$frames[[14]][[2]]$screen$lines, function(x) x[[1]][[1]]) 22 | ) 23 | 24 | fake(load_frames, "system.file", "") 25 | expect_snapshot(error = TRUE, load_frames(cast)) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/testthat/test-gif.R: -------------------------------------------------------------------------------- 1 | test_that("write_gif", { 2 | skip_on_ci() 3 | 4 | if (!is_windows() && !is_macos() && !is_linux()) { 5 | skip("Unsupported OS") 6 | } 7 | if (is_linux() && R.Version()$arch != "x86_64") { 8 | skip("Unsupported OS") 9 | } 10 | if (is.null(suppressMessages(find_phantom()))) { 11 | install_phantomjs() 12 | } 13 | if (is.null(suppressMessages(find_phantom()))) { 14 | skip("Could not install phantom.js") 15 | } 16 | 17 | withr::local_options(cli.ansi = FALSE) 18 | 19 | code <- paste( 20 | sep = "\n", 21 | "# comment", 22 | "letters", 23 | "message('hi there')", 24 | "" 25 | ) 26 | 27 | cast <- record(textConnection(code), typing_speed = 0, rows = "auto") 28 | cast$output$time <- seq_along(cast$output$time) / 10 29 | 30 | gif <- tempfile(fileext = ".gif") 31 | on.exit(unlink(gif), add = TRUE) 32 | expect_snapshot( 33 | write_gif(cast, gif, show = FALSE, rows = "auto") 34 | ) 35 | 36 | mgif <- magick::image_read(gif) 37 | expect_true( 38 | all(magick::image_info(mgif)$format == "GIF") 39 | ) 40 | 41 | rs <- NULL 42 | fake(write_gif, "is_rstudio", TRUE) 43 | fake(write_gif, "view_image_in_rstudio", function(path) rs <<- path) 44 | suppressMessages( 45 | write_gif(cast, gif, show = TRUE) 46 | ) 47 | expect_false(is.null(rs)) 48 | 49 | rs <- NULL 50 | fake(write_gif, "is_rstudio", FALSE) 51 | fake(write_gif, "image_display", function(anim) rs <<- TRUE) 52 | suppressMessages( 53 | write_gif(cast, gif, show = TRUE) 54 | ) 55 | expect_true(rs) 56 | }) 57 | 58 | test_that("write_gif errors", { 59 | skip_on_ci() 60 | withr::local_options(cli.dynamic = FALSE, cli.ansi = FALSE) 61 | fake(write_gif, "find_phantom", NULL) 62 | expect_snapshot(error = TRUE, suppressMessages(write_gif())) 63 | 64 | fake( 65 | write_gif, 66 | "find_phantom", 67 | asNamespace("processx")$get_tool("px") 68 | ) 69 | cast <- record(textConnection("1+1\n")) 70 | gif <- tempfile(fileext = ".gif") 71 | on.exit(unlink(gif), add = TRUE) 72 | expect_snapshot( 73 | error = TRUE, 74 | write_gif(cast, gif), 75 | # this is kind of random, maybe 3, maybe 4 frames 76 | transform = function(x) sub("4", "3", x) 77 | ) 78 | }) 79 | -------------------------------------------------------------------------------- /tests/testthat/test-html.R: -------------------------------------------------------------------------------- 1 | test_that("no markup", { 2 | withr::local_options(asciicast_typing_speed = 0) 3 | cast <- record( 4 | quote({ 5 | options(cli.num_colors = 8) 6 | cat(paste("pre", "nothing", "post")) 7 | }), 8 | echo = FALSE, 9 | rows = 2 10 | ) 11 | tmp <- tempfile("ac-html-", fileext = ".html") 12 | on.exit(unlink(tmp), add = TRUE) 13 | write_html(cast, tmp) 14 | expect_snapshot_file(tmp, name = "html-no-markup.html") 15 | }) 16 | 17 | test_that("8 colors", { 18 | withr::local_options(asciicast_typing_speed = 0) 19 | cast <- record( 20 | quote({ 21 | options(cli.num_colors = 8) 22 | cat(paste( 23 | "pre", 24 | cli::col_yellow("yellow"), 25 | cli::bg_yellow(" yellow"), 26 | "post" 27 | )) 28 | }), 29 | echo = FALSE, 30 | rows = 2 31 | ) 32 | tmp <- tempfile("ac-html-", fileext = ".html") 33 | on.exit(unlink(tmp), add = TRUE) 34 | write_html(cast, tmp) 35 | expect_snapshot_file(tmp, name = "html-8-colors.html") 36 | }) 37 | 38 | test_that("styles", { 39 | withr::local_options(asciicast_typing_speed = 0) 40 | cast <- record( 41 | quote({ 42 | options(cli.num_colors = 8) 43 | cat(paste( 44 | "pre", 45 | cli::style_bold("bold"), 46 | cli::style_italic("italic"), 47 | cli::style_underline("underline"), 48 | "post" 49 | )) 50 | }), 51 | echo = FALSE, 52 | rows = 2 53 | ) 54 | tmp <- tempfile("ac-html-", fileext = ".html") 55 | on.exit(unlink(tmp), add = TRUE) 56 | write_html(cast, tmp) 57 | expect_snapshot_file(tmp, name = "html-styles.html") 58 | }) 59 | 60 | test_that("256 colors", { 61 | withr::local_options(asciicast_typing_speed = 0) 62 | cast <- record( 63 | quote({ 64 | options(cli.num_colors = 256) 65 | mycol <- cli::make_ansi_style("orange") 66 | cat(paste("pre", mycol("orange"), "post")) 67 | }), 68 | echo = FALSE, 69 | rows = 2 70 | ) 71 | tmp <- tempfile("ac-html-", fileext = ".html") 72 | on.exit(unlink(tmp), add = TRUE) 73 | write_html(cast, tmp) 74 | expect_snapshot_file(tmp, name = "html-256-colors.html") 75 | }) 76 | 77 | test_that("unknown theme", { 78 | cast <- record("ls()") 79 | expect_snapshot( 80 | error = TRUE, 81 | write_html(cast, tempfile(), theme = "file1868d5a82f56e") 82 | ) 83 | }) 84 | 85 | test_that("prefix", { 86 | withr::local_options(asciicast_typing_speed = 0) 87 | cast <- record("cat('foobar')", echo = FALSE, rows = 2) 88 | tmp <- tempfile("ac-html-", fileext = ".html") 89 | on.exit(unlink(tmp), add = TRUE) 90 | write_html(cast, tmp, prefix = "#> ") 91 | expect_snapshot_file(tmp, name = "html-prefix.html") 92 | }) 93 | 94 | test_that("true color", { 95 | mode <- if (l10n_info()[["UTF-8"]]) "utf8" else "ascii" 96 | cast <- record( 97 | quote( 98 | withr::with_options( 99 | list(cli.num_colors = cli::truecolor), 100 | cli::ansi_palette_show("dichro", colors = cli::truecolor) 101 | ) 102 | ), 103 | cols = 200, 104 | rows = "auto" 105 | ) 106 | tmp <- tempfile("ac-html-", fileext = ".html") 107 | on.exit(unlink(tmp), add = TRUE) 108 | write_html(cast, tmp) 109 | expect_snapshot_file(tmp, name = "html-truecolor.html", variant = mode) 110 | }) 111 | 112 | test_that("create_markup_{fg,bg}", { 113 | theme <- to_html_theme(interpret_theme(NULL)) 114 | expect_snapshot({ 115 | create_markup_fg("4", theme = theme) 116 | create_markup_fg("12", theme = theme) 117 | create_markup_fg("#010203", theme = theme) 118 | }) 119 | expect_snapshot({ 120 | create_markup_bg("4", theme = theme) 121 | create_markup_bg("12", theme = theme) 122 | create_markup_bg("#010203", theme = thene) 123 | }) 124 | }) 125 | 126 | test_that("hyperlink", { 127 | withr::local_options(asciicast_typing_speed = 0) 128 | cast <- record( 129 | quote({ 130 | options(cli.num_colors = 256, cli.hyperlink = TRUE) 131 | st_from_bel <- function(x) { 132 | gsub("\007", "\033\\", x, fixed = TRUE) 133 | } 134 | cat(paste( 135 | "pre", 136 | st_from_bel( 137 | cli::style_hyperlink("text", "https://example.com") 138 | ), 139 | "post" 140 | )) 141 | }), 142 | echo = FALSE, 143 | rows = 2 144 | ) 145 | tmp <- tempfile("ac-html-", fileext = ".html") 146 | on.exit(unlink(tmp), add = TRUE) 147 | write_html(cast, tmp) 148 | expect_snapshot_file(tmp, name = "html-hyperlink.html") 149 | }) 150 | -------------------------------------------------------------------------------- /tests/testthat/test-htmlwidget.R: -------------------------------------------------------------------------------- 1 | test_that("asciinema_player", { 2 | code <- paste( 3 | sep = "\n", 4 | "# comment", 5 | "letters", 6 | "message('hi there')", 7 | "" 8 | ) 9 | 10 | cast <- record(textConnection(code), typing_speed = 0, rows = "auto") 11 | cast$output$time <- seq_along(cast$output$time) / 10 12 | 13 | wdgt <- asciinema_player(cast, poster_frame = 0.5) 14 | expect_s3_class(wdgt, "asciinema_player") 15 | expect_s3_class(wdgt, "htmlwidget") 16 | 17 | wdgt <- asciinema_player(cast, poster_text = "Look at this") 18 | expect_s3_class(wdgt, "asciinema_player") 19 | expect_s3_class(wdgt, "htmlwidget") 20 | }) 21 | -------------------------------------------------------------------------------- /tests/testthat/test-install-phantomjs.R: -------------------------------------------------------------------------------- 1 | # We don't test the download currently. It does work, and will work as 2 | # long as the files are there on GH. It is also tested in write_gif, 3 | # if phantom is not installed. 4 | return() 5 | 6 | test_that("install_phantomjs", { 7 | if (!is_windows() && !is_macos() && !is_linux()) { 8 | skip("Unsupported OS") 9 | } 10 | if (is_linux() && R.Version()$arch != "x86_64") { 11 | skip("Unsupported OS") 12 | } 13 | suppressMessages(install_phantomjs( 14 | baseURL = "https://github.com/wch/webshot/releases/download/v0.3.1" 15 | )) 16 | expect_true(TRUE) 17 | expect_true(file.exists(find_phantom())) 18 | }) 19 | 20 | test_that("install_phantomjs errors", { 21 | fake(install_phantomjs, "is_windows", FALSE) 22 | fake(install_phantomjs, "is_macos", FALSE) 23 | fake(install_phantomjs, "is_linux", FALSE) 24 | expect_message( 25 | install_phantomjs(), 26 | "this platform is not supported" 27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/testthat/test-knitr.R: -------------------------------------------------------------------------------- 1 | test_that("basics", { 2 | dir.create(tmp <- tempfile()) 3 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 4 | 5 | opath <- test_path("fixtures", "test-1.Rmd") 6 | tpath <- file.path(tmp, basename(opath)) 7 | file.copy(opath, tpath) 8 | rmarkdown::render(tpath, quiet = TRUE) 9 | 10 | mdpath <- sub(".Rmd$", ".md", tpath) 11 | expect_snapshot_file(mdpath) 12 | 13 | svgs <- sort(dir( 14 | file.path(tmp, "test-1_files", "figure-gfm"), 15 | pattern = "[.]svg$", 16 | full.names = TRUE 17 | )) 18 | 19 | for (svg in svgs) { 20 | expect_snapshot_file(svg) 21 | } 22 | 23 | processed <- file.path( 24 | tmp, 25 | "test-1_files", 26 | "figure-gfm", 27 | "5-process.svg-processed" 28 | ) 29 | expect_true(file.exists(processed)) 30 | }) 31 | 32 | test_that("options are set temporarily", { 33 | withr::local_options(asciicast_end_wait = 0) 34 | 35 | dir.create(tmp <- tempfile()) 36 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 37 | 38 | opath <- test_path("fixtures", "test-1.Rmd") 39 | tpath <- file.path(tmp, basename(opath)) 40 | file.copy(opath, tpath) 41 | rmarkdown::render(tpath, quiet = TRUE) 42 | 43 | mdpath <- sub(".Rmd$", ".md", tpath) 44 | expect_snapshot_file(mdpath) 45 | 46 | expect_equal( 47 | getOption("asciicast_end_wait"), 48 | 0 49 | ) 50 | }) 51 | 52 | test_that("crash", { 53 | withr::local_options(asciicast_end_wait = 0) 54 | 55 | dir.create(tmp <- tempfile()) 56 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 57 | 58 | opath <- test_path("fixtures", "test-2.Rmd") 59 | tpath <- file.path(tmp, basename(opath)) 60 | file.copy(opath, tpath) 61 | expect_snapshot( 62 | error = TRUE, 63 | suppressMessages(rmarkdown::render(tpath, quiet = TRUE)) 64 | ) 65 | }) 66 | 67 | test_that("caching", { 68 | withr::local_options(asciicast_end_wait = 0) 69 | 70 | dir.create(tmp <- tempfile()) 71 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 72 | 73 | opath <- test_path("fixtures", "test-3.Rmd") 74 | tpath <- file.path(tmp, basename(opath)) 75 | file.copy(opath, tpath) 76 | 77 | suppressMessages(rmarkdown::render(tpath, quiet = TRUE)) 78 | svg <- file.path(tmp, "test-3_files", "figure-gfm", "1-cached.svg") 79 | expect_true(file.exists(svg)) 80 | hash <- cli::hash_file_sha256(svg) 81 | 82 | rmarkdown::render(tpath, quiet = TRUE) 83 | expect_true(file.exists(svg)) 84 | expect_equal( 85 | hash, 86 | cli::hash_file_sha256(svg) 87 | ) 88 | }) 89 | 90 | test_that("cpp11", { 91 | dir.create(tmp <- tempfile()) 92 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 93 | 94 | opath <- test_path("fixtures", "test-4.Rmd") 95 | tpath <- file.path(tmp, basename(opath)) 96 | file.copy(opath, tpath) 97 | rmarkdown::render(tpath, quiet = TRUE) 98 | 99 | mdpath <- sub(".Rmd$", ".md", tpath) 100 | expect_snapshot_file(mdpath) 101 | 102 | svgs <- sort(dir( 103 | file.path(tmp, "test-4_files", "figure-gfm"), 104 | pattern = "[.]svg$", 105 | full.names = TRUE 106 | )) 107 | 108 | for (svg in svgs) { 109 | expect_snapshot_file(svg) 110 | } 111 | }) 112 | 113 | test_that("eng_asciicast_output_type", { 114 | withr::local_options(asciicast_knitr_output = "html") 115 | expect_equal(eng_asciicast_output_type(), "html") 116 | 117 | withr::local_options(asciicast_knitr_output = NULL, asciicast_at = "all") 118 | withr::local_envvar(IN_PKGDOWN = "true") 119 | expect_equal(eng_asciicast_output_type(), "svg") 120 | 121 | withr::local_options(asciicast_at = "end") 122 | expect_equal(eng_asciicast_output_type(), "html") 123 | 124 | withr::local_options(asciicast_knitr_output = NULL, asciicast_at = NULL) 125 | withr::local_envvar(IN_PKGDOWN = NA_character_) 126 | withr::local_options(asciicast_knitr_svg = TRUE) 127 | expect_equal(eng_asciicast_output_type(), "svg") 128 | 129 | withr::local_options(asciicast_knitr_svg = FALSE) 130 | expect_equal(eng_asciicast_output_type(), "svg") 131 | }) 132 | 133 | test_that("html output", { 134 | dir.create(tmp <- tempfile()) 135 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 136 | 137 | opath <- test_path("fixtures", "test-5.Rmd") 138 | tpath <- file.path(tmp, basename(opath)) 139 | file.copy(opath, tpath) 140 | rmarkdown::render(tpath, quiet = TRUE) 141 | 142 | mdpath <- sub(".Rmd$", ".md", tpath) 143 | expect_snapshot_file(mdpath) 144 | }) 145 | 146 | test_that("html output with style", { 147 | dir.create(tmp <- tempfile()) 148 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 149 | 150 | opath <- test_path("fixtures", "test-6.Rmd") 151 | tpath <- file.path(tmp, basename(opath)) 152 | file.copy(opath, tpath) 153 | rmarkdown::render(tpath, quiet = TRUE) 154 | 155 | mdpath <- sub(".Rmd$", ".md", tpath) 156 | expect_snapshot_file(mdpath) 157 | }) 158 | -------------------------------------------------------------------------------- /tests/testthat/test-merge.R: -------------------------------------------------------------------------------- 1 | test_that("merge", { 2 | withr::local_options(asciicast_typing_speed = 0) 3 | 4 | dir.create(tmp <- tempfile()) 5 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 6 | 7 | code1 <- paste( 8 | sep = "\n", 9 | "# comment", 10 | "1:100", 11 | "" 12 | ) 13 | 14 | code2 <- paste( 15 | sep = "\n", 16 | "# comment", 17 | "letters", 18 | "" 19 | ) 20 | 21 | cast1 <- record(textConnection(code1)) 22 | cast2 <- record(textConnection(code2)) 23 | 24 | json <- file.path(tmp, "cast1.json") 25 | write_json(cast1, json) 26 | 27 | cast <- merge_casts( 28 | cast1, 29 | pause(10), 30 | clear_screen(), 31 | json, 32 | pause(10), 33 | cast2 34 | ) 35 | 36 | svg <- file.path(tmp, paste0("merge-", 1:3, ".svg")) 37 | on.exit(unlink(svg), add = TRUE) 38 | 39 | write_svg(cast, svg[1], at = 5) 40 | write_svg(cast, svg[2], at = 15) 41 | write_svg(cast, svg[3], at = "end") 42 | 43 | expect_snapshot_file(svg[1]) 44 | expect_snapshot_file(svg[2]) 45 | expect_snapshot_file(svg[3]) 46 | }) 47 | 48 | test_that("merge error", { 49 | expect_snapshot(error = TRUE, merge_casts(pause(5), clear_screen())) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/testthat/test-read.R: -------------------------------------------------------------------------------- 1 | test_that("read_cast from file", { 2 | code <- paste( 3 | sep = "\n", 4 | "# comment", 5 | "1+1", 6 | "" 7 | ) 8 | 9 | cast <- record(textConnection(code)) 10 | 11 | json <- tempfile(fileext = ".json") 12 | on.exit(unlink(json), add = TRUE) 13 | write_json(cast, json) 14 | 15 | cast2 <- read_cast(json) 16 | expect_equal(cast, cast2) 17 | }) 18 | 19 | test_that("read cast from asciinema.org & from URL", { 20 | cast <- read_cast(258660) 21 | cast2 <- read_cast( 22 | "https://asciinema.org/a/uHQwIVpiZvu0Ioio8KYx6Uwlj.cast?dl=1" 23 | ) 24 | expect_equal(cast, cast2) 25 | json <- tempfile(fileext = ".json") 26 | on.exit(unlink(json), add = TRUE) 27 | write_json(cast, json) 28 | }) 29 | 30 | test_that("errors", { 31 | v1 <- test_path("fixtures", "v1.json") 32 | expect_snapshot(error = TRUE, read_cast(v1)) 33 | 34 | badver <- test_path("fixtures", "badver.json") 35 | expect_snapshot(error = TRUE, read_cast(badver)) 36 | 37 | bad <- test_path("fixtures", "bad.json") 38 | expect_snapshot(error = TRUE, read_cast(bad)) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/testthat/test-record-output.R: -------------------------------------------------------------------------------- 1 | test_that("record_output", { 2 | code <- c( 3 | "1:50", 4 | "message('hi there!')" 5 | ) 6 | 7 | out1 <- record_output(code) 8 | expect_snapshot(out1) 9 | 10 | out2 <- record_output(code, stdout = FALSE) 11 | expect_snapshot(out2) 12 | 13 | out3 <- record_output(code, stderr = FALSE) 14 | expect_snapshot(out3) 15 | 16 | out4 <- record_output(code, echo = TRUE) 17 | expect_snapshot(out4) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/testthat/test-rstudio.R: -------------------------------------------------------------------------------- 1 | test_that("is_rstudio", { 2 | expect_false(is_rstudio()) 3 | 4 | fake(is_rstudio, "loadedNamespaces", "rstudioapi") 5 | fake(is_rstudio, "rstudioapi::isAvailable", TRUE) 6 | expect_true(is_rstudio()) 7 | }) 8 | 9 | test_that("view_image_in_rstudio", { 10 | dir.create(tmp <- tempfile()) 11 | path2 <- file.path(tmp, "out.html") 12 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 13 | 14 | path <- NULL 15 | on.exit(unlink(path), add = TRUE) 16 | 17 | fake(view_image_in_rstudio, "rstudioapi::viewer", function(x) path <<- x) 18 | 19 | view_image_in_rstudio(tempdir()) 20 | expect_false(is.null(path)) 21 | 22 | file.copy(path, path2) 23 | expect_snapshot_file( 24 | path2, 25 | transform = function(x) { 26 | sub('asciicast-preview-[^"]*"', 'asciicast-preview-xxx"', x) 27 | }, 28 | variant = os_type() 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/testthat/test-src-c.R: -------------------------------------------------------------------------------- 1 | test_that("Unicode output", { 2 | if (!l10n_info()$`UTF-8`) { 3 | skip("Needs UTF-8 system") 4 | } 5 | 6 | withr::local_options(asciicast_typing_speed = 0) 7 | 8 | code <- quote({ 9 | cat("\u0100\u2500\U1F600") 10 | }) 11 | 12 | cast <- record(code) 13 | expect_true(any(grepl("\u0100\u2500\U1F600", cast$output$data))) 14 | }) 15 | 16 | test_that("Invalid UTF-8", { 17 | if (!l10n_info()$`UTF-8`) { 18 | skip("Needs UTF-8 system") 19 | } 20 | 21 | withr::local_options(asciicast_typing_speed = 0) 22 | 23 | cast <- record(textConnection(c( 24 | "cat(rawToChar(as.raw(c(226, 148))))", 25 | 'cast("nope")' 26 | ))) 27 | 28 | expect_false(any(grepl("nope", cast$output$data))) 29 | }) 30 | 31 | test_that("Special characters go through JSON", { 32 | if (!l10n_info()$`UTF-8`) { 33 | skip("Needs UTF-8 system") 34 | } 35 | 36 | withr::local_options(asciicast_typing_speed = 0) 37 | 38 | # just backslash it seems..., plus some control characters 39 | code <- quote({ 40 | cat("foo\\bar\fbaz") 41 | }) 42 | 43 | cast <- record(textConnection(deparse(code))) 44 | expect_true(any(grepl("foo\\bar\fbaz", cast$output$data, fixed = TRUE))) 45 | }) 46 | 47 | test_that("usage", { 48 | if (!has_embedded()) { 49 | skip("Needs embedded R") 50 | } 51 | rem <- find_rem() 52 | env <- setup_env() 53 | out <- processx::run(rem, env = env, error_on_status = FALSE) 54 | expect_equal(out$status, 5) 55 | expect_match(out$stderr, "Usage:.*rem.*-i") 56 | }) 57 | -------------------------------------------------------------------------------- /tests/testthat/test-svg.R: -------------------------------------------------------------------------------- 1 | test_that("write_svg", { 2 | withr::local_options(asciicast_typing_speed = 0) 3 | 4 | dir.create(tmp <- tempfile()) 5 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 6 | 7 | hello <- system.file(package = "asciicast", "examples", "hello.R") 8 | cast <- record(hello, interactive = FALSE) 9 | 10 | svg <- file.path(tmp, "hello.svg") 11 | write_svg( 12 | cast, 13 | svg, 14 | at = "end", 15 | padding = 5, 16 | omit_last_line = TRUE, 17 | rows = "auto" 18 | ) 19 | expect_snapshot_file(svg) 20 | 21 | svg1 <- file.path(tmp, "hello1.svg") 22 | cast$output$time <- seq_along(cast$output$time) / 100 23 | write_svg( 24 | cast, 25 | svg1, 26 | start_at = min(cast$output$time), 27 | end_at = max(cast$output$time) 28 | ) 29 | expect_snapshot_file(svg1) 30 | }) 31 | 32 | test_that("themes", { 33 | withr::local_options(asciicast_typing_speed = 0) 34 | 35 | dir.create(tmp <- tempfile()) 36 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 37 | 38 | hello <- system.file(package = "asciicast", "examples", "hello.R") 39 | cast <- record(hello, interactive = FALSE) 40 | 41 | svg <- file.path(tmp, "hello2.svg") 42 | write_svg(cast, svg, theme = "solarized-light", at = "end") 43 | expect_snapshot_file(svg) 44 | }) 45 | 46 | test_that("write_svg errors", { 47 | withr::local_options(asciicast_typing_speed = 0) 48 | 49 | dir.create(tmp <- tempfile()) 50 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 51 | 52 | hello <- system.file(package = "asciicast", "examples", "hello.R") 53 | cast <- record(hello, interactive = FALSE) 54 | 55 | svg <- file.path(tmp, "foobar.svg") 56 | expect_snapshot(error = TRUE, write_svg(cast, svg, theme = "foobarxx")) 57 | 58 | fake(check_svg_support, "is_svg_supported", FALSE) 59 | expect_snapshot(error = TRUE, check_svg_support()) 60 | }) 61 | 62 | test_that("play", { 63 | path <- NULL 64 | on.exit(unlink(path), add = TRUE) 65 | 66 | hello <- system.file(package = "asciicast", "examples", "hello.R") 67 | cast <- record(hello, interactive = FALSE) 68 | 69 | fake(play, "play_svg", function(x, ...) path <<- x) 70 | play(cast, at = "end") 71 | 72 | expect_false(is.null(path)) 73 | }) 74 | 75 | test_that("remove_last_line edge case", { 76 | cast <- record(textConnection("1"), interactive = FALSE) 77 | cast$output <- cast$output[cast$output$type != "o", ] 78 | 79 | expect_equal(remove_last_line(cast), cast) 80 | }) 81 | -------------------------------------------------------------------------------- /tests/testthat/test-test.R: -------------------------------------------------------------------------------- 1 | test_that("expect_snapshot_r_process", { 2 | if (packageVersion("cli") < "3.4.1.9000") skip("Needs newer cli") 3 | testthat::local_edition(3) 4 | expect_snapshot_r_process(cat("'4.2.2'")) 5 | expect_snapshot_r_process(1 + "") 6 | expect_snapshot_r_process(cat("\033[31m\033[1mboldred\033[22m\033[39m")) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | test_that("with_cli_process", { 2 | withr::local_options(cli.ansi = FALSE) 3 | expect_snapshot( 4 | with_cli_process("I am practicing, mom!", 1:10) 5 | ) 6 | }) 7 | 8 | test_that("is_rcmd_check", { 9 | withr::local_envvar(NOT_CRAN = "true") 10 | expect_false(is_rcmd_check()) 11 | 12 | withr::local_envvar( 13 | NOT_CRAN = NA_character_, 14 | "_R_CHECK_PACKAGE_NAME_" = "asciicast" 15 | ) 16 | expect_true(is_rcmd_check()) 17 | 18 | withr::local_envvar( 19 | NOT_CRAN = NA_character_, 20 | "_R_CHECK_PACKAGE_NAME_" = NA_character_ 21 | ) 22 | expect_false(is_rcmd_check()) 23 | }) 24 | --------------------------------------------------------------------------------