├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ansi.yml │ ├── cellbuf.yml │ ├── charmtone.yml │ ├── colors.yml │ ├── conpty.yml │ ├── editor.yml │ ├── errors.yml │ ├── examples.yml │ ├── generate.yml │ ├── golden.yml │ ├── higherorder.yml │ ├── input-fuzz.yml │ ├── input.yml │ ├── json.yml │ ├── maps.yml │ ├── mosaic.yml │ ├── open.yml │ ├── ordered.yml │ ├── slice.yml │ ├── sshkey.yml │ ├── strings.yml │ ├── teatest-v2.yml │ ├── teatest.yml │ ├── term.yml │ ├── termios-goos.yml │ ├── termios.yml │ ├── vt.yml │ ├── wcwidth.yml │ ├── windows-generate.yml │ ├── windows.yml │ └── xpty.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── Taskfile.yaml ├── ansi ├── ansi.go ├── ascii.go ├── background.go ├── background_test.go ├── c0.go ├── c1.go ├── charset.go ├── clipboard.go ├── clipboard_test.go ├── color.go ├── color_test.go ├── ctrl.go ├── cursor.go ├── cwd.go ├── cwd_test.go ├── doc.go ├── finalterm.go ├── fixtures │ ├── UTF-8-demo.txt │ ├── demo.vte │ └── graphics │ │ └── JigokudaniMonkeyPark.png ├── focus.go ├── gen.go ├── go.mod ├── go.sum ├── graphics.go ├── graphics_test.go ├── hyperlink.go ├── hyperlink_test.go ├── iterm2.go ├── iterm2 │ ├── file.go │ └── file_test.go ├── iterm2_test.go ├── keypad.go ├── kitty.go ├── kitty │ ├── decoder.go │ ├── decoder_test.go │ ├── encoder.go │ ├── encoder_test.go │ ├── graphics.go │ ├── options.go │ └── options_test.go ├── method.go ├── mode.go ├── mode_test.go ├── modes.go ├── mouse.go ├── mouse_test.go ├── notification.go ├── parser.go ├── parser │ ├── const.go │ ├── seq.go │ └── transition_table.go ├── parser_apc_test.go ├── parser_csi_test.go ├── parser_dcs_test.go ├── parser_decode.go ├── parser_decode_test.go ├── parser_esc_test.go ├── parser_handler.go ├── parser_osc_test.go ├── parser_sync.go ├── parser_test.go ├── passthrough.go ├── passthrough_test.go ├── paste.go ├── reset.go ├── screen.go ├── sgr.go ├── sgr_test.go ├── sixel │ ├── .gitattributes │ ├── color.go │ ├── color_test.go │ ├── decoder.go │ ├── encoder.go │ ├── palette.go │ ├── palette_sort.go │ ├── palette_test.go │ ├── raster.go │ ├── raster_test.go │ ├── repeat.go │ ├── repeat_test.go │ ├── sixel_bench_test.go │ ├── sixel_test.go │ └── util.go ├── status.go ├── style.go ├── style_test.go ├── termcap.go ├── title.go ├── title_test.go ├── truncate.go ├── truncate_test.go ├── util.go ├── width.go ├── width_test.go ├── winop.go ├── wrap.go ├── wrap_test.go └── xterm.go ├── cellbuf ├── buffer.go ├── buffer_test.go ├── cell.go ├── errors.go ├── geom.go ├── go.mod ├── go.sum ├── hardscroll.go ├── hashmap.go ├── link.go ├── pen.go ├── screen.go ├── sequence.go ├── sequence_test.go ├── style.go ├── tabstop.go ├── tabstop_test.go ├── utils.go ├── wrap.go ├── wrap_test.go └── writer.go ├── colors ├── colors.go ├── go.mod └── go.sum ├── conpty ├── conpty_other.go ├── conpty_windows.go ├── doc.go ├── exec_windows.go ├── go.mod └── go.sum ├── editor ├── editor.go ├── editor_test.go └── go.mod ├── errors ├── go.mod ├── join.go └── join_test.go ├── examples ├── JetBrainsMono-Regular.ttf ├── cellbuf │ ├── main.go │ ├── winsize_other.go │ └── winsize_windows.go ├── faketty │ └── main.go ├── go.mod ├── go.sum ├── img2term │ └── main.go ├── layout │ └── main.go ├── mosaic │ ├── main.go │ └── pekinas.jpg ├── parserlog │ └── main.go ├── parserlog2 │ └── main.go └── pen │ └── main.go ├── exp ├── charmtone │ ├── charmtone.go │ ├── charmtone │ │ └── main.go │ ├── charmtone_test.go │ ├── go.mod │ └── go.sum ├── golden │ ├── go.mod │ ├── go.sum │ ├── golden.go │ ├── golden_test.go │ └── testdata │ │ ├── TestRequireEqualNoUpdate.golden │ │ ├── TestRequireEqualUpdate.golden │ │ └── TestRequireWithLineBreaks.golden ├── higherorder │ ├── go.mod │ ├── higherorder.go │ └── higherorder_test.go ├── maps │ ├── go.mod │ ├── go.sum │ ├── maps.go │ └── maps_test.go ├── open │ ├── cmd │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── open.go │ ├── open_unix.go │ └── open_windows.go ├── ordered │ ├── go.mod │ ├── go.sum │ ├── ordered.go │ └── ordered_test.go ├── slice │ ├── go.mod │ ├── slice.go │ └── slice_test.go ├── strings │ ├── go.mod │ ├── join.go │ └── join_test.go └── teatest │ ├── app_test.go │ ├── go.mod │ ├── go.sum │ ├── send_test.go │ ├── teatest.go │ ├── teatest_test.go │ ├── testdata │ ├── TestApp.golden │ ├── TestAppSendToOtherProgram.golden │ └── TestRequireEqualOutputUpdate.golden │ └── v2 │ ├── app_test.go │ ├── go.mod │ ├── go.sum │ ├── send_test.go │ ├── teatest.go │ ├── teatest_test.go │ └── testdata │ ├── TestApp.golden │ ├── TestAppSendToOtherProgram.golden │ └── TestRequireEqualOutputUpdate.golden ├── go.work ├── go.work.sum ├── input ├── cancelreader_other.go ├── cancelreader_windows.go ├── clipboard.go ├── color.go ├── cursor.go ├── da1.go ├── doc.go ├── driver.go ├── driver_other.go ├── driver_test.go ├── driver_windows.go ├── driver_windows_test.go ├── focus.go ├── focus_test.go ├── go.mod ├── go.sum ├── input.go ├── key.go ├── key_test.go ├── kitty.go ├── mod.go ├── mode.go ├── mouse.go ├── mouse_test.go ├── parse.go ├── parse_test.go ├── paste.go ├── table.go ├── termcap.go ├── terminfo.go └── xterm.go ├── json ├── go.mod ├── json.go └── json_test.go ├── mosaic ├── README.md ├── fixtures │ └── charm-wish.png ├── go.mod ├── go.sum ├── mosaic.go └── mosaic_test.go ├── scripts ├── builds └── dependabot ├── sshkey ├── _examples │ ├── key │ ├── key.pub │ └── main.go ├── go.mod ├── go.sum └── sshkey.go ├── term ├── go.mod ├── go.sum ├── term.go ├── term_other.go ├── term_test.go ├── term_unix.go ├── term_unix_bsd.go ├── term_unix_other.go ├── term_windows.go ├── terminal.go └── util.go ├── termios ├── bit_bsd.go ├── bit_darwin.go ├── bit_other.go ├── go.mod ├── go.sum ├── syscalls_bsd.go ├── syscalls_darwin.go ├── syscalls_linux.go ├── termios.go ├── termios_bsd.go ├── termios_linux.go ├── termios_other.go ├── termios_solaris.go └── termios_test.go ├── vt ├── buffer.go ├── buffer_test.go ├── callbacks.go ├── cc.go ├── cell.go ├── charset.go ├── csi.go ├── csi_cursor.go ├── csi_mode.go ├── csi_screen.go ├── csi_sgr.go ├── cursor.go ├── damage.go ├── dcs.go ├── esc.go ├── focus.go ├── go.mod ├── go.sum ├── handlers.go ├── key.go ├── mode.go ├── mouse.go ├── options.go ├── osc.go ├── screen.go ├── terminal.go ├── terminal_test.go ├── utf8.go ├── utils.go └── vt.go ├── wcwidth ├── go.mod ├── go.sum └── wcwidth.go ├── windows ├── doc.go ├── go.mod ├── go.sum ├── syscall_windows.go ├── types_windows.go └── zsyscall_windows.go └── xpty ├── conpty.go ├── conpty_other.go ├── conpty_windows.go ├── go.mod ├── go.sum ├── pty.go ├── pty_other.go ├── pty_unix.go └── xpty.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ttf filter=lfs diff=lfs merge=lfs -text 2 | *.png filter=lfs diff=lfs merge=lfs -text 3 | *.jpg filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /ansi @aymanbagabas 2 | /conpty @aymanbagabas 3 | /editor @caarlos0 4 | /errors @caarlos0 5 | /exp/golden @caarlos0 6 | /exp/higherorder @meowgorithm 7 | /exp/ordered @meowgorithm 8 | /exp/slice @meowgorithm 9 | /exp/strings @meowgorithm 10 | /exp/teatest @caarlos0 11 | /input @aymanbagabas 12 | /scripts @caarlos0 13 | /term @aymanbagabas 14 | /termios @caarlos0 @aymanbagabas 15 | /windows @aymanbagabas 16 | /xpty @aymanbagabas 17 | -------------------------------------------------------------------------------- /.github/workflows/ansi.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: ansi 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - ansi/** 11 | - .github/workflows/ansi.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./ansi 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./ansi/go.mod 27 | cache: true 28 | cache-dependency-path: ./ansi/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./ansi/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/cellbuf.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: cellbuf 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - cellbuf/** 11 | - .github/workflows/cellbuf.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./cellbuf 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./cellbuf/go.mod 27 | cache: true 28 | cache-dependency-path: ./cellbuf/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./cellbuf/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/charmtone.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: charmtone 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/charmtone/** 11 | - .github/workflows/charmtone.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/charmtone 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/charmtone/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/charmtone/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/charmtone/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/colors.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: colors 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - colors/** 11 | - .github/workflows/colors.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./colors 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./colors/go.mod 27 | cache: true 28 | cache-dependency-path: ./colors/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./colors/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/conpty.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: conpty 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - conpty/** 11 | - .github/workflows/conpty.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./conpty 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./conpty/go.mod 27 | cache: true 28 | cache-dependency-path: ./conpty/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./conpty/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/editor.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: editor 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - editor/** 11 | - .github/workflows/editor.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./editor 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./editor/go.mod 27 | cache: true 28 | cache-dependency-path: ./editor/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./editor/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/errors.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: errors 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - errors/** 11 | - .github/workflows/errors.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./errors 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./errors/go.mod 27 | cache: true 28 | cache-dependency-path: ./errors/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./errors/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: examples 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - examples/** 11 | - .github/workflows/examples.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./examples 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./examples/go.mod 27 | cache: true 28 | cache-dependency-path: ./examples/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./examples/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/generate.yml: -------------------------------------------------------------------------------- 1 | name: generate 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | workflow_dispatch: {} 8 | 9 | permissions: 10 | contents: write 11 | actions: write 12 | 13 | jobs: 14 | generate: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: stable 21 | cache: true 22 | - run: ./scripts/dependabot 23 | - run: ./scripts/builds 24 | - uses: stefanzweifel/git-auto-commit-action@v5 25 | with: 26 | commit_message: "ci: auto-update configuration" 27 | branch: main 28 | commit_user_name: actions-user 29 | commit_user_email: actions@github.com 30 | -------------------------------------------------------------------------------- /.github/workflows/golden.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: golden 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/golden/** 11 | - .github/workflows/golden.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/golden 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/golden/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/golden/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/golden/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/higherorder.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: higherorder 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/higherorder/** 11 | - .github/workflows/higherorder.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/higherorder 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/higherorder/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/higherorder/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/higherorder/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/input-fuzz.yml: -------------------------------------------------------------------------------- 1 | name: input-fuzz 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths: 9 | - input/** 10 | - .github/workflows/input-fuzz.yml 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | runs-on: ${{ matrix.os }} 18 | defaults: 19 | run: 20 | working-directory: ./input 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version-file: ./input/go.mod 26 | cache: true 27 | cache-dependency-path: ./input.sum 28 | - run: go test -run="^$" -fuzz=FuzzParseSequence -fuzztime=1m -v ./... 29 | -------------------------------------------------------------------------------- /.github/workflows/input.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: input 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - input/** 11 | - .github/workflows/input.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./input 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./input/go.mod 27 | cache: true 28 | cache-dependency-path: ./input/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./input/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/json.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: json 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - json/** 11 | - .github/workflows/json.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./json 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./json/go.mod 27 | cache: true 28 | cache-dependency-path: ./json/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./json/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/maps.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: maps 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/maps/** 11 | - .github/workflows/maps.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/maps 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/maps/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/maps/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/maps/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/mosaic.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: mosaic 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - mosaic/** 11 | - .github/workflows/mosaic.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./mosaic 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./mosaic/go.mod 27 | cache: true 28 | cache-dependency-path: ./mosaic/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./mosaic/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/open.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: open 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/open/** 11 | - .github/workflows/open.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/open 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/open/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/open/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/open/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/ordered.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: ordered 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/ordered/** 11 | - .github/workflows/ordered.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/ordered 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/ordered/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/ordered/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/ordered/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/slice.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: slice 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/slice/** 11 | - .github/workflows/slice.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/slice 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/slice/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/slice/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/slice/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/sshkey.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: sshkey 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - sshkey/** 11 | - .github/workflows/sshkey.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./sshkey 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./sshkey/go.mod 27 | cache: true 28 | cache-dependency-path: ./sshkey/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./sshkey/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/strings.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: strings 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/strings/** 11 | - .github/workflows/strings.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/strings 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/strings/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/strings/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/strings/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/teatest-v2.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: teatest-v2 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/teatest/v2/** 11 | - .github/workflows/teatest-v2.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/teatest/v2 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/teatest/v2/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/teatest/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/teatest/v2/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/teatest.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: teatest 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - exp/teatest/** 11 | - .github/workflows/teatest.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./exp/teatest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./exp/teatest/go.mod 27 | cache: true 28 | cache-dependency-path: ./exp/teatest/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./exp/teatest/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/term.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: term 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - term/** 11 | - .github/workflows/term.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./term 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./term/go.mod 27 | cache: true 28 | cache-dependency-path: ./term/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./term/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/termios.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: termios 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - termios/** 11 | - .github/workflows/termios.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./termios 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./termios/go.mod 27 | cache: true 28 | cache-dependency-path: ./termios/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./termios/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/vt.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: vt 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - vt/** 11 | - .github/workflows/vt.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./vt 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./vt/go.mod 27 | cache: true 28 | cache-dependency-path: ./vt/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./vt/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/wcwidth.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: wcwidth 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - wcwidth/** 11 | - .github/workflows/wcwidth.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./wcwidth 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./wcwidth/go.mod 27 | cache: true 28 | cache-dependency-path: ./wcwidth/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./wcwidth/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/windows-generate.yml: -------------------------------------------------------------------------------- 1 | name: windows-generate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - windows/** 9 | - .github/workflows/windows-generate.yml 10 | workflow_dispatch: {} 11 | 12 | permissions: 13 | contents: write 14 | actions: write 15 | 16 | jobs: 17 | generate: 18 | runs-on: windows-latest 19 | defaults: 20 | run: 21 | working-directory: ./windows 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./windows/go.mod 27 | cache: true 28 | cache-dependency-path: ./windows/go.sum 29 | - run: go generate ./... 30 | - uses: stefanzweifel/git-auto-commit-action@v5 31 | with: 32 | commit_message: "ci: generate windows syscalls" 33 | branch: main 34 | commit_user_name: actions-user 35 | commit_user_email: actions@github.com 36 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: windows 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - windows/** 11 | - .github/workflows/windows.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./windows 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./windows/go.mod 27 | cache: true 28 | cache-dependency-path: ./windows/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./windows/... 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/xpty.yml: -------------------------------------------------------------------------------- 1 | # auto-generated by scripts/builds. DO NOT EDIT. 2 | name: xpty 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | paths: 10 | - xpty/** 11 | - .github/workflows/xpty.yml 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | defaults: 20 | run: 21 | working-directory: ./xpty 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./xpty/go.mod 27 | cache: true 28 | cache-dependency-path: ./xpty/go.sum 29 | - run: go build -v ./... 30 | - run: go test -race -v ./... 31 | dependabot: 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | permissions: 35 | pull-requests: write 36 | contents: write 37 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 38 | steps: 39 | - id: metadata 40 | uses: dependabot/fetch-metadata@v2 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | - run: | 44 | gh pr review --approve "$PR_URL" 45 | gh pr merge --squash --auto "$PR_URL" 46 | env: 47 | PR_URL: ${{github.event.pull_request.html_url}} 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | lint: 50 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 51 | with: 52 | directory: ./xpty/... 53 | 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | !go.work 18 | 19 | testdata 20 | *.png 21 | 22 | # MacOS invisible file 23 | .DS_Store 24 | 25 | # Allow graphics used for bench test 26 | !ansi/fixtures/graphics/*.png 27 | !mosaic/fixtures/*.png 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | linters: 5 | enable: 6 | - bodyclose 7 | - exhaustive 8 | - goconst 9 | - godot 10 | - godox 11 | - gomoddirectives 12 | - goprintffuncname 13 | - gosec 14 | - misspell 15 | - nakedret 16 | - nestif 17 | - nilerr 18 | - noctx 19 | - nolintlint 20 | - prealloc 21 | - revive 22 | - rowserrcheck 23 | - sqlclosecheck 24 | - tparallel 25 | - unconvert 26 | - unparam 27 | - whitespace 28 | - wrapcheck 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - common-false-positives 33 | issues: 34 | max-issues-per-linter: 0 35 | max-same-issues: 0 36 | formatters: 37 | enable: 38 | - gofumpt 39 | - goimports 40 | exclusions: 41 | generated: lax 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Charmbracelet, Inc. 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 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | vars: 6 | PACKAGES: [ 7 | ansi, 8 | cellbuf, 9 | colors, 10 | conpty, 11 | editor, 12 | errors, 13 | examples, 14 | exp/golden, 15 | exp/higherorder, 16 | exp/maps, 17 | exp/open, 18 | exp/ordered, 19 | exp/slice, 20 | exp/strings, 21 | exp/teatest, 22 | exp/teatest/v2, 23 | input, 24 | json, 25 | sshkey, 26 | term, 27 | termios, 28 | vt, 29 | wcwidth, 30 | windows, 31 | xpty 32 | ] 33 | 34 | tasks: 35 | fmt: 36 | desc: Run gofumpt for all packages 37 | cmds: 38 | - for: { var: PACKAGES } 39 | cmd: cd {{.ITEM}} && gofmt -s -w . 40 | 41 | modernize: 42 | desc: Run gofumpt for all packages 43 | cmds: 44 | - for: { var: PACKAGES } 45 | cmd: cd {{.ITEM}} && modernize -fix ./... 46 | 47 | lint:all: 48 | desc: Run all linters for all packages 49 | cmds: 50 | - task: lint 51 | - task: lint:soft 52 | 53 | lint: 54 | desc: Run base linters for all packages 55 | cmds: 56 | - for: { var: PACKAGES } 57 | cmd: cd {{.ITEM}} && golangci-lint run 58 | 59 | lint:soft: 60 | desc: Run soft linters for all packages 61 | cmds: 62 | - for: { var: PACKAGES } 63 | cmd: cd {{.ITEM}} && golangci-lint run --config=../.golangci-soft.yml 64 | 65 | test: 66 | desc: Run tests for all packages 67 | cmds: 68 | - for: { var: PACKAGES } 69 | cmd: cd {{.ITEM}} && go test ./... {{.CLI_ARGS}} 70 | -------------------------------------------------------------------------------- /ansi/ansi.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import "io" 4 | 5 | // Execute is a function that "execute" the given escape sequence by writing it 6 | // to the provided output writter. 7 | // 8 | // This is a syntactic sugar over [io.WriteString]. 9 | func Execute(w io.Writer, s string) (int, error) { 10 | return io.WriteString(w, s) 11 | } 12 | -------------------------------------------------------------------------------- /ansi/ascii.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | const ( 4 | // SP is the space character (Char: \x20). 5 | SP = 0x20 6 | // DEL is the delete character (Caret: ^?, Char: \x7f). 7 | DEL = 0x7F 8 | ) 9 | -------------------------------------------------------------------------------- /ansi/background_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | func TestSetForegroundColorNil(t *testing.T) { 10 | s := ansi.SetForegroundColor(nil) 11 | if s != "\x1b]10;\x07" { 12 | t.Errorf("Unexpected string for SetForegroundColor: got %q", s) 13 | } 14 | } 15 | 16 | func TestStringImplementations(t *testing.T) { 17 | foregroundColor := ansi.SetForegroundColor(ansi.BrightMagenta) 18 | backgroundColor := ansi.SetBackgroundColor(ansi.ExtendedColor(255)) 19 | cursorColor := ansi.SetCursorColor(ansi.TrueColor(0xffeeaa)) 20 | 21 | if foregroundColor != "\x1b]10;#ff00ff\x07" { 22 | t.Errorf("Unexpected string for SetForegroundColor: got %q", 23 | foregroundColor) 24 | } 25 | if backgroundColor != "\x1b]11;#eeeeee\x07" { 26 | t.Errorf("Unexpected string for SetBackgroundColor: got %q", 27 | backgroundColor) 28 | } 29 | if cursorColor != "\x1b]12;#ffeeaa\x07" { 30 | t.Errorf("Unexpected string for SetCursorColor: got %q", 31 | cursorColor) 32 | } 33 | } 34 | 35 | func TestColorizer(t *testing.T) { 36 | hex := ansi.HexColorizer{ansi.BrightBlack} 37 | xrgb := ansi.XRGBColorizer{ansi.ExtendedColor(235)} 38 | xrgba := ansi.XRGBAColorizer{ansi.TrueColor(0x00ff00)} 39 | 40 | if seq := ansi.SetForegroundColor(hex); seq != "\x1b]10;#808080\x07" { 41 | t.Errorf("Unexpected sequence for HexColorizer: got %q", seq) 42 | } 43 | if seq := ansi.SetForegroundColor(xrgb); seq != "\x1b]10;rgb:2626/2626/2626\x07" { 44 | t.Errorf("Unexpected sequence for XRGBColorizer: got %q", seq) 45 | } 46 | if seq := ansi.SetForegroundColor(xrgba); seq != "\x1b]10;rgba:0000/ffff/0000/ffff\x07" { 47 | t.Errorf("Unexpected sequence for XRGBAColorizer: got %q", seq) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ansi/c1.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // C1 control characters. 4 | // 5 | // These range from (0x80-0x9F) as defined in ISO 6429 (ECMA-48). 6 | // See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes 7 | const ( 8 | // PAD is the padding character. 9 | PAD = 0x80 10 | // HOP is the high octet preset character. 11 | HOP = 0x81 12 | // BPH is the break permitted here character. 13 | BPH = 0x82 14 | // NBH is the no break here character. 15 | NBH = 0x83 16 | // IND is the index character. 17 | IND = 0x84 18 | // NEL is the next line character. 19 | NEL = 0x85 20 | // SSA is the start of selected area character. 21 | SSA = 0x86 22 | // ESA is the end of selected area character. 23 | ESA = 0x87 24 | // HTS is the horizontal tab set character. 25 | HTS = 0x88 26 | // HTJ is the horizontal tab with justification character. 27 | HTJ = 0x89 28 | // VTS is the vertical tab set character. 29 | VTS = 0x8A 30 | // PLD is the partial line forward character. 31 | PLD = 0x8B 32 | // PLU is the partial line backward character. 33 | PLU = 0x8C 34 | // RI is the reverse index character. 35 | RI = 0x8D 36 | // SS2 is the single shift 2 character. 37 | SS2 = 0x8E 38 | // SS3 is the single shift 3 character. 39 | SS3 = 0x8F 40 | // DCS is the device control string character. 41 | DCS = 0x90 42 | // PU1 is the private use 1 character. 43 | PU1 = 0x91 44 | // PU2 is the private use 2 character. 45 | PU2 = 0x92 46 | // STS is the set transmit state character. 47 | STS = 0x93 48 | // CCH is the cancel character. 49 | CCH = 0x94 50 | // MW is the message waiting character. 51 | MW = 0x95 52 | // SPA is the start of guarded area character. 53 | SPA = 0x96 54 | // EPA is the end of guarded area character. 55 | EPA = 0x97 56 | // SOS is the start of string character. 57 | SOS = 0x98 58 | // SGCI is the single graphic character introducer character. 59 | SGCI = 0x99 60 | // SCI is the single character introducer character. 61 | SCI = 0x9A 62 | // CSI is the control sequence introducer character. 63 | CSI = 0x9B 64 | // ST is the string terminator character. 65 | ST = 0x9C 66 | // OSC is the operating system command character. 67 | OSC = 0x9D 68 | // PM is the privacy message character. 69 | PM = 0x9E 70 | // APC is the application program command character. 71 | APC = 0x9F 72 | ) 73 | -------------------------------------------------------------------------------- /ansi/charset.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // SelectCharacterSet sets the G-set character designator to the specified 4 | // character set. 5 | // 6 | // ESC Ps Pd 7 | // 8 | // Where Ps is the G-set character designator, and Pd is the identifier. 9 | // For 94-character sets, the designator can be one of: 10 | // - ( G0 11 | // - ) G1 12 | // - * G2 13 | // - + G3 14 | // 15 | // For 96-character sets, the designator can be one of: 16 | // - - G1 17 | // - . G2 18 | // - / G3 19 | // 20 | // Some common 94-character sets are: 21 | // - 0 DEC Special Drawing Set 22 | // - A United Kingdom (UK) 23 | // - B United States (USASCII) 24 | // 25 | // Examples: 26 | // 27 | // ESC ( B Select character set G0 = United States (USASCII) 28 | // ESC ( 0 Select character set G0 = Special Character and Line Drawing Set 29 | // ESC ) 0 Select character set G1 = Special Character and Line Drawing Set 30 | // ESC * A Select character set G2 = United Kingdom (UK) 31 | // 32 | // See: https://vt100.net/docs/vt510-rm/SCS.html 33 | func SelectCharacterSet(gset byte, charset byte) string { 34 | return "\x1b" + string(gset) + string(charset) 35 | } 36 | 37 | // SCS is an alias for SelectCharacterSet. 38 | func SCS(gset byte, charset byte) string { 39 | return SelectCharacterSet(gset, charset) 40 | } 41 | 42 | // Locking Shift 1 Right (LS1R) shifts G1 into GR character set. 43 | const LS1R = "\x1b~" 44 | 45 | // Locking Shift 2 (LS2) shifts G2 into GL character set. 46 | const LS2 = "\x1bn" 47 | 48 | // Locking Shift 2 Right (LS2R) shifts G2 into GR character set. 49 | const LS2R = "\x1b}" 50 | 51 | // Locking Shift 3 (LS3) shifts G3 into GL character set. 52 | const LS3 = "\x1bo" 53 | 54 | // Locking Shift 3 Right (LS3R) shifts G3 into GR character set. 55 | const LS3R = "\x1b|" 56 | -------------------------------------------------------------------------------- /ansi/clipboard_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | func TestClipboardNewClipboard(t *testing.T) { 10 | tt := []struct { 11 | name byte 12 | data string 13 | expect string 14 | }{ 15 | {'c', "Hello Test", "\x1b]52;c;SGVsbG8gVGVzdA==\x07"}, 16 | {'p', "Ansi Test", "\x1b]52;p;QW5zaSBUZXN0\x07"}, 17 | {'c', "", "\x1b]52;c;\x07"}, 18 | {'p', "?", "\x1b]52;p;Pw==\x07"}, 19 | {ansi.SystemClipboard, "test", "\x1b]52;c;dGVzdA==\x07"}, 20 | } 21 | for _, tp := range tt { 22 | cb := ansi.SetClipboard(tp.name, tp.data) 23 | if cb != tp.expect { 24 | t.Errorf("SetClipboard(%q, %q) = %q, want %q", tp.name, tp.data, cb, tp.expect) 25 | } 26 | } 27 | } 28 | 29 | func TestClipboardReset(t *testing.T) { 30 | cb := ansi.ResetClipboard(ansi.PrimaryClipboard) 31 | if cb != "\x1b]52;p;\x07" { 32 | t.Errorf("Unexpected clipboard reset: %q", cb) 33 | } 34 | } 35 | 36 | func TestClipboardRequest(t *testing.T) { 37 | cb := ansi.RequestClipboard(ansi.PrimaryClipboard) 38 | if cb != "\x1b]52;p;?\x07" { 39 | t.Errorf("Unexpected clipboard request: %q", cb) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ansi/cwd.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | ) 7 | 8 | // NotifyWorkingDirectory returns a sequence that notifies the terminal 9 | // of the current working directory. 10 | // 11 | // OSC 7 ; Pt BEL 12 | // 13 | // Where Pt is a URL in the format "file://[host]/[path]". 14 | // Set host to "localhost" if this is a path on the local computer. 15 | // 16 | // See: https://wezfurlong.org/wezterm/shell-integration.html#osc-7-escape-sequence-to-set-the-working-directory 17 | // See: https://iterm2.com/documentation-escape-codes.html#:~:text=RemoteHost%20and%20CurrentDir%3A-,OSC%207,-%3B%20%5BPs%5D%20ST 18 | func NotifyWorkingDirectory(host string, paths ...string) string { 19 | path := path.Join(paths...) 20 | u := &url.URL{ 21 | Scheme: "file", 22 | Host: host, 23 | Path: path, 24 | } 25 | return "\x1b]7;" + u.String() + "\x07" 26 | } 27 | -------------------------------------------------------------------------------- /ansi/cwd_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | func TestNotifyWorkingDirectory_LocalFile(t *testing.T) { 10 | h := ansi.NotifyWorkingDirectory("localhost", "path", "to", "file") 11 | if h != "\x1b]7;file://localhost/path/to/file\x07" { 12 | t.Errorf("Unexpected url: %s", h) 13 | } 14 | } 15 | 16 | func TestNotifyWorkingDirectory_RemoteFile(t *testing.T) { 17 | h := ansi.NotifyWorkingDirectory("example.com", "path", "to", "file") 18 | if h != "\x1b]7;file://example.com/path/to/file\x07" { 19 | t.Errorf("Unexpected url: %s", h) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ansi/doc.go: -------------------------------------------------------------------------------- 1 | // Package ansi defines common ANSI escape sequences based on the ECMA-48 2 | // specs. 3 | // 4 | // All sequences use 7-bit C1 control codes, which are supported by most 5 | // terminal emulators. OSC sequences are terminated by a BEL for wider 6 | // compatibility with terminals. 7 | package ansi 8 | -------------------------------------------------------------------------------- /ansi/finalterm.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import "strings" 4 | 5 | // FinalTerm returns an escape sequence that is used for shell integrations. 6 | // Originally, FinalTerm designed the protocol hence the name. 7 | // 8 | // OSC 133 ; Ps ; Pm ST 9 | // OSC 133 ; Ps ; Pm BEL 10 | // 11 | // See: https://iterm2.com/documentation-shell-integration.html 12 | func FinalTerm(pm ...string) string { 13 | return "\x1b]133;" + strings.Join(pm, ";") + "\x07" 14 | } 15 | 16 | // FinalTermPrompt returns an escape sequence that is used for shell 17 | // integrations prompt marks. This is sent just before the start of the shell 18 | // prompt. 19 | // 20 | // This is an alias for FinalTerm("A"). 21 | func FinalTermPrompt(pm ...string) string { 22 | if len(pm) == 0 { 23 | return FinalTerm("A") 24 | } 25 | return FinalTerm(append([]string{"A"}, pm...)...) 26 | } 27 | 28 | // FinalTermCmdStart returns an escape sequence that is used for shell 29 | // integrations command start marks. This is sent just after the end of the 30 | // shell prompt, before the user enters a command. 31 | // 32 | // This is an alias for FinalTerm("B"). 33 | func FinalTermCmdStart(pm ...string) string { 34 | if len(pm) == 0 { 35 | return FinalTerm("B") 36 | } 37 | return FinalTerm(append([]string{"B"}, pm...)...) 38 | } 39 | 40 | // FinalTermCmdExecuted returns an escape sequence that is used for shell 41 | // integrations command executed marks. This is sent just before the start of 42 | // the command output. 43 | // 44 | // This is an alias for FinalTerm("C"). 45 | func FinalTermCmdExecuted(pm ...string) string { 46 | if len(pm) == 0 { 47 | return FinalTerm("C") 48 | } 49 | return FinalTerm(append([]string{"C"}, pm...)...) 50 | } 51 | 52 | // FinalTermCmdFinished returns an escape sequence that is used for shell 53 | // integrations command finished marks. 54 | // 55 | // If the command was sent after 56 | // [FinalTermCmdStart], it indicates that the command was aborted. If the 57 | // command was sent after [FinalTermCmdExecuted], it indicates the end of the 58 | // command output. If neither was sent, [FinalTermCmdFinished] should be 59 | // ignored. 60 | // 61 | // This is an alias for FinalTerm("D"). 62 | func FinalTermCmdFinished(pm ...string) string { 63 | if len(pm) == 0 { 64 | return FinalTerm("D") 65 | } 66 | return FinalTerm(append([]string{"D"}, pm...)...) 67 | } 68 | -------------------------------------------------------------------------------- /ansi/fixtures/demo.vte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charmbracelet/x/6ba1785cd7b9b808c0b2ba21c6aa42649859ce28/ansi/fixtures/demo.vte -------------------------------------------------------------------------------- /ansi/fixtures/graphics/JigokudaniMonkeyPark.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e43df98a45ede3b2bc5ec4f686b0b2953da933f9483607ebbdd2fac43fadf0f0 3 | size 698785 4 | -------------------------------------------------------------------------------- /ansi/focus.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // Focus is an escape sequence to notify the terminal that it has focus. 4 | // This is used with [FocusEventMode]. 5 | const Focus = "\x1b[I" 6 | 7 | // Blur is an escape sequence to notify the terminal that it has lost focus. 8 | // This is used with [FocusEventMode]. 9 | const Blur = "\x1b[O" 10 | -------------------------------------------------------------------------------- /ansi/gen.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/format" 10 | "log" 11 | "os" 12 | 13 | . "github.com/charmbracelet/x/ansi/parser" 14 | ) 15 | 16 | func main() { 17 | var f bytes.Buffer 18 | table := GenerateTransitionTable() 19 | _, _ = f.WriteString(`package parser 20 | 21 | // Code generated by gen.go. DO NOT EDIT. 22 | 23 | // Table is a DEC ANSI transition table. 24 | var Table = TransitionTable{ 25 | `) 26 | for i, v := range table { 27 | code := i & 0xFF 28 | state := v & TransitionStateMask 29 | action := v >> TransitionActionShift 30 | next := v & TransitionStateMask 31 | 32 | format := "\t%s< GroundState { 35 | format += " | %s" 36 | args = append(args, StateNames[next]) 37 | } 38 | format += "," 39 | fmt.Fprintf(&f, format, args...) 40 | fmt.Fprintf(&f, " // %d: %s << IndexStateShift | 0x%02x", i, StateNames[state], code) 41 | fmt.Fprintln(&f) 42 | } 43 | 44 | fmt.Fprintln(&f, "}") 45 | content, err := format.Source(f.Bytes()) 46 | if err != nil { 47 | log.Fatalf("formatting source: %v", err) 48 | } 49 | 50 | if err := os.WriteFile("parser/table.go", content, os.ModePerm); err != nil { 51 | log.Fatalf("writing file: %v", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ansi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/ansi 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/bits-and-blooms/bitset v1.22.0 7 | github.com/lucasb-eyer/go-colorful v1.2.0 8 | github.com/mattn/go-runewidth v0.0.16 9 | github.com/rivo/uniseg v0.4.7 10 | ) 11 | -------------------------------------------------------------------------------- /ansi/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= 2 | github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 3 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 4 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 5 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 6 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 7 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 8 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 9 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 10 | -------------------------------------------------------------------------------- /ansi/hyperlink.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import "strings" 4 | 5 | // SetHyperlink returns a sequence for starting a hyperlink. 6 | // 7 | // OSC 8 ; Params ; Uri ST 8 | // OSC 8 ; Params ; Uri BEL 9 | // 10 | // To reset the hyperlink, omit the URI. 11 | // 12 | // See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda 13 | func SetHyperlink(uri string, params ...string) string { 14 | var p string 15 | if len(params) > 0 { 16 | p = strings.Join(params, ":") 17 | } 18 | return "\x1b]8;" + p + ";" + uri + "\x07" 19 | } 20 | 21 | // ResetHyperlink returns a sequence for resetting the hyperlink. 22 | // 23 | // This is equivalent to SetHyperlink("", params...). 24 | // 25 | // See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda 26 | func ResetHyperlink(params ...string) string { 27 | return SetHyperlink("", params...) 28 | } 29 | -------------------------------------------------------------------------------- /ansi/hyperlink_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | func TestNewHyperlink_NoParams(t *testing.T) { 10 | h := ansi.SetHyperlink("https://example.com") 11 | if h != "\x1b]8;;https://example.com\x07" { 12 | t.Errorf("Unexpected hyperlink: %s", h) 13 | } 14 | } 15 | 16 | func TestNewHyperlinkParams(t *testing.T) { 17 | h := ansi.SetHyperlink("https://example.com", "color=blue", "size=12") 18 | if h != "\x1b]8;color=blue:size=12;https://example.com\x07" { 19 | t.Errorf("Unexpected hyperlink: %s", h) 20 | } 21 | } 22 | 23 | func TestHyperlinkReset(t *testing.T) { 24 | h := ansi.SetHyperlink("") 25 | if h != "\x1b]8;;\x07" { 26 | t.Errorf("Unexpected hyperlink: %s", h) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ansi/iterm2.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import "fmt" 4 | 5 | // ITerm2 returns a sequence that uses the iTerm2 proprietary protocol. Use the 6 | // iterm2 package for a more convenient API. 7 | // 8 | // OSC 1337 ; key = value ST 9 | // 10 | // Example: 11 | // 12 | // ITerm2(iterm2.File{...}) 13 | // 14 | // See https://iterm2.com/documentation-escape-codes.html 15 | // See https://iterm2.com/documentation-images.html 16 | func ITerm2(data any) string { 17 | return "\x1b]1337;" + fmt.Sprint(data) + "\x07" 18 | } 19 | -------------------------------------------------------------------------------- /ansi/keypad.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // Keypad Application Mode (DECKPAM) is a mode that determines whether the 4 | // keypad sends application sequences or ANSI sequences. 5 | // 6 | // This works like enabling [DECNKM]. 7 | // Use [NumericKeypadMode] to set the numeric keypad mode. 8 | // 9 | // ESC = 10 | // 11 | // See: https://vt100.net/docs/vt510-rm/DECKPAM.html 12 | const ( 13 | KeypadApplicationMode = "\x1b=" 14 | DECKPAM = KeypadApplicationMode 15 | ) 16 | 17 | // Keypad Numeric Mode (DECKPNM) is a mode that determines whether the keypad 18 | // sends application sequences or ANSI sequences. 19 | // 20 | // This works the same as disabling [DECNKM]. 21 | // 22 | // ESC > 23 | // 24 | // See: https://vt100.net/docs/vt510-rm/DECKPNM.html 25 | const ( 26 | KeypadNumericMode = "\x1b>" 27 | DECKPNM = KeypadNumericMode 28 | ) 29 | -------------------------------------------------------------------------------- /ansi/kitty/decoder.go: -------------------------------------------------------------------------------- 1 | package kitty 2 | 3 | import ( 4 | "compress/zlib" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | "image/png" 9 | "io" 10 | ) 11 | 12 | // Decoder is a decoder for the Kitty graphics protocol. It supports decoding 13 | // images in the 24-bit [RGB], 32-bit [RGBA], and [PNG] formats. It can also 14 | // decompress data using zlib. 15 | // The default format is 32-bit [RGBA]. 16 | type Decoder struct { 17 | // Uses zlib decompression. 18 | Decompress bool 19 | 20 | // Can be one of [RGB], [RGBA], or [PNG]. 21 | Format int 22 | 23 | // Width of the image in pixels. This can be omitted if the image is [PNG] 24 | // formatted. 25 | Width int 26 | 27 | // Height of the image in pixels. This can be omitted if the image is [PNG] 28 | // formatted. 29 | Height int 30 | } 31 | 32 | // Decode decodes the image data from r in the specified format. 33 | func (d *Decoder) Decode(r io.Reader) (image.Image, error) { 34 | if d.Decompress { 35 | zr, err := zlib.NewReader(r) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to create zlib reader: %w", err) 38 | } 39 | 40 | defer zr.Close() //nolint:errcheck 41 | r = zr 42 | } 43 | 44 | if d.Format == 0 { 45 | d.Format = RGBA 46 | } 47 | 48 | switch d.Format { 49 | case RGBA, RGB: 50 | return d.decodeRGBA(r, d.Format == RGBA) 51 | 52 | case PNG: 53 | return png.Decode(r) 54 | 55 | default: 56 | return nil, fmt.Errorf("unsupported format: %d", d.Format) 57 | } 58 | } 59 | 60 | // decodeRGBA decodes the image data in 32-bit RGBA or 24-bit RGB formats. 61 | func (d *Decoder) decodeRGBA(r io.Reader, alpha bool) (image.Image, error) { 62 | m := image.NewRGBA(image.Rect(0, 0, d.Width, d.Height)) 63 | 64 | var buf []byte 65 | if alpha { 66 | buf = make([]byte, 4) 67 | } else { 68 | buf = make([]byte, 3) 69 | } 70 | 71 | for y := range d.Height { 72 | for x := range d.Width { 73 | if _, err := io.ReadFull(r, buf[:]); err != nil { 74 | return nil, fmt.Errorf("failed to read pixel data: %w", err) 75 | } 76 | if alpha { 77 | m.SetRGBA(x, y, color.RGBA{buf[0], buf[1], buf[2], buf[3]}) 78 | } else { 79 | m.SetRGBA(x, y, color.RGBA{buf[0], buf[1], buf[2], 0xff}) 80 | } 81 | } 82 | } 83 | 84 | return m, nil 85 | } 86 | -------------------------------------------------------------------------------- /ansi/kitty/encoder.go: -------------------------------------------------------------------------------- 1 | package kitty 2 | 3 | import ( 4 | "compress/zlib" 5 | "fmt" 6 | "image" 7 | "image/png" 8 | "io" 9 | ) 10 | 11 | // Encoder is an encoder for the Kitty graphics protocol. It supports encoding 12 | // images in the 24-bit [RGB], 32-bit [RGBA], and [PNG] formats, and 13 | // compressing the data using zlib. 14 | // The default format is 32-bit [RGBA]. 15 | type Encoder struct { 16 | // Uses zlib compression. 17 | Compress bool 18 | 19 | // Can be one of [RGBA], [RGB], or [PNG]. 20 | Format int 21 | } 22 | 23 | // Encode encodes the image data in the specified format and writes it to w. 24 | func (e *Encoder) Encode(w io.Writer, m image.Image) error { 25 | if m == nil { 26 | return nil 27 | } 28 | 29 | if e.Compress { 30 | zw := zlib.NewWriter(w) 31 | defer zw.Close() //nolint:errcheck 32 | w = zw 33 | } 34 | 35 | if e.Format == 0 { 36 | e.Format = RGBA 37 | } 38 | 39 | switch e.Format { 40 | case RGBA, RGB: 41 | bounds := m.Bounds() 42 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 43 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 44 | r, g, b, a := m.At(x, y).RGBA() 45 | switch e.Format { 46 | case RGBA: 47 | w.Write([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)}) //nolint:errcheck 48 | case RGB: 49 | w.Write([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8)}) //nolint:errcheck 50 | } 51 | } 52 | } 53 | 54 | case PNG: 55 | if err := png.Encode(w, m); err != nil { 56 | return fmt.Errorf("failed to encode PNG: %w", err) 57 | } 58 | 59 | default: 60 | return fmt.Errorf("unsupported format: %d", e.Format) 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /ansi/modes.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // Modes represents the terminal modes that can be set or reset. By default, 4 | // all modes are [ModeNotRecognized]. 5 | type Modes map[Mode]ModeSetting 6 | 7 | // Get returns the setting of a terminal mode. If the mode is not set, it 8 | // returns [ModeNotRecognized]. 9 | func (m Modes) Get(mode Mode) ModeSetting { 10 | return m[mode] 11 | } 12 | 13 | // Delete deletes a terminal mode. This has the same effect as setting the mode 14 | // to [ModeNotRecognized]. 15 | func (m Modes) Delete(mode Mode) { 16 | delete(m, mode) 17 | } 18 | 19 | // Set sets a terminal mode to [ModeSet]. 20 | func (m Modes) Set(modes ...Mode) { 21 | for _, mode := range modes { 22 | m[mode] = ModeSet 23 | } 24 | } 25 | 26 | // PermanentlySet sets a terminal mode to [ModePermanentlySet]. 27 | func (m Modes) PermanentlySet(modes ...Mode) { 28 | for _, mode := range modes { 29 | m[mode] = ModePermanentlySet 30 | } 31 | } 32 | 33 | // Reset sets a terminal mode to [ModeReset]. 34 | func (m Modes) Reset(modes ...Mode) { 35 | for _, mode := range modes { 36 | m[mode] = ModeReset 37 | } 38 | } 39 | 40 | // PermanentlyReset sets a terminal mode to [ModePermanentlyReset]. 41 | func (m Modes) PermanentlyReset(modes ...Mode) { 42 | for _, mode := range modes { 43 | m[mode] = ModePermanentlyReset 44 | } 45 | } 46 | 47 | // IsSet returns true if the mode is set to [ModeSet] or [ModePermanentlySet]. 48 | func (m Modes) IsSet(mode Mode) bool { 49 | return m[mode].IsSet() 50 | } 51 | 52 | // IsPermanentlySet returns true if the mode is set to [ModePermanentlySet]. 53 | func (m Modes) IsPermanentlySet(mode Mode) bool { 54 | return m[mode].IsPermanentlySet() 55 | } 56 | 57 | // IsReset returns true if the mode is set to [ModeReset] or [ModePermanentlyReset]. 58 | func (m Modes) IsReset(mode Mode) bool { 59 | return m[mode].IsReset() 60 | } 61 | 62 | // IsPermanentlyReset returns true if the mode is set to [ModePermanentlyReset]. 63 | func (m Modes) IsPermanentlyReset(mode Mode) bool { 64 | return m[mode].IsPermanentlyReset() 65 | } 66 | -------------------------------------------------------------------------------- /ansi/notification.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // Notify sends a desktop notification using iTerm's OSC 9. 4 | // 5 | // OSC 9 ; Mc ST 6 | // OSC 9 ; Mc BEL 7 | // 8 | // Where Mc is the notification body. 9 | // 10 | // See: https://iterm2.com/documentation-escape-codes.html 11 | func Notify(s string) string { 12 | return "\x1b]9;" + s + "\x07" 13 | } 14 | -------------------------------------------------------------------------------- /ansi/parser/const.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // Action is a DEC ANSI parser action. 4 | type Action = byte 5 | 6 | // These are the actions that the parser can take. 7 | const ( 8 | NoneAction Action = iota 9 | ClearAction 10 | CollectAction 11 | PrefixAction 12 | DispatchAction 13 | ExecuteAction 14 | StartAction // Start of a data string 15 | PutAction // Put into the data string 16 | ParamAction 17 | PrintAction 18 | 19 | IgnoreAction = NoneAction 20 | ) 21 | 22 | // nolint: unused 23 | var ActionNames = []string{ 24 | "NoneAction", 25 | "ClearAction", 26 | "CollectAction", 27 | "PrefixAction", 28 | "DispatchAction", 29 | "ExecuteAction", 30 | "StartAction", 31 | "PutAction", 32 | "ParamAction", 33 | "PrintAction", 34 | } 35 | 36 | // State is a DEC ANSI parser state. 37 | type State = byte 38 | 39 | // These are the states that the parser can be in. 40 | const ( 41 | GroundState State = iota 42 | CsiEntryState 43 | CsiIntermediateState 44 | CsiParamState 45 | DcsEntryState 46 | DcsIntermediateState 47 | DcsParamState 48 | DcsStringState 49 | EscapeState 50 | EscapeIntermediateState 51 | OscStringState 52 | SosStringState 53 | PmStringState 54 | ApcStringState 55 | 56 | // Utf8State is not part of the DEC ANSI standard. It is used to handle 57 | // UTF-8 sequences. 58 | Utf8State 59 | ) 60 | 61 | // nolint: unused 62 | var StateNames = []string{ 63 | "GroundState", 64 | "CsiEntryState", 65 | "CsiIntermediateState", 66 | "CsiParamState", 67 | "DcsEntryState", 68 | "DcsIntermediateState", 69 | "DcsParamState", 70 | "DcsStringState", 71 | "EscapeState", 72 | "EscapeIntermediateState", 73 | "OscStringState", 74 | "SosStringState", 75 | "PmStringState", 76 | "ApcStringState", 77 | "Utf8State", 78 | } 79 | -------------------------------------------------------------------------------- /ansi/parser_apc_test.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSosPmApcSequence(t *testing.T) { 8 | cases := []testCase{ 9 | { 10 | name: "apc7", 11 | input: "\x1b_Gf=24,s=10,v=20,o=z;aGVsbG8gd29ybGQ=\x1b\\", 12 | expected: []any{ 13 | []byte("Gf=24,s=10,v=20,o=z;aGVsbG8gd29ybGQ="), 14 | Cmd('\\'), 15 | }, 16 | }, 17 | } 18 | 19 | for _, c := range cases { 20 | t.Run(c.name, func(t *testing.T) { 21 | dispatcher := &testDispatcher{} 22 | parser := testParser(dispatcher) 23 | parser.Parse([]byte(c.input)) 24 | assertEqual(t, len(c.expected), len(dispatcher.dispatched)) 25 | assertEqual(t, c.expected, dispatcher.dispatched) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ansi/parser_dcs_test.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/charmbracelet/x/ansi/parser" 9 | ) 10 | 11 | func TestDcsSequence(t *testing.T) { 12 | cases := []testCase{ 13 | { 14 | name: "max_params", 15 | input: fmt.Sprintf("\x1bP%sp\x1b\\", strings.Repeat("1;", 33)), 16 | expected: []any{ 17 | dcsSequence{ 18 | Cmd: 'p', 19 | Params: Params{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 20 | Data: []byte{}, 21 | }, 22 | Cmd('\\'), 23 | }, 24 | }, 25 | { 26 | name: "reset", 27 | input: "\x1b[3;1\x1bP1$tx\x9c", 28 | expected: []any{ 29 | dcsSequence{ 30 | Cmd: 't' | '$'< ST 12 | // 13 | // Note: Screen limits the length of string sequences to 768 bytes (since 2014). 14 | // Use zero to indicate no limit, otherwise, this will chunk the returned 15 | // string into limit sized chunks. 16 | // 17 | // See: https://www.gnu.org/software/screen/manual/screen.html#String-Escapes 18 | // See: https://git.savannah.gnu.org/cgit/screen.git/tree/src/screen.h?id=c184c6ec27683ff1a860c45be5cf520d896fd2ef#n44 19 | func ScreenPassthrough(seq string, limit int) string { 20 | var b bytes.Buffer 21 | b.WriteString("\x1bP") 22 | if limit > 0 { 23 | for i := 0; i < len(seq); i += limit { 24 | end := min(i+limit, len(seq)) 25 | b.WriteString(seq[i:end]) 26 | if end < len(seq) { 27 | b.WriteString("\x1b\\\x1bP") 28 | } 29 | } 30 | } else { 31 | b.WriteString(seq) 32 | } 33 | b.WriteString("\x1b\\") 34 | return b.String() 35 | } 36 | 37 | // TmuxPassthrough wraps the given ANSI sequence in a special DCS passthrough 38 | // sequence to be sent to the outer terminal. This is used to send raw escape 39 | // sequences to the outer terminal when running inside Tmux. 40 | // 41 | // DCS tmux ; ST 42 | // 43 | // Where is the given sequence in which all occurrences of ESC 44 | // (0x1b) are doubled i.e. replaced with ESC ESC (0x1b 0x1b). 45 | // 46 | // Note: this needs the `allow-passthrough` option to be set to `on`. 47 | // 48 | // See: https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it 49 | func TmuxPassthrough(seq string) string { 50 | var b bytes.Buffer 51 | b.WriteString("\x1bPtmux;") 52 | for i := range len(seq) { 53 | if seq[i] == ESC { 54 | b.WriteByte(ESC) 55 | } 56 | b.WriteByte(seq[i]) 57 | } 58 | b.WriteString("\x1b\\") 59 | return b.String() 60 | } 61 | -------------------------------------------------------------------------------- /ansi/passthrough_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | var passthroughCases = []struct { 10 | name string 11 | seq string 12 | limit int 13 | screen string 14 | tmux string 15 | }{ 16 | { 17 | name: "empty", 18 | seq: "", 19 | screen: "\x1bP\x1b\\", 20 | tmux: "\x1bPtmux;\x1b\\", 21 | }, 22 | { 23 | name: "short", 24 | seq: "hello", 25 | screen: "\x1bPhello\x1b\\", 26 | tmux: "\x1bPtmux;hello\x1b\\", 27 | }, 28 | { 29 | name: "limit", 30 | seq: "foobarbaz", 31 | limit: 3, 32 | screen: "\x1bPfoo\x1b\\\x1bPbar\x1b\\\x1bPbaz\x1b\\", 33 | tmux: "\x1bPtmux;foobarbaz\x1b\\", 34 | }, 35 | { 36 | name: "escaped", 37 | seq: "\x1b]52;c;Zm9vYmFy\x07", 38 | screen: "\x1bP\x1b]52;c;Zm9vYmFy\x07\x1b\\", 39 | tmux: "\x1bPtmux;\x1b\x1b]52;c;Zm9vYmFy\x07\x1b\\", 40 | }, 41 | } 42 | 43 | func TestScreenPassthrough(t *testing.T) { 44 | for i, tt := range passthroughCases { 45 | t.Run(tt.name, func(t *testing.T) { 46 | got := ansi.ScreenPassthrough(tt.seq, tt.limit) 47 | if got != tt.screen { 48 | t.Errorf("case: %d, ScreenPassthrough() = %q, want %q", i+1, got, tt.screen) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestTmuxPassthrough(t *testing.T) { 55 | for i, tt := range passthroughCases { 56 | t.Run(tt.name, func(t *testing.T) { 57 | got := ansi.TmuxPassthrough(tt.seq) 58 | if got != tt.tmux { 59 | t.Errorf("case: %d, TmuxPassthrough() = %q, want %q", i+1, got, tt.tmux) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ansi/paste.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // BracketedPasteStart is the control sequence to enable bracketed paste mode. 4 | const BracketedPasteStart = "\x1b[200~" 5 | 6 | // BracketedPasteEnd is the control sequence to disable bracketed paste mode. 7 | const BracketedPasteEnd = "\x1b[201~" 8 | -------------------------------------------------------------------------------- /ansi/reset.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // ResetInitialState (RIS) resets the terminal to its initial state. 4 | // 5 | // ESC c 6 | // 7 | // See: https://vt100.net/docs/vt510-rm/RIS.html 8 | const ( 9 | ResetInitialState = "\x1bc" 10 | RIS = ResetInitialState 11 | ) 12 | -------------------------------------------------------------------------------- /ansi/sixel/.gitattributes: -------------------------------------------------------------------------------- 1 | ansi/fixtures/graphics/*.png filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /ansi/sixel/raster.go: -------------------------------------------------------------------------------- 1 | package sixel 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // ErrInvalidRaster is returned when Raster Attributes are invalid. 10 | var ErrInvalidRaster = fmt.Errorf("invalid raster attributes") 11 | 12 | // WriteRaster writes Raster attributes to a writer. If ph and pv are 0, they 13 | // are omitted. 14 | func WriteRaster(w io.Writer, pan, pad, ph, pv int) (n int, err error) { 15 | if pad == 0 { 16 | return WriteRaster(w, 1, 1, ph, pv) 17 | } 18 | 19 | if ph <= 0 && pv <= 0 { 20 | return fmt.Fprintf(w, "%c%d;%d", RasterAttribute, pan, pad) 21 | } 22 | 23 | return fmt.Fprintf(w, "%c%d;%d;%d;%d", RasterAttribute, pan, pad, ph, pv) 24 | } 25 | 26 | // Raster represents Sixel raster attributes. 27 | type Raster struct { 28 | Pan, Pad, Ph, Pv int 29 | } 30 | 31 | // WriteTo writes Raster attributes to a writer. 32 | func (r Raster) WriteTo(w io.Writer) (int64, error) { 33 | n, err := WriteRaster(w, r.Pan, r.Pad, r.Ph, r.Pv) 34 | return int64(n), err 35 | } 36 | 37 | // String returns the Raster as a string. 38 | func (r Raster) String() string { 39 | var b strings.Builder 40 | r.WriteTo(&b) //nolint:errcheck 41 | return b.String() 42 | } 43 | 44 | // DecodeRaster decodes a Raster from a byte slice. It returns the Raster and 45 | // the number of bytes read. 46 | func DecodeRaster(data []byte) (r Raster, n int) { 47 | if len(data) == 0 || data[0] != RasterAttribute { 48 | return 49 | } 50 | 51 | ptr := &r.Pan 52 | for n = 1; n < len(data); n++ { 53 | if data[n] == ';' { 54 | if ptr == &r.Pan { 55 | ptr = &r.Pad 56 | } else if ptr == &r.Pad { 57 | ptr = &r.Ph 58 | } else if ptr == &r.Ph { 59 | ptr = &r.Pv 60 | } else { 61 | n++ 62 | break 63 | } 64 | } else if data[n] >= '0' && data[n] <= '9' { 65 | *ptr = (*ptr)*10 + int(data[n]-'0') 66 | } else { 67 | break 68 | } 69 | } 70 | 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /ansi/sixel/repeat.go: -------------------------------------------------------------------------------- 1 | package sixel 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // ErrInvalidRepeat is returned when a Repeat is invalid 10 | var ErrInvalidRepeat = fmt.Errorf("invalid repeat") 11 | 12 | // WriteRepeat writes a Repeat to a writer. A repeat character is in the range 13 | // of '?' (0x3F) to '~' (0x7E). 14 | func WriteRepeat(w io.Writer, count int, char byte) (int, error) { 15 | return fmt.Fprintf(w, "%c%d%c", RepeatIntroducer, count, char) 16 | } 17 | 18 | // Repeat represents a Sixel repeat introducer. 19 | type Repeat struct { 20 | Count int 21 | Char byte 22 | } 23 | 24 | // WriteTo writes a Repeat to a writer. 25 | func (r Repeat) WriteTo(w io.Writer) (int64, error) { 26 | n, err := WriteRepeat(w, r.Count, r.Char) 27 | return int64(n), err 28 | } 29 | 30 | // String returns the Repeat as a string. 31 | func (r Repeat) String() string { 32 | var b strings.Builder 33 | r.WriteTo(&b) //nolint:errcheck 34 | return b.String() 35 | } 36 | 37 | // DecodeRepeat decodes a Repeat from a byte slice. It returns the Repeat and 38 | // the number of bytes read. 39 | func DecodeRepeat(data []byte) (r Repeat, n int) { 40 | if len(data) == 0 || data[0] != RepeatIntroducer { 41 | return 42 | } 43 | 44 | if len(data) < 3 { // The minimum length is 3: the introducer, a digit, and a character. 45 | return 46 | } 47 | 48 | for n = 1; n < len(data); n++ { 49 | if data[n] >= '0' && data[n] <= '9' { 50 | r.Count = r.Count*10 + int(data[n]-'0') 51 | } else { 52 | r.Char = data[n] 53 | n++ // Include the character in the count. 54 | break 55 | } 56 | } 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /ansi/sixel/sixel_bench_test.go: -------------------------------------------------------------------------------- 1 | package sixel 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image" 7 | "image/png" 8 | "io" 9 | "os" 10 | "testing" 11 | 12 | "github.com/charmbracelet/x/ansi" 13 | // gosixel "github.com/mattn/go-sixel" 14 | ) 15 | 16 | // func BenchmarkEncodingGoSixel(b *testing.B) { 17 | // for i := 0; i < b.N; i++ { 18 | // raw, err := loadImage("./../fixtures/graphics/JigokudaniMonkeyPark.png") 19 | // if err != nil { 20 | // os.Exit(1) 21 | // } 22 | 23 | // b := bytes.NewBuffer(nil) 24 | // enc := gosixel.NewEncoder(b) 25 | // if err := enc.Encode(raw); err != nil { 26 | // fmt.Fprintln(os.Stderr, err) 27 | // os.Exit(1) 28 | // } 29 | 30 | // // fmt.Println(b) 31 | // } 32 | // } 33 | 34 | func writeSixelGraphics(w io.Writer, m image.Image) error { 35 | e := &Encoder{} 36 | 37 | data := bytes.NewBuffer(nil) 38 | if err := e.Encode(data, m); err != nil { 39 | return fmt.Errorf("failed to encode sixel image: %w", err) 40 | } 41 | 42 | _, err := io.WriteString(w, ansi.SixelGraphics(0, 1, 0, data.Bytes())) 43 | return err 44 | } 45 | 46 | func BenchmarkEncodingXSixel(b *testing.B) { 47 | for i := 0; i < b.N; i++ { 48 | raw, err := loadImage("./../fixtures/graphics/JigokudaniMonkeyPark.png") 49 | if err != nil { 50 | os.Exit(1) 51 | } 52 | 53 | b := bytes.NewBuffer(nil) 54 | if err := writeSixelGraphics(b, raw); err != nil { 55 | fmt.Fprintln(os.Stderr, err) 56 | os.Exit(1) 57 | } 58 | 59 | // fmt.Println(b) 60 | } 61 | } 62 | 63 | func loadImage(path string) (image.Image, error) { 64 | f, err := os.Open(path) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return png.Decode(f) 69 | } 70 | -------------------------------------------------------------------------------- /ansi/sixel/util.go: -------------------------------------------------------------------------------- 1 | package sixel 2 | 3 | func max(a, b int) int { //nolint:predeclared 4 | if a > b { 5 | return a 6 | } 7 | return b 8 | } 9 | -------------------------------------------------------------------------------- /ansi/style_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/charmbracelet/x/ansi" 8 | ) 9 | 10 | func TestReset(t *testing.T) { 11 | var s ansi.Style 12 | if s.String() != "\x1b[m" { 13 | t.Errorf("Unexpected reset sequence: %q", ansi.ResetStyle) 14 | } 15 | } 16 | 17 | func TestBold(t *testing.T) { 18 | var s ansi.Style 19 | s = s.Bold() 20 | if s.String() != "\x1b[1m" { 21 | t.Errorf("Unexpected bold sequence: %q", s) 22 | } 23 | } 24 | 25 | func TestDefaultBackground(t *testing.T) { 26 | var s ansi.Style 27 | s = s.DefaultBackgroundColor() 28 | if s.String() != "\x1b[49m" { 29 | t.Errorf("Unexpected default background sequence: %q", s) 30 | } 31 | } 32 | 33 | func TestSequence(t *testing.T) { 34 | var s ansi.Style 35 | s = s.Bold().Underline().ForegroundColor(ansi.ExtendedColor(255)) 36 | if s.String() != "\x1b[1;4;38;5;255m" { 37 | t.Errorf("Unexpected sequence: %q", s) 38 | } 39 | } 40 | 41 | func TestColorColor(t *testing.T) { 42 | s := ansi.NewStyle().Bold().Underline().ForegroundColor(color.Black) 43 | if s.String() != "\x1b[1;4;38;2;0;0;0m" { 44 | t.Errorf("Unexpected sequence: %q", s) 45 | } 46 | } 47 | 48 | func BenchmarkStyle(b *testing.B) { 49 | b.ReportAllocs() 50 | for i := 0; i < b.N; i++ { 51 | _ = ansi.NewStyle(). 52 | Bold(). 53 | DoubleUnderline(). 54 | ForegroundColor(color.RGBA{255, 255, 255, 255}). 55 | String() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ansi/termcap.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import ( 4 | "encoding/hex" 5 | "strings" 6 | ) 7 | 8 | // RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings. 9 | // 10 | // DCS + q ST 11 | // 12 | // Where is a list of Termcap/Terminfo capabilities, encoded in 2-digit 13 | // hexadecimals, separated by semicolons. 14 | // 15 | // See: https://man7.org/linux/man-pages/man5/terminfo.5.html 16 | // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands 17 | func XTGETTCAP(caps ...string) string { 18 | if len(caps) == 0 { 19 | return "" 20 | } 21 | 22 | s := "\x1bP+q" 23 | for i, c := range caps { 24 | if i > 0 { 25 | s += ";" 26 | } 27 | s += strings.ToUpper(hex.EncodeToString([]byte(c))) 28 | } 29 | 30 | return s + "\x1b\\" 31 | } 32 | 33 | // RequestTermcap is an alias for [XTGETTCAP]. 34 | func RequestTermcap(caps ...string) string { 35 | return XTGETTCAP(caps...) 36 | } 37 | 38 | // RequestTerminfo is an alias for [XTGETTCAP]. 39 | func RequestTerminfo(caps ...string) string { 40 | return XTGETTCAP(caps...) 41 | } 42 | -------------------------------------------------------------------------------- /ansi/title.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | // SetIconNameWindowTitle returns a sequence for setting the icon name and 4 | // window title. 5 | // 6 | // OSC 0 ; title ST 7 | // OSC 0 ; title BEL 8 | // 9 | // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands 10 | func SetIconNameWindowTitle(s string) string { 11 | return "\x1b]0;" + s + "\x07" 12 | } 13 | 14 | // SetIconName returns a sequence for setting the icon name. 15 | // 16 | // OSC 1 ; title ST 17 | // OSC 1 ; title BEL 18 | // 19 | // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands 20 | func SetIconName(s string) string { 21 | return "\x1b]1;" + s + "\x07" 22 | } 23 | 24 | // SetWindowTitle returns a sequence for setting the window title. 25 | // 26 | // OSC 2 ; title ST 27 | // OSC 2 ; title BEL 28 | // 29 | // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands 30 | func SetWindowTitle(s string) string { 31 | return "\x1b]2;" + s + "\x07" 32 | } 33 | 34 | // DECSWT is a sequence for setting the window title. 35 | // 36 | // This is an alias for [SetWindowTitle]("1;"). 37 | // See: EK-VT520-RM 5–156 https://vt100.net/dec/ek-vt520-rm.pdf 38 | func DECSWT(name string) string { 39 | return SetWindowTitle("1;" + name) 40 | } 41 | 42 | // DECSIN is a sequence for setting the icon name. 43 | // 44 | // This is an alias for [SetWindowTitle]("L;"). 45 | // See: EK-VT520-RM 5–134 https://vt100.net/dec/ek-vt520-rm.pdf 46 | func DECSIN(name string) string { 47 | return SetWindowTitle("L;" + name) 48 | } 49 | -------------------------------------------------------------------------------- /ansi/title_test.go: -------------------------------------------------------------------------------- 1 | package ansi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | func TestSetIconNameWindowTitle(t *testing.T) { 10 | if ansi.SetIconNameWindowTitle("hello") != "\x1b]0;hello\x07" { 11 | t.Errorf("expected: %q, got: %q", "\x1b]0;hello\x07", ansi.SetIconNameWindowTitle("hello")) 12 | } 13 | } 14 | 15 | func TestSetIconName(t *testing.T) { 16 | if ansi.SetIconName("hello") != "\x1b]1;hello\x07" { 17 | t.Errorf("expected: %q, got: %q", "\x1b]1;hello\x07", ansi.SetIconName("hello")) 18 | } 19 | } 20 | 21 | func TestSetWindowTitle(t *testing.T) { 22 | if ansi.SetWindowTitle("hello") != "\x1b]2;hello\x07" { 23 | t.Errorf("expected: %q, got: %q", "\x1b]2;hello\x07", ansi.SetWindowTitle("hello")) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ansi/winop.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // ResizeWindowWinOp is a window operation that resizes the terminal 10 | // window. 11 | ResizeWindowWinOp = 4 12 | 13 | // RequestWindowSizeWinOp is a window operation that requests a report of 14 | // the size of the terminal window in pixels. The response is in the form: 15 | // CSI 4 ; height ; width t 16 | RequestWindowSizeWinOp = 14 17 | 18 | // RequestCellSizeWinOp is a window operation that requests a report of 19 | // the size of the terminal cell size in pixels. The response is in the form: 20 | // CSI 6 ; height ; width t 21 | RequestCellSizeWinOp = 16 22 | ) 23 | 24 | // WindowOp (XTWINOPS) is a sequence that manipulates the terminal window. 25 | // 26 | // CSI Ps ; Ps ; Ps t 27 | // 28 | // Ps is a semicolon-separated list of parameters. 29 | // See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps;Ps;Ps-t.1EB0 30 | func WindowOp(p int, ps ...int) string { 31 | if p <= 0 { 32 | return "" 33 | } 34 | 35 | if len(ps) == 0 { 36 | return "\x1b[" + strconv.Itoa(p) + "t" 37 | } 38 | 39 | params := make([]string, 0, len(ps)+1) 40 | params = append(params, strconv.Itoa(p)) 41 | for _, p := range ps { 42 | if p >= 0 { 43 | params = append(params, strconv.Itoa(p)) 44 | } 45 | } 46 | 47 | return "\x1b[" + strings.Join(params, ";") + "t" 48 | } 49 | 50 | // XTWINOPS is an alias for [WindowOp]. 51 | func XTWINOPS(p int, ps ...int) string { 52 | return WindowOp(p, ps...) 53 | } 54 | -------------------------------------------------------------------------------- /cellbuf/errors.go: -------------------------------------------------------------------------------- 1 | package cellbuf 2 | 3 | import "errors" 4 | 5 | // ErrOutOfBounds is returned when the given x, y position is out of bounds. 6 | var ErrOutOfBounds = errors.New("out of bounds") 7 | -------------------------------------------------------------------------------- /cellbuf/geom.go: -------------------------------------------------------------------------------- 1 | package cellbuf 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // Position represents an x, y position. 8 | type Position = image.Point 9 | 10 | // Pos is a shorthand for Position{X: x, Y: y}. 11 | func Pos(x, y int) Position { 12 | return image.Pt(x, y) 13 | } 14 | 15 | // Rectange represents a rectangle. 16 | type Rectangle = image.Rectangle 17 | 18 | // Rect is a shorthand for Rectangle. 19 | func Rect(x, y, w, h int) Rectangle { 20 | return image.Rect(x, y, x+w, y+h) 21 | } 22 | -------------------------------------------------------------------------------- /cellbuf/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/cellbuf 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/colorprofile v0.3.0 7 | github.com/charmbracelet/x/ansi v0.9.2 8 | github.com/charmbracelet/x/term v0.2.1 9 | github.com/mattn/go-runewidth v0.0.16 10 | github.com/rivo/uniseg v0.4.7 11 | ) 12 | 13 | require ( 14 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 15 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 16 | golang.org/x/sys v0.31.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /cellbuf/go.sum: -------------------------------------------------------------------------------- 1 | github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= 2 | github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0= 3 | github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= 4 | github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 5 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 6 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 7 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 8 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 9 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 10 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 11 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 12 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 13 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 14 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 15 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 16 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 17 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 18 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 19 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 20 | -------------------------------------------------------------------------------- /cellbuf/link.go: -------------------------------------------------------------------------------- 1 | package cellbuf 2 | 3 | import ( 4 | "github.com/charmbracelet/colorprofile" 5 | ) 6 | 7 | // Convert converts a hyperlink to respect the given color profile. 8 | func ConvertLink(h Link, p colorprofile.Profile) Link { 9 | if p == colorprofile.NoTTY { 10 | return Link{} 11 | } 12 | 13 | return h 14 | } 15 | -------------------------------------------------------------------------------- /cellbuf/pen.go: -------------------------------------------------------------------------------- 1 | package cellbuf 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/charmbracelet/x/ansi" 7 | ) 8 | 9 | // PenWriter is a writer that writes to a buffer and keeps track of the current 10 | // pen style and link state for the purpose of wrapping with newlines. 11 | type PenWriter struct { 12 | w io.Writer 13 | p *ansi.Parser 14 | style Style 15 | link Link 16 | } 17 | 18 | // NewPenWriter returns a new PenWriter. 19 | func NewPenWriter(w io.Writer) *PenWriter { 20 | pw := &PenWriter{w: w} 21 | pw.p = ansi.NewParser() 22 | pw.p.SetParamsSize(32) // 32 parameters 23 | pw.p.SetDataSize(4 * 1024 * 1024) // 4MB of data buffer 24 | handleCsi := func(cmd ansi.Cmd, params ansi.Params) { 25 | if cmd == 'm' { 26 | ReadStyle(params, &pw.style) 27 | } 28 | } 29 | handleOsc := func(cmd int, data []byte) { 30 | if cmd == 8 { 31 | ReadLink(data, &pw.link) 32 | } 33 | } 34 | pw.p.SetHandler(ansi.Handler{ 35 | HandleCsi: handleCsi, 36 | HandleOsc: handleOsc, 37 | }) 38 | return pw 39 | } 40 | 41 | // Style returns the current pen style. 42 | func (w *PenWriter) Style() Style { 43 | return w.style 44 | } 45 | 46 | // Link returns the current pen link. 47 | func (w *PenWriter) Link() Link { 48 | return w.link 49 | } 50 | 51 | // Write writes to the buffer. 52 | func (w *PenWriter) Write(p []byte) (int, error) { 53 | for i := range p { 54 | b := p[i] 55 | w.p.Advance(b) 56 | if b == '\n' { 57 | if !w.style.Empty() { 58 | _, _ = w.w.Write([]byte(ansi.ResetStyle)) 59 | } 60 | if !w.link.Empty() { 61 | _, _ = w.w.Write([]byte(ansi.ResetHyperlink())) 62 | } 63 | } 64 | 65 | _, _ = w.w.Write([]byte{b}) 66 | if b == '\n' { 67 | if !w.link.Empty() { 68 | _, _ = w.w.Write([]byte(ansi.SetHyperlink(w.link.URL, w.link.Params))) 69 | } 70 | if !w.style.Empty() { 71 | _, _ = w.w.Write([]byte(w.style.Sequence())) 72 | } 73 | } 74 | } 75 | 76 | return len(p), nil 77 | } 78 | 79 | // Close closes the writer and resets the style and link if necessary. 80 | func (w *PenWriter) Close() error { 81 | if !w.style.Empty() { 82 | _, _ = w.w.Write([]byte(ansi.ResetStyle)) 83 | } 84 | if !w.link.Empty() { 85 | _, _ = w.w.Write([]byte(ansi.ResetHyperlink())) 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /cellbuf/style.go: -------------------------------------------------------------------------------- 1 | package cellbuf 2 | 3 | import ( 4 | "github.com/charmbracelet/colorprofile" 5 | ) 6 | 7 | // Convert converts a style to respect the given color profile. 8 | func ConvertStyle(s Style, p colorprofile.Profile) Style { 9 | switch p { 10 | case colorprofile.TrueColor: 11 | return s 12 | case colorprofile.Ascii: 13 | s.Fg = nil 14 | s.Bg = nil 15 | s.Ul = nil 16 | case colorprofile.NoTTY: 17 | return Style{} 18 | } 19 | 20 | if s.Fg != nil { 21 | s.Fg = p.Convert(s.Fg) 22 | } 23 | if s.Bg != nil { 24 | s.Bg = p.Convert(s.Bg) 25 | } 26 | if s.Ul != nil { 27 | s.Ul = p.Convert(s.Ul) 28 | } 29 | 30 | return s 31 | } 32 | -------------------------------------------------------------------------------- /cellbuf/utils.go: -------------------------------------------------------------------------------- 1 | package cellbuf 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Height returns the height of a string. 8 | func Height(s string) int { 9 | return strings.Count(s, "\n") + 1 10 | } 11 | 12 | func min(a, b int) int { //nolint:predeclared 13 | if a > b { 14 | return b 15 | } 16 | return a 17 | } 18 | 19 | func max(a, b int) int { //nolint:predeclared 20 | if a > b { 21 | return a 22 | } 23 | return b 24 | } 25 | 26 | func clamp(v, low, high int) int { 27 | if high < low { 28 | low, high = high, low 29 | } 30 | return min(high, max(low, v)) 31 | } 32 | 33 | func abs(a int) int { 34 | if a < 0 { 35 | return -a 36 | } 37 | return a 38 | } 39 | -------------------------------------------------------------------------------- /colors/colors.go: -------------------------------------------------------------------------------- 1 | package colors 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | //nolint:revive 6 | var ( 7 | WhiteBright = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"} 8 | 9 | Normal = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#dddddd"} 10 | NormalDim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"} 11 | 12 | Gray = lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"} 13 | GrayMid = lipgloss.AdaptiveColor{Light: "#B2B2B2", Dark: "#4A4A4A"} 14 | GrayDark = lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#222222"} 15 | GrayBright = lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"} 16 | GrayBrightDim = lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"} 17 | 18 | Indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} 19 | IndigoDim = lipgloss.AdaptiveColor{Light: "#9498FF", Dark: "#494690"} 20 | IndigoSubtle = lipgloss.AdaptiveColor{Light: "#7D79F6", Dark: "#514DC1"} 21 | IndigoSubtleDim = lipgloss.AdaptiveColor{Light: "#BBBDFF", Dark: "#383584"} 22 | 23 | YellowGreen = lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"} 24 | YellowGreenDull = lipgloss.AdaptiveColor{Light: "#6BCB94", Dark: "#9BA92F"} 25 | 26 | Fuschia = lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"} 27 | FuchsiaDim = lipgloss.AdaptiveColor{Light: "#F1A8FF", Dark: "#99519E"} 28 | FuchsiaDull = lipgloss.AdaptiveColor{Dark: "#AD58B4", Light: "#F793FF"} 29 | FuchsiaDullDim = lipgloss.AdaptiveColor{Light: "#F6C9FF", Dark: "#6B3A6F"} 30 | 31 | Green = lipgloss.Color("#04B575") 32 | GreenDim = lipgloss.AdaptiveColor{Light: "#72D2B0", Dark: "#0B5137"} 33 | 34 | Red = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"} 35 | RedDull = lipgloss.AdaptiveColor{Light: "#FF6F91", Dark: "#C74665"} 36 | ) 37 | -------------------------------------------------------------------------------- /colors/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/colors 2 | 3 | go 1.23.0 4 | 5 | require github.com/charmbracelet/lipgloss v1.0.0 6 | 7 | require ( 8 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 9 | github.com/charmbracelet/x/ansi v0.4.2 // indirect 10 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 11 | github.com/mattn/go-isatty v0.0.20 // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/muesli/termenv v0.15.2 // indirect 14 | github.com/rivo/uniseg v0.4.7 // indirect 15 | golang.org/x/sys v0.26.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /colors/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 | github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= 4 | github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= 5 | github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= 6 | github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 7 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 8 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 9 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 10 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 11 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 12 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 13 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 14 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 15 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 16 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 17 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 18 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 20 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 21 | -------------------------------------------------------------------------------- /conpty/conpty_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package conpty 5 | 6 | // ConPty represents a Windows Console Pseudo-terminal. 7 | // https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-the-communication-channels 8 | type ConPty struct{} 9 | 10 | // New creates a new ConPty. 11 | // This function is not supported on non-Windows platforms. 12 | func New(int, int, int) (*ConPty, error) { 13 | return nil, ErrUnsupported 14 | } 15 | 16 | // Size returns the size of the ConPty. 17 | func (*ConPty) Size() (int, int, error) { 18 | return 0, 0, ErrUnsupported 19 | } 20 | 21 | // Close closes the ConPty. 22 | func (*ConPty) Close() error { 23 | return ErrUnsupported 24 | } 25 | 26 | // Fd returns the file descriptor of the ConPty. 27 | func (*ConPty) Fd() uintptr { 28 | return 0 29 | } 30 | 31 | // Read implements io.Reader. 32 | func (*ConPty) Read([]byte) (int, error) { 33 | return 0, ErrUnsupported 34 | } 35 | 36 | // Write implements io.Writer. 37 | func (*ConPty) Write([]byte) (int, error) { 38 | return 0, ErrUnsupported 39 | } 40 | 41 | // Resize resizes the ConPty. 42 | func (*ConPty) Resize(int, int) error { 43 | return ErrUnsupported 44 | } 45 | -------------------------------------------------------------------------------- /conpty/doc.go: -------------------------------------------------------------------------------- 1 | // Package conpty implements Windows Console Pseudo-terminal support. 2 | // 3 | // https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session 4 | package conpty 5 | 6 | import "errors" 7 | 8 | // ErrUnsupported is returned when the current platform is not supported. 9 | var ErrUnsupported = errors.New("conpty: unsupported platform") 10 | -------------------------------------------------------------------------------- /conpty/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/conpty 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 7 | golang.org/x/sys v0.33.0 8 | ) 9 | -------------------------------------------------------------------------------- /conpty/go.sum: -------------------------------------------------------------------------------- 1 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= 2 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= 3 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 4 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 5 | -------------------------------------------------------------------------------- /editor/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/editor 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /errors/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/errors 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /errors/join.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "strings" 4 | 5 | // Join returns an error that wraps the given errors. 6 | // Any nil error values are discarded. 7 | // Join returns nil if every value in errs is nil. 8 | // The error formats as the concatenation of the strings obtained 9 | // by calling the Error method of each element of errs, with a newline 10 | // between each string. 11 | // 12 | // A non-nil error returned by Join implements the Unwrap() []error method. 13 | // 14 | // This is copied from Go 1.20 errors.Unwrap, with some tuning to avoid using unsafe. 15 | // The main goal is to have this available in older Go versions. 16 | func Join(errs ...error) error { 17 | var nonNil []error 18 | for _, err := range errs { 19 | if err == nil { 20 | continue 21 | } 22 | nonNil = append(nonNil, err) 23 | } 24 | if len(nonNil) == 0 { 25 | return nil 26 | } 27 | return &joinError{ 28 | errs: nonNil, 29 | } 30 | } 31 | 32 | type joinError struct { 33 | errs []error 34 | } 35 | 36 | func (e *joinError) Error() string { 37 | strs := make([]string, 0, len(e.errs)) 38 | for _, err := range e.errs { 39 | strs = append(strs, err.Error()) 40 | } 41 | return strings.Join(strs, "\n") 42 | } 43 | 44 | func (e *joinError) Unwrap() []error { 45 | return e.errs 46 | } 47 | -------------------------------------------------------------------------------- /errors/join_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestJoin(t *testing.T) { 9 | t.Run("nil", func(t *testing.T) { 10 | err := Join(nil, nil, nil) 11 | if err != nil { 12 | t.Errorf("expected nil, got %v", err) 13 | } 14 | }) 15 | t.Run("one err", func(t *testing.T) { 16 | expected := fmt.Errorf("fake") 17 | err := Join(nil, expected, nil) 18 | je := err.(*joinError) 19 | un := je.Unwrap() 20 | if len(un) != 1 { 21 | t.Fatalf("expected 1 err, got %d", len(un)) 22 | } 23 | if s := un[0].Error(); s != expected.Error() { 24 | t.Errorf("expected %v, got %v", expected, un[0]) 25 | } 26 | if s := err.Error(); s != expected.Error() { 27 | t.Errorf("expected %s, got %s", expected, err) 28 | } 29 | }) 30 | t.Run("many errs", func(t *testing.T) { 31 | expected1 := fmt.Errorf("fake 1") 32 | expected2 := fmt.Errorf("fake 2") 33 | err := Join(nil, expected1, nil, nil, expected2, nil) 34 | je := err.(*joinError) 35 | un := je.Unwrap() 36 | if len(un) != 2 { 37 | t.Fatalf("expected 2 err, got %d", len(un)) 38 | } 39 | if s := un[0].Error(); s != expected1.Error() { 40 | t.Errorf("expected %v, got %v", expected1, un[0]) 41 | } 42 | if s := un[1].Error(); s != expected2.Error() { 43 | t.Errorf("expected %v, got %v", expected2, un[1]) 44 | } 45 | expectedS := expected1.Error() + "\n" + expected2.Error() 46 | if s := err.Error(); s != expectedS { 47 | t.Errorf("expected %s, got %s", expectedS, err) 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /examples/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1f376439c75ab33392eb7a2f5ec999809493b378728d723327655c9fcb45cea9 3 | size 171876 4 | -------------------------------------------------------------------------------- /examples/cellbuf/winsize_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | func listenForResize(fn func()) { 13 | sig := make(chan os.Signal, 1) 14 | signal.Notify(sig, syscall.SIGWINCH) 15 | 16 | for range sig { 17 | fn() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/cellbuf/winsize_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | func listenForResize(func()) {} 7 | -------------------------------------------------------------------------------- /examples/faketty/main.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "syscall" 13 | 14 | "github.com/creack/pty" 15 | ) 16 | 17 | var ( 18 | rows int 19 | cols int 20 | ) 21 | 22 | func init() { 23 | flag.IntVar(&rows, "rows", 24, "number of rows") 24 | flag.IntVar(&cols, "cols", 80, "number of columns") 25 | flag.Parse() 26 | } 27 | 28 | func main() { 29 | if len(os.Args) < 2 { 30 | fmt.Fprintf(os.Stderr, "usage: %s [command] [args...]\n", os.Args[0]) 31 | fmt.Fprintf(os.Stderr, " %s -cols=80 -rows=24 [command] [args...]\n", os.Args[0]) 32 | 33 | os.Exit(1) 34 | } 35 | 36 | newStdin := os.Stdin 37 | newStderr := os.Stderr 38 | 39 | winsize := pty.Winsize{ 40 | Rows: uint16(rows), //nolint:gosec 41 | Cols: uint16(cols), //nolint:gosec 42 | } 43 | 44 | ptm1, pts1, err := pty.Open() 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "error creating pty: %v\n", err) 47 | os.Exit(1) 48 | } 49 | if err := pty.Setsize(ptm1, &winsize); err != nil { 50 | fmt.Fprintf(os.Stderr, "error setting pty size: %v\n", err) 51 | os.Exit(1) 52 | } 53 | 54 | go io.Copy(os.Stdout, ptm1) //nolint:errcheck 55 | 56 | newStdout := os.Stdout 57 | 58 | ptm2, pts2, err := pty.Open() 59 | if err != nil { 60 | fmt.Fprintf(os.Stderr, "error creating pty: %v\n", err) 61 | os.Exit(1) 62 | } 63 | 64 | if err := pty.Setsize(ptm2, &winsize); err != nil { 65 | fmt.Fprintf(os.Stderr, "error setting pty size: %v\n", err) 66 | os.Exit(1) 67 | } 68 | 69 | go io.Copy(newStderr, ptm2) //nolint:errcheck 70 | 71 | if err := syscall.Dup2(int(newStdin.Fd()), int(os.Stdin.Fd())); err != nil { 72 | fmt.Fprintf(os.Stderr, "error duplicating stdin file descriptor: %v\n", err) 73 | os.Exit(1) 74 | } 75 | if err := syscall.Dup2(int(newStdout.Fd()), int(os.Stdout.Fd())); err != nil { 76 | fmt.Fprintf(os.Stderr, "error duplicating stdout file descriptor: %v\n", err) 77 | os.Exit(1) 78 | } 79 | 80 | n := flag.NFlag() 81 | c := exec.Command(os.Args[n+1], os.Args[n+2:]...) //nolint:gosec 82 | c.Stdout = pts1 83 | c.Stderr = pts2 84 | c.Stdin = newStdin 85 | 86 | if err := c.Run(); err != nil { 87 | fmt.Fprintf(os.Stderr, "error running command: %v\n", err) 88 | os.Exit(1) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/examples 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc 7 | github.com/charmbracelet/lipgloss v1.1.0 8 | github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250125233033-58a153eb00e6 9 | github.com/charmbracelet/x/ansi v0.9.2 10 | github.com/charmbracelet/x/cellbuf v0.0.13 11 | github.com/charmbracelet/x/input v0.3.4 12 | github.com/charmbracelet/x/mosaic v0.0.0-20250313150240-c09addb0e197 13 | github.com/creack/pty v1.1.24 14 | github.com/lucasb-eyer/go-colorful v1.2.0 15 | ) 16 | 17 | require ( 18 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 19 | github.com/bits-and-blooms/bitset v1.22.0 // indirect 20 | github.com/charmbracelet/x/windows v0.2.0 // indirect 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/mattn/go-runewidth v0.0.16 // indirect 23 | github.com/muesli/termenv v0.16.0 // indirect 24 | ) 25 | 26 | require ( 27 | github.com/charmbracelet/x/term v0.2.1 28 | github.com/muesli/cancelreader v0.2.2 // indirect 29 | github.com/rivo/uniseg v0.4.7 30 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 31 | golang.org/x/image v0.25.0 // indirect 32 | golang.org/x/sys v0.30.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /examples/img2term/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "image" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | _ "image/jpeg" 12 | _ "image/png" 13 | 14 | "github.com/charmbracelet/x/ansi" 15 | "github.com/charmbracelet/x/ansi/sixel" 16 | ) 17 | 18 | // $ go run . ./../../ansi/fixtures/graphics/JigokudaniMonkeyPark.png 19 | func main() { 20 | flag.Parse() 21 | args := flag.Args() 22 | if len(args) == 0 { 23 | flag.Usage() 24 | os.Exit(1) 25 | } 26 | 27 | f, err := os.Open(args[0]) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | defer f.Close() //nolint:errcheck 33 | img, _, err := image.Decode(f) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | if _, err := writeSixel(os.Stdout, img); err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | 43 | func writeSixel(w io.Writer, img image.Image) (int, error) { 44 | var buf bytes.Buffer 45 | var e sixel.Encoder 46 | if err := e.Encode(&buf, img); err != nil { 47 | return 0, err 48 | } 49 | 50 | return io.WriteString(w, ansi.SixelGraphics(0, 1, 0, buf.Bytes())) 51 | } 52 | -------------------------------------------------------------------------------- /examples/mosaic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/jpeg" 7 | "os" 8 | 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/charmbracelet/x/mosaic" 11 | ) 12 | 13 | func main() { 14 | dogImg, err := loadImage("./pekinas.jpg") 15 | if err != nil { 16 | fmt.Print(err) 17 | os.Exit(1) 18 | } 19 | 20 | m := mosaic.New().Width(80).Height(40) 21 | 22 | fmt.Println(lipgloss.JoinVertical(lipgloss.Right, lipgloss.JoinHorizontal(lipgloss.Center, m.Render(dogImg)))) 23 | } 24 | 25 | func loadImage(path string) (image.Image, error) { 26 | f, err := os.Open(path) 27 | defer f.Close() //nolint:errcheck 28 | if err != nil { 29 | return nil, err 30 | } 31 | return jpeg.Decode(f) 32 | } 33 | -------------------------------------------------------------------------------- /examples/mosaic/pekinas.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:73b4eabeff12c4b09f1306ffa489a5b6d30ad2a1b8dc2bce882020c3be341665 3 | size 279891 4 | -------------------------------------------------------------------------------- /examples/pen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | 8 | "github.com/charmbracelet/x/ansi" 9 | "github.com/charmbracelet/x/cellbuf" 10 | ) 11 | 12 | func main() { 13 | pw := cellbuf.NewPenWriter(os.Stdout) 14 | defer pw.Close() 15 | 16 | data, err := io.ReadAll(os.Stdin) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | io.WriteString(pw, ansi.Wrap(string(data), 10, "")) 22 | } 23 | -------------------------------------------------------------------------------- /exp/charmtone/charmtone_test.go: -------------------------------------------------------------------------------- 1 | package charmtone 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lucasb-eyer/go-colorful" 7 | ) 8 | 9 | func TestValidateHexes(t *testing.T) { 10 | for _, key := range Keys() { 11 | if _, err := colorful.Hex(key.Hex()); err != nil { 12 | t.Errorf("Key %s: %v", key, err) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /exp/charmtone/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/charmtone 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c 7 | github.com/lucasb-eyer/go-colorful v1.2.0 8 | ) 9 | 10 | require ( 11 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 12 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 13 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 14 | github.com/charmbracelet/x/term v0.2.1 // indirect 15 | github.com/mattn/go-runewidth v0.0.16 // indirect 16 | github.com/muesli/cancelreader v0.2.2 // indirect 17 | github.com/rivo/uniseg v0.4.7 // indirect 18 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 19 | golang.org/x/sys v0.32.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /exp/golden/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/golden 2 | 3 | go 1.23.0 4 | 5 | require github.com/aymanbagabas/go-udiff v0.2.0 6 | -------------------------------------------------------------------------------- /exp/golden/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 2 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 3 | -------------------------------------------------------------------------------- /exp/golden/golden_test.go: -------------------------------------------------------------------------------- 1 | package golden 2 | 3 | import "testing" 4 | 5 | func TestRequireEqualUpdate(t *testing.T) { 6 | *update = true 7 | RequireEqual(t, []byte("test")) 8 | } 9 | 10 | func TestRequireEqualNoUpdate(t *testing.T) { 11 | *update = false 12 | RequireEqual(t, []byte("test")) 13 | } 14 | 15 | func TestRequireWithLineBreaks(t *testing.T) { 16 | *update = false 17 | RequireEqual(t, []byte("foo\nbar\nbaz\n")) 18 | } 19 | -------------------------------------------------------------------------------- /exp/golden/testdata/TestRequireEqualNoUpdate.golden: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /exp/golden/testdata/TestRequireEqualUpdate.golden: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /exp/golden/testdata/TestRequireWithLineBreaks.golden: -------------------------------------------------------------------------------- 1 | foo 2 | bar 3 | baz 4 | -------------------------------------------------------------------------------- /exp/higherorder/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/higherorder 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /exp/higherorder/higherorder.go: -------------------------------------------------------------------------------- 1 | package higherorder 2 | 3 | // Foldl applies a function to each element of a list, starting from the left. 4 | // A single value is returned. 5 | func Foldl[A any](f func(x, y A) A, start A, list []A) A { 6 | for _, v := range list { 7 | start = f(start, v) 8 | } 9 | return start 10 | } 11 | 12 | // Foldr applies a function to each element of a list, starting from the right. 13 | // A single value is returned. 14 | func Foldr[A any](f func(x, y A) A, start A, list []A) A { 15 | for i := len(list) - 1; i >= 0; i-- { 16 | start = f(start, list[i]) 17 | } 18 | return start 19 | } 20 | 21 | // Map applies a given function to each element of a list, returning a new list. 22 | func Map[A, B any](f func(A) B, list []A) []B { 23 | res := make([]B, len(list)) 24 | for i, v := range list { 25 | res[i] = f(v) 26 | } 27 | return res 28 | } 29 | 30 | // Filter applies a function to each element of a list, if the function returns false those elements are removed, returning a new list 31 | func Filter[A any](f func(A) bool, list []A) []A { 32 | res := make([]A, 0) 33 | for _, v := range list { 34 | if f(v) { 35 | res = append(res, v) 36 | } 37 | } 38 | return res 39 | } 40 | -------------------------------------------------------------------------------- /exp/higherorder/higherorder_test.go: -------------------------------------------------------------------------------- 1 | package higherorder 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func Test_Foldl(t *testing.T) { 10 | x := Foldl(func(a, b int) int { 11 | return a + b 12 | }, 0, []int{1, 2, 3}) 13 | 14 | const expect = 6 15 | if x != expect { 16 | t.Errorf("Expected %d, got %d", expect, x) 17 | } 18 | } 19 | 20 | func Test_Foldr(t *testing.T) { 21 | x := Foldl(func(a, b int) int { 22 | return a - b 23 | }, 6, []int{1, 2, 3}) 24 | 25 | const expect = 0 26 | if x != expect { 27 | t.Errorf("Expected %d, got %d", expect, x) 28 | } 29 | } 30 | 31 | func Test_Map(t *testing.T) { 32 | { 33 | // Map over ints, returning the square of each int. 34 | // (Take ints, return ints.) 35 | x := Map(func(a int) int { 36 | return a * a 37 | }, []int{2, 3, 4}) 38 | 39 | expected := []int{4, 9, 16} 40 | for i, v := range x { 41 | if v != expected[i] { 42 | t.Errorf("Index %d: expected %d, got %d", i, expected[i], v) 43 | } 44 | } 45 | } 46 | { 47 | // Map over strings, returning the length of each string. 48 | // (Take ints, return strings.) 49 | x := Map(func(a string) int { 50 | return len([]rune(a)) 51 | }, []string{"one", "two", "three"}) 52 | 53 | expected := []int{3, 3, 5} 54 | for i, v := range x { 55 | if v != expected[i] { 56 | t.Errorf("Index %d: expected %d, got %d", i, expected[i], v) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func Test_Filter(t *testing.T) { 63 | t.Run("with string slices", func(t *testing.T) { 64 | got := Filter(func(a string) bool { 65 | return strings.HasPrefix(a, "t") 66 | }, []string{"one", "two", "three"}) 67 | 68 | want := []string{"two", "three"} 69 | 70 | if !reflect.DeepEqual(got, want) { 71 | t.Errorf("Expected %v, got %v", want, got) 72 | } 73 | }) 74 | 75 | t.Run("with int slices", func(t *testing.T) { 76 | got := Filter(func(a int) bool { 77 | return a%2 == 0 78 | }, []int{1, 2, 3, 4, 5}) 79 | 80 | want := []int{2, 4} 81 | 82 | if !reflect.DeepEqual(got, want) { 83 | t.Errorf("Expected %v, got %v", want, got) 84 | } 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /exp/maps/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/maps 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /exp/maps/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charmbracelet/x/6ba1785cd7b9b808c0b2ba21c6aa42649859ce28/exp/maps/go.sum -------------------------------------------------------------------------------- /exp/maps/maps.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | import ( 4 | "cmp" 5 | "slices" 6 | ) 7 | 8 | // SortedKeys returns the keys of the map m. 9 | // The keys will be sorted. 10 | func SortedKeys[M ~map[K]V, K cmp.Ordered, V any](m M) []K { 11 | r := Keys(m) 12 | slices.Sort(r) 13 | return r 14 | } 15 | 16 | // Keys returns the keys of the map m. 17 | func Keys[M ~map[K]V, K cmp.Ordered, V any](m M) []K { 18 | r := make([]K, 0, len(m)) 19 | for k := range m { 20 | r = append(r, k) 21 | } 22 | return r 23 | } 24 | -------------------------------------------------------------------------------- /exp/maps/maps_test.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestSortedKeys(t *testing.T) { 9 | m := map[string]int{ 10 | "foo": 1, 11 | "bar": 10, 12 | "aaaaa": 11, 13 | } 14 | 15 | keys := SortedKeys(m) 16 | if slices.Compare(keys, []string{"aaaaa", "bar", "foo"}) != 0 { 17 | t.Fatalf("unexpected keys order: %v", keys) 18 | } 19 | } 20 | 21 | func TestKeys(t *testing.T) { 22 | m := map[string]int{ 23 | "foo": 1, 24 | "bar": 10, 25 | "aaaaa": 11, 26 | } 27 | 28 | keys := Keys(m) 29 | for _, s := range []string{"aaaaa", "bar", "foo"} { 30 | if !slices.Contains(keys, s) { 31 | t.Fatalf("unexpected keys: %v", keys) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /exp/open/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/charmbracelet/x/exp/open" 8 | ) 9 | 10 | func main() { 11 | fmt.Println(open.Open(context.Background(), "https://charm.sh")) 12 | } 13 | -------------------------------------------------------------------------------- /exp/open/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/open 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /exp/open/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charmbracelet/x/6ba1785cd7b9b808c0b2ba21c6aa42649859ce28/exp/open/go.sum -------------------------------------------------------------------------------- /exp/open/open.go: -------------------------------------------------------------------------------- 1 | package open 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // ErrNotSupported occurs when no ways to open a file are found. 10 | var ErrNotSupported = errors.New("not supported") 11 | 12 | // Open the given input. 13 | func Open(ctx context.Context, input string) error { 14 | return With(ctx, "", input) 15 | } 16 | 17 | // With opens the given input using the given app. 18 | func With(ctx context.Context, app, input string) error { 19 | cmd := buildCmd(ctx, app, input) 20 | if cmd == nil { 21 | return ErrNotSupported 22 | } 23 | out, err := cmd.CombinedOutput() 24 | if err != nil { 25 | return fmt.Errorf("open: %w: %s", err, string(out)) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /exp/open/open_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package open 5 | 6 | import ( 7 | "context" 8 | "os/exec" 9 | ) 10 | 11 | func buildCmd(ctx context.Context, app, path string) *exec.Cmd { 12 | if _, err := exec.LookPath("open"); err == nil { 13 | var arg []string 14 | if app != "" { 15 | arg = append(arg, "-a", app) 16 | } 17 | arg = append(arg, path) 18 | return exec.CommandContext(ctx, "open", arg...) 19 | } 20 | if app != "" { 21 | return exec.CommandContext(ctx, app, path) 22 | } 23 | if _, err := exec.LookPath("xdg-open"); err == nil { 24 | return exec.CommandContext(ctx, "xdg-open", path) 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /exp/open/open_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package open 5 | 6 | import ( 7 | "context" 8 | "os/exec" 9 | ) 10 | 11 | func buildCmd(ctx context.Context, app, path string) *exec.Cmd { 12 | if app != "" { 13 | return exec.Command("cmd", "/C", "start", "", app, path) 14 | } 15 | return exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", path) 16 | } 17 | -------------------------------------------------------------------------------- /exp/ordered/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/ordered 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /exp/ordered/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charmbracelet/x/6ba1785cd7b9b808c0b2ba21c6aa42649859ce28/exp/ordered/go.sum -------------------------------------------------------------------------------- /exp/ordered/ordered.go: -------------------------------------------------------------------------------- 1 | package ordered 2 | 3 | import "cmp" 4 | 5 | // Min returns the smaller of a and b. 6 | func Min[T cmp.Ordered](a, b T) T { 7 | if a < b { 8 | return a 9 | } 10 | return b 11 | } 12 | 13 | // Max returns the larger of a and b. 14 | func Max[T cmp.Ordered](a, b T) T { 15 | if a > b { 16 | return a 17 | } 18 | return b 19 | } 20 | 21 | // Clamp returns a value clamped between the given low and high values. 22 | func Clamp[T cmp.Ordered](n, low, high T) T { 23 | if low > high { 24 | low, high = high, low 25 | } 26 | return Min(high, Max(low, n)) 27 | } 28 | 29 | // First returns the first non-default value of a fixed number of 30 | // arguments of [cmp.Ordered] types. 31 | func First[T cmp.Ordered](x T, y ...T) T { 32 | var empty T 33 | if x != empty { 34 | return x 35 | } 36 | for _, s := range y { 37 | if s != empty { 38 | return s 39 | } 40 | } 41 | return empty 42 | } 43 | -------------------------------------------------------------------------------- /exp/slice/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/slice 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /exp/slice/slice.go: -------------------------------------------------------------------------------- 1 | // Package slice provides utility functions for working with slices in Go. 2 | package slice 3 | 4 | import ( 5 | "slices" 6 | ) 7 | 8 | // GroupBy groups a slice of items by a key function. 9 | func GroupBy[T any, K comparable](list []T, key func(T) K) map[K][]T { 10 | groups := make(map[K][]T) 11 | 12 | for _, item := range list { 13 | k := key(item) 14 | groups[k] = append(groups[k], item) 15 | } 16 | 17 | return groups 18 | } 19 | 20 | // Take returns the first n elements of the given slice. If there are not 21 | // enough elements in the slice, the whole slice is returned. 22 | func Take[A any](slice []A, n int) []A { 23 | if n > len(slice) { 24 | return slice 25 | } 26 | return slice[:n] 27 | } 28 | 29 | // Last returns the last element of a slice and true. If the slice is empty, it 30 | // returns the zero value and false. 31 | func Last[T any](list []T) (T, bool) { 32 | if len(list) == 0 { 33 | var zero T 34 | return zero, false 35 | } 36 | return list[len(list)-1], true 37 | } 38 | 39 | // Uniq returns a new slice with all duplicates removed. 40 | func Uniq[T comparable](list []T) []T { 41 | seen := make(map[T]struct{}, len(list)) 42 | uniqList := make([]T, 0, len(list)) 43 | 44 | for _, item := range list { 45 | if _, ok := seen[item]; !ok { 46 | seen[item] = struct{}{} 47 | uniqList = append(uniqList, item) 48 | } 49 | } 50 | 51 | return uniqList 52 | } 53 | 54 | // Intersperse puts an item between each element of a slice, returning a new 55 | // slice. 56 | func Intersperse[T any](slice []T, insert T) []T { 57 | if len(slice) <= 1 { 58 | return slice 59 | } 60 | 61 | // Create a new slice with the required capacity. 62 | result := make([]T, len(slice)*2-1) 63 | 64 | for i := range slice { 65 | // Fill the new slice with original elements and the insertion string. 66 | result[i*2] = slice[i] 67 | 68 | // Add the insertion string between items (except the last one). 69 | if i < len(slice)-1 { 70 | result[i*2+1] = insert 71 | } 72 | } 73 | 74 | return result 75 | } 76 | 77 | // ContainsAny checks if any of the given values present in the list. 78 | func ContainsAny[T comparable](list []T, values ...T) bool { 79 | return slices.ContainsFunc(list, func(v T) bool { 80 | return slices.Contains(values, v) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /exp/strings/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/strings 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /exp/teatest/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/teatest 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/bubbletea v1.3.4 7 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a 8 | ) 9 | 10 | require ( 11 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 12 | github.com/aymanbagabas/go-udiff v0.2.0 // indirect 13 | github.com/charmbracelet/lipgloss v1.0.0 // indirect 14 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 15 | github.com/charmbracelet/x/term v0.2.1 // indirect 16 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 17 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mattn/go-localereader v0.0.1 // indirect 20 | github.com/mattn/go-runewidth v0.0.16 // indirect 21 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 22 | github.com/muesli/cancelreader v0.2.2 // indirect 23 | github.com/muesli/termenv v0.15.2 // indirect 24 | github.com/rivo/uniseg v0.4.7 // indirect 25 | golang.org/x/sync v0.11.0 // indirect 26 | golang.org/x/sys v0.30.0 // indirect 27 | golang.org/x/text v0.19.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /exp/teatest/send_test.go: -------------------------------------------------------------------------------- 1 | package teatest_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/x/exp/teatest" 11 | ) 12 | 13 | func TestAppSendToOtherProgram(t *testing.T) { 14 | m1 := &connectedModel{ 15 | name: "m1", 16 | } 17 | m2 := &connectedModel{ 18 | name: "m2", 19 | } 20 | 21 | tm1 := teatest.NewTestModel(t, m1, teatest.WithInitialTermSize(70, 30)) 22 | t.Cleanup(func() { 23 | if err := tm1.Quit(); err != nil { 24 | t.Fatal(err) 25 | } 26 | }) 27 | tm2 := teatest.NewTestModel(t, m2, teatest.WithInitialTermSize(70, 30)) 28 | t.Cleanup(func() { 29 | if err := tm2.Quit(); err != nil { 30 | t.Fatal(err) 31 | } 32 | }) 33 | m1.programs = append(m1.programs, tm2) 34 | m2.programs = append(m2.programs, tm1) 35 | 36 | tm1.Type("pp") 37 | tm2.Type("pppp") 38 | 39 | tm1.Type("q") 40 | tm2.Type("q") 41 | 42 | out1 := readBts(t, tm1.FinalOutput(t, teatest.WithFinalTimeout(time.Second))) 43 | out2 := readBts(t, tm2.FinalOutput(t, teatest.WithFinalTimeout(time.Second))) 44 | 45 | if string(out1) != string(out2) { 46 | t.Errorf("output of both models should be the same, got:\n%v\nand:\n%v\n", string(out1), string(out2)) 47 | } 48 | 49 | teatest.RequireEqualOutput(t, out1) 50 | } 51 | 52 | type connectedModel struct { 53 | name string 54 | programs []interface{ Send(tea.Msg) } 55 | msgs []string 56 | } 57 | 58 | type ping string 59 | 60 | func (m *connectedModel) Init() tea.Cmd { 61 | return nil 62 | } 63 | 64 | func (m *connectedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 65 | switch msg := msg.(type) { 66 | case tea.KeyMsg: 67 | switch msg.String() { 68 | case "p": 69 | send := ping("from " + m.name) 70 | m.msgs = append(m.msgs, string(send)) 71 | for _, p := range m.programs { 72 | p.Send(send) 73 | } 74 | fmt.Printf("sent ping %q to others\n", send) 75 | case "q": 76 | return m, tea.Quit 77 | } 78 | case ping: 79 | fmt.Printf("rcvd ping %q on %s\n", msg, m.name) 80 | m.msgs = append(m.msgs, string(msg)) 81 | } 82 | return m, nil 83 | } 84 | 85 | func (m *connectedModel) View() string { 86 | return "All pings:\n" + strings.Join(m.msgs, "\n") 87 | } 88 | -------------------------------------------------------------------------------- /exp/teatest/teatest_test.go: -------------------------------------------------------------------------------- 1 | package teatest 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "testing/iotest" 8 | "time" 9 | 10 | tea "github.com/charmbracelet/bubbletea" 11 | ) 12 | 13 | func TestWaitForErrorReader(t *testing.T) { 14 | err := doWaitFor(iotest.ErrReader(fmt.Errorf("fake")), func(bts []byte) bool { 15 | return true 16 | }, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond)) 17 | if err == nil { 18 | t.Fatal("expected an error, got nil") 19 | } 20 | if err.Error() != "WaitFor: fake" { 21 | t.Fatalf("unexpected error: %s", err.Error()) 22 | } 23 | } 24 | 25 | func TestWaitForTimeout(t *testing.T) { 26 | err := doWaitFor(strings.NewReader("nope"), func(bts []byte) bool { 27 | return false 28 | }, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond)) 29 | if err == nil { 30 | t.Fatal("expected an error, got nil") 31 | } 32 | if err.Error() != "WaitFor: condition not met after 1ms. Last output:\nnope" { 33 | t.Fatalf("unexpected error: %s", err.Error()) 34 | } 35 | } 36 | 37 | type m string 38 | 39 | func (m m) Init() tea.Cmd { return nil } 40 | func (m m) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } 41 | func (m m) View() string { return string(m) } 42 | 43 | func TestWaitFinishedWithTimeoutFn(t *testing.T) { 44 | tm := NewTestModel(t, m("a")) 45 | var timedOut bool 46 | tm.WaitFinished(t, WithFinalTimeout(time.Nanosecond), WithTimeoutFn(func(testing.TB) { 47 | timedOut = true 48 | })) 49 | if !timedOut { 50 | t.Fatal("expected timedOut to be set") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /exp/teatest/testdata/TestApp.golden: -------------------------------------------------------------------------------- 1 | [?25l[?2004h Hi. This program will exit in 10 seconds. To quit sooner press any key 2 | Hi. This program will exit in 9 seconds. To quit sooner press any key. 3 |  [?2004l[?25h[?1002l[?1003l[?1006l -------------------------------------------------------------------------------- /exp/teatest/testdata/TestAppSendToOtherProgram.golden: -------------------------------------------------------------------------------- 1 | [?25l[?2004h All pings: 2 | from m1 3 | from m1 4 | from m2 5 | from m2 6 | from m2 7 | from m2 [?2004l[?25h[?1002l[?1003l[?1006l -------------------------------------------------------------------------------- /exp/teatest/testdata/TestRequireEqualOutputUpdate.golden: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /exp/teatest/v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/exp/teatest/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1.0.20250420102230-7ecd51915026 7 | github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f 8 | ) 9 | 10 | require ( 11 | github.com/aymanbagabas/go-udiff v0.2.0 // indirect 12 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 13 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 14 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 15 | github.com/charmbracelet/x/input v0.3.4 // indirect 16 | github.com/charmbracelet/x/term v0.2.1 // indirect 17 | github.com/charmbracelet/x/windows v0.2.0 // indirect 18 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 19 | github.com/mattn/go-runewidth v0.0.16 // indirect 20 | github.com/muesli/cancelreader v0.2.2 // indirect 21 | github.com/rivo/uniseg v0.4.7 // indirect 22 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 23 | golang.org/x/sync v0.13.0 // indirect 24 | golang.org/x/sys v0.32.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /exp/teatest/v2/send_test.go: -------------------------------------------------------------------------------- 1 | package teatest_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | tea "github.com/charmbracelet/bubbletea/v2" 10 | "github.com/charmbracelet/x/exp/teatest/v2" 11 | ) 12 | 13 | func TestAppSendToOtherProgram(t *testing.T) { 14 | m1 := &connectedModel{ 15 | name: "m1", 16 | } 17 | m2 := &connectedModel{ 18 | name: "m2", 19 | } 20 | 21 | tm1 := teatest.NewTestModel(t, m1, teatest.WithInitialTermSize(70, 30)) 22 | t.Cleanup(func() { 23 | if err := tm1.Quit(); err != nil { 24 | t.Fatal(err) 25 | } 26 | }) 27 | tm2 := teatest.NewTestModel(t, m2, teatest.WithInitialTermSize(70, 30)) 28 | t.Cleanup(func() { 29 | if err := tm2.Quit(); err != nil { 30 | t.Fatal(err) 31 | } 32 | }) 33 | m1.programs = append(m1.programs, tm2) 34 | m2.programs = append(m2.programs, tm1) 35 | 36 | tm1.Type("pp") 37 | tm2.Type("pppp") 38 | 39 | tm1.Type("q") 40 | tm2.Type("q") 41 | 42 | out1 := readBts(t, tm1.FinalOutput(t, teatest.WithFinalTimeout(time.Second))) 43 | out2 := readBts(t, tm2.FinalOutput(t, teatest.WithFinalTimeout(time.Second))) 44 | 45 | if string(out1) != string(out2) { 46 | t.Errorf("output of both models should be the same, got:\n%v\nand:\n%v\n", string(out1), string(out2)) 47 | } 48 | 49 | teatest.RequireEqualOutput(t, out1) 50 | } 51 | 52 | type connectedModel struct { 53 | name string 54 | programs []interface{ Send(tea.Msg) } 55 | msgs []string 56 | } 57 | 58 | type ping string 59 | 60 | func (m *connectedModel) Init() tea.Cmd { 61 | return nil 62 | } 63 | 64 | func (m *connectedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 65 | switch msg := msg.(type) { 66 | case tea.KeyMsg: 67 | switch msg.String() { 68 | case "p": 69 | send := ping("from " + m.name) 70 | m.msgs = append(m.msgs, string(send)) 71 | for _, p := range m.programs { 72 | p.Send(send) 73 | } 74 | fmt.Printf("sent ping %q to others\n", send) 75 | case "q": 76 | return m, tea.Quit 77 | } 78 | case ping: 79 | fmt.Printf("rcvd ping %q on %s\n", msg, m.name) 80 | m.msgs = append(m.msgs, string(msg)) 81 | } 82 | return m, nil 83 | } 84 | 85 | func (m *connectedModel) View() string { 86 | return "All pings:\n" + strings.Join(m.msgs, "\n") 87 | } 88 | -------------------------------------------------------------------------------- /exp/teatest/v2/teatest_test.go: -------------------------------------------------------------------------------- 1 | package teatest 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "testing/iotest" 8 | "time" 9 | 10 | tea "github.com/charmbracelet/bubbletea/v2" 11 | ) 12 | 13 | func TestWaitForErrorReader(t *testing.T) { 14 | err := doWaitFor(iotest.ErrReader(fmt.Errorf("fake")), func(bts []byte) bool { 15 | return true 16 | }, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond)) 17 | if err == nil { 18 | t.Fatal("expected an error, got nil") 19 | } 20 | if err.Error() != "WaitFor: fake" { 21 | t.Fatalf("unexpected error: %s", err.Error()) 22 | } 23 | } 24 | 25 | func TestWaitForTimeout(t *testing.T) { 26 | err := doWaitFor(strings.NewReader("nope"), func(bts []byte) bool { 27 | return false 28 | }, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond)) 29 | if err == nil { 30 | t.Fatal("expected an error, got nil") 31 | } 32 | if err.Error() != "WaitFor: condition not met after 1ms. Last output:\nnope" { 33 | t.Fatalf("unexpected error: %s", err.Error()) 34 | } 35 | } 36 | 37 | type m string 38 | 39 | func (m m) Init() tea.Cmd { return nil } 40 | func (m m) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil } 41 | func (m m) View() string { return string(m) } 42 | 43 | func TestWaitFinishedWithTimeoutFn(t *testing.T) { 44 | tm := NewTestModel(t, m("a")) 45 | var timedOut bool 46 | tm.WaitFinished(t, WithFinalTimeout(time.Nanosecond), WithTimeoutFn(func(testing.TB) { 47 | timedOut = true 48 | })) 49 | if !timedOut { 50 | t.Fatal("expected timedOut to be set") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /exp/teatest/v2/testdata/TestApp.golden: -------------------------------------------------------------------------------- 1 | [?2004h[?25l Hi. This program will exit in 10 seconds. To quit sooner press any key 9 seconds. To quit sooner press any key. 2 | [?25h[?2004l 3 | -------------------------------------------------------------------------------- /exp/teatest/v2/testdata/TestAppSendToOtherProgram.golden: -------------------------------------------------------------------------------- 1 | [?2004h[?25l All pings: 2 | from m1 3 | from m1 4 | from m2 5 | from m2 6 | from m2 7 | from m2 [?25h[?2004l 8 | -------------------------------------------------------------------------------- /exp/teatest/v2/testdata/TestRequireEqualOutputUpdate.golden: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.0 2 | 3 | use ( 4 | ./ansi 5 | ./cellbuf 6 | ./colors 7 | ./conpty 8 | ./editor 9 | ./errors 10 | ./examples 11 | ./exp/charmtone 12 | ./exp/golden 13 | ./exp/higherorder 14 | ./exp/maps 15 | ./exp/open 16 | ./exp/ordered 17 | ./exp/slice 18 | ./exp/strings 19 | ./exp/teatest 20 | ./exp/teatest/v2 21 | ./input 22 | ./json 23 | ./mosaic 24 | ./sshkey 25 | ./term 26 | ./termios 27 | ./vt 28 | ./wcwidth 29 | ./windows 30 | ./xpty 31 | ) 32 | -------------------------------------------------------------------------------- /input/cancelreader_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package input 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/muesli/cancelreader" 10 | ) 11 | 12 | func newCancelreader(r io.Reader, _ int) (cancelreader.CancelReader, error) { 13 | return cancelreader.NewReader(r) //nolint:wrapcheck 14 | } 15 | -------------------------------------------------------------------------------- /input/clipboard.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import "github.com/charmbracelet/x/ansi" 4 | 5 | // ClipboardSelection represents a clipboard selection. The most common 6 | // clipboard selections are "system" and "primary" and selections. 7 | type ClipboardSelection = byte 8 | 9 | // Clipboard selections. 10 | const ( 11 | SystemClipboard ClipboardSelection = ansi.SystemClipboard 12 | PrimaryClipboard ClipboardSelection = ansi.PrimaryClipboard 13 | ) 14 | 15 | // ClipboardEvent is a clipboard read message event. This message is emitted when 16 | // a terminal receives an OSC52 clipboard read message event. 17 | type ClipboardEvent struct { 18 | Content string 19 | Selection ClipboardSelection 20 | } 21 | 22 | // String returns the string representation of the clipboard message. 23 | func (e ClipboardEvent) String() string { 24 | return e.Content 25 | } 26 | -------------------------------------------------------------------------------- /input/cursor.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import "image" 4 | 5 | // CursorPositionEvent represents a cursor position event. Where X is the 6 | // zero-based column and Y is the zero-based row. 7 | type CursorPositionEvent image.Point 8 | -------------------------------------------------------------------------------- /input/da1.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import "github.com/charmbracelet/x/ansi" 4 | 5 | // PrimaryDeviceAttributesEvent is an event that represents the terminal 6 | // primary device attributes. 7 | type PrimaryDeviceAttributesEvent []int 8 | 9 | func parsePrimaryDevAttrs(params ansi.Params) Event { 10 | // Primary Device Attributes 11 | da1 := make(PrimaryDeviceAttributesEvent, len(params)) 12 | for i, p := range params { 13 | if !p.HasMore() { 14 | da1[i] = p.Param(0) 15 | } 16 | } 17 | return da1 18 | } 19 | -------------------------------------------------------------------------------- /input/doc.go: -------------------------------------------------------------------------------- 1 | // Package input provides a set of utilities for handling input events in a 2 | // terminal environment. It includes support for reading input events, parsing 3 | // escape sequences, and handling clipboard events. 4 | // The package is designed to work with various terminal types and supports 5 | // customization through flags and options. 6 | package input 7 | -------------------------------------------------------------------------------- /input/driver_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package input 5 | 6 | // ReadEvents reads input events from the terminal. 7 | // 8 | // It reads the events available in the input buffer and returns them. 9 | func (d *Reader) ReadEvents() ([]Event, error) { 10 | return d.readEvents() 11 | } 12 | 13 | // parseWin32InputKeyEvent parses a Win32 input key events. This function is 14 | // only available on Windows. 15 | func (p *Parser) parseWin32InputKeyEvent(*win32InputState, uint16, uint16, rune, bool, uint32, uint16) Event { 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /input/driver_test.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkDriver(b *testing.B) { 10 | input := "\x1b\x1b[Ztest\x00\x1b]10;1234/1234/1234\x07\x1b[27;2;27~" 11 | rdr := strings.NewReader(input) 12 | drv, err := NewReader(rdr, "dumb", 0) 13 | if err != nil { 14 | b.Fatalf("could not create driver: %v", err) 15 | } 16 | 17 | b.ReportAllocs() 18 | b.ResetTimer() 19 | for i := 0; i < b.N; i++ { 20 | rdr.Reset(input) 21 | if _, err := drv.ReadEvents(); err != nil && err != io.EOF { 22 | b.Errorf("error reading input: %v", err) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /input/focus.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | // FocusEvent represents a terminal focus event. 4 | // This occurs when the terminal gains focus. 5 | type FocusEvent struct{} 6 | 7 | // BlurEvent represents a terminal blur event. 8 | // This occurs when the terminal loses focus. 9 | type BlurEvent struct{} 10 | -------------------------------------------------------------------------------- /input/focus_test.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFocus(t *testing.T) { 8 | var p Parser 9 | _, e := p.parseSequence([]byte("\x1b[I")) 10 | switch e.(type) { 11 | case FocusEvent: 12 | // ok 13 | default: 14 | t.Error("invalid sequence") 15 | } 16 | } 17 | 18 | func TestBlur(t *testing.T) { 19 | var p Parser 20 | _, e := p.parseSequence([]byte("\x1b[O")) 21 | switch e.(type) { 22 | case BlurEvent: 23 | // ok 24 | default: 25 | t.Error("invalid sequence") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /input/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/input 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/x/ansi v0.9.2 7 | github.com/charmbracelet/x/windows v0.2.1 8 | github.com/muesli/cancelreader v0.2.2 9 | github.com/rivo/uniseg v0.4.7 10 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e 11 | golang.org/x/sys v0.33.0 12 | ) 13 | 14 | require ( 15 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 16 | github.com/mattn/go-runewidth v0.0.16 // indirect 17 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /input/go.sum: -------------------------------------------------------------------------------- 1 | github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= 2 | github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 3 | github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I= 4 | github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M= 5 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 6 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 7 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 8 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 9 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 10 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 11 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 12 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 13 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 14 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 15 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 16 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 17 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 18 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 19 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 20 | -------------------------------------------------------------------------------- /input/input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Event represents a terminal event. 9 | type Event any 10 | 11 | // UnknownEvent represents an unknown event. 12 | type UnknownEvent string 13 | 14 | // String returns a string representation of the unknown event. 15 | func (e UnknownEvent) String() string { 16 | return fmt.Sprintf("%q", string(e)) 17 | } 18 | 19 | // MultiEvent represents multiple messages event. 20 | type MultiEvent []Event 21 | 22 | // String returns a string representation of the multiple messages event. 23 | func (e MultiEvent) String() string { 24 | var sb strings.Builder 25 | for _, ev := range e { 26 | sb.WriteString(fmt.Sprintf("%v\n", ev)) 27 | } 28 | return sb.String() 29 | } 30 | 31 | // WindowSizeEvent is used to report the terminal size. Note that Windows does 32 | // not have support for reporting resizes via SIGWINCH signals and relies on 33 | // the Windows Console API to report window size changes. 34 | type WindowSizeEvent struct { 35 | Width int 36 | Height int 37 | } 38 | 39 | // WindowOpEvent is a window operation (XTWINOPS) report event. This is used to 40 | // report various window operations such as reporting the window size or cell 41 | // size. 42 | type WindowOpEvent struct { 43 | Op int 44 | Args []int 45 | } 46 | -------------------------------------------------------------------------------- /input/mod.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | // KeyMod represents modifier keys. 4 | type KeyMod int 5 | 6 | // Modifier keys. 7 | const ( 8 | ModShift KeyMod = 1 << iota 9 | ModAlt 10 | ModCtrl 11 | ModMeta 12 | 13 | // These modifiers are used with the Kitty protocol. 14 | // XXX: Meta and Super are swapped in the Kitty protocol, 15 | // this is to preserve compatibility with XTerm modifiers. 16 | 17 | ModHyper 18 | ModSuper // Windows/Command keys 19 | 20 | // These are key lock states. 21 | 22 | ModCapsLock 23 | ModNumLock 24 | ModScrollLock // Defined in Windows API only 25 | ) 26 | 27 | // Contains reports whether m contains the given modifiers. 28 | // 29 | // Example: 30 | // 31 | // m := ModAlt | ModCtrl 32 | // m.Contains(ModCtrl) // true 33 | // m.Contains(ModAlt | ModCtrl) // true 34 | // m.Contains(ModAlt | ModCtrl | ModShift) // false 35 | func (m KeyMod) Contains(mods KeyMod) bool { 36 | return m&mods == mods 37 | } 38 | -------------------------------------------------------------------------------- /input/mode.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import "github.com/charmbracelet/x/ansi" 4 | 5 | // ModeReportEvent is a message that represents a mode report event (DECRPM). 6 | // 7 | // See: https://vt100.net/docs/vt510-rm/DECRPM.html 8 | type ModeReportEvent struct { 9 | // Mode is the mode number. 10 | Mode ansi.Mode 11 | 12 | // Value is the mode value. 13 | Value ansi.ModeSetting 14 | } 15 | -------------------------------------------------------------------------------- /input/parse_test.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "image/color" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/charmbracelet/x/ansi" 9 | ) 10 | 11 | func TestParseSequence_Events(t *testing.T) { 12 | input := []byte("\x1b\x1b[Ztest\x00\x1b]10;rgb:1234/1234/1234\x07\x1b[27;2;27~\x1b[?1049;2$y\x1b[4;1$y") 13 | want := []Event{ 14 | KeyPressEvent{Code: KeyTab, Mod: ModShift | ModAlt}, 15 | KeyPressEvent{Code: 't', Text: "t"}, 16 | KeyPressEvent{Code: 'e', Text: "e"}, 17 | KeyPressEvent{Code: 's', Text: "s"}, 18 | KeyPressEvent{Code: 't', Text: "t"}, 19 | KeyPressEvent{Code: KeySpace, Mod: ModCtrl}, 20 | ForegroundColorEvent{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}}, 21 | KeyPressEvent{Code: KeyEscape, Mod: ModShift}, 22 | ModeReportEvent{Mode: ansi.AltScreenSaveCursorMode, Value: ansi.ModeReset}, 23 | ModeReportEvent{Mode: ansi.InsertReplaceMode, Value: ansi.ModeSet}, 24 | } 25 | 26 | var p Parser 27 | for i := 0; len(input) != 0; i++ { 28 | if i >= len(want) { 29 | t.Fatalf("reached end of want events") 30 | } 31 | n, got := p.parseSequence(input) 32 | if !reflect.DeepEqual(got, want[i]) { 33 | t.Errorf("got %#v (%T), want %#v (%T)", got, got, want[i], want[i]) 34 | } 35 | input = input[n:] 36 | } 37 | } 38 | 39 | func BenchmarkParseSequence(b *testing.B) { 40 | var p Parser 41 | input := []byte("\x1b\x1b[Ztest\x00\x1b]10;1234/1234/1234\x07\x1b[27;2;27~") 42 | b.ReportAllocs() 43 | b.ResetTimer() 44 | for i := 0; i < b.N; i++ { 45 | p.parseSequence(input) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /input/paste.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | // PasteEvent is an message that is emitted when a terminal receives pasted text 4 | // using bracketed-paste. 5 | type PasteEvent string 6 | 7 | // PasteStartEvent is an message that is emitted when the terminal starts the 8 | // bracketed-paste text. 9 | type PasteStartEvent struct{} 10 | 11 | // PasteEndEvent is an message that is emitted when the terminal ends the 12 | // bracketed-paste text. 13 | type PasteEndEvent struct{} 14 | -------------------------------------------------------------------------------- /input/termcap.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | // CapabilityEvent represents a Termcap/Terminfo response event. Termcap 10 | // responses are generated by the terminal in response to RequestTermcap 11 | // (XTGETTCAP) requests. 12 | // 13 | // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands 14 | type CapabilityEvent string 15 | 16 | func parseTermcap(data []byte) CapabilityEvent { 17 | // XTGETTCAP 18 | if len(data) == 0 { 19 | return CapabilityEvent("") 20 | } 21 | 22 | var tc strings.Builder 23 | split := bytes.Split(data, []byte{';'}) 24 | for _, s := range split { 25 | parts := bytes.SplitN(s, []byte{'='}, 2) 26 | if len(parts) == 0 { 27 | return CapabilityEvent("") 28 | } 29 | 30 | name, err := hex.DecodeString(string(parts[0])) 31 | if err != nil || len(name) == 0 { 32 | continue 33 | } 34 | 35 | var value []byte 36 | if len(parts) > 1 { 37 | value, err = hex.DecodeString(string(parts[1])) 38 | if err != nil { 39 | continue 40 | } 41 | } 42 | 43 | if tc.Len() > 0 { 44 | tc.WriteByte(';') 45 | } 46 | tc.WriteString(string(name)) 47 | if len(value) > 0 { 48 | tc.WriteByte('=') 49 | tc.WriteString(string(value)) 50 | } 51 | } 52 | 53 | return CapabilityEvent(tc.String()) 54 | } 55 | -------------------------------------------------------------------------------- /input/xterm.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/charmbracelet/x/ansi" 5 | ) 6 | 7 | func parseXTermModifyOtherKeys(params ansi.Params) Event { 8 | // XTerm modify other keys starts with ESC [ 27 ; ; ~ 9 | xmod, _, _ := params.Param(1, 1) 10 | xrune, _, _ := params.Param(2, 1) 11 | mod := KeyMod(xmod - 1) 12 | r := rune(xrune) 13 | 14 | switch r { 15 | case ansi.BS: 16 | return KeyPressEvent{Mod: mod, Code: KeyBackspace} 17 | case ansi.HT: 18 | return KeyPressEvent{Mod: mod, Code: KeyTab} 19 | case ansi.CR: 20 | return KeyPressEvent{Mod: mod, Code: KeyEnter} 21 | case ansi.ESC: 22 | return KeyPressEvent{Mod: mod, Code: KeyEscape} 23 | case ansi.DEL: 24 | return KeyPressEvent{Mod: mod, Code: KeyBackspace} 25 | } 26 | 27 | // CSI 27 ; ; ~ keys defined in XTerm modifyOtherKeys 28 | k := KeyPressEvent{Code: r, Mod: mod} 29 | if k.Mod <= ModShift { 30 | k.Text = string(r) 31 | } 32 | 33 | return k 34 | } 35 | 36 | // TerminalVersionEvent is a message that represents the terminal version. 37 | type TerminalVersionEvent string 38 | 39 | // ModifyOtherKeysEvent represents a modifyOtherKeys event. 40 | // 41 | // 0: disable 42 | // 1: enable mode 1 43 | // 2: enable mode 2 44 | // 45 | // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ 46 | // See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys 47 | type ModifyOtherKeysEvent uint8 48 | -------------------------------------------------------------------------------- /json/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/json 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | // Package json provides functions to facilitate dealing with JSON. 2 | package json 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | // Reader takes an input, marshal it to JSON and returns a io.Reader of it. 13 | func Reader[T any](v T) io.Reader { 14 | bts, err := json.Marshal(v) 15 | if err != nil { 16 | return &ErrorReader{err} 17 | } 18 | return bytes.NewReader(bts) 19 | } 20 | 21 | // ErrorReader is a reader that always errors with the given error. 22 | type ErrorReader struct { 23 | err error 24 | } 25 | 26 | func (r *ErrorReader) Read(_ []byte) (int, error) { 27 | return 0, r.err 28 | } 29 | 30 | // From parses a io.Reader with JSON. 31 | func From[T any](r io.Reader, t T) (T, error) { 32 | bts, err := io.ReadAll(r) 33 | if err != nil { 34 | return t, fmt.Errorf("failed to read response: %w", err) 35 | } 36 | if err := json.Unmarshal(bts, &t); err != nil { 37 | return t, fmt.Errorf("failed to parse body: %w: %s", err, bts) 38 | } 39 | return t, nil 40 | } 41 | 42 | // Write writes the given data as JSON. 43 | func Write(w http.ResponseWriter, data any) error { 44 | bts, err := json.Marshal(data) 45 | if err != nil { 46 | return err 47 | } 48 | w.Header().Add("Content-Type", "application/json") 49 | _, err = w.Write(bts) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestReader(t *testing.T) { 11 | r := Reader(map[string]int{ 12 | "foo": 2, 13 | }) 14 | bts, err := io.ReadAll(r) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | if string(bts) != `{"foo":2}` { 19 | t.Fatalf("wrong json: %s", string(bts)) 20 | } 21 | } 22 | 23 | func TestFrom(t *testing.T) { 24 | in := map[string]int{"foo": 10, "bar": 20} 25 | m, err := From(Reader(in), map[string]int{}) 26 | if err != nil { 27 | t.Fatalf("unexpected err: %v", err) 28 | } 29 | if !reflect.DeepEqual(m, in) { 30 | t.Fatalf("maps should be equal: %v vs %v", in, m) 31 | } 32 | } 33 | 34 | func TestErrReader(t *testing.T) { 35 | err := fmt.Errorf("foo") 36 | _, err2 := io.ReadAll(&ErrorReader{err}) 37 | if err != err2 { 38 | t.Fatalf("expected same error") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mosaic/README.md: -------------------------------------------------------------------------------- 1 | # Mosaic 2 | 3 | Mosaic is a tool that allows you to display images in your terminal programs. It 4 | will break down your image to contain a certain number of pixels per cell, then 5 | render those cells. This works best with monospaced fonts. 6 | 7 | > [!NOTE] 8 | > We will be providing a more full-fledged implementation of image 9 | > support for Bubble Tea, but this package is one step in that direction. 10 | 11 | To use Mosaic, you need to... 12 | 13 | 1. Open an image file e.g. `f, err := os.Open(path)` 14 | 2. Decode the image e.g. `img, err := jpeg.Decode(f)` 15 | 3. Create a new Mosaic renderer e.g. `m := mosaic.New().Width(80).Height(40)` 16 | 4. Render the image with Mosaic! e.g. `m.Render(dogImg)` 17 | 18 | Here's a full-blown example: 19 | 20 | ``` go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "image" 26 | "image/jpeg" 27 | "os" 28 | 29 | "github.com/charmbracelet/lipgloss" 30 | "github.com/charmbracelet/x/mosaic" 31 | ) 32 | 33 | func main() { 34 | dogImg, err := loadImage("./pekinas.jpg") 35 | if err != nil { 36 | fmt.Print(err) 37 | os.Exit(1) 38 | } 39 | 40 | m := mosaic.New().Width(80).Height(40) 41 | 42 | fmt.Println(lipgloss.JoinVertical(lipgloss.Right, lipgloss.JoinHorizontal(lipgloss.Center, m.Render(dogImg)))) 43 | } 44 | 45 | func loadImage(path string) (image.Image, error) { 46 | f, err := os.Open(path) 47 | defer f.Close() 48 | if err != nil { 49 | return nil, err 50 | } 51 | return jpeg.Decode(f) 52 | } 53 | ``` 54 | 55 | Check out all of the mosaic [examples](https://github.com/charmbracelet/x/tree/main/examples/mosaic)! 56 | 57 | ## Feedback 58 | 59 | We'd love to hear your thoughts on this project. Feel free to drop us a note! 60 | 61 | - [Twitter](https://twitter.com/charmcli) 62 | - [The Fediverse](https://mastodon.social/@charmcli) 63 | - [Bluesky](https://bsky.app/profile/charm.sh) 64 | - [Discord](https://charm.sh/chat) 65 | 66 | ## License 67 | 68 | [MIT](https://github.com/charmbracelet/x/raw/main/LICENSE) 69 | 70 | --- 71 | 72 | Part of [Charm](https://charm.sh). 73 | 74 | The Charm logo 75 | 76 | Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة 77 | -------------------------------------------------------------------------------- /mosaic/fixtures/charm-wish.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b6beed501cdf17c8423644486f2010b69303e6228464652d83fe9b99c7aad7e9 3 | size 34322 4 | -------------------------------------------------------------------------------- /mosaic/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/mosaic 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/x/ansi v0.8.0 7 | golang.org/x/image v0.25.0 8 | ) 9 | 10 | require ( 11 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /mosaic/go.sum: -------------------------------------------------------------------------------- 1 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 2 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 3 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 4 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 5 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 6 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 7 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 8 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 9 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 10 | golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= 11 | golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= 12 | -------------------------------------------------------------------------------- /scripts/builds: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC2016 4 | find . -type f -name go.mod | sort | while read -r mod; do 5 | dir="$(dirname "$mod")" 6 | name="$(basename "$dir")" 7 | # get the parent directory when the module is nested semver-style 8 | if [[ "$name" =~ ^v[0-9]+.*$ ]]; then 9 | ver="$(basename "$dir")" 10 | dir="$(dirname "$dir")" 11 | name="$(basename "$dir")-$ver" 12 | fi 13 | 14 | sum="$dir/go.sum" 15 | echo "# auto-generated by scripts/builds. DO NOT EDIT. 16 | name: $name 17 | 18 | on: 19 | push: 20 | branches: 21 | - main 22 | pull_request: 23 | paths: 24 | - $(dirname "$mod" | cut -f2- -d/)/** 25 | - .github/workflows/${name}.yml 26 | 27 | jobs: 28 | build: 29 | strategy: 30 | matrix: 31 | os: [ubuntu-latest, macos-latest, windows-latest] 32 | runs-on: \${{ matrix.os }} 33 | defaults: 34 | run: 35 | working-directory: $(dirname "$mod") 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: actions/setup-go@v5 39 | with: 40 | go-version-file: $mod 41 | cache: true 42 | cache-dependency-path: $sum 43 | - run: go build -v ./... 44 | - run: go test -race -v ./... 45 | dependabot: 46 | needs: [build] 47 | runs-on: ubuntu-latest 48 | permissions: 49 | pull-requests: write 50 | contents: write 51 | if: \${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 52 | steps: 53 | - id: metadata 54 | uses: dependabot/fetch-metadata@v2 55 | with: 56 | github-token: \"\${{ secrets.GITHUB_TOKEN }}\" 57 | - run: | 58 | gh pr review --approve \"\$PR_URL\" 59 | gh pr merge --squash --auto \"\$PR_URL\" 60 | env: 61 | PR_URL: \${{github.event.pull_request.html_url}} 62 | GITHUB_TOKEN: \${{secrets.GITHUB_TOKEN}} 63 | lint: 64 | uses: charmbracelet/meta/.github/workflows/lint.yml@main 65 | with: 66 | directory: $(dirname "$mod")/... 67 | " >"./.github/workflows/${name}.yml" 68 | done 69 | -------------------------------------------------------------------------------- /scripts/dependabot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo '# auto-generated by scripts/dependabot. DO NOT EDIT. 4 | 5 | version: 2 6 | 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | day: "monday" 13 | time: "05:00" 14 | timezone: "America/New_York" 15 | labels: 16 | - "dependencies" 17 | commit-message: 18 | prefix: "chore" 19 | include: "scope"' >./.github/dependabot.yml 20 | 21 | find . -type f -name go.mod | cut -f2- -d'/' | sort | while read -r mod; do 22 | echo ' 23 | - package-ecosystem: "gomod" 24 | directory: "/'"$(dirname "$mod")"'" 25 | schedule: 26 | interval: "weekly" 27 | day: "monday" 28 | time: "05:00" 29 | timezone: "America/New_York" 30 | labels: 31 | - "dependencies" 32 | commit-message: 33 | prefix: "chore" 34 | include: "scope"' >>./.github/dependabot.yml 35 | done 36 | -------------------------------------------------------------------------------- /sshkey/_examples/key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCPsyZVkz 3 | HWCy4ydV8FbCRAAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAINPsF1vqqjVMhsYg 4 | 0wn2w7KXSkjn9OyY1p1WjCpl2k5tAAAAoB7O0zxl192uyIc5IRyCP8M+p2eLaQoYKMH52Y 5 | HcrvatVb06mJf8RkSVkZxht4iaO6qwUyF437UttUMIs5pUvLppHU6WPc2n8bdO8H5eQSGn 6 | tBcHH22x+cg/k52X0srqcjvBU5bzviz0b7Az5rJWhwb3Nl5n+HSVggD5r7X5Sqbc1DZl/A 7 | JOJHD9QIpPw+v8kfwevT9SZWRPHtEOzcFxbfY= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /sshkey/_examples/key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINPsF1vqqjVMhsYg0wn2w7KXSkjn9OyY1p1WjCpl2k5t carlos@darkstar 2 | -------------------------------------------------------------------------------- /sshkey/_examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/x/sshkey" 7 | ) 8 | 9 | func main() { 10 | // password is "asd". 11 | signer, err := sshkey.Open("./key") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | if signer != nil { 17 | fmt.Println("Key opened!") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sshkey/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/sshkey 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/huh v0.7.0 7 | golang.org/x/crypto v0.37.0 8 | ) 9 | 10 | require ( 11 | github.com/atotto/clipboard v0.1.4 // indirect 12 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 13 | github.com/catppuccin/go v0.3.0 // indirect 14 | github.com/charmbracelet/bubbles v0.21.0 // indirect 15 | github.com/charmbracelet/bubbletea v1.3.4 // indirect 16 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 17 | github.com/charmbracelet/lipgloss v1.1.0 // indirect 18 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 19 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 20 | github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect 21 | github.com/charmbracelet/x/term v0.2.1 // indirect 22 | github.com/dustin/go-humanize v1.0.1 // indirect 23 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 24 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/mattn/go-localereader v0.0.1 // indirect 27 | github.com/mattn/go-runewidth v0.0.16 // indirect 28 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 29 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 30 | github.com/muesli/cancelreader v0.2.2 // indirect 31 | github.com/muesli/termenv v0.16.0 // indirect 32 | github.com/rivo/uniseg v0.4.7 // indirect 33 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 34 | golang.org/x/sync v0.13.0 // indirect 35 | golang.org/x/sys v0.32.0 // indirect 36 | golang.org/x/text v0.24.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /term/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/term 2 | 3 | go 1.23.0 4 | 5 | require golang.org/x/sys v0.33.0 6 | -------------------------------------------------------------------------------- /term/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 2 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 3 | -------------------------------------------------------------------------------- /term/term.go: -------------------------------------------------------------------------------- 1 | // Package term provides a platform-independent interfaces for interacting with 2 | // Terminal and TTY devices. 3 | package term 4 | 5 | // State contains platform-specific state of a terminal. 6 | type State struct { 7 | state 8 | } 9 | 10 | // IsTerminal returns whether the given file descriptor is a terminal. 11 | func IsTerminal(fd uintptr) bool { 12 | return isTerminal(fd) 13 | } 14 | 15 | // MakeRaw puts the terminal connected to the given file descriptor into raw 16 | // mode and returns the previous state of the terminal so that it can be 17 | // restored. 18 | func MakeRaw(fd uintptr) (*State, error) { 19 | return makeRaw(fd) 20 | } 21 | 22 | // GetState returns the current state of a terminal which may be useful to 23 | // restore the terminal after a signal. 24 | func GetState(fd uintptr) (*State, error) { 25 | return getState(fd) 26 | } 27 | 28 | // SetState sets the given state of the terminal. 29 | func SetState(fd uintptr, state *State) error { 30 | return setState(fd, state) 31 | } 32 | 33 | // Restore restores the terminal connected to the given file descriptor to a 34 | // previous state. 35 | func Restore(fd uintptr, oldState *State) error { 36 | return restore(fd, oldState) 37 | } 38 | 39 | // GetSize returns the visible dimensions of the given terminal. 40 | // 41 | // These dimensions don't include any scrollback buffer height. 42 | func GetSize(fd uintptr) (width, height int, err error) { 43 | return getSize(fd) 44 | } 45 | 46 | // ReadPassword reads a line of input from a terminal without local echo. This 47 | // is commonly used for inputting passwords and other sensitive data. The slice 48 | // returned does not include the \n. 49 | func ReadPassword(fd uintptr) ([]byte, error) { 50 | return readPassword(fd) 51 | } 52 | -------------------------------------------------------------------------------- /term/term_other.go: -------------------------------------------------------------------------------- 1 | //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9 2 | // +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!zos,!windows,!solaris,!plan9 3 | 4 | package term 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | type state struct{} 12 | 13 | func isTerminal(fd uintptr) bool { 14 | return false 15 | } 16 | 17 | func makeRaw(fd uintptr) (*State, error) { 18 | return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 19 | } 20 | 21 | func getState(fd uintptr) (*State, error) { 22 | return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 23 | } 24 | 25 | func restore(fd uintptr, state *State) error { 26 | return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 27 | } 28 | 29 | func getSize(fd uintptr) (width, height int, err error) { 30 | return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 31 | } 32 | 33 | func setState(fd uintptr, state *State) error { 34 | return fmt.Errorf("terminal: SetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 35 | } 36 | 37 | func readPassword(fd uintptr) ([]byte, error) { 38 | return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 39 | } 40 | -------------------------------------------------------------------------------- /term/term_test.go: -------------------------------------------------------------------------------- 1 | package term_test 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/charmbracelet/x/term" 9 | ) 10 | 11 | func TestIsTerminalTempFile(t *testing.T) { 12 | file, err := os.CreateTemp("", "TestIsTerminalTempFile") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer os.Remove(file.Name()) 17 | defer file.Close() 18 | 19 | if term.IsTerminal(file.Fd()) { 20 | t.Fatalf("IsTerminal unexpectedly returned true for temporary file %s", file.Name()) 21 | } 22 | } 23 | 24 | func TestIsTerminalTerm(t *testing.T) { 25 | if runtime.GOOS != "linux" { 26 | t.Skipf("unknown terminal path for GOOS %v", runtime.GOOS) 27 | } 28 | file, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | defer file.Close() 33 | 34 | if !term.IsTerminal(file.Fd()) { 35 | t.Fatalf("IsTerminal unexpectedly returned false for terminal file %s", file.Name()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /term/term_unix_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 2 | // +build darwin dragonfly freebsd netbsd openbsd 3 | 4 | package term 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | const ( 9 | ioctlReadTermios = unix.TIOCGETA 10 | ioctlWriteTermios = unix.TIOCSETA 11 | ) 12 | -------------------------------------------------------------------------------- /term/term_unix_other.go: -------------------------------------------------------------------------------- 1 | //go:build aix || linux || solaris || zos 2 | // +build aix linux solaris zos 3 | 4 | package term 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | const ( 9 | ioctlReadTermios = unix.TCGETS 10 | ioctlWriteTermios = unix.TCSETS 11 | ) 12 | -------------------------------------------------------------------------------- /term/terminal.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // File represents a file that has a file descriptor and can be read from, 8 | // written to, and closed. 9 | type File interface { 10 | io.ReadWriteCloser 11 | Fd() uintptr 12 | } 13 | -------------------------------------------------------------------------------- /term/util.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "io" 5 | "runtime" 6 | ) 7 | 8 | // readPasswordLine reads from reader until it finds \n or io.EOF. 9 | // The slice returned does not include the \n. 10 | // readPasswordLine also ignores any \r it finds. 11 | // Windows uses \r as end of line. So, on Windows, readPasswordLine 12 | // reads until it finds \r and ignores any \n it finds during processing. 13 | func readPasswordLine(reader io.Reader) ([]byte, error) { 14 | var buf [1]byte 15 | var ret []byte 16 | 17 | for { 18 | n, err := reader.Read(buf[:]) 19 | if n > 0 { 20 | switch buf[0] { 21 | case '\b': 22 | if len(ret) > 0 { 23 | ret = ret[:len(ret)-1] 24 | } 25 | case '\n': 26 | if runtime.GOOS != "windows" { 27 | return ret, nil 28 | } 29 | // otherwise ignore \n 30 | case '\r': 31 | if runtime.GOOS == "windows" { 32 | return ret, nil 33 | } 34 | // otherwise ignore \r 35 | default: 36 | ret = append(ret, buf[0]) 37 | } 38 | continue 39 | } 40 | if err != nil { 41 | if err == io.EOF && len(ret) > 0 { 42 | return ret, nil 43 | } 44 | return ret, err //nolint:wrapcheck 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /termios/bit_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build netbsd || openbsd 2 | // +build netbsd openbsd 3 | 4 | package termios 5 | 6 | func speed(b uint32) int32 { return int32(b) } 7 | func bit(b uint32) uint32 { return b } 8 | -------------------------------------------------------------------------------- /termios/bit_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package termios 5 | 6 | func speed(b uint32) uint64 { return uint64(b) } 7 | func bit(b uint32) uint64 { return uint64(b) } 8 | -------------------------------------------------------------------------------- /termios/bit_other.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !netbsd && !openbsd 2 | // +build !darwin,!netbsd,!openbsd 3 | 4 | package termios 5 | 6 | func speed(b uint32) uint32 { return b } 7 | func bit(b uint32) uint32 { return b } 8 | -------------------------------------------------------------------------------- /termios/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/termios 2 | 3 | go 1.23.0 4 | 5 | require golang.org/x/sys v0.33.0 6 | -------------------------------------------------------------------------------- /termios/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 2 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 3 | -------------------------------------------------------------------------------- /termios/syscalls_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && netbsd && freebsd && netbsd 2 | // +build darwin,netbsd,freebsd,netbsd 3 | 4 | package term 5 | 6 | import "syscall" 7 | 8 | func init() { 9 | allCcOpts[STATUS] = syscall.VSTATUS 10 | allCcOpts[DSUSP] = syscall.VDSUSP 11 | } 12 | -------------------------------------------------------------------------------- /termios/syscalls_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package termios 5 | 6 | import "syscall" 7 | 8 | func init() { 9 | allLineOpts[IUTF8] = syscall.IUTF8 10 | } 11 | -------------------------------------------------------------------------------- /termios/syscalls_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package termios 5 | 6 | import "syscall" 7 | 8 | func init() { 9 | allCcOpts[SWTCH] = syscall.VSWTC 10 | allInputOpts[IUCLC] = syscall.IUCLC 11 | allLineOpts[IUTF8] = syscall.IUTF8 12 | allLineOpts[XCASE] = syscall.XCASE 13 | allOutputOpts[OLCUC] = syscall.OLCUC 14 | } 15 | -------------------------------------------------------------------------------- /termios/termios_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || netbsd || freebsd || openbsd || dragonfly 2 | // +build darwin netbsd freebsd openbsd dragonfly 3 | 4 | package termios 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | const ( 9 | ioctlGets = unix.TIOCGETA 10 | ioctlSets = unix.TIOCSETA 11 | ioctlGetWinSize = unix.TIOCGWINSZ 12 | ioctlSetWinSize = unix.TIOCSWINSZ 13 | ) 14 | -------------------------------------------------------------------------------- /termios/termios_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package termios 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | const ( 9 | ioctlGets = unix.TCGETS 10 | ioctlSets = unix.TCSETS 11 | ioctlGetWinSize = unix.TIOCGWINSZ 12 | ioctlSetWinSize = unix.TIOCSWINSZ 13 | ) 14 | -------------------------------------------------------------------------------- /termios/termios_other.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || netbsd || freebsd || openbsd || linux || dragonfly 2 | // +build darwin netbsd freebsd openbsd linux dragonfly 3 | 4 | package termios 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | func setSpeed(term *unix.Termios, ispeed, ospeed uint32) { 9 | term.Ispeed = speed(ispeed) 10 | term.Ospeed = speed(ospeed) 11 | } 12 | 13 | func getSpeed(term *unix.Termios) (uint32, uint32) { //nolint:unused 14 | return uint32(term.Ispeed), uint32(term.Ospeed) //nolint:gosec 15 | } 16 | -------------------------------------------------------------------------------- /termios/termios_solaris.go: -------------------------------------------------------------------------------- 1 | //go:build solaris 2 | // +build solaris 3 | 4 | package termios 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | // see https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c 9 | // see https://github.com/omniti-labs/illumos-omnios/blob/master/usr/src/uts/common/sys/termios.h 10 | const ( 11 | ioctlSets = unix.TCSETA 12 | ioctlGets = unix.TCGETA 13 | ioctlSetWinSize = (int('T') << 8) | 103 14 | ioctlGetWinSize = (int('T') << 8) | 104 15 | ) 16 | 17 | func setSpeed(*unix.Termios, uint32, uint32) { 18 | // TODO: support setting speed on Solaris? 19 | // see cfgetospeed(3C) and cfsetospeed(3C) 20 | // see cfgetispeed(3C) and cfsetispeed(3C) 21 | // https://github.com/omniti-labs/illumos-omnios/blob/master/usr/src/uts/common/sys/termios.h#L103 22 | } 23 | 24 | func getSpeed(*unix.Termios) (uint32, uint32) { 25 | return 0, 0 26 | } 27 | -------------------------------------------------------------------------------- /vt/buffer.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import "github.com/charmbracelet/x/cellbuf" 4 | 5 | // Buffer is a 2D grid of cells representing a screen or terminal. 6 | type Buffer = cellbuf.Buffer 7 | -------------------------------------------------------------------------------- /vt/callbacks.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import "github.com/charmbracelet/x/cellbuf" 4 | 5 | // Callbacks represents a set of callbacks for a terminal. 6 | type Callbacks struct { 7 | // Bell callback. When set, this function is called when a bell character is 8 | // received. 9 | Bell func() 10 | 11 | // Damage callback. When set, this function is called when a cell is damaged 12 | // or changed. 13 | Damage func(Damage) 14 | 15 | // Title callback. When set, this function is called when the terminal title 16 | // changes. 17 | Title func(string) 18 | 19 | // IconName callback. When set, this function is called when the terminal 20 | // icon name changes. 21 | IconName func(string) 22 | 23 | // AltScreen callback. When set, this function is called when the alternate 24 | // screen is activated or deactivated. 25 | AltScreen func(bool) 26 | 27 | // CursorPosition callback. When set, this function is called when the cursor 28 | // position changes. 29 | CursorPosition func(old, new cellbuf.Position) //nolint:predeclared,revive 30 | 31 | // CursorVisibility callback. When set, this function is called when the 32 | // cursor visibility changes. 33 | CursorVisibility func(visible bool) 34 | 35 | // CursorStyle callback. When set, this function is called when the cursor 36 | // style changes. 37 | CursorStyle func(style CursorStyle, blink bool) 38 | } 39 | -------------------------------------------------------------------------------- /vt/cc.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import ( 4 | "github.com/charmbracelet/x/ansi" 5 | "github.com/charmbracelet/x/cellbuf" 6 | ) 7 | 8 | // handleControl handles a control character. 9 | func (t *Terminal) handleControl(r byte) { 10 | if !t.handlers.handleCc(r) { 11 | t.logf("unhandled sequence: ControlCode %q", r) 12 | } 13 | } 14 | 15 | // linefeed is the same as [index], except that it respects [ansi.LNM] mode. 16 | func (t *Terminal) linefeed() { 17 | t.index() 18 | if t.isModeSet(ansi.LineFeedNewLineMode) { 19 | t.carriageReturn() 20 | } 21 | } 22 | 23 | // index moves the cursor down one line, scrolling up if necessary. This 24 | // always resets the phantom state i.e. pending wrap state. 25 | func (t *Terminal) index() { 26 | x, y := t.scr.CursorPosition() 27 | scroll := t.scr.ScrollRegion() 28 | // TODO: Handle scrollback whenever we add it. 29 | if y == scroll.Max.Y-1 && x >= scroll.Min.X && x < scroll.Max.X { 30 | t.scr.ScrollUp(1) 31 | } else if y < scroll.Max.Y-1 || !cellbuf.Pos(x, y).In(scroll) { 32 | t.scr.moveCursor(0, 1) 33 | } 34 | t.atPhantom = false 35 | } 36 | 37 | // horizontalTabSet sets a horizontal tab stop at the current cursor position. 38 | func (t *Terminal) horizontalTabSet() { 39 | x, _ := t.scr.CursorPosition() 40 | t.tabstops.Set(x) 41 | } 42 | 43 | // reverseIndex moves the cursor up one line, or scrolling down. This does not 44 | // reset the phantom state i.e. pending wrap state. 45 | func (t *Terminal) reverseIndex() { 46 | x, y := t.scr.CursorPosition() 47 | scroll := t.scr.ScrollRegion() 48 | if y == scroll.Min.Y && x >= scroll.Min.X && x < scroll.Max.X { 49 | t.scr.ScrollDown(1) 50 | } else { 51 | t.scr.moveCursor(0, -1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /vt/cell.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import ( 4 | "github.com/charmbracelet/x/cellbuf" 5 | ) 6 | 7 | // Cell represents a single cell in the terminal screen. 8 | type Cell = cellbuf.Cell 9 | 10 | // Link represents a hyperlink in the terminal screen. 11 | type Link = cellbuf.Link 12 | 13 | // Style represents the Style of a cell. 14 | type Style = cellbuf.Style 15 | 16 | // Rectangle represents a rectangle in the terminal screen. 17 | type Rectangle = cellbuf.Rectangle 18 | 19 | // Position represents a position in the terminal screen. 20 | type Position = cellbuf.Position 21 | -------------------------------------------------------------------------------- /vt/charset.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | // CharSet represents a character set designator. 4 | // This can be used to select a character set for G0 or G1 and others. 5 | type CharSet map[byte]string 6 | 7 | // Character sets. 8 | var ( 9 | UK = CharSet{ 10 | '$': "£", // U+00A3 11 | } 12 | SpecialDrawing = CharSet{ 13 | '`': "◆", // U+25C6 14 | 'a': "▒", // U+2592 15 | 'b': "␉", // U+2409 16 | 'c': "␌", // U+240C 17 | 'd': "␍", // U+240D 18 | 'e': "␊", // U+240A 19 | 'f': "°", // U+00B0 20 | 'g': "±", // U+00B1 21 | 'h': "␤", // U+2424 22 | 'i': "␋", // U+240B 23 | 'j': "┘", // U+2518 24 | 'k': "┐", // U+2510 25 | 'l': "┌", // U+250C 26 | 'm': "└", // U+2514 27 | 'n': "┼", // U+253C 28 | 'o': "⎺", // U+23BA 29 | 'p': "⎻", // U+23BB 30 | 'q': "─", // U+2500 31 | 'r': "⎼", // U+23BC 32 | 's': "⎽", // U+23BD 33 | 't': "├", // U+251C 34 | 'u': "┤", // U+2524 35 | 'v': "┴", // U+2534 36 | 'w': "┬", // U+252C 37 | 'x': "│", // U+2502 38 | 'y': "⩽", // U+2A7D 39 | 'z': "⩾", // U+2A7E 40 | '{': "π", // U+03C0 41 | '|': "≠", // U+2260 42 | '}': "£", // U+00A3 43 | '~': "·", // U+00B7 44 | } 45 | ) 46 | -------------------------------------------------------------------------------- /vt/csi.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/x/ansi" 8 | ) 9 | 10 | func (t *Terminal) handleCsi(cmd ansi.Cmd, params ansi.Params) { 11 | if !t.handlers.handleCsi(cmd, params) { 12 | t.logf("unhandled sequence: CSI %q", paramsString(cmd, params)) 13 | } 14 | } 15 | 16 | func (t *Terminal) handleRequestMode(params ansi.Params, isAnsi bool) { 17 | n, _, ok := params.Param(0, 0) 18 | if !ok || n == 0 { 19 | return 20 | } 21 | 22 | var mode ansi.Mode = ansi.DECMode(n) 23 | if isAnsi { 24 | mode = ansi.ANSIMode(n) 25 | } 26 | 27 | setting := t.modes[mode] 28 | t.buf.WriteString(ansi.ReportMode(mode, setting)) 29 | } 30 | 31 | func paramsString(cmd ansi.Cmd, params ansi.Params) string { 32 | var s strings.Builder 33 | if mark := cmd.Prefix(); mark != 0 { 34 | s.WriteByte(mark) 35 | } 36 | params.ForEach(-1, func(i, p int, more bool) { 37 | s.WriteString(fmt.Sprintf("%d", p)) 38 | if i < len(params)-1 { 39 | if more { 40 | s.WriteByte(':') 41 | } else { 42 | s.WriteByte(';') 43 | } 44 | } 45 | }) 46 | if inter := cmd.Intermediate(); inter != 0 { 47 | s.WriteByte(inter) 48 | } 49 | if final := cmd.Final(); final != 0 { 50 | s.WriteByte(final) 51 | } 52 | return s.String() 53 | } 54 | -------------------------------------------------------------------------------- /vt/csi_screen.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import "github.com/charmbracelet/x/cellbuf" 4 | 5 | // eraseCharacter erases n characters starting from the cursor position. It 6 | // does not move the cursor. This is equivalent to [ansi.ECH]. 7 | func (t *Terminal) eraseCharacter(n int) { 8 | x, y := t.scr.CursorPosition() 9 | rect := cellbuf.Rect(x, y, n, 1) 10 | t.scr.Fill(t.scr.blankCell(), rect) 11 | t.atPhantom = false 12 | // ECH does not move the cursor. 13 | } 14 | -------------------------------------------------------------------------------- /vt/csi_sgr.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import ( 4 | "github.com/charmbracelet/x/ansi" 5 | "github.com/charmbracelet/x/cellbuf" 6 | ) 7 | 8 | // handleSgr handles SGR escape sequences. 9 | // handleSgr handles Select Graphic Rendition (SGR) escape sequences. 10 | func (t *Terminal) handleSgr(params ansi.Params) { 11 | cellbuf.ReadStyle(params, &t.scr.cur.Pen) 12 | } 13 | -------------------------------------------------------------------------------- /vt/cursor.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | // CursorStyle represents a cursor style. 4 | type CursorStyle int 5 | 6 | // Cursor styles. 7 | const ( 8 | CursorBlock CursorStyle = iota 9 | CursorUnderline 10 | CursorBar 11 | ) 12 | 13 | // Cursor represents a cursor in a terminal. 14 | type Cursor struct { 15 | Pen Style 16 | Link Link 17 | 18 | Position 19 | 20 | Style CursorStyle 21 | Steady bool // Not blinking 22 | Hidden bool 23 | } 24 | -------------------------------------------------------------------------------- /vt/damage.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import "github.com/charmbracelet/x/cellbuf" 4 | 5 | // Damage represents a damaged area. 6 | type Damage interface { 7 | // Bounds returns the bounds of the damaged area. 8 | Bounds() Rectangle 9 | } 10 | 11 | // CellDamage represents a damaged cell. 12 | type CellDamage struct { 13 | X, Y int 14 | Width int 15 | } 16 | 17 | // Bounds returns the bounds of the damaged area. 18 | func (d CellDamage) Bounds() Rectangle { 19 | return cellbuf.Rect(d.X, d.Y, d.Width, 1) 20 | } 21 | 22 | // RectDamage represents a damaged rectangle. 23 | type RectDamage Rectangle 24 | 25 | // Bounds returns the bounds of the damaged area. 26 | func (d RectDamage) Bounds() Rectangle { 27 | return Rectangle(d) 28 | } 29 | 30 | // X returns the x-coordinate of the damaged area. 31 | func (d RectDamage) X() int { 32 | return Rectangle(d).Min.X 33 | } 34 | 35 | // Y returns the y-coordinate of the damaged area. 36 | func (d RectDamage) Y() int { 37 | return Rectangle(d).Min.Y 38 | } 39 | 40 | // Width returns the width of the damaged area. 41 | func (d RectDamage) Width() int { 42 | return Rectangle(d).Dx() 43 | } 44 | 45 | // Height returns the height of the damaged area. 46 | func (d RectDamage) Height() int { 47 | return Rectangle(d).Dy() 48 | } 49 | 50 | // ScreenDamage represents a damaged screen. 51 | type ScreenDamage struct { 52 | Width, Height int 53 | } 54 | 55 | // Bounds returns the bounds of the damaged area. 56 | func (d ScreenDamage) Bounds() Rectangle { 57 | return cellbuf.Rect(0, 0, d.Width, d.Height) 58 | } 59 | 60 | // MoveDamage represents a moved area. 61 | // The area is moved from the source to the destination. 62 | type MoveDamage struct { 63 | Src, Dst Rectangle 64 | } 65 | 66 | // ScrollDamage represents a scrolled area. 67 | // The area is scrolled by the given deltas. 68 | type ScrollDamage struct { 69 | Rectangle 70 | Dx, Dy int 71 | } 72 | -------------------------------------------------------------------------------- /vt/dcs.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import "github.com/charmbracelet/x/ansi" 4 | 5 | // handleDcs handles a DCS escape sequence. 6 | func (t *Terminal) handleDcs(cmd ansi.Cmd, params ansi.Params, data []byte) { 7 | if !t.handlers.handleDcs(cmd, params, data) { 8 | t.logf("unhandled sequence: DCS %q %q", paramsString(cmd, params), data) 9 | } 10 | } 11 | 12 | // handleApc handles an APC escape sequence. 13 | func (t *Terminal) handleApc(data []byte) { 14 | if !t.handlers.handleApc(data) { 15 | t.logf("unhandled sequence: APC %q", data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vt/esc.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import ( 4 | "github.com/charmbracelet/x/ansi" 5 | ) 6 | 7 | // handleEsc handles an escape sequence. 8 | func (t *Terminal) handleEsc(cmd ansi.Cmd) { 9 | if !t.handlers.handleEsc(int(cmd)) { 10 | var str string 11 | if inter := cmd.Intermediate(); inter != 0 { 12 | str += string(inter) + " " 13 | } 14 | if final := cmd.Final(); final != 0 { 15 | str += string(final) 16 | } 17 | t.logf("unhandled sequence: ESC %q", str) 18 | } 19 | } 20 | 21 | // fullReset performs a full terminal reset as in [ansi.RIS]. 22 | func (t *Terminal) fullReset() { 23 | t.scrs[0].Reset() 24 | t.scrs[1].Reset() 25 | t.resetTabStops() 26 | 27 | // TODO: Do we reset all modes here? Investigate. 28 | t.resetModes() 29 | 30 | t.gl, t.gr = 0, 1 31 | t.gsingle = 0 32 | t.charsets = [4]CharSet{} 33 | t.atPhantom = false 34 | } 35 | -------------------------------------------------------------------------------- /vt/focus.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import ( 4 | "github.com/charmbracelet/x/ansi" 5 | ) 6 | 7 | // Focus sends the terminal a focus event if focus events mode is enabled. 8 | // This is the opposite of [Blur]. 9 | func (t *Terminal) Focus() { 10 | t.focus(true) 11 | } 12 | 13 | // Blur sends the terminal a blur event if focus events mode is enabled. 14 | // This is the opposite of [Focus]. 15 | func (t *Terminal) Blur() { 16 | t.focus(false) 17 | } 18 | 19 | func (t *Terminal) focus(focus bool) { 20 | if mode, ok := t.modes[ansi.FocusEventMode]; ok && mode.IsSet() { 21 | if focus { 22 | t.buf.WriteString(ansi.Focus) 23 | } else { 24 | t.buf.WriteString(ansi.Blur) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vt/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/vt 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/x/ansi v0.9.2 7 | github.com/charmbracelet/x/cellbuf v0.0.11 8 | github.com/charmbracelet/x/input v0.3.4 9 | github.com/mattn/go-runewidth v0.0.16 10 | github.com/rivo/uniseg v0.4.7 11 | ) 12 | 13 | require ( 14 | github.com/charmbracelet/colorprofile v0.2.0 // indirect 15 | github.com/charmbracelet/x/term v0.2.1 // indirect 16 | github.com/charmbracelet/x/windows v0.2.0 // indirect 17 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 18 | github.com/muesli/cancelreader v0.2.2 // indirect 19 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 20 | golang.org/x/sys v0.30.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /vt/mode.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | import "github.com/charmbracelet/x/ansi" 4 | 5 | // resetModes resets all modes to their default values. 6 | func (t *Terminal) resetModes() { 7 | t.modes = map[ansi.Mode]ansi.ModeSetting{ 8 | // Recognized modes and their default values. 9 | ansi.CursorKeysMode: ansi.ModeReset, 10 | ansi.OriginMode: ansi.ModeReset, 11 | ansi.AutoWrapMode: ansi.ModeSet, 12 | ansi.X10MouseMode: ansi.ModeReset, 13 | ansi.LineFeedNewLineMode: ansi.ModeReset, 14 | ansi.TextCursorEnableMode: ansi.ModeSet, 15 | ansi.NumericKeypadMode: ansi.ModeReset, 16 | ansi.LeftRightMarginMode: ansi.ModeReset, 17 | ansi.NormalMouseMode: ansi.ModeReset, 18 | ansi.HighlightMouseMode: ansi.ModeReset, 19 | ansi.ButtonEventMouseMode: ansi.ModeReset, 20 | ansi.AnyEventMouseMode: ansi.ModeReset, 21 | ansi.FocusEventMode: ansi.ModeReset, 22 | ansi.SgrExtMouseMode: ansi.ModeReset, 23 | ansi.AltScreenMode: ansi.ModeReset, 24 | ansi.SaveCursorMode: ansi.ModeReset, 25 | ansi.AltScreenSaveCursorMode: ansi.ModeReset, 26 | ansi.BracketedPasteMode: ansi.ModeReset, 27 | } 28 | 29 | // Set mode effects. 30 | for mode, setting := range t.modes { 31 | t.setMode(mode, setting) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /vt/options.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | // Logger represents a logger interface. 4 | type Logger interface { 5 | Printf(format string, v ...any) 6 | } 7 | 8 | // Option is a terminal option. 9 | type Option func(*Terminal) 10 | 11 | // WithLogger returns an [Option] that sets the terminal's logger. 12 | // The logger is used for debugging and logging. 13 | // By default, the terminal does not log anything. 14 | // 15 | // Example: 16 | // 17 | // vterm := vt.NewTerminal(80, 24, vt.WithLogger(log.Default())) 18 | func WithLogger(logger Logger) Option { 19 | return func(t *Terminal) { 20 | t.logger = logger 21 | } 22 | } 23 | 24 | // logf logs a formatted message if the terminal has a logger. 25 | func (t *Terminal) logf(format string, v ...any) { 26 | if t.logger != nil { 27 | t.logger.Printf(format, v...) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vt/utils.go: -------------------------------------------------------------------------------- 1 | package vt 2 | 3 | func max(a, b int) int { //nolint:predeclared 4 | if a > b { 5 | return a 6 | } 7 | return b 8 | } 9 | 10 | func min(a, b int) int { //nolint:predeclared 11 | if a > b { 12 | return b 13 | } 14 | return a 15 | } 16 | 17 | func clamp(v, low, high int) int { 18 | if high < low { 19 | low, high = high, low 20 | } 21 | return min(high, max(low, v)) 22 | } 23 | -------------------------------------------------------------------------------- /vt/vt.go: -------------------------------------------------------------------------------- 1 | // Package vt is a virtual terminal emulator that can be used to emulate a 2 | // modern terminal application. 3 | package vt 4 | -------------------------------------------------------------------------------- /wcwidth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/wcwidth 2 | 3 | go 1.23.0 4 | 5 | require github.com/mattn/go-runewidth v0.0.16 6 | 7 | require github.com/rivo/uniseg v0.2.0 // indirect 8 | -------------------------------------------------------------------------------- /wcwidth/go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 2 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 3 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 4 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 5 | -------------------------------------------------------------------------------- /wcwidth/wcwidth.go: -------------------------------------------------------------------------------- 1 | package wcwidth 2 | 3 | import ( 4 | "github.com/mattn/go-runewidth" 5 | ) 6 | 7 | // RuneWidth returns fixed-width width of rune. 8 | // 9 | // Deprecated: this is now a wrapper around go-runewidth. Use go-runewidth 10 | // directly. 11 | func RuneWidth(r rune) int { 12 | return runewidth.RuneWidth(r) 13 | } 14 | 15 | // StringWidth returns fixed-width width of string. 16 | // 17 | // Deprecated: this is now a wrapper around go-runewidth. Use go-runewidth 18 | // directly. 19 | func StringWidth(s string) (n int) { 20 | return runewidth.StringWidth(s) 21 | } 22 | -------------------------------------------------------------------------------- /windows/doc.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go 4 | -------------------------------------------------------------------------------- /windows/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/windows 2 | 3 | go 1.23.0 4 | 5 | require golang.org/x/sys v0.33.0 6 | -------------------------------------------------------------------------------- /windows/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 2 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 3 | -------------------------------------------------------------------------------- /windows/syscall_windows.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import "golang.org/x/sys/windows" 4 | 5 | var NewLazySystemDLL = windows.NewLazySystemDLL 6 | 7 | type Handle = windows.Handle 8 | 9 | //sys ReadConsoleInput(console Handle, buf *InputRecord, toread uint32, read *uint32) (err error) = kernel32.ReadConsoleInputW 10 | //sys PeekConsoleInput(console Handle, buf *InputRecord, toread uint32, read *uint32) (err error) = kernel32.PeekConsoleInputW 11 | //sys GetNumberOfConsoleInputEvents(console Handle, numevents *uint32) (err error) = kernel32.GetNumberOfConsoleInputEvents 12 | //sys FlushConsoleInputBuffer(console Handle) (err error) = kernel32.FlushConsoleInputBuffer 13 | -------------------------------------------------------------------------------- /xpty/conpty.go: -------------------------------------------------------------------------------- 1 | package xpty 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/charmbracelet/x/conpty" 7 | ) 8 | 9 | // ConPty is a Windows console pty. 10 | type ConPty struct { 11 | *conpty.ConPty 12 | } 13 | 14 | var _ Pty = &ConPty{} 15 | 16 | // NewConPty creates a new ConPty. 17 | func NewConPty(width, height int, opts ...PtyOption) (*ConPty, error) { 18 | var opt Options 19 | for _, o := range opts { 20 | o(opt) 21 | } 22 | 23 | c, err := conpty.New(width, height, opt.Flags) 24 | if err != nil { 25 | return nil, err //nolint:wrapcheck 26 | } 27 | 28 | return &ConPty{c}, nil 29 | } 30 | 31 | // Name returns the name of the ConPty. 32 | func (c *ConPty) Name() string { 33 | return "windows-pty" 34 | } 35 | 36 | // Start starts a command on the ConPty. 37 | // This is a wrapper around conpty.Spawn. 38 | func (c *ConPty) Start(cmd *exec.Cmd) error { 39 | return c.start(cmd) 40 | } 41 | -------------------------------------------------------------------------------- /xpty/conpty_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package xpty 5 | 6 | import "os/exec" 7 | 8 | func (c *ConPty) start(*exec.Cmd) error { 9 | return ErrUnsupported 10 | } 11 | -------------------------------------------------------------------------------- /xpty/conpty_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package xpty 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "syscall" 11 | 12 | "golang.org/x/sys/windows" 13 | ) 14 | 15 | func (c *ConPty) start(cmd *exec.Cmd) error { 16 | pid, proc, err := c.ConPty.Spawn(cmd.Path, cmd.Args, &syscall.ProcAttr{ 17 | Dir: cmd.Dir, 18 | Env: cmd.Env, 19 | Sys: cmd.SysProcAttr, 20 | }) 21 | if err != nil { 22 | return err //nolint:wrapcheck 23 | } 24 | 25 | cmd.Process, err = os.FindProcess(pid) 26 | if err != nil { 27 | // If we can't find the process via os.FindProcess, terminate the 28 | // process as that's what we rely on for all further operations on the 29 | // object. 30 | if tErr := windows.TerminateProcess(windows.Handle(proc), 1); tErr != nil { 31 | return fmt.Errorf("failed to terminate process after process not found: %w", tErr) 32 | } 33 | return fmt.Errorf("failed to find process after starting: %w", err) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /xpty/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/x/xpty 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/x/conpty v0.1.0 7 | github.com/charmbracelet/x/term v0.2.1 8 | github.com/charmbracelet/x/termios v0.1.1 9 | github.com/creack/pty v1.1.24 10 | golang.org/x/sys v0.33.0 11 | ) 12 | 13 | require github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect 14 | -------------------------------------------------------------------------------- /xpty/go.sum: -------------------------------------------------------------------------------- 1 | github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= 2 | github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= 3 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= 4 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= 5 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 6 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 7 | github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= 8 | github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= 9 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 10 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 11 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 12 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 13 | -------------------------------------------------------------------------------- /xpty/pty_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris 2 | // +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris 3 | 4 | package xpty 5 | 6 | func (p *UnixPty) setWinsize(int, int, int, int) error { 7 | return ErrUnsupported 8 | } 9 | 10 | func (*UnixPty) size() (int, int, error) { 11 | return 0, 0, ErrUnsupported 12 | } 13 | -------------------------------------------------------------------------------- /xpty/pty_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package xpty 5 | 6 | import ( 7 | "github.com/charmbracelet/x/termios" 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | // setWinsize sets window size for the PTY. 12 | func (p *UnixPty) setWinsize(width, height, x, y int) error { 13 | var rErr error 14 | if err := p.Control(func(fd uintptr) { 15 | rErr = termios.SetWinsize(int(fd), &unix.Winsize{ 16 | Row: uint16(height), //nolint:gosec 17 | Col: uint16(width), //nolint:gosec 18 | Xpixel: uint16(x), //nolint:gosec 19 | Ypixel: uint16(y), //nolint:gosec 20 | }) 21 | }); err != nil { 22 | rErr = err 23 | } 24 | return rErr 25 | } 26 | 27 | // size returns the size of the PTY. 28 | func (p *UnixPty) size() (width, height int, err error) { 29 | var rErr error 30 | if err := p.Control(func(fd uintptr) { 31 | ws, err := termios.GetWinsize(int(fd)) 32 | if err != nil { 33 | rErr = err 34 | return 35 | } 36 | width = int(ws.Col) 37 | height = int(ws.Row) 38 | }); err != nil { 39 | rErr = err 40 | } 41 | 42 | return width, height, rErr 43 | } 44 | --------------------------------------------------------------------------------