mirror of https://github.com/sharkdp/bat.git
Compare commits
275 Commits
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | b7e44c76dc | |
dependabot[bot] | f3cc69733f | |
dependabot[bot] | 3625f0ea1c | |
dependabot[bot] | e6e2d4c65d | |
dependabot[bot] | 340e873eff | |
Keith Hall | 3407bf4bf6 | |
ccQpein | 25cd4991d2 | |
ccQpein | 503b2c5126 | |
ccQpein | 026bc05d70 | |
ccQpein | 61005f19fa | |
someposer | 8f8c953ab6 | |
dependabot[bot] | b4e3a84e1a | |
dependabot[bot] | f7c39e8353 | |
dependabot[bot] | 37d9f0533c | |
dependabot[bot] | d560f2a515 | |
Stéphane Blondon | bb4d1cbd2e | |
Stéphane Blondon | 23ec433167 | |
Sharun | 9eaed3e3f0 | |
sblondon | d5bd4aa93f | |
Keith Hall | 66b70dd8ed | |
Michael Vorburger | 01731478a6 | |
Rivera Calzadillas | f8c5429a6c | |
Rivera Calzadillas | f71226adbb | |
一个不知名の睡觉高手 | e8d777b73a | |
dependabot[bot] | 3cff44b652 | |
dependabot[bot] | 26302a8b08 | |
dependabot[bot] | adc5bd0402 | |
dependabot[bot] | e3c3be950a | |
dependabot[bot] | 8d92dc2083 | |
dependabot[bot] | a1f85b9e06 | |
dependabot[bot] | 424c02dfa7 | |
David Peter | 018a482621 | |
guoguangwu | 4790def1ef | |
Hamir Mahal | 07c26adc35 | |
David Peter | f29f9387b5 | |
Hamir Mahal | c290bfff1e | |
dependabot[bot] | 42153f2b99 | |
dependabot[bot] | 6d7537d3ec | |
dependabot[bot] | b30ec9f975 | |
dependabot[bot] | a7074f10d4 | |
David Peter | d185f0973b | |
cyqsimon | 071874ea8f | |
cyqsimon | 46a2c004a2 | |
cyqsimon | 26ac179548 | |
Lena | 4c85483486 | |
einfachIrgendwer0815 | 487bed2d95 | |
David Peter | 6f69682552 | |
David Peter | bc5beaec5d | |
einfachIrgendwer0815 | 83b00bc653 | |
einfachIrgendwer0815 | f041ff8c5f | |
einfachIrgendwer0815 | 1fbdbfc4b2 | |
einfachIrgendwer0815 | 2323aa0def | |
einfachIrgendwer0815 | 6c2ce63101 | |
einfachIrgendwer0815 | 13204c46e2 | |
Ethan P | 9bb0271e7d | |
Ethan P | 0e4e10edb6 | |
Ethan P | 0c7e5299bf | |
Ethan P | c36ed32816 | |
David Peter | e1a3fc5529 | |
David Peter | 1ae9e843ed | |
David Peter | dbe4cfb763 | |
David Peter | 4549f83689 | |
David Peter | e6e8f847be | |
David Peter | b9e249f782 | |
David Peter | 3ffa3648cf | |
David Peter | 5c2cc53882 | |
David Peter | a6f01af8de | |
David Peter | 85a549e293 | |
David Peter | b718889ba2 | |
David Peter | 708c74f6af | |
David Peter | 74d666f5c0 | |
David Peter | 7604fe5567 | |
David Peter | 0080b043c4 | |
cyqsimon | c7bce46622 | |
cyqsimon | 2b4339663c | |
cyqsimon | 6a6b02117b | |
cyqsimon | 511cd30105 | |
cyqsimon | 92915e22e7 | |
cyqsimon | d499191b0a | |
cyqsimon | 152d69fe98 | |
cyqsimon | 81aa24310c | |
cyqsimon | 75cdabaf13 | |
Oliver Looney | 1f628203e5 | |
David Peter | 1b9fc1d5af | |
David Peter | bc1ca1a346 | |
David Peter | f735120978 | |
Ethan P. | 25b5a41189 | |
Ethan P. | c94cf4e14e | |
Ethan P. | 84d80eebd0 | |
Ethan P. | 915dd9fbf8 | |
Ethan P. | 9d77c1373c | |
Oliver Looney | c3f2ddf509 | |
Oliver looney | 8a51172b11 | |
einfachIrgendwer0815 | 875046e4cd | |
mxaddict | a5bd9f51be | |
Ethan P | 5a2a20af42 | |
Ethan P | 61029c8bd2 | |
Ethan P | 1023399c5e | |
Ethan P | 6549e26f5d | |
Ethan P | 165c495e75 | |
Ethan P | 6b9b085be3 | |
Ethan P | 2d46d54ae3 | |
Ethan P | 3d04699710 | |
Ethan P | 054421268f | |
Ethan P | 414403b062 | |
Ethan P | c29bf2ff28 | |
David Peter | ab4e5ed52e | |
David Tolnay | 1a54c9bf6d | |
Oliver looney | 02077db53e | |
Oliver looney | 7ce010d9ed | |
dependabot[bot] | 95993cf37e | |
David Peter | 3761df9112 | |
Ethan P. | adfaef19da | |
dependabot[bot] | f7bea6de5b | |
dependabot[bot] | 65aae5d0a1 | |
dependabot[bot] | e3866b1f7e | |
dependabot[bot] | 23de8e093b | |
dependabot[bot] | 196a4cb18f | |
Andy Kipp | 695cf1f387 | |
Andy Kipp | 0af1df5258 | |
Oliver looney | a8d07333e9 | |
Oliver looney | 7f12989127 | |
Oliver looney | 60e32cf823 | |
Oliver looney | e9a6aaa30f | |
Oliver looney | 9be2a36a01 | |
Oliver looney | 22254936a2 | |
Oliver looney | f6d76e0104 | |
Oliver looney | c911829771 | |
Oliver looney | b33e33fe26 | |
Oliver looney | 9239b125b1 | |
David Peter | 2086cd2668 | |
Filip Razek | 1b88267320 | |
Filip Razek | e586751208 | |
Filip Razek | e7256a624b | |
Filip Razek | 5c1f47359e | |
Filip Razek | 45ee2dc4c7 | |
David Peter | db66e4459b | |
cyqsimon | 55e02e101d | |
cyqsimon | 230abfd2bc | |
cyqsimon | c0f2d6f934 | |
cyqsimon | 9f36a7a284 | |
cyqsimon | e4d637a3d8 | |
rhysd | 98a2b6bc17 | |
rhysd | 8e66bc8722 | |
dependabot[bot] | cd81c7fa6b | |
Oliver Looney | b4fdb5dc36 | |
dependabot[bot] | c76ed99db2 | |
dependabot[bot] | 06aef22943 | |
dependabot[bot] | 128b0d6dd3 | |
dependabot[bot] | 15dc20109f | |
dependabot[bot] | 5c4bcd6611 | |
Keith Hall | ecf4029dc7 | |
Oliver looney | c261b41578 | |
Oliver Looney | 6f1cc80d68 | |
Oliver looney | 3b0ade9cb8 | |
Oliver looney | 57016f4e04 | |
Víctor González Prieto | 497342fabb | |
David Tolnay | bf56cd90f0 | |
David Tolnay | 0acb979e9e | |
Oliver Looney | d7503bfc09 | |
Martin Nordholts | b89dc15be1 | |
cyqsimon | 15ab4478c9 | |
Oliver looney | 5b4ce684a1 | |
Oliver looney | 0027055a83 | |
Oliver looney | 321b3ec81b | |
Oliver looney | 1679460f42 | |
Oliver looney | 907af9e35f | |
Oliver looney | 12b74dfb4e | |
Oliver looney | fd84e4f49f | |
cyqsimon | f0a6fe216d | |
cyqsimon | d792dc5804 | |
cyqsimon | 8a08025091 | |
cyqsimon | 586c804b1e | |
cyqsimon | e30161ac3c | |
cyqsimon | 3865908439 | |
cyqsimon | 9474b4cf8b | |
cyqsimon | b48bda21a3 | |
cyqsimon | daf33709a0 | |
cyqsimon | 36073a3d95 | |
cyqsimon | 12fa2cb1eb | |
cyqsimon | 1f10d846a3 | |
cyqsimon | 22531eab90 | |
cyqsimon | 0c1b80faab | |
cyqsimon | 2c9bf229e1 | |
cyqsimon | 822e81bb24 | |
cyqsimon | ad628c0471 | |
cyqsimon | f483d2df42 | |
cyqsimon | 4ad3002543 | |
cyqsimon | cfd622d6e1 | |
cyqsimon | 1c7c9a6b6d | |
cyqsimon | 0c93ca80f4 | |
cyqsimon | de6d418d42 | |
cyqsimon | c016b462c0 | |
cyqsimon | 7e1fbcfe95 | |
cyqsimon | 4815b6155e | |
cyqsimon | 075b5b288a | |
cyqsimon | 7cfd1e0d78 | |
cyqsimon | 9f7d70f642 | |
cyqsimon | 0fea82cff9 | |
cyqsimon | 64840fbbae | |
cyqsimon | 827b3eca2f | |
cyqsimon | 9478d2dfe8 | |
cyqsimon | d24501ab5e | |
cyqsimon | 9f4259721a | |
cyqsimon | 77e491161c | |
cyqsimon | 97780f987e | |
cyqsimon | d1bc0ef0d4 | |
cyqsimon | 52f94b4623 | |
cyqsimon | 37fd050100 | |
cyqsimon | 83286975ff | |
cyqsimon | f705fcb984 | |
cyqsimon | 9ca1f20f43 | |
Oliver looney | 6ad800e43a | |
Oliver looney | 069318b139 | |
Oliver looney | b9b554248d | |
Oliver looney | 4863d428dd | |
Oliver looney | 2e103ee6b3 | |
Lena | 28990bc451 | |
cyqsimon | 748e2a681f | |
Broono Lu | bfa0b5241f | |
Oliver looney | 4af4bfc0f1 | |
cyqsimon | 51203ff750 | |
dependabot[bot] | 96cef9a24e | |
Oliver Kiddle | b43d31b75a | |
dependabot[bot] | ad3ff26960 | |
dependabot[bot] | 86b40993c3 | |
dependabot[bot] | 31bed250ba | |
Cosmic Horror | 7658334645 | |
cyqsimon | 491ae70aa9 | |
cyqsimon | d64c568196 | |
cyqsimon | b5982a6174 | |
Maria José Solano | 04e7d2a313 | |
cyqsimon | bcc2de86b4 | |
cyqsimon | 1296aea836 | |
cyqsimon | 5498c24c33 | |
cyqsimon | 79a03b4299 | |
cyqsimon | f3a5e9a73c | |
cyqsimon | 2710a19ecb | |
cyqsimon | 6d0ef259f6 | |
cyqsimon | b1577cc083 | |
cyqsimon | 28d947fd8b | |
cyqsimon | b000db8f32 | |
dependabot[bot] | 116a6cc9a8 | |
dependabot[bot] | c8291a36b7 | |
dependabot[bot] | 8180c76890 | |
dependabot[bot] | a0f33b1cdc | |
dependabot[bot] | 8b60dae81c | |
cyqsimon | 4b33093f9e | |
einfachIrgendwer0815 | 3d87b25b19 | |
dependabot[bot] | f2f6902279 | |
dependabot[bot] | c0b17e73e1 | |
dependabot[bot] | 94544d963b | |
dependabot[bot] | 72abbd22de | |
dependabot[bot] | 64e10ffb21 | |
Martin Nordholts | 35d8146bba | |
Martin Nordholts | a5a7ede698 | |
dependabot[bot] | b551049706 | |
dependabot[bot] | 99cfc13eab | |
dependabot[bot] | 4b0b5afa13 | |
dependabot[bot] | d343428441 | |
dependabot[bot] | 16e409ec87 | |
dependabot[bot] | 94d059f258 | |
dependabot[bot] | c8b9de889d | |
dependabot[bot] | 75340d54f9 | |
dependabot[bot] | b28383e0fa | |
dependabot[bot] | 8e866db281 | |
dependabot[bot] | 0eb157e090 | |
dependabot[bot] | 85636c28bc | |
dependabot[bot] | a70e5c6c65 | |
dependabot[bot] | 32e01f740b | |
dependabot[bot] | 7b20f8fc7b | |
Martin Nordholts | 86ac48d68e | |
Martin Nordholts | c42fc810ea | |
Martin Nordholts | 6baebd79fa | |
Martin Nordholts | c6cae09f99 |
|
@ -26,4 +26,4 @@ guidelines for adding new syntaxes:
|
||||||
[Name or description of the syntax/language here]
|
[Name or description of the syntax/language here]
|
||||||
|
|
||||||
**Guideline Criteria:**
|
**Guideline Criteria:**
|
||||||
[packagecontro.io link here]
|
[packagecontrol.io link here]
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# This workflow triggers auto-merge of any PR that dependabot creates so that
|
||||||
|
# PRs will be merged automatically without maintainer intervention if CI passes
|
||||||
|
name: Auto-merge dependabot PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-merge:
|
||||||
|
if: github.repository == 'sharkdp/bat' && startsWith(github.head_ref, 'dependabot/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: auto-merge
|
||||||
|
url: https://github.com/sharkdp/bat/blob/main/.github/workflows/Auto-merge-dependabot-PRs.yml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.AUTO_MERGE_GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: |
|
||||||
|
gh pr review ${{ github.event.pull_request.number }} --comment --body "If CI passes, this dependabot PR will be [auto-merged](https://github.com/sharkdp/bat/blob/main/.github/workflows/Auto-merge-dependabot-PRs.yml) 🚀"
|
||||||
|
- run: |
|
||||||
|
gh pr merge --auto --squash ${{ github.event.pull_request.number }}
|
|
@ -163,17 +163,18 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, dpkg_arch: arm64, use-cross: true }
|
||||||
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, dpkg_arch: armhf, use-cross: true }
|
||||||
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, dpkg_arch: musl-linux-armhf, use-cross: true }
|
||||||
- { target: i686-pc-windows-msvc , os: windows-2019 }
|
- { target: i686-pc-windows-msvc , os: windows-2019, }
|
||||||
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, dpkg_arch: i686, use-cross: true }
|
||||||
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, dpkg_arch: musl-linux-i686, use-cross: true }
|
||||||
- { target: x86_64-apple-darwin , os: macos-12 }
|
- { target: x86_64-apple-darwin , os: macos-12, }
|
||||||
- { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
- { target: aarch64-apple-darwin , os: macos-14, }
|
||||||
- { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
- { target: x86_64-pc-windows-gnu , os: windows-2019, }
|
||||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
- { target: x86_64-pc-windows-msvc , os: windows-2019, }
|
||||||
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, dpkg_arch: amd64, use-cross: true }
|
||||||
|
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, dpkg_arch: musl-linux-amd64, use-cross: true }
|
||||||
env:
|
env:
|
||||||
BUILD_CMD: cargo
|
BUILD_CMD: cargo
|
||||||
steps:
|
steps:
|
||||||
|
@ -337,16 +338,7 @@ jobs:
|
||||||
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
|
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
|
||||||
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
|
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
|
||||||
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
|
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
|
||||||
|
DPKG_ARCH="${{ matrix.job.dpkg_arch }}"
|
||||||
unset DPKG_ARCH
|
|
||||||
case ${{ matrix.job.target }} in
|
|
||||||
aarch64-*-linux-*) DPKG_ARCH=arm64 ;;
|
|
||||||
arm-*-linux-*hf) DPKG_ARCH=armhf ;;
|
|
||||||
i686-*-linux-*) DPKG_ARCH=i686 ;;
|
|
||||||
x86_64-*-linux-*) DPKG_ARCH=amd64 ;;
|
|
||||||
*) DPKG_ARCH=notset ;;
|
|
||||||
esac;
|
|
||||||
|
|
||||||
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
|
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
|
||||||
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
|
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
@ -453,7 +445,7 @@ jobs:
|
||||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Publish archives and packages
|
- name: Publish archives and packages
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
if: steps.is-release.outputs.IS_RELEASE
|
if: steps.is-release.outputs.IS_RELEASE
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Changelog
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-changelog:
|
||||||
|
name: Check for changelog entry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# dependabot PRs are automerged if CI passes; we shouldn't block these
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
|
PR_BASE: ${{ github.base_ref }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Fetch PR base
|
||||||
|
run: git fetch --no-tags --prune --depth=1 origin
|
||||||
|
|
||||||
|
# cannot use `github.actor`: the triggering commit may be authored by a maintainer
|
||||||
|
- name: Get PR submitter
|
||||||
|
id: get-submitter
|
||||||
|
run: curl -sSfL https://api.github.com/repos/sharkdp/bat/pulls/${PR_NUMBER} | jq -r '"submitter=" + .user.login' | tee -a $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Search for added line in changelog
|
||||||
|
env:
|
||||||
|
PR_SUBMITTER: ${{ steps.get-submitter.outputs.submitter }}
|
||||||
|
run: |
|
||||||
|
ADDED=$(git diff -U0 "origin/${PR_BASE}" HEAD -- CHANGELOG.md | grep -P '^\+[^\+].+$')
|
||||||
|
echo "Added lines in CHANGELOG.md:"
|
||||||
|
echo "$ADDED"
|
||||||
|
echo "Grepping for PR info (see CONTRIBUTING.md):"
|
||||||
|
grep "#${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED"
|
63
CHANGELOG.md
63
CHANGELOG.md
|
@ -1,3 +1,66 @@
|
||||||
|
# unreleased
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Set terminal title to file names when Paging is not Paging::Never #2807 (@Oliver-Looney)
|
||||||
|
- `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
|
||||||
|
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
|
||||||
|
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
|
||||||
|
- Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Fix long file name wrapping in header, see #2835 (@FilipRazek)
|
||||||
|
- Fix `NO_COLOR` support, see #2767 (@acuteenvy)
|
||||||
|
- Fix handling of inputs with OSC ANSI escape sequences, see #2541 and #2544 (@eth-p)
|
||||||
|
- Fix handling of inputs with combined ANSI color and attribute sequences, see #2185 and #2856 (@eth-p)
|
||||||
|
- Fix panel width when line 10000 wraps, see #2854 (@eth-p)
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
- Upgrade to Rust 2021 edition #2748 (@cyqsimon)
|
||||||
|
- Refactor and cleanup build script #2756 (@cyqsimon)
|
||||||
|
- Checks changelog has been written to for PRs in CI #2766 (@cyqsimon)
|
||||||
|
- Use GitHub API to get correct PR submitter #2791 (@cyqsimon)
|
||||||
|
- Minor benchmark script improvements #2768 (@cyqsimon)
|
||||||
|
- Update Arch Linux package URL in README files #2779 (@brunobell)
|
||||||
|
- Update and improve `zsh` completion, see #2772 (@okapia)
|
||||||
|
- More extensible syntax mapping mechanism #2755 (@cyqsimon)
|
||||||
|
- Use proper Architecture for Debian packages built for musl, see #2811 (@Enselic)
|
||||||
|
- Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay)
|
||||||
|
- Update git-version dependency to use Syn v2, see #2816 (@dtolnay)
|
||||||
|
- Update git2 dependency to v0.18.2, see #2852 (@eth-p)
|
||||||
|
- Improve performance when color output disabled, see #2397 and #2857 (@eth-p)
|
||||||
|
- Relax syntax mapping rule restrictions to allow brace expansion #2865 (@cyqsimon)
|
||||||
|
- Apply clippy fixes #2864 (@cyqsimon)
|
||||||
|
- Faster startup by offloading glob matcher building to a worker thread #2868 (@cyqsimon)
|
||||||
|
- Display which theme is the default one in basic output (no colors), see #2937 (@sblondon)
|
||||||
|
- Display which theme is the default one in colored output, see #2838 (@sblondon)
|
||||||
|
- Add aarch64-apple-darwin ("Apple Silicon") binary tarballs to releases, see #2967 (@someposer)
|
||||||
|
- Update the Lisp syntax, see #2970 (@ccqpein)
|
||||||
|
|
||||||
|
## Syntaxes
|
||||||
|
|
||||||
|
- `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp)
|
||||||
|
- Upgrade JQ syntax, see #2820 (@dependabot[bot])
|
||||||
|
- Add syntax mapping for quadman quadlets #2866 (@cyqsimon)
|
||||||
|
- Map containers .conf files to TOML syntax #2867 (@cyqsimon)
|
||||||
|
- Associate `xsh` files with `xonsh` syntax that is Python, see #2840 (@anki-code).
|
||||||
|
- Added auto detect syntax for `.jsonc` #2795 (@mxaddict)
|
||||||
|
- Added auto detect syntax for `.aws/{config,credentials}` #2795 (@mxaddict)
|
||||||
|
- Add syntax mapping for Wireguard config #2874 (@cyqsimon)
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
## `bat` as a library
|
||||||
|
|
||||||
|
- Changes to `syntax_mapping::SyntaxMapping` #2755 (@cyqsimon)
|
||||||
|
- `SyntaxMapping::get_syntax_for` is now correctly public
|
||||||
|
- [BREAKING] `SyntaxMapping::{empty,builtin}` are removed; use `SyntaxMapping::new` instead
|
||||||
|
- [BREAKING] `SyntaxMapping::mappings` is replaced by `SyntaxMapping::{builtin,custom,all}_mappings`
|
||||||
|
- Make `Controller::run_with_error_handler`'s error handler `FnMut`, see #2831 (@rhysd)
|
||||||
|
- Improve compile time by 20%, see #2815 (@dtolnay)
|
||||||
|
|
||||||
# v0.24.0
|
# v0.24.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
|
@ -6,21 +6,42 @@ Thank you for considering to contribute to `bat`!
|
||||||
|
|
||||||
## Add an entry to the changelog
|
## Add an entry to the changelog
|
||||||
|
|
||||||
If your contribution changes the behavior of `bat` (as opposed to a typo-fix
|
Keeping the [`CHANGELOG.md`](CHANGELOG.md) file up-to-date makes the release
|
||||||
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file
|
process much easier and therefore helps to get your changes into a new `bat`
|
||||||
and describe your changes. This makes the release process much easier and
|
release faster. However, not every change to the repository requires a
|
||||||
therefore helps to get your changes into a new `bat` release faster.
|
changelog entry. Below are a few examples of that.
|
||||||
|
|
||||||
|
Please update the changelog if your contribution contains changes regarding
|
||||||
|
any of the following:
|
||||||
|
- the behavior of `bat`
|
||||||
|
- syntax mappings
|
||||||
|
- syntax definitions
|
||||||
|
- themes
|
||||||
|
- the build system, linting, or CI workflows
|
||||||
|
|
||||||
|
A changelog entry is not necessary when:
|
||||||
|
- updating documentation
|
||||||
|
- fixing typos
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
> For PRs, a CI workflow verifies that a suitable changelog entry is
|
||||||
|
> added. If such an entry is missing, the workflow will fail. If your
|
||||||
|
> changes do not need an entry to the changelog (see above), that
|
||||||
|
> workflow failure can be disregarded.
|
||||||
|
|
||||||
|
|
||||||
|
### Changelog entry format
|
||||||
|
|
||||||
The top of the `CHANGELOG` contains a *"unreleased"* section with a few
|
The top of the `CHANGELOG` contains a *"unreleased"* section with a few
|
||||||
subsections (Features, Bugfixes, …). Please add your entry to the subsection
|
subsections (Features, Bugfixes, …). Please add your entry to the subsection
|
||||||
that best describes your change.
|
that best describes your change.
|
||||||
|
|
||||||
Entries follow this format:
|
Entries must follow this format:
|
||||||
```
|
```
|
||||||
- Short description of what has been changed, see #123 (@user)
|
- Short description of what has been changed, see #123 (@user)
|
||||||
```
|
```
|
||||||
Here, `#123` is the number of the original issue and/or your pull request.
|
Please replace `#123` with the number of your pull request (not issue) and
|
||||||
Please replace `@user` by your GitHub username.
|
`@user` by your GitHub username.
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
62
Cargo.toml
62
Cargo.toml
|
@ -8,8 +8,8 @@ name = "bat"
|
||||||
repository = "https://github.com/sharkdp/bat"
|
repository = "https://github.com/sharkdp/bat"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
exclude = ["assets/syntaxes/*", "assets/themes/*"]
|
exclude = ["assets/syntaxes/*", "assets/themes/*"]
|
||||||
build = "build.rs"
|
build = "build/main.rs"
|
||||||
edition = '2018'
|
edition = '2021'
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -41,32 +41,33 @@ regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine
|
||||||
regex-fancy = ["syntect/regex-fancy"] # Use the rust-only "fancy-regex" engine
|
regex-fancy = ["syntect/regex-fancy"] # Use the rust-only "fancy-regex" engine
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nu-ansi-term = "0.49.0"
|
nu-ansi-term = "0.50.0"
|
||||||
ansi_colours = "^1.2"
|
ansi_colours = "^1.2"
|
||||||
bincode = "1.0"
|
bincode = "1.0"
|
||||||
console = "0.15.5"
|
console = "0.15.8"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
once_cell = "1.18"
|
once_cell = "1.19"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
wild = { version = "2.1", optional = true }
|
wild = { version = "2.2", optional = true }
|
||||||
content_inspector = "0.2.4"
|
content_inspector = "0.2.4"
|
||||||
shell-words = { version = "1.1.0", optional = true }
|
shell-words = { version = "1.1.0", optional = true }
|
||||||
unicode-width = "0.1.10"
|
unicode-width = "0.1.11"
|
||||||
globset = "0.4"
|
globset = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_derive = "1.0"
|
||||||
|
serde_yaml = "0.9.28"
|
||||||
semver = "1.0"
|
semver = "1.0"
|
||||||
path_abs = { version = "0.5", default-features = false }
|
path_abs = { version = "0.5", default-features = false }
|
||||||
clircle = "0.4"
|
clircle = "0.5"
|
||||||
bugreport = { version = "0.5.0", optional = true }
|
bugreport = { version = "0.5.0", optional = true }
|
||||||
etcetera = { version = "0.8.0", optional = true }
|
etcetera = { version = "0.8.0", optional = true }
|
||||||
grep-cli = { version = "0.1.9", optional = true }
|
grep-cli = { version = "0.1.10", optional = true }
|
||||||
regex = { version = "1.8.3", optional = true }
|
regex = { version = "1.10.2", optional = true }
|
||||||
walkdir = { version = "2.3", optional = true }
|
walkdir = { version = "2.4", optional = true }
|
||||||
bytesize = { version = "1.3.0" }
|
bytesize = { version = "1.3.0" }
|
||||||
encoding_rs = "0.8.33"
|
encoding_rs = "0.8.33"
|
||||||
os_str_bytes = { version = "~6.4", optional = true }
|
os_str_bytes = { version = "~7.0", optional = true }
|
||||||
run_script = { version = "^0.10.0", optional = true}
|
run_script = { version = "^0.10.1", optional = true}
|
||||||
|
|
||||||
[dependencies.git2]
|
[dependencies.git2]
|
||||||
version = "0.18"
|
version = "0.18"
|
||||||
|
@ -74,32 +75,45 @@ optional = true
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dependencies.syntect]
|
[dependencies.syntect]
|
||||||
version = "5.0.0"
|
version = "5.2.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["parsing"]
|
features = ["parsing"]
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "4.4.6"
|
version = "4.4.12"
|
||||||
optional = true
|
optional = true
|
||||||
features = ["wrap_help", "cargo"]
|
features = ["wrap_help", "cargo"]
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
home = "0.5.4"
|
home = "0.5.9"
|
||||||
plist = "1.4.3"
|
plist = "1.6.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0.10"
|
assert_cmd = "2.0.12"
|
||||||
expect-test = "1.4.1"
|
expect-test = "1.4.1"
|
||||||
serial_test = { version = "2.0.0", default-features = false }
|
serial_test = { version = "2.0.0", default-features = false }
|
||||||
predicates = "3.0.3"
|
predicates = "3.1.0"
|
||||||
wait-timeout = "0.2.0"
|
wait-timeout = "0.2.0"
|
||||||
tempfile = "3.8.0"
|
tempfile = "3.8.1"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dev-dependencies]
|
[target.'cfg(unix)'.dev-dependencies]
|
||||||
nix = { version = "0.26.2", default-features = false, features = ["term"] }
|
nix = { version = "0.26.4", default-features = false, features = ["term"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
anyhow = "1.0.78"
|
||||||
|
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||||
|
itertools = "0.13.0"
|
||||||
|
once_cell = "1.18"
|
||||||
|
regex = "1.10.2"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
serde_with = { version = "3.8.1", default-features = false, features = ["macros"] }
|
||||||
|
toml = { version = "0.8.9", features = ["preserve_order"] }
|
||||||
|
walkdir = "2.4"
|
||||||
|
|
||||||
[build-dependencies.clap]
|
[build-dependencies.clap]
|
||||||
version = "4.4.6"
|
version = "4.4.12"
|
||||||
optional = true
|
optional = true
|
||||||
features = ["wrap_help", "cargo"]
|
features = ["wrap_help", "cargo"]
|
||||||
|
|
||||||
|
|
48
README.md
48
README.md
|
@ -254,7 +254,7 @@ Please report any issues with the help syntax in [this repository](https://githu
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
|
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
|
||||||
|
|
||||||
### On Ubuntu (using `apt`)
|
### On Ubuntu (using `apt`)
|
||||||
*... and other Debian-based Linux distributions.*
|
*... and other Debian-based Linux distributions.*
|
||||||
|
@ -296,7 +296,7 @@ apk add bat
|
||||||
|
|
||||||
### On Arch Linux
|
### On Arch Linux
|
||||||
|
|
||||||
You can install [the `bat` package](https://www.archlinux.org/packages/community/x86_64/bat/)
|
You can install [the `bat` package](https://www.archlinux.org/packages/extra/x86_64/bat/)
|
||||||
from the official sources:
|
from the official sources:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -602,7 +602,8 @@ set, `less` is used by default. If you want to use a different pager, you can ei
|
||||||
`PAGER` variable or set the `BAT_PAGER` environment variable to override what is specified in
|
`PAGER` variable or set the `BAT_PAGER` environment variable to override what is specified in
|
||||||
`PAGER`.
|
`PAGER`.
|
||||||
|
|
||||||
**Note**: If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
|
>[!NOTE]
|
||||||
|
> If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
|
||||||
|
|
||||||
If you want to pass command-line arguments to the pager, you can also set them via the
|
If you want to pass command-line arguments to the pager, you can also set them via the
|
||||||
`PAGER`/`BAT_PAGER` variables:
|
`PAGER`/`BAT_PAGER` variables:
|
||||||
|
@ -613,20 +614,37 @@ export BAT_PAGER="less -RF"
|
||||||
|
|
||||||
Instead of using environment variables, you can also use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file) to configure the pager (`--pager` option).
|
Instead of using environment variables, you can also use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file) to configure the pager (`--pager` option).
|
||||||
|
|
||||||
**Note**: By default, if the pager is set to `less` (and no command-line options are specified),
|
|
||||||
`bat` will pass the following command line options to the pager: `-R`/`--RAW-CONTROL-CHARS`,
|
|
||||||
`-F`/`--quit-if-one-screen` and `-X`/`--no-init`. The last option (`-X`) is only used for `less`
|
|
||||||
versions older than 530.
|
|
||||||
|
|
||||||
The `-R` option is needed to interpret ANSI colors correctly. The second option (`-F`) instructs
|
### Using `less` as a pager
|
||||||
less to exit immediately if the output size is smaller than the vertical size of the terminal.
|
|
||||||
This is convenient for small files because you do not have to press `q` to quit the pager. The
|
|
||||||
third option (`-X`) is needed to fix a bug with the `--quit-if-one-screen` feature in old versions
|
|
||||||
of `less`. Unfortunately, it also breaks mouse-wheel support in `less`.
|
|
||||||
|
|
||||||
If you want to enable mouse-wheel scrolling on older versions of `less`, you can pass just `-R` (as
|
When using `less` as a pager, `bat` will automatically pass extra options along to `less`
|
||||||
in the example above, this will disable the quit-if-one-screen feature). For less 530 or newer,
|
to improve the experience. Specifically, `-R`/`--RAW-CONTROL-CHARS`, `-F`/`--quit-if-one-screen`,
|
||||||
it should work out of the box.
|
and under certain conditions, `-X`/`--no-init` and/or `-S`/`--chop-long-lines`.
|
||||||
|
|
||||||
|
>[!IMPORTANT]
|
||||||
|
> These options will not be added if:
|
||||||
|
> - The pager is not named `less`.
|
||||||
|
> - The `--pager` argument contains any command-line arguments (e.g. `--pager="less -R"`).
|
||||||
|
> - The `BAT_PAGER` environment variable contains any command-line arguments (e.g. `export BAT_PAGER="less -R"`)
|
||||||
|
>
|
||||||
|
> The `--quit-if-one-screen` option will not be added when:
|
||||||
|
> - The `--paging=always` argument is used.
|
||||||
|
> - The `BAT_PAGING` environment is set to `always`.
|
||||||
|
|
||||||
|
The `-R` option is needed to interpret ANSI colors correctly.
|
||||||
|
|
||||||
|
The `-F` option instructs `less` to exit immediately if the output size is smaller than
|
||||||
|
the vertical size of the terminal. This is convenient for small files because you do not
|
||||||
|
have to press `q` to quit the pager.
|
||||||
|
|
||||||
|
The `-X` option is needed to fix a bug with the `--quit-if-one-screen` feature in versions
|
||||||
|
of `less` older than version 530. Unfortunately, it also breaks mouse-wheel support in `less`.
|
||||||
|
If you want to enable mouse-wheel scrolling on older versions of `less` and do not mind losing
|
||||||
|
the quit-if-one-screen feature, you can set the pager (via `--pager` or `BAT_PAGER`) to `less -R`.
|
||||||
|
For `less` 530 or newer, it should work out of the box.
|
||||||
|
|
||||||
|
The `-S` option is added when `bat`'s `-S`/`--chop-long-lines` option is used. This tells `less`
|
||||||
|
to truncate any lines larger than the terminal width.
|
||||||
|
|
||||||
### Indentation
|
### Indentation
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,8 @@ complete -c $bat -s d -l diff -d "Only show lines with Git changes" -n __bat_no_
|
||||||
|
|
||||||
complete -c $bat -l diff-context -x -d "Show N context lines around Git changes" -n "__fish_seen_argument -s d -l diff"
|
complete -c $bat -l diff-context -x -d "Show N context lines around Git changes" -n "__fish_seen_argument -s d -l diff"
|
||||||
|
|
||||||
|
complete -c $bat -l generate-config-file -f -d "Generates a default configuration file" -n __fish_is_first_arg
|
||||||
|
|
||||||
complete -c $bat -l file-name -x -d "Specify the display name" -n __bat_no_excl_args
|
complete -c $bat -l file-name -x -d "Specify the display name" -n __bat_no_excl_args
|
||||||
|
|
||||||
complete -c $bat -s f -l force-colorization -d "Force color and decorations" -n __bat_no_excl_args
|
complete -c $bat -s f -l force-colorization -d "Force color and decorations" -n __bat_no_excl_args
|
||||||
|
@ -173,6 +175,12 @@ complete -c $bat -l list-themes -f -d "List syntax highlighting themes" -n __fis
|
||||||
|
|
||||||
complete -c $bat -s m -l map-syntax -x -a "(__bat_complete_map_syntax)" -d "Map <glob pattern>:<language syntax>" -n __bat_no_excl_args
|
complete -c $bat -s m -l map-syntax -x -a "(__bat_complete_map_syntax)" -d "Map <glob pattern>:<language syntax>" -n __bat_no_excl_args
|
||||||
|
|
||||||
|
complete -c $bat -l no-config -d "Do not use the configuration file"
|
||||||
|
|
||||||
|
complete -c $bat -l no-custom-assets -d "Do not load custom assets"
|
||||||
|
|
||||||
|
complete -c $bat -l no-lessopen -d "Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)"
|
||||||
|
|
||||||
complete -c $bat -s n -l number -d "Only show line numbers, no other decorations" -n __bat_no_excl_args
|
complete -c $bat -s n -l number -d "Only show line numbers, no other decorations" -n __bat_no_excl_args
|
||||||
|
|
||||||
complete -c $bat -l pager -x -a "$pager_opts" -d "Which pager to use" -n __bat_no_excl_args
|
complete -c $bat -l pager -x -a "$pager_opts" -d "Which pager to use" -n __bat_no_excl_args
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
#compdef {{PROJECT_EXECUTABLE}}
|
#compdef {{PROJECT_EXECUTABLE}}
|
||||||
|
|
||||||
local context state state_descr line
|
local curcontext="$curcontext" ret=1
|
||||||
|
local -a state state_descr line
|
||||||
typeset -A opt_args
|
typeset -A opt_args
|
||||||
|
|
||||||
(( $+functions[_{{PROJECT_EXECUTABLE}}_cache_subcommand] )) ||
|
(( $+functions[_{{PROJECT_EXECUTABLE}}_cache_subcommand] )) ||
|
||||||
_{{PROJECT_EXECUTABLE}}_cache_subcommand() {
|
_{{PROJECT_EXECUTABLE}}_cache_subcommand() {
|
||||||
local -a args
|
local -a args
|
||||||
args=(
|
args=(
|
||||||
'(-b --build -c --clear)'{-b,--build}'[Initialize or update the syntax/theme cache]'
|
'(-b --build -c --clear)'{-b,--build}'[initialize or update the syntax/theme cache]'
|
||||||
'(-b --build -c --clear)'{-c,--clear}'[Remove the cached syntax definitions and themes]'
|
'(-b --build -c --clear)'{-c,--clear}'[remove the cached syntax definitions and themes]'
|
||||||
'(--source)'--source='[Use a different directory to load syntaxes and themes from]:directory:_files -/'
|
--source='[specify directory to load syntaxes and themes from]:directory:_files -/'
|
||||||
'(--target)'--target='[Use a different directory to store the cached syntax and theme set]:directory:_files -/'
|
--target='[specify directory to store the cached syntax and theme set in]:directory:_files -/'
|
||||||
'(--blank)'--blank'[Create completely new syntax and theme sets]'
|
--blank'[create completely new syntax and theme sets]'
|
||||||
'(: -)'{-h,--help}'[Prints help information]'
|
--acknowledgements'[build acknowledgements.bin]'
|
||||||
'*: :'
|
'(: -)'{-h,--help}'[show help information]'
|
||||||
)
|
)
|
||||||
|
|
||||||
_arguments -S -s $args
|
_arguments -S -s $args
|
||||||
|
@ -23,69 +24,79 @@ _{{PROJECT_EXECUTABLE}}_cache_subcommand() {
|
||||||
_{{PROJECT_EXECUTABLE}}_main() {
|
_{{PROJECT_EXECUTABLE}}_main() {
|
||||||
local -a args
|
local -a args
|
||||||
args=(
|
args=(
|
||||||
'(-A --show-all)'{-A,--show-all}'[Show non-printable characters (space, tab, newline, ..)]'
|
'(-A --show-all)'{-A,--show-all}'[show non-printable characters (space, tab, newline, ..)]'
|
||||||
'*'{-p,--plain}'[Show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]'
|
--nonprintable-notation='[specify how to display non-printable characters when using --show-all]:notation:(caret unicode)'
|
||||||
'(-l --language)'{-l+,--language=}'[Set the language for syntax highlighting]:<language>:->language'
|
\*{-p,--plain}'[show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]'
|
||||||
'(-H --highlight-line)'{-H,--highlight-line}'[Highlight lines N through M]:<N\:M>...'
|
'(-l --language)'{-l+,--language=}'[set the language for syntax highlighting]:language:->languages'
|
||||||
'(--file-name)'--file-name'[Specify the name to display for a file]:<name>...:_files'
|
\*{-H+,--highlight-line=}'[highlight specified block of lines]:start\:end'
|
||||||
'(-d --diff)'--diff'[Only show lines that have been added/removed/modified]'
|
\*--file-name='[specify the name to display for a file]:name:_files'
|
||||||
'(--diff-context)'--diff-context'[Include N lines of context around added/removed/modified lines when using `--diff`]:<N> (lines):()'
|
'(-d --diff)'--diff'[only show lines that have been added/removed/modified]'
|
||||||
'(--tabs)'--tabs'[Set the tab width to T spaces]:<T> (tab width):()'
|
--diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines'
|
||||||
'(--wrap)'--wrap='[Specify the text-wrapping mode]:<when>:(auto never character)'
|
--tabs='[set the tab width]:tab width [4]'
|
||||||
'(--terminal-width)'--terminal-width'[Explicitly set the width of the terminal instead of determining it automatically]:<width>'
|
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)'
|
||||||
'(-n --number)'{-n,--number}'[Show line numbers]'
|
'!(--wrap)'{-S,--chop-long-lines}
|
||||||
'(--color)'--color='[When to use colors]:<when>:(auto never always)'
|
--terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width'
|
||||||
'(--italic-text)'--italic-text='[Use italics in output]:<when>:(always never)'
|
'(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]'
|
||||||
'(--decorations)'--decorations='[When to show the decorations]:<when>:(auto never always)'
|
--color='[specify when to use colors]:when:(auto never always)'
|
||||||
'(--paging)'--paging='[Specify when to use the pager]:<when>:(auto never always)'
|
--italic-text='[use italics in output]:when:(always never)'
|
||||||
'(-m --map-syntax)'{-m+,--map-syntax=}'[Use the specified syntax for files matching the glob pattern]:<glob\:syntax>...'
|
--decorations='[specify when to show the decorations]:when:(auto never always)'
|
||||||
'(--theme)'--theme='[Set the color theme for syntax highlighting]:<theme>:->theme'
|
--paging='[specify when to use the pager]:when:(auto never always)'
|
||||||
'(: --list-themes --list-languages -L)'--list-themes'[Display all supported highlighting themes]'
|
'(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps'
|
||||||
'(--style)'--style='[Comma-separated list of style elements to display]:<components>:->style'
|
'(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes'
|
||||||
'(-r --line-range)'{-r+,--line-range=}'[Only print the lines from N to M]:<N\:M>...'
|
'(: --list-themes --list-languages -L)'--list-themes'[show all supported highlighting themes]'
|
||||||
'(: --list-themes --list-languages -L)'{-L,--list-languages}'[Display all supported languages]'
|
--style='[comma-separated list of style elements to display]: : _values "style [default]"
|
||||||
'(: --no-config)'--no-config'[Do not use the configuration file]'
|
default auto full plain changes header header-filename header-filesize grid rule numbers snip'
|
||||||
'(: --no-custom-assets)'--no-custom-assets'[Do not load custom assets]'
|
\*{-r+,--line-range=}'[only print the specified line range]:start\:end'
|
||||||
'(: --lessopen)'--lessopen'[Enable the $LESSOPEN preprocessor]'
|
'(* -)'{-L,--list-languages}'[display all supported languages]'
|
||||||
'(: --no-lessopen)'--no-lessopen'[Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]'
|
"--no-config[don't use the configuration file]"
|
||||||
'(: --config-dir)'--config-dir'[Show bat'"'"'s configuration directory]'
|
"--no-custom-assets[don't load custom assets]"
|
||||||
'(: --config-file)'--config-file'[Show path to the configuration file]'
|
'(--no-lessopen)'--lessopen'[enable the $LESSOPEN preprocessor]'
|
||||||
'(: --generate-config-file)'--generate-config-file'[Generates a default configuration file]'
|
'(--lessopen)'--no-lessopen'[disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]'
|
||||||
'(: --cache-dir)'--cache-dir'[Show bat'"'"'s cache directory]'
|
'(* -)'--config-dir"[show bat's configuration directory]"
|
||||||
'(: -)'{-h,--help}'[Print this help message]'
|
'(* -)'--config-file'[show path to the configuration file]'
|
||||||
'(: -)'{-V,--version}'[Show version information]'
|
'(* -)'--generate-config-file'[generate a default configuration file]'
|
||||||
'*: :_files'
|
'(* -)'--cache-dir"[show bat's cache directory]"
|
||||||
|
'(* -)'{-h,--help}'[show help information]'
|
||||||
|
'(* -)'{-V,--version}'[show version information]'
|
||||||
|
'*: :{ _files || compadd cache }'
|
||||||
)
|
)
|
||||||
|
|
||||||
_arguments -S -s $args
|
_arguments -S -s $args && ret=0
|
||||||
|
|
||||||
case "$state" in
|
case "$state" in
|
||||||
language)
|
syntax-maps)
|
||||||
|
if ! compset -P '*:'; then
|
||||||
|
_message -e patterns 'glob pattern:language'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
;& # fall-through
|
||||||
|
|
||||||
|
languages)
|
||||||
local IFS=$'\n'
|
local IFS=$'\n'
|
||||||
local -a languages
|
local -a languages
|
||||||
languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') )
|
languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') )
|
||||||
|
|
||||||
_describe 'language' languages
|
_describe 'language' languages && ret=0
|
||||||
;;
|
;;
|
||||||
|
|
||||||
theme)
|
themes)
|
||||||
local IFS=$'\n'
|
local -a themes expl
|
||||||
local -a themes
|
themes=( ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} )
|
||||||
themes=( $({{PROJECT_EXECUTABLE}} --list-themes | sort) )
|
|
||||||
|
|
||||||
_values 'theme' $themes
|
_wanted themes expl 'theme' compadd -a themes && ret=0
|
||||||
;;
|
|
||||||
|
|
||||||
style)
|
|
||||||
_values -s , 'style' default auto full plain changes header header-filename header-filesize grid rule numbers snip
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
case $words[2] in
|
case $words[2] in
|
||||||
cache)
|
cache)
|
||||||
## Completion of the 'cache' command itself is removed for better UX
|
## Completion of the 'cache' command itself is removed for better UX
|
||||||
## See https://github.com/sharkdp/bat/issues/2085#issuecomment-1271646802
|
## See https://github.com/sharkdp/bat/issues/2085#issuecomment-1271646802
|
||||||
|
shift words
|
||||||
|
(( CURRENT-- ))
|
||||||
|
curcontext="${curcontext%:*}-${words[1]}:"
|
||||||
_{{PROJECT_EXECUTABLE}}_cache_subcommand
|
_{{PROJECT_EXECUTABLE}}_cache_subcommand
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
Submodule assets/syntaxes/01_Packages contains modified content
|
||||||
|
diff --git syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax
|
||||||
|
index 05a4fed6..78a7bf55 100644
|
||||||
|
--- syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax
|
||||||
|
+++ syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax
|
||||||
|
@@ -5,7 +5,7 @@ name: JavaScript
|
||||||
|
file_extensions:
|
||||||
|
- js
|
||||||
|
- htc
|
||||||
|
-first_line_match: ^#!\s*/.*\b(node|js)\b
|
||||||
|
+first_line_match: ^#!\s*/.*\b(node|bun|js)\b
|
||||||
|
scope: source.js
|
||||||
|
variables:
|
||||||
|
bin_digit: '[01_]'
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
Subproject commit 98233f96d4827a1a576c0b8bf87a68b9c97e4306
|
Subproject commit 3366b10be91aaab7a61ae0bc0a5af5cc375e58d1
|
|
@ -1 +1 @@
|
||||||
Subproject commit 687058289c1a888e0895378432d66b41609a84d8
|
Subproject commit b7e53e5d86814f04a48d2e441bcf5f9fdf07e9c1
|
|
@ -1 +1 @@
|
||||||
Subproject commit f41e5fc8381fe200a4e18c410bb41e911110a6e9
|
Subproject commit 209559b72f7e8848c988828088231b3a4d8b6838
|
|
@ -1 +1 @@
|
||||||
Subproject commit 43dc527731731666d6d2b1311e86951a8ce07fec
|
Subproject commit 86d4ee7a1f884851a1d21d66249687f527fced32
|
108
build.rs
108
build.rs
|
@ -1,108 +0,0 @@
|
||||||
// TODO: Re-enable generation of shell completion files (below) when clap 3 is out.
|
|
||||||
// For more details, see https://github.com/sharkdp/bat/issues/372
|
|
||||||
|
|
||||||
// For bat-as-a-library, no build script is required. The build script is for
|
|
||||||
// the manpage and completions, which are only relevant to the bat application.
|
|
||||||
#[cfg(not(feature = "application"))]
|
|
||||||
fn main() {}
|
|
||||||
|
|
||||||
#[cfg(feature = "application")]
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
// Read environment variables.
|
|
||||||
let project_name = option_env!("PROJECT_NAME").unwrap_or("bat");
|
|
||||||
let executable_name = option_env!("PROJECT_EXECUTABLE").unwrap_or(project_name);
|
|
||||||
let executable_name_uppercase = executable_name.to_uppercase();
|
|
||||||
static PROJECT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
/// Generates a file from a template.
|
|
||||||
fn template(
|
|
||||||
variables: &HashMap<&str, &str>,
|
|
||||||
in_file: &str,
|
|
||||||
out_file: impl AsRef<Path>,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut content = fs::read_to_string(in_file)?;
|
|
||||||
|
|
||||||
for (variable_name, value) in variables {
|
|
||||||
// Replace {{variable_name}} by the value
|
|
||||||
let pattern = format!("{{{{{variable_name}}}}}", variable_name = variable_name);
|
|
||||||
content = content.replace(&pattern, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::write(out_file, content)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("PROJECT_NAME", project_name);
|
|
||||||
variables.insert("PROJECT_EXECUTABLE", executable_name);
|
|
||||||
variables.insert("PROJECT_EXECUTABLE_UPPERCASE", &executable_name_uppercase);
|
|
||||||
variables.insert("PROJECT_VERSION", PROJECT_VERSION);
|
|
||||||
|
|
||||||
let out_dir_env = std::env::var_os("BAT_ASSETS_GEN_DIR")
|
|
||||||
.or_else(|| std::env::var_os("OUT_DIR"))
|
|
||||||
.expect("BAT_ASSETS_GEN_DIR or OUT_DIR to be set in build.rs");
|
|
||||||
let out_dir = Path::new(&out_dir_env);
|
|
||||||
|
|
||||||
fs::create_dir_all(out_dir.join("assets/manual")).unwrap();
|
|
||||||
fs::create_dir_all(out_dir.join("assets/completions")).unwrap();
|
|
||||||
|
|
||||||
template(
|
|
||||||
&variables,
|
|
||||||
"assets/manual/bat.1.in",
|
|
||||||
out_dir.join("assets/manual/bat.1"),
|
|
||||||
)?;
|
|
||||||
template(
|
|
||||||
&variables,
|
|
||||||
"assets/completions/bat.bash.in",
|
|
||||||
out_dir.join("assets/completions/bat.bash"),
|
|
||||||
)?;
|
|
||||||
template(
|
|
||||||
&variables,
|
|
||||||
"assets/completions/bat.fish.in",
|
|
||||||
out_dir.join("assets/completions/bat.fish"),
|
|
||||||
)?;
|
|
||||||
template(
|
|
||||||
&variables,
|
|
||||||
"assets/completions/_bat.ps1.in",
|
|
||||||
out_dir.join("assets/completions/_bat.ps1"),
|
|
||||||
)?;
|
|
||||||
template(
|
|
||||||
&variables,
|
|
||||||
"assets/completions/bat.zsh.in",
|
|
||||||
out_dir.join("assets/completions/bat.zsh"),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[macro_use]
|
|
||||||
// extern crate clap;
|
|
||||||
|
|
||||||
// use clap::Shell;
|
|
||||||
// use std::fs;
|
|
||||||
|
|
||||||
// include!("src/clap_app.rs");
|
|
||||||
|
|
||||||
// const BIN_NAME: &str = "bat";
|
|
||||||
|
|
||||||
// fn main() {
|
|
||||||
// let outdir = std::env::var_os("SHELL_COMPLETIONS_DIR").or(std::env::var_os("OUT_DIR"));
|
|
||||||
|
|
||||||
// let outdir = match outdir {
|
|
||||||
// None => return,
|
|
||||||
// Some(outdir) => outdir,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fs::create_dir_all(&outdir).unwrap();
|
|
||||||
|
|
||||||
// let mut app = build_app(true);
|
|
||||||
// app.gen_completions(BIN_NAME, Shell::Bash, &outdir);
|
|
||||||
// app.gen_completions(BIN_NAME, Shell::Fish, &outdir);
|
|
||||||
// app.gen_completions(BIN_NAME, Shell::Zsh, &outdir);
|
|
||||||
// app.gen_completions(BIN_NAME, Shell::PowerShell, &outdir);
|
|
||||||
// }
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
use std::{env, fs, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::util::render_template;
|
||||||
|
|
||||||
|
/// Generate manpage and shell completions for the bat application.
|
||||||
|
pub fn gen_man_and_comp() -> anyhow::Result<()> {
|
||||||
|
println!("cargo:rerun-if-changed=assets/manual/");
|
||||||
|
println!("cargo:rerun-if-changed=assets/completions/");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-env-changed=PROJECT_NAME");
|
||||||
|
println!("cargo:rerun-if-env-changed=PROJECT_EXECUTABLE");
|
||||||
|
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
|
||||||
|
println!("cargo:rerun-if-env-changed=BAT_ASSETS_GEN_DIR");
|
||||||
|
|
||||||
|
// Read environment variables.
|
||||||
|
let project_name = env::var("PROJECT_NAME").unwrap_or("bat".into());
|
||||||
|
let executable_name = env::var("PROJECT_EXECUTABLE").unwrap_or(project_name.clone());
|
||||||
|
let executable_name_uppercase = executable_name.to_uppercase();
|
||||||
|
let project_version = env::var("CARGO_PKG_VERSION")?;
|
||||||
|
|
||||||
|
let variables = [
|
||||||
|
("PROJECT_NAME", project_name),
|
||||||
|
("PROJECT_EXECUTABLE", executable_name),
|
||||||
|
("PROJECT_EXECUTABLE_UPPERCASE", executable_name_uppercase),
|
||||||
|
("PROJECT_VERSION", project_version),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let Some(out_dir) = env::var_os("BAT_ASSETS_GEN_DIR")
|
||||||
|
.or_else(|| env::var_os("OUT_DIR"))
|
||||||
|
.map(PathBuf::from)
|
||||||
|
else {
|
||||||
|
anyhow::bail!("BAT_ASSETS_GEN_DIR or OUT_DIR should be set for build.rs");
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::create_dir_all(out_dir.join("assets/manual")).unwrap();
|
||||||
|
fs::create_dir_all(out_dir.join("assets/completions")).unwrap();
|
||||||
|
|
||||||
|
render_template(
|
||||||
|
&variables,
|
||||||
|
"assets/manual/bat.1.in",
|
||||||
|
out_dir.join("assets/manual/bat.1"),
|
||||||
|
)?;
|
||||||
|
render_template(
|
||||||
|
&variables,
|
||||||
|
"assets/completions/bat.bash.in",
|
||||||
|
out_dir.join("assets/completions/bat.bash"),
|
||||||
|
)?;
|
||||||
|
render_template(
|
||||||
|
&variables,
|
||||||
|
"assets/completions/bat.fish.in",
|
||||||
|
out_dir.join("assets/completions/bat.fish"),
|
||||||
|
)?;
|
||||||
|
render_template(
|
||||||
|
&variables,
|
||||||
|
"assets/completions/_bat.ps1.in",
|
||||||
|
out_dir.join("assets/completions/_bat.ps1"),
|
||||||
|
)?;
|
||||||
|
render_template(
|
||||||
|
&variables,
|
||||||
|
"assets/completions/bat.zsh.in",
|
||||||
|
out_dir.join("assets/completions/bat.zsh"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
#[cfg(feature = "application")]
|
||||||
|
mod application;
|
||||||
|
mod syntax_mapping;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
// only watch manually-designated files
|
||||||
|
// see: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed
|
||||||
|
println!("cargo:rerun-if-changed=build/");
|
||||||
|
|
||||||
|
syntax_mapping::build_static_mappings()?;
|
||||||
|
|
||||||
|
#[cfg(feature = "application")]
|
||||||
|
application::gen_man_and_comp()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,294 @@
|
||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
env, fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde_derive::Deserialize;
|
||||||
|
use serde_with::DeserializeFromStr;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
/// Known mapping targets.
|
||||||
|
///
|
||||||
|
/// Corresponds to `syntax_mapping::MappingTarget`.
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Hash, DeserializeFromStr)]
|
||||||
|
pub enum MappingTarget {
|
||||||
|
MapTo(String),
|
||||||
|
MapToUnknown,
|
||||||
|
MapExtensionToUnknown,
|
||||||
|
}
|
||||||
|
impl FromStr for MappingTarget {
|
||||||
|
type Err = Infallible;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"MappingTarget::MapToUnknown" => Ok(Self::MapToUnknown),
|
||||||
|
"MappingTarget::MapExtensionToUnknown" => Ok(Self::MapExtensionToUnknown),
|
||||||
|
syntax => Ok(Self::MapTo(syntax.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MappingTarget {
|
||||||
|
fn codegen(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::MapTo(syntax) => format!(r###"MappingTarget::MapTo(r#"{syntax}"#)"###),
|
||||||
|
Self::MapToUnknown => "MappingTarget::MapToUnknown".into(),
|
||||||
|
Self::MapExtensionToUnknown => "MappingTarget::MapExtensionToUnknown".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)]
|
||||||
|
/// A single matcher.
|
||||||
|
///
|
||||||
|
/// Codegen converts this into a `Lazy<Option<GlobMatcher>>`.
|
||||||
|
struct Matcher(Vec<MatcherSegment>);
|
||||||
|
/// Parse a matcher.
|
||||||
|
///
|
||||||
|
/// Note that this implementation is rather strict: it will greedily interpret
|
||||||
|
/// every valid environment variable replacement as such, then immediately
|
||||||
|
/// hard-error if it finds a '$' anywhere in the remaining text segments.
|
||||||
|
///
|
||||||
|
/// The reason for this strictness is I currently cannot think of a valid reason
|
||||||
|
/// why you would ever need '$' as plaintext in a glob pattern. Therefore any
|
||||||
|
/// such occurrences are likely human errors.
|
||||||
|
///
|
||||||
|
/// If we later discover some edge cases, it's okay to make it more permissive.
|
||||||
|
///
|
||||||
|
/// Revision history:
|
||||||
|
/// - 2024-02-20: allow `{` and `}` (glob brace expansion)
|
||||||
|
impl FromStr for Matcher {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
use MatcherSegment as Seg;
|
||||||
|
static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap());
|
||||||
|
|
||||||
|
let mut segments = vec![];
|
||||||
|
let mut text_start = 0;
|
||||||
|
for capture in VAR_REGEX.captures_iter(s) {
|
||||||
|
let match_0 = capture.get(0).unwrap();
|
||||||
|
|
||||||
|
// text before this var
|
||||||
|
let text_end = match_0.start();
|
||||||
|
segments.push(Seg::Text(s[text_start..text_end].into()));
|
||||||
|
text_start = match_0.end();
|
||||||
|
|
||||||
|
// this var
|
||||||
|
segments.push(Seg::Env(capture.get(1).unwrap().as_str().into()));
|
||||||
|
}
|
||||||
|
// possible trailing text
|
||||||
|
segments.push(Seg::Text(s[text_start..].into()));
|
||||||
|
|
||||||
|
// cleanup empty text segments
|
||||||
|
let non_empty_segments = segments
|
||||||
|
.into_iter()
|
||||||
|
.filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true))
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
if non_empty_segments
|
||||||
|
.windows(2)
|
||||||
|
.any(|segs| segs[0].is_text() && segs[1].is_text())
|
||||||
|
{
|
||||||
|
unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// guard empty case
|
||||||
|
if non_empty_segments.is_empty() {
|
||||||
|
bail!(r#"Parsed an empty matcher: "{s}""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// guard variable syntax leftover fragments
|
||||||
|
if non_empty_segments
|
||||||
|
.iter()
|
||||||
|
.filter_map(Seg::text)
|
||||||
|
.any(|t| t.contains('$'))
|
||||||
|
{
|
||||||
|
bail!(r#"Invalid matcher: "{s}""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(non_empty_segments))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Matcher {
|
||||||
|
fn codegen(&self) -> String {
|
||||||
|
match self.0.len() {
|
||||||
|
0 => unreachable!("0-length matcher should never be created"),
|
||||||
|
// if-let guard would be ideal here
|
||||||
|
// see: https://github.com/rust-lang/rust/issues/51114
|
||||||
|
1 if self.0[0].is_text() => {
|
||||||
|
let s = self.0[0].text().unwrap();
|
||||||
|
format!(r###"Lazy::new(|| Some(build_matcher_fixed(r#"{s}"#)))"###)
|
||||||
|
}
|
||||||
|
// parser logic ensures that this case can only happen when there are dynamic segments
|
||||||
|
_ => {
|
||||||
|
let segs = self.0.iter().map(MatcherSegment::codegen).join(", ");
|
||||||
|
format!(r###"Lazy::new(|| build_matcher_dynamic(&[{segs}]))"###)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A segment in a matcher.
|
||||||
|
///
|
||||||
|
/// Corresponds to `syntax_mapping::MatcherSegment`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
enum MatcherSegment {
|
||||||
|
Text(String),
|
||||||
|
Env(String),
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl MatcherSegment {
|
||||||
|
fn is_text(&self) -> bool {
|
||||||
|
matches!(self, Self::Text(_))
|
||||||
|
}
|
||||||
|
fn is_env(&self) -> bool {
|
||||||
|
matches!(self, Self::Env(_))
|
||||||
|
}
|
||||||
|
fn text(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Self::Text(t) => Some(t),
|
||||||
|
Self::Env(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn env(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Self::Text(_) => None,
|
||||||
|
Self::Env(t) => Some(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn codegen(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Text(s) => format!(r###"MatcherSegment::Text(r#"{s}"#)"###),
|
||||||
|
Self::Env(s) => format!(r###"MatcherSegment::Env(r#"{s}"#)"###),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct that models a single .toml file in /src/syntax_mapping/builtins/.
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
struct MappingDefModel {
|
||||||
|
mappings: IndexMap<MappingTarget, Vec<Matcher>>,
|
||||||
|
}
|
||||||
|
impl MappingDefModel {
|
||||||
|
fn into_mapping_list(self) -> MappingList {
|
||||||
|
let list = self
|
||||||
|
.mappings
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(target, matchers)| {
|
||||||
|
matchers
|
||||||
|
.into_iter()
|
||||||
|
.map(|matcher| (matcher, target.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
MappingList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct MappingList(Vec<(Matcher, MappingTarget)>);
|
||||||
|
impl MappingList {
|
||||||
|
fn codegen(&self) -> String {
|
||||||
|
let array_items: Vec<_> = self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|(matcher, target)| {
|
||||||
|
format!("({m}, {t})", m = matcher.codegen(), t = target.codegen())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let len = array_items.len();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"/// Generated by build script from /src/syntax_mapping/builtins/.\n\
|
||||||
|
pub(crate) static BUILTIN_MAPPINGS: [(Lazy<Option<GlobMatcher>>, MappingTarget); {len}] = [\n{items}\n];",
|
||||||
|
items = array_items.join(",\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of paths to all mapping definition files that should be
|
||||||
|
/// included for the current target platform.
|
||||||
|
fn get_def_paths() -> anyhow::Result<Vec<PathBuf>> {
|
||||||
|
let source_subdirs = [
|
||||||
|
"common",
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
"unix-family",
|
||||||
|
#[cfg(any(
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "openbsd",
|
||||||
|
target_os = "macos"
|
||||||
|
))]
|
||||||
|
"bsd-family",
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
"linux",
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"macos",
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
"windows",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut toml_paths = vec![];
|
||||||
|
for subdir in source_subdirs {
|
||||||
|
let wd = WalkDir::new(Path::new("src/syntax_mapping/builtins").join(subdir));
|
||||||
|
let paths = wd
|
||||||
|
.into_iter()
|
||||||
|
.filter_map_ok(|entry| {
|
||||||
|
let path = entry.path();
|
||||||
|
(path.is_file() && path.extension().map(|ext| ext == "toml").unwrap_or(false))
|
||||||
|
.then(|| path.to_owned())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
toml_paths.extend(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
toml_paths.sort_by_key(|path| {
|
||||||
|
path.file_name()
|
||||||
|
.expect("file name should not terminate in ..")
|
||||||
|
.to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(toml_paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_all_mappings() -> anyhow::Result<MappingList> {
|
||||||
|
let mut all_mappings = vec![];
|
||||||
|
|
||||||
|
for path in get_def_paths()? {
|
||||||
|
let toml_string = fs::read_to_string(path)?;
|
||||||
|
let mappings = toml::from_str::<MappingDefModel>(&toml_string)?.into_mapping_list();
|
||||||
|
all_mappings.extend(mappings.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let duplicates = all_mappings
|
||||||
|
.iter()
|
||||||
|
.duplicates_by(|(matcher, _)| matcher)
|
||||||
|
.collect_vec();
|
||||||
|
if !duplicates.is_empty() {
|
||||||
|
bail!("Rules with duplicate matchers found: {duplicates:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MappingList(all_mappings))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the static syntax mappings defined in /src/syntax_mapping/builtins/
|
||||||
|
/// into a .rs source file, which is to be inserted with `include!`.
|
||||||
|
pub fn build_static_mappings() -> anyhow::Result<()> {
|
||||||
|
println!("cargo:rerun-if-changed=src/syntax_mapping/builtins/");
|
||||||
|
|
||||||
|
let mappings = read_all_mappings()?;
|
||||||
|
|
||||||
|
let codegen_path = Path::new(&env::var_os("OUT_DIR").ok_or(anyhow!("OUT_DIR is unset"))?)
|
||||||
|
.join("codegen_static_syntax_mappings.rs");
|
||||||
|
|
||||||
|
fs::write(codegen_path, mappings.codegen())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::{collections::HashMap, fs, path::Path};
|
||||||
|
|
||||||
|
/// Generates a file from a template.
|
||||||
|
pub fn render_template(
|
||||||
|
variables: &HashMap<&str, String>,
|
||||||
|
in_file: &str,
|
||||||
|
out_file: impl AsRef<Path>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut content = fs::read_to_string(in_file)?;
|
||||||
|
|
||||||
|
for (variable_name, value) in variables {
|
||||||
|
// Replace {{variable_name}} by the value
|
||||||
|
let pattern = format!("{{{{{variable_name}}}}}");
|
||||||
|
content = content.replace(&pattern, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(out_file, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -181,7 +181,7 @@ man 2 select
|
||||||
|
|
||||||
## インストール
|
## インストール
|
||||||
|
|
||||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
|
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
|
||||||
|
|
||||||
### On Ubuntu (`apt` を使用)
|
### On Ubuntu (`apt` を使用)
|
||||||
*... や他のDebianベースのLinuxディストリビューション*
|
*... や他のDebianベースのLinuxディストリビューション*
|
||||||
|
@ -219,7 +219,7 @@ apk add bat
|
||||||
|
|
||||||
### On Arch Linux
|
### On Arch Linux
|
||||||
|
|
||||||
[Arch Linuxの公式リソース](https://www.archlinux.org/packages/community/x86_64/bat/)
|
[Arch Linuxの公式リソース](https://www.archlinux.org/packages/extra/x86_64/bat/)
|
||||||
からインストールできます。
|
からインストールできます。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
@ -214,7 +214,7 @@ man 2 select
|
||||||
|
|
||||||
## 설치
|
## 설치
|
||||||
|
|
||||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
|
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
|
||||||
|
|
||||||
### Ubuntu에서 (`apt` 사용)
|
### Ubuntu에서 (`apt` 사용)
|
||||||
*... 그리고 다른 Debian 기반의 Linux 배포판들에서.*
|
*... 그리고 다른 Debian 기반의 Linux 배포판들에서.*
|
||||||
|
@ -264,7 +264,7 @@ apk add bat
|
||||||
### Arch Linux에서
|
### Arch Linux에서
|
||||||
|
|
||||||
공식 소스를 통해
|
공식 소스를 통해
|
||||||
[`bat` 패키지](https://www.archlinux.org/packages/community/x86_64/bat/)를
|
[`bat` 패키지](https://www.archlinux.org/packages/extra/x86_64/bat/)를
|
||||||
설치할 수 있습니다:
|
설치할 수 있습니다:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<a href="#установка">Установка</a> •
|
<a href="#установка">Установка</a> •
|
||||||
<a href="#кастомизация">Кастомизация</a> •
|
<a href="#кастомизация">Кастомизация</a> •
|
||||||
<a href="#цели-и-альтернативы">Цели и альтернативы </a><br>
|
<a href="#цели-и-альтернативы">Цели и альтернативы </a><br>
|
||||||
[<a href="../README.md">English]
|
[<a href="../README.md">English</a>]
|
||||||
[<a href="README-zh.md">中文</a>]
|
[<a href="README-zh.md">中文</a>]
|
||||||
[<a href="README-ja.md">日本語</a>]
|
[<a href="README-ja.md">日本語</a>]
|
||||||
[<a href="README-ko.md">한국어</a>]
|
[<a href="README-ko.md">한국어</a>]
|
||||||
|
@ -160,7 +160,7 @@ man 2 select
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
|
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
|
||||||
|
|
||||||
### Ubuntu (с помощью `apt`)
|
### Ubuntu (с помощью `apt`)
|
||||||
*... и другие дистрибутивы основанные на Debian.*
|
*... и другие дистрибутивы основанные на Debian.*
|
||||||
|
@ -201,7 +201,7 @@ apk add bat
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
|
|
||||||
Вы можете установить [`bat`](https://www.archlinux.org/packages/community/x86_64/bat/) из официального источника:
|
Вы можете установить [`bat`](https://www.archlinux.org/packages/extra/x86_64/bat/) из официального источника:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pacman -S bat
|
pacman -S bat
|
||||||
|
|
|
@ -191,7 +191,7 @@ man 2 select
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
|
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
|
||||||
|
|
||||||
### Ubuntu (使用 `apt`)
|
### Ubuntu (使用 `apt`)
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ apk add bat
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
|
|
||||||
你可以用下面下列命令从官方源中安装[`bat`包](https://www.archlinux.org/packages/community/x86_64/bat/):
|
你可以用下面下列命令从官方源中安装[`bat`包](https://www.archlinux.org/packages/extra/x86_64/bat/):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pacman -S bat
|
pacman -S bat
|
||||||
|
@ -412,7 +412,7 @@ bat --list-themes | fzf --preview="bat --theme={} --color=always /path/to/file"
|
||||||
|
|
||||||
### 输出样式
|
### 输出样式
|
||||||
|
|
||||||
你可以用`--style`参数来控制`bat`输出的样式。使用`--style=numbers,chanegs`可以只开启 Git 修改和行号显示而不添加其他内容。`BAT_STYLE`环境变量具有相同功能。
|
你可以用`--style`参数来控制`bat`输出的样式。使用`--style=numbers,changes`可以只开启 Git 修改和行号显示而不添加其他内容。`BAT_STYLE`环境变量具有相同功能。
|
||||||
|
|
||||||
### 添加新的语言和语法
|
### 添加新的语言和语法
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,12 @@ Options:
|
||||||
--list-themes
|
--list-themes
|
||||||
Display a list of supported themes for syntax highlighting.
|
Display a list of supported themes for syntax highlighting.
|
||||||
|
|
||||||
|
-s, --squeeze-blank
|
||||||
|
Squeeze consecutive empty lines into a single empty line.
|
||||||
|
|
||||||
|
--squeeze-limit <squeeze-limit>
|
||||||
|
Set the maximum number of consecutive empty lines to be printed.
|
||||||
|
|
||||||
--style <components>
|
--style <components>
|
||||||
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
|
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
|
||||||
to display in addition to the file contents. The argument is a comma-separated list of
|
to display in addition to the file contents. The argument is a comma-separated list of
|
||||||
|
@ -123,6 +129,9 @@ Options:
|
||||||
set a default style, add the '--style=".."' option to the configuration file or export the
|
set a default style, add the '--style=".."' option to the configuration file or export the
|
||||||
BAT_STYLE environment variable (e.g.: export BAT_STYLE="..").
|
BAT_STYLE environment variable (e.g.: export BAT_STYLE="..").
|
||||||
|
|
||||||
|
By default, the following components are enabled:
|
||||||
|
changes, grid, header-filename, numbers, snip
|
||||||
|
|
||||||
Possible values:
|
Possible values:
|
||||||
|
|
||||||
* default: enables recommended style components (default).
|
* default: enables recommended style components (default).
|
||||||
|
@ -160,6 +169,9 @@ Options:
|
||||||
--acknowledgements
|
--acknowledgements
|
||||||
Show acknowledgements.
|
Show acknowledgements.
|
||||||
|
|
||||||
|
--set-terminal-title
|
||||||
|
Sets terminal title to filenames when using a pager.
|
||||||
|
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help (see a summary with '-h')
|
Print help (see a summary with '-h')
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,16 @@
|
||||||
- [ ] Update the version and the min. supported Rust version in `README.md` and
|
- [ ] Update the version and the min. supported Rust version in `README.md` and
|
||||||
`doc/README-*.md`. Check with
|
`doc/README-*.md`. Check with
|
||||||
`git grep -i -e 'rust.*1\.' -e '1\..*rust' | grep README | grep -v tests/`.
|
`git grep -i -e 'rust.*1\.' -e '1\..*rust' | grep README | grep -v tests/`.
|
||||||
- [ ] Update `CHANGELOG.md`. Introduce a section for the new release.
|
|
||||||
|
## CHANGELOG.md updates
|
||||||
|
|
||||||
|
- [ ] Go to https://github.com/sharkdp/bat/releases/new, click "Choose a tag",
|
||||||
|
type the name of the tag that will be created later, click "Generate release
|
||||||
|
notes". DO NOT ACTUALLY CREATE ANY RELEASE IN THIS STEP.
|
||||||
|
- [ ] Compare current `CHANGELOG.md` with auto-generated release notes and add
|
||||||
|
missing entries. Expect in particular dependabot PRs to not be in
|
||||||
|
`CHANGELOG.md` since they are [auto-merged] if CI passes.
|
||||||
|
- [ ] Introduce a section for the new release and perform final touch-ups.
|
||||||
|
|
||||||
## Update syntaxes and themes (build assets)
|
## Update syntaxes and themes (build assets)
|
||||||
|
|
||||||
|
@ -71,3 +80,5 @@
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[auto-merged]: https://github.com/sharkdp/bat/blob/master/.github/workflows/Auto-merge-dependabot-PRs.yml
|
||||||
|
|
|
@ -43,6 +43,8 @@ Options:
|
||||||
Set the color theme for syntax highlighting.
|
Set the color theme for syntax highlighting.
|
||||||
--list-themes
|
--list-themes
|
||||||
Display all supported highlighting themes.
|
Display all supported highlighting themes.
|
||||||
|
-s, --squeeze-blank
|
||||||
|
Squeeze consecutive empty lines.
|
||||||
--style <components>
|
--style <components>
|
||||||
Comma-separated list of style elements to display (*default*, auto, full, plain, changes,
|
Comma-separated list of style elements to display (*default*, auto, full, plain, changes,
|
||||||
header, header-filename, header-filesize, grid, rule, numbers, snip).
|
header, header-filename, header-filesize, grid, rule, numbers, snip).
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 817 KiB After Width: | Height: | Size: 79 KiB |
|
@ -13,6 +13,6 @@ fn main() {
|
||||||
|
|
||||||
println!("Themes:");
|
println!("Themes:");
|
||||||
for theme in printer.themes() {
|
for theme in printer.themes() {
|
||||||
println!("- {}", theme);
|
println!("- {theme}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -380,7 +380,7 @@ fn asset_from_contents<T: serde::de::DeserializeOwned>(
|
||||||
} else {
|
} else {
|
||||||
bincode::deserialize_from(contents)
|
bincode::deserialize_from(contents)
|
||||||
}
|
}
|
||||||
.map_err(|_| format!("Could not parse {}", description).into())
|
.map_err(|_| format!("Could not parse {description}").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn asset_from_cache<T: serde::de::DeserializeOwned>(
|
fn asset_from_cache<T: serde::de::DeserializeOwned>(
|
||||||
|
@ -396,7 +396,7 @@ fn asset_from_cache<T: serde::de::DeserializeOwned>(
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
asset_from_contents(&contents[..], description, compressed)
|
asset_from_contents(&contents[..], description, compressed)
|
||||||
.map_err(|_| format!("Could not parse cached {}", description).into())
|
.map_err(|_| format!("Could not parse cached {description}").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
@ -441,7 +441,7 @@ mod tests {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
SyntaxDetectionTest {
|
SyntaxDetectionTest {
|
||||||
assets: HighlightingAssets::from_binary(),
|
assets: HighlightingAssets::from_binary(),
|
||||||
syntax_mapping: SyntaxMapping::builtin(),
|
syntax_mapping: SyntaxMapping::new(),
|
||||||
temp_dir: TempDir::new().expect("creation of temporary directory"),
|
temp_dir: TempDir::new().expect("creation of temporary directory"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,7 @@ mod tests {
|
||||||
let file_path = self.temp_dir.path().join(file_name);
|
let file_path = self.temp_dir.path().join(file_name);
|
||||||
{
|
{
|
||||||
let mut temp_file = File::create(&file_path).unwrap();
|
let mut temp_file = File::create(&file_path).unwrap();
|
||||||
writeln!(temp_file, "{}", first_line).unwrap();
|
writeln!(temp_file, "{first_line}").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let input = Input::ordinary_file(&file_path);
|
let input = Input::ordinary_file(&file_path);
|
||||||
|
@ -514,8 +514,7 @@ mod tests {
|
||||||
|
|
||||||
if !consistent {
|
if !consistent {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Inconsistent syntax detection:\nFor File: {}\nFor Reader: {}",
|
"Inconsistent syntax detection:\nFor File: {as_file}\nFor Reader: {as_reader}"
|
||||||
as_file, as_reader
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::path::Path;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
|
||||||
if !missing_contexts.is_empty() {
|
if !missing_contexts.is_empty() {
|
||||||
println!("Some referenced contexts could not be found!");
|
println!("Some referenced contexts could not be found!");
|
||||||
for context in missing_contexts {
|
for context in missing_contexts {
|
||||||
println!("- {}", context);
|
println!("- {context}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,7 @@ pub(crate) fn asset_to_contents<T: serde::Serialize>(
|
||||||
} else {
|
} else {
|
||||||
bincode::serialize_into(&mut contents, asset)
|
bincode::serialize_into(&mut contents, asset)
|
||||||
}
|
}
|
||||||
.map_err(|_| format!("Could not serialize {}", description))?;
|
.map_err(|_| format!("Could not serialize {description}"))?;
|
||||||
Ok(contents)
|
Ok(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ fn handle_license(path: &Path) -> Result<Option<String>> {
|
||||||
} else if license_not_needed_in_acknowledgements(&license_text) {
|
} else if license_not_needed_in_acknowledgements(&license_text) {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
Err(format!("ERROR: License is of unknown type: {:?}", path).into())
|
Err(format!("ERROR: License is of unknown type: {path:?}").into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ fn append_to_acknowledgements(
|
||||||
relative_path: &str,
|
relative_path: &str,
|
||||||
license_text: &str,
|
license_text: &str,
|
||||||
) {
|
) {
|
||||||
write!(acknowledgements, "## {}\n\n{}", relative_path, license_text).ok();
|
write!(acknowledgements, "## {relative_path}\n\n{license_text}").ok();
|
||||||
|
|
||||||
// Make sure the last char is a newline to not mess up formatting later
|
// Make sure the last char is a newline to not mess up formatting later
|
||||||
if acknowledgements
|
if acknowledgements
|
||||||
|
|
|
@ -3,8 +3,7 @@ use super::*;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use once_cell::unsync::OnceCell;
|
use once_cell::unsync::OnceCell;
|
||||||
|
|
||||||
|
@ -89,7 +88,7 @@ impl TryFrom<ThemeSet> for LazyThemeSet {
|
||||||
let lazy_theme = LazyTheme {
|
let lazy_theme = LazyTheme {
|
||||||
serialized: crate::assets::build_assets::asset_to_contents(
|
serialized: crate::assets::build_assets::asset_to_contents(
|
||||||
&theme,
|
&theme,
|
||||||
&format!("theme {}", name),
|
&format!("theme {name}"),
|
||||||
COMPRESS_LAZY_THEMES,
|
COMPRESS_LAZY_THEMES,
|
||||||
)?,
|
)?,
|
||||||
deserialized: OnceCell::new(),
|
deserialized: OnceCell::new(),
|
||||||
|
|
|
@ -29,6 +29,10 @@ fn is_truecolor_terminal() -> bool {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn env_no_color() -> bool {
|
||||||
|
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub matches: ArgMatches,
|
pub matches: ArgMatches,
|
||||||
interactive_output: bool,
|
interactive_output: bool,
|
||||||
|
@ -117,7 +121,11 @@ impl App {
|
||||||
_ => unreachable!("other values for --paging are not allowed"),
|
_ => unreachable!("other values for --paging are not allowed"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut syntax_mapping = SyntaxMapping::builtin();
|
let mut syntax_mapping = SyntaxMapping::new();
|
||||||
|
// start building glob matchers for builtin mappings immediately
|
||||||
|
// this is an appropriate approach because it's statistically likely that
|
||||||
|
// all the custom mappings need to be checked
|
||||||
|
syntax_mapping.start_offload_build_all();
|
||||||
|
|
||||||
if let Some(values) = self.matches.get_many::<String>("ignored-suffix") {
|
if let Some(values) = self.matches.get_many::<String>("ignored-suffix") {
|
||||||
for suffix in values {
|
for suffix in values {
|
||||||
|
@ -126,7 +134,9 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(values) = self.matches.get_many::<String>("map-syntax") {
|
if let Some(values) = self.matches.get_many::<String>("map-syntax") {
|
||||||
for from_to in values {
|
// later args take precedence over earlier ones, hence `.rev()`
|
||||||
|
// see: https://github.com/sharkdp/bat/pull/2755#discussion_r1456416875
|
||||||
|
for from_to in values.rev() {
|
||||||
let parts: Vec<_> = from_to.split(':').collect();
|
let parts: Vec<_> = from_to.split(':').collect();
|
||||||
|
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
|
@ -207,7 +217,7 @@ impl App {
|
||||||
|| match self.matches.get_one::<String>("color").map(|s| s.as_str()) {
|
|| match self.matches.get_one::<String>("color").map(|s| s.as_str()) {
|
||||||
Some("always") => true,
|
Some("always") => true,
|
||||||
Some("never") => false,
|
Some("never") => false,
|
||||||
Some("auto") => env::var_os("NO_COLOR").is_none() && self.interactive_output,
|
Some("auto") => !env_no_color() && self.interactive_output,
|
||||||
_ => unreachable!("other values for --color are not allowed"),
|
_ => unreachable!("other values for --color are not allowed"),
|
||||||
},
|
},
|
||||||
paging_mode,
|
paging_mode,
|
||||||
|
@ -283,6 +293,17 @@ impl App {
|
||||||
use_custom_assets: !self.matches.get_flag("no-custom-assets"),
|
use_custom_assets: !self.matches.get_flag("no-custom-assets"),
|
||||||
#[cfg(feature = "lessopen")]
|
#[cfg(feature = "lessopen")]
|
||||||
use_lessopen: self.matches.get_flag("lessopen"),
|
use_lessopen: self.matches.get_flag("lessopen"),
|
||||||
|
set_terminal_title: self.matches.get_flag("set-terminal-title"),
|
||||||
|
squeeze_lines: if self.matches.get_flag("squeeze-blank") {
|
||||||
|
Some(
|
||||||
|
self.matches
|
||||||
|
.get_one::<usize>("squeeze-limit")
|
||||||
|
.map(|limit| limit.to_owned())
|
||||||
|
.unwrap_or(1),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ pub fn assets_from_cache_or_binary(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_asset(path: PathBuf, description: &str) {
|
fn clear_asset(path: PathBuf, description: &str) {
|
||||||
print!("Clearing {} ... ", description);
|
print!("Clearing {description} ... ");
|
||||||
match fs::remove_file(&path) {
|
match fs::remove_file(&path) {
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
println!("skipped (not present)");
|
println!("skipped (not present)");
|
||||||
|
|
|
@ -19,7 +19,7 @@ static VERSION: Lazy<String> = Lazy::new(|| {
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn build_app(interactive_output: bool) -> Command {
|
pub fn build_app(interactive_output: bool) -> Command {
|
||||||
let color_when = if interactive_output && env::var_os("NO_COLOR").is_none() {
|
let color_when = if interactive_output && !crate::app::env_no_color() {
|
||||||
ColorChoice::Auto
|
ColorChoice::Auto
|
||||||
} else {
|
} else {
|
||||||
ColorChoice::Never
|
ColorChoice::Never
|
||||||
|
@ -387,6 +387,21 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||||
.help("Display all supported highlighting themes.")
|
.help("Display all supported highlighting themes.")
|
||||||
.long_help("Display a list of supported themes for syntax highlighting."),
|
.long_help("Display a list of supported themes for syntax highlighting."),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("squeeze-blank")
|
||||||
|
.long("squeeze-blank")
|
||||||
|
.short('s')
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Squeeze consecutive empty lines.")
|
||||||
|
.long_help("Squeeze consecutive empty lines into a single empty line.")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("squeeze-limit")
|
||||||
|
.long("squeeze-limit")
|
||||||
|
.value_parser(|s: &str| s.parse::<usize>().map_err(|_| "Requires a non-negative number".to_owned()))
|
||||||
|
.long_help("Set the maximum number of consecutive empty lines to be printed.")
|
||||||
|
.hide_short_help(true)
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("style")
|
Arg::new("style")
|
||||||
.long("style")
|
.long("style")
|
||||||
|
@ -415,7 +430,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(invalid) = invalid_vals.next() {
|
if let Some(invalid) = invalid_vals.next() {
|
||||||
Err(format!("Unknown style, '{}'", invalid))
|
Err(format!("Unknown style, '{invalid}'"))
|
||||||
} else {
|
} else {
|
||||||
Ok(val.to_owned())
|
Ok(val.to_owned())
|
||||||
}
|
}
|
||||||
|
@ -432,6 +447,8 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||||
pre-defined style ('full'). To set a default style, add the \
|
pre-defined style ('full'). To set a default style, add the \
|
||||||
'--style=\"..\"' option to the configuration file or export the \
|
'--style=\"..\"' option to the configuration file or export the \
|
||||||
BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\
|
BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\
|
||||||
|
By default, the following components are enabled:\n \
|
||||||
|
changes, grid, header-filename, numbers, snip\n\n\
|
||||||
Possible values:\n\n \
|
Possible values:\n\n \
|
||||||
* default: enables recommended style components (default).\n \
|
* default: enables recommended style components (default).\n \
|
||||||
* full: enables all available components.\n \
|
* full: enables all available components.\n \
|
||||||
|
@ -567,6 +584,13 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.hide_short_help(true)
|
.hide_short_help(true)
|
||||||
.help("Show acknowledgements."),
|
.help("Show acknowledgements."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("set-terminal-title")
|
||||||
|
.long("set-terminal-title")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.hide_short_help(true)
|
||||||
|
.help("Sets terminal title to filenames when using a pager."),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if the current directory contains a file name cache. Otherwise,
|
// Check if the current directory contains a file name cache. Otherwise,
|
||||||
|
|
|
@ -30,6 +30,7 @@ use directories::PROJECT_DIRS;
|
||||||
use globset::GlobMatcher;
|
use globset::GlobMatcher;
|
||||||
|
|
||||||
use bat::{
|
use bat::{
|
||||||
|
assets::HighlightingAssets,
|
||||||
config::Config,
|
config::Config,
|
||||||
controller::Controller,
|
controller::Controller,
|
||||||
error::*,
|
error::*,
|
||||||
|
@ -78,9 +79,11 @@ fn run_cache_subcommand(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_syntax_mapping_to_paths<'a>(
|
fn get_syntax_mapping_to_paths<'r, 't, I>(mappings: I) -> HashMap<&'t str, Vec<String>>
|
||||||
mappings: &[(GlobMatcher, MappingTarget<'a>)],
|
where
|
||||||
) -> HashMap<&'a str, Vec<String>> {
|
I: IntoIterator<Item = (&'r GlobMatcher, &'r MappingTarget<'t>)>,
|
||||||
|
't: 'r, // target text outlives rule
|
||||||
|
{
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for mapping in mappings {
|
for mapping in mappings {
|
||||||
if let (matcher, MappingTarget::MapTo(s)) = mapping {
|
if let (matcher, MappingTarget::MapTo(s)) = mapping {
|
||||||
|
@ -123,7 +126,7 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
|
||||||
|
|
||||||
languages.sort_by_key(|lang| lang.name.to_uppercase());
|
languages.sort_by_key(|lang| lang.name.to_uppercase());
|
||||||
|
|
||||||
let configured_languages = get_syntax_mapping_to_paths(config.syntax_mapping.mappings());
|
let configured_languages = get_syntax_mapping_to_paths(config.syntax_mapping.all_mappings());
|
||||||
|
|
||||||
for lang in &mut languages {
|
for lang in &mut languages {
|
||||||
if let Some(additional_paths) = configured_languages.get(lang.name.as_str()) {
|
if let Some(additional_paths) = configured_languages.get(lang.name.as_str()) {
|
||||||
|
@ -197,19 +200,31 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
|
||||||
let stdout = io::stdout();
|
let stdout = io::stdout();
|
||||||
let mut stdout = stdout.lock();
|
let mut stdout = stdout.lock();
|
||||||
|
|
||||||
if config.colored_output {
|
let default_theme = HighlightingAssets::default_theme();
|
||||||
for theme in assets.themes() {
|
for theme in assets.themes() {
|
||||||
|
let default_theme_info = if default_theme == theme {
|
||||||
|
" (default)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
if config.colored_output {
|
||||||
writeln!(
|
writeln!(
|
||||||
stdout,
|
stdout,
|
||||||
"Theme: {}\n",
|
"Theme: {}{}\n",
|
||||||
Style::new().bold().paint(theme.to_string())
|
Style::new().bold().paint(theme.to_string()),
|
||||||
|
default_theme_info
|
||||||
)?;
|
)?;
|
||||||
config.theme = theme.to_string();
|
config.theme = theme.to_string();
|
||||||
Controller::new(&config, &assets)
|
Controller::new(&config, &assets)
|
||||||
.run(vec![theme_preview_file()], None)
|
.run(vec![theme_preview_file()], None)
|
||||||
.ok();
|
.ok();
|
||||||
writeln!(stdout)?;
|
writeln!(stdout)?;
|
||||||
|
} else {
|
||||||
|
writeln!(stdout, "{theme}{default_theme_info}")?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.colored_output {
|
||||||
writeln!(
|
writeln!(
|
||||||
stdout,
|
stdout,
|
||||||
"Further themes can be installed to '{}', \
|
"Further themes can be installed to '{}', \
|
||||||
|
@ -218,18 +233,35 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
|
||||||
https://github.com/sharkdp/bat#adding-new-themes",
|
https://github.com/sharkdp/bat#adding-new-themes",
|
||||||
config_dir.join("themes").to_string_lossy()
|
config_dir.join("themes").to_string_lossy()
|
||||||
)?;
|
)?;
|
||||||
} else {
|
|
||||||
for theme in assets.themes() {
|
|
||||||
writeln!(stdout, "{}", theme)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_terminal_title_to(new_terminal_title: String) {
|
||||||
|
let osc_command_for_setting_terminal_title = "\x1b]0;";
|
||||||
|
let osc_end_command = "\x07";
|
||||||
|
print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
|
||||||
|
let mut new_terminal_title = "bat: ".to_string();
|
||||||
|
for (index, input) in inputs.iter().enumerate() {
|
||||||
|
new_terminal_title += input.description().title();
|
||||||
|
if index < inputs.len() - 1 {
|
||||||
|
new_terminal_title += ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_terminal_title
|
||||||
|
}
|
||||||
|
|
||||||
fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> {
|
fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> {
|
||||||
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
|
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
|
||||||
let controller = Controller::new(config, &assets);
|
let controller = Controller::new(config, &assets);
|
||||||
|
if config.paging_mode != PagingMode::Never && config.set_terminal_title {
|
||||||
|
set_terminal_title_to(get_new_terminal_title(&inputs));
|
||||||
|
}
|
||||||
controller.run(inputs, None)
|
controller.run(inputs, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,24 +281,25 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||||
.info(OperatingSystem::default())
|
.info(OperatingSystem::default())
|
||||||
.info(CommandLine::default())
|
.info(CommandLine::default())
|
||||||
.info(EnvironmentVariables::list(&[
|
.info(EnvironmentVariables::list(&[
|
||||||
"SHELL",
|
|
||||||
"PAGER",
|
|
||||||
"LESS",
|
|
||||||
"LANG",
|
|
||||||
"LC_ALL",
|
|
||||||
"BAT_PAGER",
|
|
||||||
"BAT_PAGING",
|
|
||||||
"BAT_CACHE_PATH",
|
"BAT_CACHE_PATH",
|
||||||
"BAT_CONFIG_PATH",
|
"BAT_CONFIG_PATH",
|
||||||
"BAT_OPTS",
|
"BAT_OPTS",
|
||||||
|
"BAT_PAGER",
|
||||||
|
"BAT_PAGING",
|
||||||
"BAT_STYLE",
|
"BAT_STYLE",
|
||||||
"BAT_TABS",
|
"BAT_TABS",
|
||||||
"BAT_THEME",
|
"BAT_THEME",
|
||||||
"XDG_CONFIG_HOME",
|
|
||||||
"XDG_CACHE_HOME",
|
|
||||||
"COLORTERM",
|
"COLORTERM",
|
||||||
"NO_COLOR",
|
"LANG",
|
||||||
|
"LC_ALL",
|
||||||
|
"LESS",
|
||||||
"MANPAGER",
|
"MANPAGER",
|
||||||
|
"NO_COLOR",
|
||||||
|
"PAGER",
|
||||||
|
"SHELL",
|
||||||
|
"TERM",
|
||||||
|
"XDG_CACHE_HOME",
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
]))
|
]))
|
||||||
.info(FileContent::new("System Config file", system_config_file()))
|
.info(FileContent::new("System Config file", system_config_file()))
|
||||||
.info(FileContent::new("Config file", config_file()))
|
.info(FileContent::new("Config file", config_file()))
|
||||||
|
|
|
@ -94,6 +94,12 @@ pub struct Config<'a> {
|
||||||
// Whether or not to use $LESSOPEN if set
|
// Whether or not to use $LESSOPEN if set
|
||||||
#[cfg(feature = "lessopen")]
|
#[cfg(feature = "lessopen")]
|
||||||
pub use_lessopen: bool,
|
pub use_lessopen: bool,
|
||||||
|
|
||||||
|
// Weather or not to set terminal title when using a pager
|
||||||
|
pub set_terminal_title: bool,
|
||||||
|
|
||||||
|
/// The maximum number of consecutive empty lines to display
|
||||||
|
pub squeeze_lines: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "minimal-application", feature = "paging"))]
|
#[cfg(all(feature = "minimal-application", feature = "paging"))]
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl<'b> Controller<'b> {
|
||||||
&self,
|
&self,
|
||||||
inputs: Vec<Input>,
|
inputs: Vec<Input>,
|
||||||
output_buffer: Option<&mut dyn std::fmt::Write>,
|
output_buffer: Option<&mut dyn std::fmt::Write>,
|
||||||
handle_error: impl Fn(&Error, &mut dyn Write),
|
mut handle_error: impl FnMut(&Error, &mut dyn Write),
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let mut output_type;
|
let mut output_type;
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ impl Decoration for LineNumberDecoration {
|
||||||
_printer: &InteractivePrinter,
|
_printer: &InteractivePrinter,
|
||||||
) -> DecorationText {
|
) -> DecorationText {
|
||||||
if continuation {
|
if continuation {
|
||||||
if line_number > self.cached_wrap_invalid_at {
|
if line_number >= self.cached_wrap_invalid_at {
|
||||||
let new_width = self.cached_wrap.width + 1;
|
let new_width = self.cached_wrap.width + 1;
|
||||||
return DecorationText {
|
return DecorationText {
|
||||||
text: self.color.paint(" ".repeat(new_width)).to_string(),
|
text: self.color.paint(" ".repeat(new_width)).to_string(),
|
||||||
|
@ -56,7 +56,7 @@ impl Decoration for LineNumberDecoration {
|
||||||
|
|
||||||
self.cached_wrap.clone()
|
self.cached_wrap.clone()
|
||||||
} else {
|
} else {
|
||||||
let plain: String = format!("{:4}", line_number);
|
let plain: String = format!("{line_number:4}");
|
||||||
DecorationText {
|
DecorationText {
|
||||||
width: plain.len(),
|
width: plain.len(),
|
||||||
text: self.color.paint(plain).to_string(),
|
text: self.color.paint(plain).to_string(),
|
||||||
|
|
|
@ -197,7 +197,7 @@ impl<'a> Input<'a> {
|
||||||
InputKind::StdIn => {
|
InputKind::StdIn => {
|
||||||
if let Some(stdout) = stdout_identifier {
|
if let Some(stdout) = stdout_identifier {
|
||||||
let input_identifier = Identifier::try_from(clircle::Stdio::Stdin)
|
let input_identifier = Identifier::try_from(clircle::Stdio::Stdin)
|
||||||
.map_err(|e| format!("Stdin: Error identifying file: {}", e))?;
|
.map_err(|e| format!("Stdin: Error identifying file: {e}"))?;
|
||||||
if stdout.surely_conflicts_with(&input_identifier) {
|
if stdout.surely_conflicts_with(&input_identifier) {
|
||||||
return Err("IO circle detected. The input from stdin is also an output. Aborting to avoid infinite loop.".into());
|
return Err("IO circle detected. The input from stdin is also an output. Aborting to avoid infinite loop.".into());
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,31 +91,27 @@ pub fn replace_nonprintable(
|
||||||
});
|
});
|
||||||
line_idx = 0;
|
line_idx = 0;
|
||||||
}
|
}
|
||||||
// carriage return
|
// ASCII control characters
|
||||||
'\x0D' => output.push_str(match nonprintable_notation {
|
'\x00'..='\x1F' => {
|
||||||
NonprintableNotation::Caret => "^M",
|
let c = u32::from(chr);
|
||||||
NonprintableNotation::Unicode => "␍",
|
|
||||||
}),
|
match nonprintable_notation {
|
||||||
// null
|
NonprintableNotation::Caret => {
|
||||||
'\x00' => output.push_str(match nonprintable_notation {
|
let caret_character = char::from_u32(0x40 + c).unwrap();
|
||||||
NonprintableNotation::Caret => "^@",
|
write!(output, "^{caret_character}").ok();
|
||||||
NonprintableNotation::Unicode => "␀",
|
}
|
||||||
}),
|
|
||||||
// bell
|
NonprintableNotation::Unicode => {
|
||||||
'\x07' => output.push_str(match nonprintable_notation {
|
let replacement_symbol = char::from_u32(0x2400 + c).unwrap();
|
||||||
NonprintableNotation::Caret => "^G",
|
output.push(replacement_symbol)
|
||||||
NonprintableNotation::Unicode => "␇",
|
}
|
||||||
}),
|
}
|
||||||
// backspace
|
}
|
||||||
'\x08' => output.push_str(match nonprintable_notation {
|
// delete
|
||||||
NonprintableNotation::Caret => "^H",
|
'\x7F' => match nonprintable_notation {
|
||||||
NonprintableNotation::Unicode => "␈",
|
NonprintableNotation::Caret => output.push_str("^?"),
|
||||||
}),
|
NonprintableNotation::Unicode => output.push('\u{2421}'),
|
||||||
// escape
|
},
|
||||||
'\x1B' => output.push_str(match nonprintable_notation {
|
|
||||||
NonprintableNotation::Caret => "^[",
|
|
||||||
NonprintableNotation::Unicode => "␛",
|
|
||||||
}),
|
|
||||||
// printable ASCII
|
// printable ASCII
|
||||||
c if c.is_ascii_alphanumeric()
|
c if c.is_ascii_alphanumeric()
|
||||||
|| c.is_ascii_punctuation()
|
|| c.is_ascii_punctuation()
|
||||||
|
|
|
@ -230,6 +230,12 @@ impl<'a> PrettyPrinter<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the maximum number of consecutive empty lines to print.
|
||||||
|
pub fn squeeze_empty_lines(&mut self, maximum: Option<usize>) -> &mut Self {
|
||||||
|
self.config.squeeze_lines = maximum;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Specify the highlighting theme
|
/// Specify the highlighting theme
|
||||||
pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self {
|
pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self {
|
||||||
self.config.theme = theme.as_ref().to_owned();
|
self.config.theme = theme.as_ref().to_owned();
|
||||||
|
|
252
src/printer.rs
252
src/printer.rs
|
@ -7,10 +7,9 @@ use nu_ansi_term::Style;
|
||||||
|
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
|
|
||||||
use console::AnsiCodeIterator;
|
|
||||||
|
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::highlighting::Color;
|
use syntect::highlighting::Color;
|
||||||
|
use syntect::highlighting::FontStyle;
|
||||||
use syntect::highlighting::Theme;
|
use syntect::highlighting::Theme;
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
|
@ -33,9 +32,39 @@ use crate::line_range::RangeCheckResult;
|
||||||
use crate::preprocessor::{expand_tabs, replace_nonprintable};
|
use crate::preprocessor::{expand_tabs, replace_nonprintable};
|
||||||
use crate::style::StyleComponent;
|
use crate::style::StyleComponent;
|
||||||
use crate::terminal::{as_terminal_escaped, to_ansi_color};
|
use crate::terminal::{as_terminal_escaped, to_ansi_color};
|
||||||
use crate::vscreen::AnsiStyle;
|
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
|
||||||
use crate::wrapping::WrappingMode;
|
use crate::wrapping::WrappingMode;
|
||||||
|
|
||||||
|
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
|
||||||
|
raw_sequence: "\x1B[4m",
|
||||||
|
parameters: "4",
|
||||||
|
intermediates: "",
|
||||||
|
final_byte: "m",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI {
|
||||||
|
raw_sequence: "\x1B[24m",
|
||||||
|
parameters: "24",
|
||||||
|
intermediates: "",
|
||||||
|
final_byte: "m",
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_SYNTECT_STYLE: syntect::highlighting::Style = syntect::highlighting::Style {
|
||||||
|
foreground: Color {
|
||||||
|
r: 127,
|
||||||
|
g: 127,
|
||||||
|
b: 127,
|
||||||
|
a: 255,
|
||||||
|
},
|
||||||
|
background: Color {
|
||||||
|
r: 127,
|
||||||
|
g: 127,
|
||||||
|
b: 127,
|
||||||
|
a: 255,
|
||||||
|
},
|
||||||
|
font_style: FontStyle::empty(),
|
||||||
|
};
|
||||||
|
|
||||||
pub enum OutputHandle<'a> {
|
pub enum OutputHandle<'a> {
|
||||||
IoWrite(&'a mut dyn io::Write),
|
IoWrite(&'a mut dyn io::Write),
|
||||||
FmtWrite(&'a mut dyn fmt::Write),
|
FmtWrite(&'a mut dyn fmt::Write),
|
||||||
|
@ -72,11 +101,15 @@ pub(crate) trait Printer {
|
||||||
|
|
||||||
pub struct SimplePrinter<'a> {
|
pub struct SimplePrinter<'a> {
|
||||||
config: &'a Config<'a>,
|
config: &'a Config<'a>,
|
||||||
|
consecutive_empty_lines: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SimplePrinter<'a> {
|
impl<'a> SimplePrinter<'a> {
|
||||||
pub fn new(config: &'a Config) -> Self {
|
pub fn new(config: &'a Config) -> Self {
|
||||||
SimplePrinter { config }
|
SimplePrinter {
|
||||||
|
config,
|
||||||
|
consecutive_empty_lines: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +138,21 @@ impl<'a> Printer for SimplePrinter<'a> {
|
||||||
_line_number: usize,
|
_line_number: usize,
|
||||||
line_buffer: &[u8],
|
line_buffer: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
// Skip squeezed lines.
|
||||||
|
if let Some(squeeze_limit) = self.config.squeeze_lines {
|
||||||
|
if String::from_utf8_lossy(line_buffer)
|
||||||
|
.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
self.consecutive_empty_lines += 1;
|
||||||
|
if self.consecutive_empty_lines > squeeze_limit {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.consecutive_empty_lines = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !out_of_range {
|
if !out_of_range {
|
||||||
if self.config.show_nonprintable {
|
if self.config.show_nonprintable {
|
||||||
let line = replace_nonprintable(
|
let line = replace_nonprintable(
|
||||||
|
@ -112,7 +160,7 @@ impl<'a> Printer for SimplePrinter<'a> {
|
||||||
self.config.tab_width,
|
self.config.tab_width,
|
||||||
self.config.nonprintable_notation,
|
self.config.nonprintable_notation,
|
||||||
);
|
);
|
||||||
write!(handle, "{}", line)?;
|
write!(handle, "{line}")?;
|
||||||
} else {
|
} else {
|
||||||
match handle {
|
match handle {
|
||||||
OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?,
|
OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?,
|
||||||
|
@ -158,6 +206,7 @@ pub(crate) struct InteractivePrinter<'a> {
|
||||||
pub line_changes: &'a Option<LineChanges>,
|
pub line_changes: &'a Option<LineChanges>,
|
||||||
highlighter_from_set: Option<HighlighterFromSet<'a>>,
|
highlighter_from_set: Option<HighlighterFromSet<'a>>,
|
||||||
background_color_highlight: Option<Color>,
|
background_color_highlight: Option<Color>,
|
||||||
|
consecutive_empty_lines: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> InteractivePrinter<'a> {
|
impl<'a> InteractivePrinter<'a> {
|
||||||
|
@ -210,11 +259,13 @@ impl<'a> InteractivePrinter<'a> {
|
||||||
panel_width = 0;
|
panel_width = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlighter_from_set = if input
|
// Get the highlighter for the output.
|
||||||
|
let is_printing_binary = input
|
||||||
.reader
|
.reader
|
||||||
.content_type
|
.content_type
|
||||||
.map_or(false, |c| c.is_binary() && !config.show_nonprintable)
|
.map_or(false, |c| c.is_binary() && !config.show_nonprintable);
|
||||||
{
|
|
||||||
|
let highlighter_from_set = if is_printing_binary || !config.colored_output {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// Determine the type of syntax for highlighting
|
// Determine the type of syntax for highlighting
|
||||||
|
@ -241,6 +292,7 @@ impl<'a> InteractivePrinter<'a> {
|
||||||
line_changes,
|
line_changes,
|
||||||
highlighter_from_set,
|
highlighter_from_set,
|
||||||
background_color_highlight,
|
background_color_highlight,
|
||||||
|
consecutive_empty_lines: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,12 +333,20 @@ impl<'a> InteractivePrinter<'a> {
|
||||||
" ".repeat(self.panel_width - 1 - text_truncated.len())
|
" ".repeat(self.panel_width - 1 - text_truncated.len())
|
||||||
);
|
);
|
||||||
if self.config.style_components.grid() {
|
if self.config.style_components.grid() {
|
||||||
format!("{} │ ", text_filled)
|
format!("{text_filled} │ ")
|
||||||
} else {
|
} else {
|
||||||
text_filled
|
text_filled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_header_component_indent_length(&self) -> usize {
|
||||||
|
if self.config.style_components.grid() && self.panel_width > 0 {
|
||||||
|
self.panel_width + 2
|
||||||
|
} else {
|
||||||
|
self.panel_width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_header_component_indent(&mut self, handle: &mut OutputHandle) -> Result<()> {
|
fn print_header_component_indent(&mut self, handle: &mut OutputHandle) -> Result<()> {
|
||||||
if self.config.style_components.grid() {
|
if self.config.style_components.grid() {
|
||||||
write!(
|
write!(
|
||||||
|
@ -302,6 +362,55 @@ impl<'a> InteractivePrinter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_header_component_with_indent(
|
||||||
|
&mut self,
|
||||||
|
handle: &mut OutputHandle,
|
||||||
|
content: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.print_header_component_indent(handle)?;
|
||||||
|
writeln!(handle, "{content}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_header_multiline_component(
|
||||||
|
&mut self,
|
||||||
|
handle: &mut OutputHandle,
|
||||||
|
content: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut content = content;
|
||||||
|
let content_width = self.config.term_width - self.get_header_component_indent_length();
|
||||||
|
while content.len() > content_width {
|
||||||
|
let (content_line, remaining) = content.split_at(content_width);
|
||||||
|
self.print_header_component_with_indent(handle, content_line)?;
|
||||||
|
content = remaining;
|
||||||
|
}
|
||||||
|
self.print_header_component_with_indent(handle, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_regions_for_line<'b>(
|
||||||
|
&mut self,
|
||||||
|
line: &'b str,
|
||||||
|
) -> Result<Vec<(syntect::highlighting::Style, &'b str)>> {
|
||||||
|
let highlighter_from_set = match self.highlighter_from_set {
|
||||||
|
Some(ref mut highlighter_from_set) => highlighter_from_set,
|
||||||
|
_ => return Ok(vec![(EMPTY_SYNTECT_STYLE, line)]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// skip syntax highlighting on long lines
|
||||||
|
let too_long = line.len() > 1024 * 16;
|
||||||
|
|
||||||
|
let for_highlighting: &str = if too_long { "\n" } else { line };
|
||||||
|
|
||||||
|
let mut highlighted_line = highlighter_from_set
|
||||||
|
.highlighter
|
||||||
|
.highlight_line(for_highlighting, highlighter_from_set.syntax_set)?;
|
||||||
|
|
||||||
|
if too_long {
|
||||||
|
highlighted_line[0].1 = &line;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(highlighted_line)
|
||||||
|
}
|
||||||
|
|
||||||
fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
|
fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
|
||||||
if self.config.tab_width > 0 {
|
if self.config.tab_width > 0 {
|
||||||
return expand_tabs(text, self.config.tab_width, cursor);
|
return expand_tabs(text, self.config.tab_width, cursor);
|
||||||
|
@ -377,31 +486,32 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header_components.iter().try_for_each(|component| {
|
header_components
|
||||||
self.print_header_component_indent(handle)?;
|
.iter()
|
||||||
|
.try_for_each(|component| match component {
|
||||||
match component {
|
StyleComponent::HeaderFilename => {
|
||||||
StyleComponent::HeaderFilename => writeln!(
|
let header_filename = format!(
|
||||||
handle,
|
"{}{}{}",
|
||||||
"{}{}{}",
|
description
|
||||||
description
|
.kind()
|
||||||
.kind()
|
.map(|kind| format!("{kind}: "))
|
||||||
.map(|kind| format!("{}: ", kind))
|
.unwrap_or_else(|| "".into()),
|
||||||
.unwrap_or_else(|| "".into()),
|
self.colors.header_value.paint(description.title()),
|
||||||
self.colors.header_value.paint(description.title()),
|
mode
|
||||||
mode
|
);
|
||||||
),
|
self.print_header_multiline_component(handle, &header_filename)
|
||||||
|
}
|
||||||
StyleComponent::HeaderFilesize => {
|
StyleComponent::HeaderFilesize => {
|
||||||
let bsize = metadata
|
let bsize = metadata
|
||||||
.size
|
.size
|
||||||
.map(|s| format!("{}", ByteSize(s)))
|
.map(|s| format!("{}", ByteSize(s)))
|
||||||
.unwrap_or_else(|| "-".into());
|
.unwrap_or_else(|| "-".into());
|
||||||
writeln!(handle, "Size: {}", self.colors.header_value.paint(bsize))
|
let header_filesize =
|
||||||
|
format!("Size: {}", self.colors.header_value.paint(bsize));
|
||||||
|
self.print_header_multiline_component(handle, &header_filesize)
|
||||||
}
|
}
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
if self.config.style_components.grid() {
|
if self.config.style_components.grid() {
|
||||||
if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
|
if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
|
||||||
|
@ -442,7 +552,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
"{}",
|
"{}",
|
||||||
self.colors
|
self.colors
|
||||||
.grid
|
.grid
|
||||||
.paint(format!("{}{}{}{}", panel, snip_left, title, snip_right))
|
.paint(format!("{panel}{snip_left}{title}{snip_right}"))
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -483,34 +593,23 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let regions = {
|
let regions = self.highlight_regions_for_line(&line)?;
|
||||||
let highlighter_from_set = match self.highlighter_from_set {
|
|
||||||
Some(ref mut highlighter_from_set) => highlighter_from_set,
|
|
||||||
_ => {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// skip syntax highlighting on long lines
|
|
||||||
let too_long = line.len() > 1024 * 16;
|
|
||||||
|
|
||||||
let for_highlighting: &str = if too_long { "\n" } else { &line };
|
|
||||||
|
|
||||||
let mut highlighted_line = highlighter_from_set
|
|
||||||
.highlighter
|
|
||||||
.highlight_line(for_highlighting, highlighter_from_set.syntax_set)?;
|
|
||||||
|
|
||||||
if too_long {
|
|
||||||
highlighted_line[0].1 = &line;
|
|
||||||
}
|
|
||||||
|
|
||||||
highlighted_line
|
|
||||||
};
|
|
||||||
|
|
||||||
if out_of_range {
|
if out_of_range {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip squeezed lines.
|
||||||
|
if let Some(squeeze_limit) = self.config.squeeze_lines {
|
||||||
|
if line.trim_end_matches(|c| c == '\r' || c == '\n').is_empty() {
|
||||||
|
self.consecutive_empty_lines += 1;
|
||||||
|
if self.consecutive_empty_lines > squeeze_limit {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.consecutive_empty_lines = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut cursor: usize = 0;
|
let mut cursor: usize = 0;
|
||||||
let mut cursor_max: usize = self.config.term_width;
|
let mut cursor_max: usize = self.config.term_width;
|
||||||
let mut cursor_total: usize = 0;
|
let mut cursor_total: usize = 0;
|
||||||
|
@ -521,7 +620,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
|
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
|
||||||
|
|
||||||
if highlight_this_line && self.config.theme == "ansi" {
|
if highlight_this_line && self.config.theme == "ansi" {
|
||||||
self.ansi_style.update("^[4m");
|
self.ansi_style.update(ANSI_UNDERLINE_ENABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
let background_color = self
|
let background_color = self
|
||||||
|
@ -548,23 +647,17 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
let italics = self.config.use_italic_text;
|
let italics = self.config.use_italic_text;
|
||||||
|
|
||||||
for &(style, region) in ®ions {
|
for &(style, region) in ®ions {
|
||||||
let ansi_iterator = AnsiCodeIterator::new(region);
|
let ansi_iterator = EscapeSequenceIterator::new(region);
|
||||||
for chunk in ansi_iterator {
|
for chunk in ansi_iterator {
|
||||||
match chunk {
|
match chunk {
|
||||||
// ANSI escape passthrough.
|
|
||||||
(ansi, true) => {
|
|
||||||
self.ansi_style.update(ansi);
|
|
||||||
write!(handle, "{}", ansi)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular text.
|
// Regular text.
|
||||||
(text, false) => {
|
EscapeSequence::Text(text) => {
|
||||||
let text = &*self.preprocess(text, &mut cursor_total);
|
let text = self.preprocess(text, &mut cursor_total);
|
||||||
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
|
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
handle,
|
handle,
|
||||||
"{}",
|
"{}{}",
|
||||||
as_terminal_escaped(
|
as_terminal_escaped(
|
||||||
style,
|
style,
|
||||||
&format!("{}{}", self.ansi_style, text_trimmed),
|
&format!("{}{}", self.ansi_style, text_trimmed),
|
||||||
|
@ -572,9 +665,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
colored_output,
|
colored_output,
|
||||||
italics,
|
italics,
|
||||||
background_color
|
background_color
|
||||||
)
|
),
|
||||||
|
self.ansi_style.to_reset_sequence(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Pad the rest of the line.
|
||||||
if text.len() != text_trimmed.len() {
|
if text.len() != text_trimmed.len() {
|
||||||
if let Some(background_color) = background_color {
|
if let Some(background_color) = background_color {
|
||||||
let ansi_style = Style {
|
let ansi_style = Style {
|
||||||
|
@ -592,6 +687,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
write!(handle, "{}", &text[text_trimmed.len()..])?;
|
write!(handle, "{}", &text[text_trimmed.len()..])?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ANSI escape passthrough.
|
||||||
|
_ => {
|
||||||
|
write!(handle, "{}", chunk.raw())?;
|
||||||
|
self.ansi_style.update(chunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -601,17 +702,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for &(style, region) in ®ions {
|
for &(style, region) in ®ions {
|
||||||
let ansi_iterator = AnsiCodeIterator::new(region);
|
let ansi_iterator = EscapeSequenceIterator::new(region);
|
||||||
for chunk in ansi_iterator {
|
for chunk in ansi_iterator {
|
||||||
match chunk {
|
match chunk {
|
||||||
// ANSI escape passthrough.
|
|
||||||
(ansi, true) => {
|
|
||||||
self.ansi_style.update(ansi);
|
|
||||||
write!(handle, "{}", ansi)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular text.
|
// Regular text.
|
||||||
(text, false) => {
|
EscapeSequence::Text(text) => {
|
||||||
let text = self.preprocess(
|
let text = self.preprocess(
|
||||||
text.trim_end_matches(|c| c == '\r' || c == '\n'),
|
text.trim_end_matches(|c| c == '\r' || c == '\n'),
|
||||||
&mut cursor_total,
|
&mut cursor_total,
|
||||||
|
@ -654,7 +749,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
// It wraps.
|
// It wraps.
|
||||||
write!(
|
write!(
|
||||||
handle,
|
handle,
|
||||||
"{}\n{}",
|
"{}{}\n{}",
|
||||||
as_terminal_escaped(
|
as_terminal_escaped(
|
||||||
style,
|
style,
|
||||||
&format!("{}{}", self.ansi_style, line_buf),
|
&format!("{}{}", self.ansi_style, line_buf),
|
||||||
|
@ -663,6 +758,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
self.config.use_italic_text,
|
self.config.use_italic_text,
|
||||||
background_color
|
background_color
|
||||||
),
|
),
|
||||||
|
self.ansi_style.to_reset_sequence(),
|
||||||
panel_wrap.clone().unwrap()
|
panel_wrap.clone().unwrap()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -691,6 +787,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
)
|
)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ANSI escape passthrough.
|
||||||
|
_ => {
|
||||||
|
write!(handle, "{}", chunk.raw())?;
|
||||||
|
self.ansi_style.update(chunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -711,8 +813,8 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if highlight_this_line && self.config.theme == "ansi" {
|
if highlight_this_line && self.config.theme == "ansi" {
|
||||||
self.ansi_style.update("^[24m");
|
write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?;
|
||||||
write!(handle, "\x1B[24m")?;
|
self.ansi_style.update(ANSI_UNDERLINE_DISABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -80,7 +80,7 @@ impl FromStr for StyleComponent {
|
||||||
"full" => Ok(StyleComponent::Full),
|
"full" => Ok(StyleComponent::Full),
|
||||||
"default" => Ok(StyleComponent::Default),
|
"default" => Ok(StyleComponent::Default),
|
||||||
"plain" => Ok(StyleComponent::Plain),
|
"plain" => Ok(StyleComponent::Plain),
|
||||||
_ => Err(format!("Unknown style '{}'", s).into()),
|
_ => Err(format!("Unknown style '{s}'").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,31 @@
|
||||||
use std::path::Path;
|
use std::{
|
||||||
|
path::Path,
|
||||||
use crate::error::Result;
|
sync::{
|
||||||
use ignored_suffixes::IgnoredSuffixes;
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
|
||||||
use globset::{Candidate, GlobBuilder, GlobMatcher};
|
use globset::{Candidate, GlobBuilder, GlobMatcher};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
use builtin::BUILTIN_MAPPINGS;
|
||||||
|
use ignored_suffixes::IgnoredSuffixes;
|
||||||
|
|
||||||
|
mod builtin;
|
||||||
pub mod ignored_suffixes;
|
pub mod ignored_suffixes;
|
||||||
|
|
||||||
|
fn make_glob_matcher(from: &str) -> Result<GlobMatcher> {
|
||||||
|
let matcher = GlobBuilder::new(from)
|
||||||
|
.case_insensitive(true)
|
||||||
|
.literal_separator(true)
|
||||||
|
.build()?
|
||||||
|
.compile_matcher();
|
||||||
|
Ok(matcher)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum MappingTarget<'a> {
|
pub enum MappingTarget<'a> {
|
||||||
|
@ -29,204 +48,108 @@ pub enum MappingTarget<'a> {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct SyntaxMapping<'a> {
|
pub struct SyntaxMapping<'a> {
|
||||||
mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
|
/// User-defined mappings at run time.
|
||||||
|
///
|
||||||
|
/// Rules in front have precedence.
|
||||||
|
custom_mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
|
||||||
|
|
||||||
pub(crate) ignored_suffixes: IgnoredSuffixes<'a>,
|
pub(crate) ignored_suffixes: IgnoredSuffixes<'a>,
|
||||||
|
|
||||||
|
/// A flag to halt glob matcher building, which is offloaded to another thread.
|
||||||
|
///
|
||||||
|
/// We have this so that we can signal the thread to halt early when appropriate.
|
||||||
|
halt_glob_build: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Drop for SyntaxMapping<'a> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// signal the offload thread to halt early
|
||||||
|
self.halt_glob_build.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SyntaxMapping<'a> {
|
impl<'a> SyntaxMapping<'a> {
|
||||||
pub fn empty() -> SyntaxMapping<'a> {
|
pub fn new() -> SyntaxMapping<'a> {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn builtin() -> SyntaxMapping<'a> {
|
/// Start a thread to build the glob matchers for all builtin mappings.
|
||||||
let mut mapping = Self::empty();
|
///
|
||||||
mapping.insert("*.h", MappingTarget::MapTo("C++")).unwrap();
|
/// The use of this function while not necessary, is useful to speed up startup
|
||||||
mapping
|
/// times by starting this work early in parallel.
|
||||||
.insert(".clang-format", MappingTarget::MapTo("YAML"))
|
///
|
||||||
.unwrap();
|
/// The thread halts if/when `halt_glob_build` is set to true.
|
||||||
mapping.insert("*.fs", MappingTarget::MapTo("F#")).unwrap();
|
pub fn start_offload_build_all(&self) {
|
||||||
mapping
|
let halt = Arc::clone(&self.halt_glob_build);
|
||||||
.insert("build", MappingTarget::MapToUnknown)
|
thread::spawn(move || {
|
||||||
.unwrap();
|
for (matcher, _) in BUILTIN_MAPPINGS.iter() {
|
||||||
mapping
|
if halt.load(Ordering::Relaxed) {
|
||||||
.insert("**/.ssh/config", MappingTarget::MapTo("SSH Config"))
|
break;
|
||||||
.unwrap();
|
}
|
||||||
mapping
|
Lazy::force(matcher);
|
||||||
.insert(
|
|
||||||
"**/bat/config",
|
|
||||||
MappingTarget::MapTo("Bourne Again Shell (bash)"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
mapping
|
|
||||||
.insert(
|
|
||||||
"/etc/profile",
|
|
||||||
MappingTarget::MapTo("Bourne Again Shell (bash)"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
mapping
|
|
||||||
.insert(
|
|
||||||
"os-release",
|
|
||||||
MappingTarget::MapTo("Bourne Again Shell (bash)"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
mapping
|
|
||||||
.insert("*.pac", MappingTarget::MapTo("JavaScript (Babel)"))
|
|
||||||
.unwrap();
|
|
||||||
mapping
|
|
||||||
.insert("fish_history", MappingTarget::MapTo("YAML"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for glob in ["*.jsonl", "*.sarif"] {
|
|
||||||
mapping.insert(glob, MappingTarget::MapTo("JSON")).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// See #2151, https://nmap.org/book/nse-language.html
|
|
||||||
mapping
|
|
||||||
.insert("*.nse", MappingTarget::MapTo("Lua"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// See #1008
|
|
||||||
mapping
|
|
||||||
.insert("rails", MappingTarget::MapToUnknown)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
mapping
|
|
||||||
.insert("Containerfile", MappingTarget::MapTo("Dockerfile"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
mapping
|
|
||||||
.insert("*.ksh", MappingTarget::MapTo("Bourne Again Shell (bash)"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Nginx and Apache syntax files both want to style all ".conf" files
|
|
||||||
// see #1131 and #1137
|
|
||||||
mapping
|
|
||||||
.insert("*.conf", MappingTarget::MapExtensionToUnknown)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for glob in &[
|
|
||||||
"/etc/nginx/**/*.conf",
|
|
||||||
"/etc/nginx/sites-*/**/*",
|
|
||||||
"nginx.conf",
|
|
||||||
"mime.types",
|
|
||||||
] {
|
|
||||||
mapping.insert(glob, MappingTarget::MapTo("nginx")).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
for glob in &[
|
|
||||||
"/etc/apache2/**/*.conf",
|
|
||||||
"/etc/apache2/sites-*/**/*",
|
|
||||||
"httpd.conf",
|
|
||||||
] {
|
|
||||||
mapping
|
|
||||||
.insert(glob, MappingTarget::MapTo("Apache Conf"))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
for glob in &[
|
|
||||||
"**/systemd/**/*.conf",
|
|
||||||
"**/systemd/**/*.example",
|
|
||||||
"*.automount",
|
|
||||||
"*.device",
|
|
||||||
"*.dnssd",
|
|
||||||
"*.link",
|
|
||||||
"*.mount",
|
|
||||||
"*.netdev",
|
|
||||||
"*.network",
|
|
||||||
"*.nspawn",
|
|
||||||
"*.path",
|
|
||||||
"*.service",
|
|
||||||
"*.scope",
|
|
||||||
"*.slice",
|
|
||||||
"*.socket",
|
|
||||||
"*.swap",
|
|
||||||
"*.target",
|
|
||||||
"*.timer",
|
|
||||||
] {
|
|
||||||
mapping.insert(glob, MappingTarget::MapTo("INI")).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// unix mail spool
|
|
||||||
for glob in &["/var/spool/mail/*", "/var/mail/*"] {
|
|
||||||
mapping.insert(glob, MappingTarget::MapTo("Email")).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// pacman hooks
|
|
||||||
mapping
|
|
||||||
.insert("*.hook", MappingTarget::MapTo("INI"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
mapping
|
|
||||||
.insert("*.ron", MappingTarget::MapTo("Rust"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/`
|
|
||||||
// See e.g. https://git-scm.com/docs/git-config#FILES
|
|
||||||
match (
|
|
||||||
std::env::var_os("XDG_CONFIG_HOME").filter(|val| !val.is_empty()),
|
|
||||||
std::env::var_os("HOME")
|
|
||||||
.filter(|val| !val.is_empty())
|
|
||||||
.map(|home| Path::new(&home).join(".config")),
|
|
||||||
) {
|
|
||||||
(Some(xdg_config_home), Some(default_config_home))
|
|
||||||
if xdg_config_home == default_config_home => {
|
|
||||||
insert_git_config_global(&mut mapping, &xdg_config_home)
|
|
||||||
}
|
}
|
||||||
(Some(xdg_config_home), Some(default_config_home)) /* else guard */ => {
|
});
|
||||||
insert_git_config_global(&mut mapping, &xdg_config_home);
|
// Note that this thread is not joined upon completion because there's
|
||||||
insert_git_config_global(&mut mapping, &default_config_home)
|
// no shared resources that need synchronization to be safely dropped.
|
||||||
}
|
// If we later add code into this thread that requires interesting
|
||||||
(Some(config_home), None) => insert_git_config_global(&mut mapping, &config_home),
|
// resources (e.g. IO), it would be a good idea to store the handle
|
||||||
(None, Some(config_home)) => insert_git_config_global(&mut mapping, &config_home),
|
// and join it on drop.
|
||||||
(None, None) => (),
|
|
||||||
};
|
|
||||||
|
|
||||||
fn insert_git_config_global(mapping: &mut SyntaxMapping, config_home: impl AsRef<Path>) {
|
|
||||||
let git_config_path = config_home.as_ref().join("git");
|
|
||||||
|
|
||||||
mapping
|
|
||||||
.insert(
|
|
||||||
&git_config_path.join("config").to_string_lossy(),
|
|
||||||
MappingTarget::MapTo("Git Config"),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
mapping
|
|
||||||
.insert(
|
|
||||||
&git_config_path.join("ignore").to_string_lossy(),
|
|
||||||
MappingTarget::MapTo("Git Ignore"),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
mapping
|
|
||||||
.insert(
|
|
||||||
&git_config_path.join("attributes").to_string_lossy(),
|
|
||||||
MappingTarget::MapTo("Git Attributes"),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
|
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
|
||||||
let glob = GlobBuilder::new(from)
|
let matcher = make_glob_matcher(from)?;
|
||||||
.case_insensitive(true)
|
self.custom_mappings.push((matcher, to));
|
||||||
.literal_separator(true)
|
|
||||||
.build()?;
|
|
||||||
self.mappings.push((glob.compile_matcher(), to));
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mappings(&self) -> &[(GlobMatcher, MappingTarget<'a>)] {
|
/// Returns an iterator over all mappings. User-defined mappings are listed
|
||||||
&self.mappings
|
/// before builtin mappings; mappings in front have higher precedence.
|
||||||
|
///
|
||||||
|
/// Builtin mappings' `GlobMatcher`s are lazily compiled.
|
||||||
|
///
|
||||||
|
/// Note that this function only returns mappings that are valid under the
|
||||||
|
/// current environment. For details see [`Self::builtin_mappings`].
|
||||||
|
pub fn all_mappings(&self) -> impl Iterator<Item = (&GlobMatcher, &MappingTarget<'a>)> {
|
||||||
|
self.custom_mappings()
|
||||||
|
.iter()
|
||||||
|
.map(|(matcher, target)| (matcher, target)) // as_ref
|
||||||
|
.chain(
|
||||||
|
// we need a map with a closure to "do" the lifetime variance
|
||||||
|
// see: https://discord.com/channels/273534239310479360/1120124565591425034/1170543402870382653
|
||||||
|
// also, clippy false positive:
|
||||||
|
// see: https://github.com/rust-lang/rust-clippy/issues/9280
|
||||||
|
#[allow(clippy::map_identity)]
|
||||||
|
self.builtin_mappings().map(|rule| rule),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
|
/// Returns an iterator over all valid builtin mappings. Mappings in front
|
||||||
|
/// have higher precedence.
|
||||||
|
///
|
||||||
|
/// The `GlabMatcher`s are lazily compiled.
|
||||||
|
///
|
||||||
|
/// Mappings that are invalid under the current environment (i.e. rule
|
||||||
|
/// requires environment variable(s) that is unset, or the joined string
|
||||||
|
/// after variable(s) replacement is not a valid glob expression) are
|
||||||
|
/// ignored.
|
||||||
|
pub fn builtin_mappings(
|
||||||
|
&self,
|
||||||
|
) -> impl Iterator<Item = (&'static GlobMatcher, &'static MappingTarget<'static>)> {
|
||||||
|
BUILTIN_MAPPINGS
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(matcher, target)| matcher.as_ref().map(|glob| (glob, target)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all user-defined mappings.
|
||||||
|
pub fn custom_mappings(&self) -> &[(GlobMatcher, MappingTarget<'a>)] {
|
||||||
|
&self.custom_mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
|
||||||
// Try matching on the file name as-is.
|
// Try matching on the file name as-is.
|
||||||
let candidate = Candidate::new(&path);
|
let candidate = Candidate::new(&path);
|
||||||
let candidate_filename = path.as_ref().file_name().map(Candidate::new);
|
let candidate_filename = path.as_ref().file_name().map(Candidate::new);
|
||||||
for (ref glob, ref syntax) in self.mappings.iter().rev() {
|
for (glob, syntax) in self.all_mappings() {
|
||||||
if glob.is_match_candidate(&candidate)
|
if glob.is_match_candidate(&candidate)
|
||||||
|| candidate_filename
|
|| candidate_filename
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -252,9 +175,46 @@ impl<'a> SyntaxMapping<'a> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic() {
|
fn builtin_mappings_work() {
|
||||||
let mut map = SyntaxMapping::empty();
|
let map = SyntaxMapping::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
map.get_syntax_for("/path/to/build"),
|
||||||
|
Some(MappingTarget::MapToUnknown)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_fixed_builtin_mappings_can_compile() {
|
||||||
|
let map = SyntaxMapping::new();
|
||||||
|
|
||||||
|
// collect call evaluates all lazy closures
|
||||||
|
// fixed builtin mappings will panic if they fail to compile
|
||||||
|
let _mappings = map.builtin_mappings().collect::<Vec<_>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_mappings_matcher_only_compile_once() {
|
||||||
|
let map = SyntaxMapping::new();
|
||||||
|
|
||||||
|
let two_iterations: Vec<_> = (0..2)
|
||||||
|
.map(|_| {
|
||||||
|
// addresses of every matcher
|
||||||
|
map.builtin_mappings()
|
||||||
|
.map(|(matcher, _)| matcher as *const _ as usize)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// if the matchers are only compiled once, their address should remain the same
|
||||||
|
assert_eq!(two_iterations[0], two_iterations[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_mappings_work() {
|
||||||
|
let mut map = SyntaxMapping::new();
|
||||||
map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML"))
|
map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML"))
|
||||||
.ok();
|
.ok();
|
||||||
map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore"))
|
map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore"))
|
||||||
|
@ -273,52 +233,32 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn user_can_override_builtin_mappings() {
|
fn custom_mappings_override_builtin() {
|
||||||
let mut map = SyntaxMapping::builtin();
|
let mut map = SyntaxMapping::new();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
map.get_syntax_for("/etc/profile"),
|
map.get_syntax_for("/path/to/httpd.conf"),
|
||||||
Some(MappingTarget::MapTo("Bourne Again Shell (bash)"))
|
Some(MappingTarget::MapTo("Apache Conf"))
|
||||||
);
|
);
|
||||||
map.insert("/etc/profile", MappingTarget::MapTo("My Syntax"))
|
map.insert("httpd.conf", MappingTarget::MapTo("My Syntax"))
|
||||||
.ok();
|
.ok();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
map.get_syntax_for("/etc/profile"),
|
map.get_syntax_for("/path/to/httpd.conf"),
|
||||||
Some(MappingTarget::MapTo("My Syntax"))
|
Some(MappingTarget::MapTo("My Syntax"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_mappings() {
|
fn custom_mappings_precedence() {
|
||||||
let map = SyntaxMapping::builtin();
|
let mut map = SyntaxMapping::new();
|
||||||
|
|
||||||
|
map.insert("/path/to/foo", MappingTarget::MapTo("alpha"))
|
||||||
|
.ok();
|
||||||
|
map.insert("/path/to/foo", MappingTarget::MapTo("bravo"))
|
||||||
|
.ok();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
map.get_syntax_for("/path/to/build"),
|
map.get_syntax_for("/path/to/foo"),
|
||||||
Some(MappingTarget::MapToUnknown)
|
Some(MappingTarget::MapTo("alpha"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// verifies that SyntaxMapping::builtin() doesn't repeat `Glob`-based keys
|
|
||||||
fn no_duplicate_builtin_keys() {
|
|
||||||
let mappings = SyntaxMapping::builtin().mappings;
|
|
||||||
for i in 0..mappings.len() {
|
|
||||||
let tail = mappings[i + 1..].into_iter();
|
|
||||||
let (dupl, _): (Vec<_>, Vec<_>) =
|
|
||||||
tail.partition(|item| item.0.glob() == mappings[i].0.glob());
|
|
||||||
|
|
||||||
// emit repeats on failure
|
|
||||||
assert_eq!(
|
|
||||||
dupl.len(),
|
|
||||||
0,
|
|
||||||
"Glob pattern `{}` mapped to multiple: {:?}",
|
|
||||||
mappings[i].0.glob().glob(),
|
|
||||||
{
|
|
||||||
let (_, mut dupl_targets): (Vec<GlobMatcher>, Vec<MappingTarget>) =
|
|
||||||
dupl.into_iter().cloned().unzip();
|
|
||||||
dupl_targets.push(mappings[i].1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use globset::GlobMatcher;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::syntax_mapping::{make_glob_matcher, MappingTarget};
|
||||||
|
|
||||||
|
// Static syntax mappings generated from /src/syntax_mapping/builtins/ by the
|
||||||
|
// build script (/build/syntax_mapping.rs).
|
||||||
|
include!(concat!(
|
||||||
|
env!("OUT_DIR"),
|
||||||
|
"/codegen_static_syntax_mappings.rs"
|
||||||
|
));
|
||||||
|
|
||||||
|
// The defined matcher strings are analysed at compile time and converted into
|
||||||
|
// lazily-compiled `GlobMatcher`s. This is so that the string searches are moved
|
||||||
|
// from run time to compile time, thus improving startup performance.
|
||||||
|
//
|
||||||
|
// To any future maintainer (including possibly myself) wondering why there is
|
||||||
|
// not a `BuiltinMatcher` enum that looks like this:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// enum BuiltinMatcher {
|
||||||
|
// Fixed(&'static str),
|
||||||
|
// Dynamic(Lazy<Option<String>>),
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Because there was. I tried it and threw it out.
|
||||||
|
//
|
||||||
|
// Naively looking at the problem from a distance, this may seem like a good
|
||||||
|
// design (strongly typed etc. etc.). It would also save on compiled size by
|
||||||
|
// extracting out common behaviour into functions. But while actually
|
||||||
|
// implementing the lazy matcher compilation logic, I realised that it's most
|
||||||
|
// convenient for `BUILTIN_MAPPINGS` to have the following type:
|
||||||
|
//
|
||||||
|
// `[(Lazy<Option<GlobMatcher>>, MappingTarget); N]`
|
||||||
|
//
|
||||||
|
// The benefit for this is that operations like listing all builtin mappings
|
||||||
|
// would be effectively memoised. The caller would not have to compile another
|
||||||
|
// `GlobMatcher` for rules that they have previously visited.
|
||||||
|
//
|
||||||
|
// Unfortunately, this means we are going to have to store a distinct closure
|
||||||
|
// for each rule anyway, which makes a `BuiltinMatcher` enum a pointless layer
|
||||||
|
// of indirection.
|
||||||
|
//
|
||||||
|
// In the current implementation, the closure within each generated rule simply
|
||||||
|
// calls either `build_matcher_fixed` or `build_matcher_dynamic`, depending on
|
||||||
|
// whether the defined matcher contains dynamic segments or not.
|
||||||
|
|
||||||
|
/// Compile a fixed glob string into a glob matcher.
|
||||||
|
///
|
||||||
|
/// A failure to compile is a fatal error.
|
||||||
|
///
|
||||||
|
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
|
||||||
|
fn build_matcher_fixed(from: &str) -> GlobMatcher {
|
||||||
|
make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join a list of matcher segments to create a glob string, replacing all
|
||||||
|
/// environment variables, then compile to a glob matcher.
|
||||||
|
///
|
||||||
|
/// Returns `None` if any replacement fails, or if the joined glob string fails
|
||||||
|
/// to compile.
|
||||||
|
///
|
||||||
|
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
|
||||||
|
fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
|
||||||
|
// join segments
|
||||||
|
let mut buf = String::new();
|
||||||
|
for seg in segs {
|
||||||
|
match seg {
|
||||||
|
MatcherSegment::Text(s) => buf.push_str(s),
|
||||||
|
MatcherSegment::Env(var) => {
|
||||||
|
let replaced = env::var(var).ok()?;
|
||||||
|
buf.push_str(&replaced);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// compile glob matcher
|
||||||
|
let matcher = make_glob_matcher(&buf).ok()?;
|
||||||
|
Some(matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A segment of a dynamic builtin matcher.
|
||||||
|
///
|
||||||
|
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum MatcherSegment {
|
||||||
|
Text(&'static str),
|
||||||
|
Env(&'static str),
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
# `/src/syntax_mapping/builtins`
|
||||||
|
|
||||||
|
The files in this directory define path/name-based syntax mappings, which amend
|
||||||
|
and take precedence over the extension/content-based syntax mappings provided by
|
||||||
|
[syntect](https://github.com/trishume/syntect).
|
||||||
|
|
||||||
|
## File organisation
|
||||||
|
|
||||||
|
Each TOML file should describe the syntax mappings of a single application, or
|
||||||
|
otherwise a set of logically-related rules.
|
||||||
|
|
||||||
|
What defines "a single application" here is deliberately vague, since the
|
||||||
|
file-splitting is purely for maintainability reasons. (Technically, we could
|
||||||
|
just as well use a single TOML file.) So just use common sense.
|
||||||
|
|
||||||
|
TOML files should reside in the corresponding subdirectory of the platform(s)
|
||||||
|
that they intend to target. At compile time, the build script will go through
|
||||||
|
each subdirectory that is applicable to the compilation target, collect the
|
||||||
|
syntax mappings defined by all TOML files, and embed them into the binary.
|
||||||
|
|
||||||
|
## File syntax
|
||||||
|
|
||||||
|
Each TOML file should contain a single section named `mappings`, with each of
|
||||||
|
its keys being a language identifier (first column of `bat -L`; also referred to
|
||||||
|
as "target").
|
||||||
|
|
||||||
|
The value of each key should be an array of strings, with each item being a glob
|
||||||
|
matcher. We will call each of these items a "rule".
|
||||||
|
|
||||||
|
For example, if `foo-application` uses both TOML and YAML configuration files,
|
||||||
|
we could write something like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# 30-foo-application.toml
|
||||||
|
[mappings]
|
||||||
|
"TOML" = [
|
||||||
|
# rules for TOML syntax go here
|
||||||
|
"/usr/share/foo-application/toml-config/*.conf",
|
||||||
|
"/etc/foo-application/toml-config/*.conf",
|
||||||
|
]
|
||||||
|
"YAML" = [
|
||||||
|
# rules for YAML syntax go here
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic environment variable replacement
|
||||||
|
|
||||||
|
In additional to the standard glob matcher syntax, rules also support dynamic
|
||||||
|
replacement of environment variables at runtime. This allows us to concisely
|
||||||
|
handle things like [XDG](https://specifications.freedesktop.org/basedir-spec/latest/).
|
||||||
|
|
||||||
|
All environment variables intended to be replaced at runtime must be enclosed in
|
||||||
|
`${}`, for example `"/foo/*/${YOUR_ENV}-suffix/*.log"`. Note that this is the
|
||||||
|
**only** admissible syntax; other variable substitution syntaxes are not
|
||||||
|
supported and will either cause a compile time error, or be treated as plain
|
||||||
|
text.
|
||||||
|
|
||||||
|
For example, if `foo-application` also supports per-user configuration files, we
|
||||||
|
could write something like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# 30-foo-application.toml
|
||||||
|
[mappings]
|
||||||
|
"TOML" = [
|
||||||
|
# rules for TOML syntax go here
|
||||||
|
"/usr/share/foo-application/toml-config/*.conf",
|
||||||
|
"/etc/foo-application/toml-config/*.conf",
|
||||||
|
"${XDG_CONFIG_HOME}/foo-application/toml-config/*.conf",
|
||||||
|
"${HOME}/.config/foo-application/toml-config/*.conf",
|
||||||
|
]
|
||||||
|
"YAML" = [
|
||||||
|
# rules for YAML syntax go here
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
If any environment variable replacement in a rule fails (for example when a
|
||||||
|
variable is unset), or if the glob string after replacements is invalid, the
|
||||||
|
entire rule will be ignored.
|
||||||
|
|
||||||
|
### Explicitly mapping to unknown
|
||||||
|
|
||||||
|
Sometimes it may be necessary to "unset" a particular syntect mapping - perhaps
|
||||||
|
a syntax's matching rules are "too greedy", and is claiming files that it should
|
||||||
|
not. In this case, there are two special identifiers:
|
||||||
|
`MappingTarget::MapToUnknown` and `MappingTarget::MapExtensionToUnknown`
|
||||||
|
(corresponding to the two variants of the `syntax_mapping::MappingTarget` enum).
|
||||||
|
|
||||||
|
An example of this would be `*.conf` files in general. So we may write something
|
||||||
|
like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# 99-unset-ambiguous-extensions.toml
|
||||||
|
[mappings]
|
||||||
|
"MappingTarget::MapExtensionToUnknown" = [
|
||||||
|
"*.conf",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ordering
|
||||||
|
|
||||||
|
At compile time, all TOML files applicable to the target are processed in
|
||||||
|
lexicographical filename order. So `00-foo.toml` takes precedence over
|
||||||
|
`10-bar.toml`, which takes precedence over `20-baz.toml`, and so on. Note that
|
||||||
|
**only** the filenames of the TOML files are taken into account; the
|
||||||
|
subdirectories they are placed in have no influence on ordering.
|
||||||
|
|
||||||
|
This behaviour can be occasionally useful for creating high/low priority rules,
|
||||||
|
such as in the aforementioned example of explicitly mapping `*.conf` files to
|
||||||
|
unknown. Generally this should not be much of a concern though, since rules
|
||||||
|
should be written as specifically as possible for each application.
|
||||||
|
|
||||||
|
Rules within each TOML file are processed (and therefore matched) in the order
|
||||||
|
in which they are defined. At runtime, the syntax selection algorithm will
|
||||||
|
short-circuit and return the target of the first matching rule.
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"Bourne Again Shell (bash)" = ["/etc/os-release", "/var/run/os-release"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"Apache Conf" = ["httpd.conf"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"INI" = ["**/.aws/credentials", "**/.aws/config"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"Bourne Again Shell (bash)" = ["**/bat/config"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"Dockerfile" = ["Containerfile"]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[mappings]
|
||||||
|
"C++" = [
|
||||||
|
# probably better than the default Objective C mapping #877
|
||||||
|
"*.h",
|
||||||
|
]
|
||||||
|
"YAML" = [".clang-format"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"F#" = ["*.fs"]
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/`
|
||||||
|
# See e.g. https://git-scm.com/docs/git-config#FILES
|
||||||
|
|
||||||
|
[mappings]
|
||||||
|
"Git Config" = ["${XDG_CONFIG_HOME}/git/config", "${HOME}/.config/git/config"]
|
||||||
|
"Git Ignore" = ["${XDG_CONFIG_HOME}/git/ignore", "${HOME}/.config/git/ignore"]
|
||||||
|
"Git Attributes" = [
|
||||||
|
"${XDG_CONFIG_HOME}/git/attributes",
|
||||||
|
"${HOME}/.config/git/attributes",
|
||||||
|
]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# JSON Lines is a simple variation of JSON #2535
|
||||||
|
[mappings]
|
||||||
|
"JSON" = ["*.jsonl", "*.jsonc"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"nginx" = ["nginx.conf", "mime.types"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
[mappings]
|
||||||
|
# See #2151, https://nmap.org/book/nse-language.html
|
||||||
|
"Lua" = ["*.nse"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 1515
|
||||||
|
[mappings]
|
||||||
|
"JavaScript (Babel)" = ["*.pac"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Rusty Object Notation #2427
|
||||||
|
[mappings]
|
||||||
|
"Rust" = ["*.ron"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# SARIF is a format for reporting static analysis results #2695
|
||||||
|
[mappings]
|
||||||
|
"JSON" = ["*.sarif"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"SSH Config" = ["**/.ssh/config"]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[mappings]
|
||||||
|
"MappingTarget::MapExtensionToUnknown" = [
|
||||||
|
# common extension used for all kinds of formats
|
||||||
|
"*.conf",
|
||||||
|
]
|
|
@ -0,0 +1,7 @@
|
||||||
|
[mappings]
|
||||||
|
"MappingTarget::MapToUnknown" = [
|
||||||
|
# "NAnt Build File" should only match *.build files, not files named "build"
|
||||||
|
"build",
|
||||||
|
# "bin/rails" scripts in a Ruby project misidentified as HTML (Rails) #1008
|
||||||
|
"rails",
|
||||||
|
]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Xonsh shell (https://xon.sh/)
|
||||||
|
[mappings]
|
||||||
|
"Python" = ["*.xsh", "*.xonshrc"]
|
|
@ -0,0 +1,8 @@
|
||||||
|
# see https://github.com/containers/image/tree/main/docs
|
||||||
|
[mappings]
|
||||||
|
"TOML" = [
|
||||||
|
"/usr/share/containers/**/*.conf",
|
||||||
|
"/etc/containers/**/*.conf",
|
||||||
|
"${HOME}/.config/containers/**/*.conf",
|
||||||
|
"${XDG_CONFIG_HOME}/containers/**/*.conf",
|
||||||
|
]
|
|
@ -0,0 +1,7 @@
|
||||||
|
[mappings]
|
||||||
|
"Bourne Again Shell (bash)" = [
|
||||||
|
"/etc/os-release",
|
||||||
|
"/usr/lib/os-release",
|
||||||
|
"/etc/initrd-release",
|
||||||
|
"/usr/lib/extension-release.d/extension-release.*",
|
||||||
|
]
|
|
@ -0,0 +1,3 @@
|
||||||
|
[mappings]
|
||||||
|
# pacman hooks
|
||||||
|
"INI" = ["/usr/share/libalpm/hooks/*.hook", "/etc/pacman.d/hooks/*.hook"]
|
|
@ -0,0 +1,7 @@
|
||||||
|
# see `man quadlet`
|
||||||
|
[mappings]
|
||||||
|
"INI" = [
|
||||||
|
"**/containers/systemd/*.{container,volume,network,kube,image}",
|
||||||
|
"**/containers/systemd/users/*.{container,volume,network,kube,image}",
|
||||||
|
"**/containers/systemd/users/*/*.{container,volume,network,kube,image}",
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
[mappings]
|
||||||
|
"INI" = [
|
||||||
|
"**/systemd/**/*.conf",
|
||||||
|
"**/systemd/**/*.example",
|
||||||
|
"*.automount",
|
||||||
|
"*.device",
|
||||||
|
"*.dnssd",
|
||||||
|
"*.link",
|
||||||
|
"*.mount",
|
||||||
|
"*.netdev",
|
||||||
|
"*.network",
|
||||||
|
"*.nspawn",
|
||||||
|
"*.path",
|
||||||
|
"*.service",
|
||||||
|
"*.scope",
|
||||||
|
"*.slice",
|
||||||
|
"*.socket",
|
||||||
|
"*.swap",
|
||||||
|
"*.target",
|
||||||
|
"*.timer",
|
||||||
|
]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"Apache Conf" = ["/etc/apache2/**/*.conf", "/etc/apache2/sites-*/**/*"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"YAML" = ["fish_history"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# KornShell is backward-compatible with the Bourne shell #2633
|
||||||
|
[mappings]
|
||||||
|
"Bourne Again Shell (bash)" = ["*.ksh"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"Email" = ["/var/spool/mail/*", "/var/mail/*"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[mappings]
|
||||||
|
"nginx" = ["/etc/nginx/**/*.conf", "/etc/nginx/sites-*/**/*"]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[mappings]
|
||||||
|
"Bourne Again Shell (bash)" = [
|
||||||
|
# used by lots of shells
|
||||||
|
"/etc/profile",
|
||||||
|
]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# see `man wg-quick`
|
||||||
|
[mappings]
|
||||||
|
"INI" = ["/etc/wireguard/*.conf"]
|
|
@ -73,7 +73,7 @@ fn internal_suffixes() {
|
||||||
let file_names = ignored_suffixes
|
let file_names = ignored_suffixes
|
||||||
.values
|
.values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|suffix| format!("test.json{}", suffix));
|
.map(|suffix| format!("test.json{suffix}"));
|
||||||
for file_name_str in file_names {
|
for file_name_str in file_names {
|
||||||
let file_name = OsStr::new(&file_name_str);
|
let file_name = OsStr::new(&file_name_str);
|
||||||
let expected_stripped_file_name = OsStr::new("test.json");
|
let expected_stripped_file_name = OsStr::new("test.json");
|
||||||
|
@ -95,7 +95,7 @@ fn external_suffixes() {
|
||||||
let file_names = ignored_suffixes
|
let file_names = ignored_suffixes
|
||||||
.values
|
.values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|suffix| format!("test.json{}", suffix));
|
.map(|suffix| format!("test.json{suffix}"));
|
||||||
for file_name_str in file_names {
|
for file_name_str in file_names {
|
||||||
let file_name = OsStr::new(&file_name_str);
|
let file_name = OsStr::new(&file_name_str);
|
||||||
let expected_stripped_file_name = OsStr::new("test.json");
|
let expected_stripped_file_name = OsStr::new("test.json");
|
||||||
|
|
783
src/vscreen.rs
783
src/vscreen.rs
|
@ -1,4 +1,8 @@
|
||||||
use std::fmt::{Display, Formatter};
|
use std::{
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
iter::Peekable,
|
||||||
|
str::CharIndices,
|
||||||
|
};
|
||||||
|
|
||||||
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
|
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
|
||||||
pub struct AnsiStyle {
|
pub struct AnsiStyle {
|
||||||
|
@ -10,7 +14,7 @@ impl AnsiStyle {
|
||||||
AnsiStyle { attributes: None }
|
AnsiStyle { attributes: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, sequence: &str) -> bool {
|
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
|
||||||
match &mut self.attributes {
|
match &mut self.attributes {
|
||||||
Some(a) => a.update(sequence),
|
Some(a) => a.update(sequence),
|
||||||
None => {
|
None => {
|
||||||
|
@ -19,6 +23,13 @@ impl AnsiStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_reset_sequence(&self) -> String {
|
||||||
|
match self.attributes {
|
||||||
|
Some(ref a) => a.to_reset_sequence(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AnsiStyle {
|
impl Display for AnsiStyle {
|
||||||
|
@ -31,6 +42,8 @@ impl Display for AnsiStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Attributes {
|
struct Attributes {
|
||||||
|
has_sgr_sequences: bool,
|
||||||
|
|
||||||
foreground: String,
|
foreground: String,
|
||||||
background: String,
|
background: String,
|
||||||
underlined: String,
|
underlined: String,
|
||||||
|
@ -61,11 +74,20 @@ struct Attributes {
|
||||||
/// ON: ^[9m
|
/// ON: ^[9m
|
||||||
/// OFF: ^[29m
|
/// OFF: ^[29m
|
||||||
strike: String,
|
strike: String,
|
||||||
|
|
||||||
|
/// The hyperlink sequence.
|
||||||
|
/// FORMAT: \x1B]8;{ID};{URL}\e\\
|
||||||
|
///
|
||||||
|
/// `\e\\` may be replaced with BEL `\x07`.
|
||||||
|
/// Setting both {ID} and {URL} to an empty string represents no hyperlink.
|
||||||
|
hyperlink: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Attributes {
|
impl Attributes {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Attributes {
|
Attributes {
|
||||||
|
has_sgr_sequences: false,
|
||||||
|
|
||||||
foreground: "".to_owned(),
|
foreground: "".to_owned(),
|
||||||
background: "".to_owned(),
|
background: "".to_owned(),
|
||||||
underlined: "".to_owned(),
|
underlined: "".to_owned(),
|
||||||
|
@ -76,34 +98,56 @@ impl Attributes {
|
||||||
underline: "".to_owned(),
|
underline: "".to_owned(),
|
||||||
italic: "".to_owned(),
|
italic: "".to_owned(),
|
||||||
strike: "".to_owned(),
|
strike: "".to_owned(),
|
||||||
|
hyperlink: "".to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the attributes with an escape sequence.
|
/// Update the attributes with an escape sequence.
|
||||||
/// Returns `false` if the sequence is unsupported.
|
/// Returns `false` if the sequence is unsupported.
|
||||||
pub fn update(&mut self, sequence: &str) -> bool {
|
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
|
||||||
let mut chars = sequence.char_indices().skip(1);
|
use EscapeSequence::*;
|
||||||
|
match sequence {
|
||||||
if let Some((_, t)) = chars.next() {
|
Text(_) => return false,
|
||||||
match t {
|
Unknown(_) => { /* defer to update_with_unsupported */ }
|
||||||
'(' => self.update_with_charset('(', chars.map(|(_, c)| c)),
|
OSC {
|
||||||
')' => self.update_with_charset(')', chars.map(|(_, c)| c)),
|
raw_sequence,
|
||||||
'[' => {
|
command,
|
||||||
if let Some((i, last)) = chars.last() {
|
..
|
||||||
// SAFETY: Always starts with ^[ and ends with m.
|
} => {
|
||||||
self.update_with_csi(last, &sequence[2..i])
|
if command.starts_with("8;") {
|
||||||
} else {
|
return self.update_with_hyperlink(raw_sequence);
|
||||||
false
|
}
|
||||||
|
/* defer to update_with_unsupported */
|
||||||
|
}
|
||||||
|
CSI {
|
||||||
|
final_byte,
|
||||||
|
parameters,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
match final_byte {
|
||||||
|
"m" => return self.update_with_sgr(parameters),
|
||||||
|
_ => {
|
||||||
|
// NOTE(eth-p): We might want to ignore these, since they involve cursor or buffer manipulation.
|
||||||
|
/* defer to update_with_unsupported */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => self.update_with_unsupported(sequence),
|
|
||||||
}
|
}
|
||||||
} else {
|
NF { nf_sequence, .. } => {
|
||||||
false
|
let mut iter = nf_sequence.chars();
|
||||||
|
match iter.next() {
|
||||||
|
Some('(') => return self.update_with_charset('(', iter),
|
||||||
|
Some(')') => return self.update_with_charset(')', iter),
|
||||||
|
_ => { /* defer to update_with_unsupported */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_with_unsupported(sequence.raw())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sgr_reset(&mut self) {
|
fn sgr_reset(&mut self) {
|
||||||
|
self.has_sgr_sequences = false;
|
||||||
|
|
||||||
self.foreground.clear();
|
self.foreground.clear();
|
||||||
self.background.clear();
|
self.background.clear();
|
||||||
self.underlined.clear();
|
self.underlined.clear();
|
||||||
|
@ -121,13 +165,14 @@ impl Attributes {
|
||||||
.map(|p| p.parse::<u16>())
|
.map(|p| p.parse::<u16>())
|
||||||
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
|
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
|
||||||
|
|
||||||
|
self.has_sgr_sequences = true;
|
||||||
while let Some(p) = iter.next() {
|
while let Some(p) = iter.next() {
|
||||||
match p {
|
match p {
|
||||||
0 => self.sgr_reset(),
|
0 => self.sgr_reset(),
|
||||||
1 => self.bold = format!("\x1B[{}m", parameters),
|
1 => self.bold = "\x1B[1m".to_owned(),
|
||||||
2 => self.dim = format!("\x1B[{}m", parameters),
|
2 => self.dim = "\x1B[2m".to_owned(),
|
||||||
3 => self.italic = format!("\x1B[{}m", parameters),
|
3 => self.italic = "\x1B[3m".to_owned(),
|
||||||
4 => self.underline = format!("\x1B[{}m", parameters),
|
4 => self.underline = "\x1B[4m".to_owned(),
|
||||||
23 => self.italic.clear(),
|
23 => self.italic.clear(),
|
||||||
24 => self.underline.clear(),
|
24 => self.underline.clear(),
|
||||||
22 => {
|
22 => {
|
||||||
|
@ -138,7 +183,7 @@ impl Attributes {
|
||||||
40..=49 => self.background = Self::parse_color(p, &mut iter),
|
40..=49 => self.background = Self::parse_color(p, &mut iter),
|
||||||
58..=59 => self.underlined = Self::parse_color(p, &mut iter),
|
58..=59 => self.underlined = Self::parse_color(p, &mut iter),
|
||||||
90..=97 => self.foreground = Self::parse_color(p, &mut iter),
|
90..=97 => self.foreground = Self::parse_color(p, &mut iter),
|
||||||
100..=107 => self.foreground = Self::parse_color(p, &mut iter),
|
100..=107 => self.background = Self::parse_color(p, &mut iter),
|
||||||
_ => {
|
_ => {
|
||||||
// Unsupported SGR sequence.
|
// Unsupported SGR sequence.
|
||||||
// Be compatible and pretend one just wasn't was provided.
|
// Be compatible and pretend one just wasn't was provided.
|
||||||
|
@ -149,19 +194,23 @@ impl Attributes {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool {
|
|
||||||
if finalizer == 'm' {
|
|
||||||
self.update_with_sgr(sequence)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_with_unsupported(&mut self, sequence: &str) -> bool {
|
fn update_with_unsupported(&mut self, sequence: &str) -> bool {
|
||||||
self.unknown_buffer.push_str(sequence);
|
self.unknown_buffer.push_str(sequence);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_with_hyperlink(&mut self, sequence: &str) -> bool {
|
||||||
|
if sequence == "8;;" {
|
||||||
|
// Empty hyperlink ID and HREF -> end of hyperlink.
|
||||||
|
self.hyperlink.clear();
|
||||||
|
} else {
|
||||||
|
self.hyperlink.clear();
|
||||||
|
self.hyperlink.push_str(sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
|
fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
|
||||||
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
|
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
|
||||||
true
|
true
|
||||||
|
@ -172,20 +221,42 @@ impl Attributes {
|
||||||
8 => match parameters.next() {
|
8 => match parameters.next() {
|
||||||
Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
|
Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
|
||||||
Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
|
Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
|
||||||
Some(c) => format!("\x1B[{};{}m", color, c),
|
Some(c) => format!("\x1B[{color};{c}m"),
|
||||||
_ => "".to_owned(),
|
_ => "".to_owned(),
|
||||||
},
|
},
|
||||||
9 => "".to_owned(),
|
9 => "".to_owned(),
|
||||||
_ => format!("\x1B[{}m", color),
|
_ => format!("\x1B[{color}m"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets an ANSI escape sequence to reset all the known attributes.
|
||||||
|
pub fn to_reset_sequence(&self) -> String {
|
||||||
|
let mut buf = String::with_capacity(17);
|
||||||
|
|
||||||
|
// TODO: Enable me in a later pull request.
|
||||||
|
// if self.has_sgr_sequences {
|
||||||
|
// buf.push_str("\x1B[m");
|
||||||
|
// }
|
||||||
|
|
||||||
|
if !self.hyperlink.is_empty() {
|
||||||
|
buf.push_str("\x1B]8;;\x1B\\"); // Disable hyperlink.
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Enable me in a later pull request.
|
||||||
|
// if !self.charset.is_empty() {
|
||||||
|
// // https://espterm.github.io/docs/VT100%20escape%20codes.html
|
||||||
|
// buf.push_str("\x1B(B\x1B)B"); // setusg0 and setusg1
|
||||||
|
// }
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Attributes {
|
impl Display for Attributes {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{}{}{}{}{}{}{}{}{}",
|
"{}{}{}{}{}{}{}{}{}{}",
|
||||||
self.foreground,
|
self.foreground,
|
||||||
self.background,
|
self.background,
|
||||||
self.underlined,
|
self.underlined,
|
||||||
|
@ -195,6 +266,7 @@ impl Display for Attributes {
|
||||||
self.underline,
|
self.underline,
|
||||||
self.italic,
|
self.italic,
|
||||||
self.strike,
|
self.strike,
|
||||||
|
self.hyperlink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,3 +282,646 @@ fn join(
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(delimiter)
|
.join(delimiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A range of indices for a raw ANSI escape sequence.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum EscapeSequenceOffsets {
|
||||||
|
Text {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
Unknown {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
NF {
|
||||||
|
// https://en.wikipedia.org/wiki/ANSI_escape_code#nF_Escape_sequences
|
||||||
|
start_sequence: usize,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
OSC {
|
||||||
|
// https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences
|
||||||
|
start_sequence: usize,
|
||||||
|
start_command: usize,
|
||||||
|
start_terminator: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
CSI {
|
||||||
|
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
|
||||||
|
start_sequence: usize,
|
||||||
|
start_parameters: usize,
|
||||||
|
start_intermediates: usize,
|
||||||
|
start_final_byte: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over the offests of ANSI/VT escape sequences within a string.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let iter = EscapeSequenceOffsetsIterator::new("\x1B[33mThis is yellow text.\x1B[m");
|
||||||
|
/// ```
|
||||||
|
struct EscapeSequenceOffsetsIterator<'a> {
|
||||||
|
text: &'a str,
|
||||||
|
chars: Peekable<CharIndices<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||||
|
pub fn new(text: &'a str) -> EscapeSequenceOffsetsIterator<'a> {
|
||||||
|
return EscapeSequenceOffsetsIterator {
|
||||||
|
text,
|
||||||
|
chars: text.char_indices().peekable(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes values from the iterator while the predicate returns true.
|
||||||
|
/// If the predicate returns false, that value is left.
|
||||||
|
fn chars_take_while(&mut self, pred: impl Fn(char) -> bool) -> Option<(usize, usize)> {
|
||||||
|
self.chars.peek()?;
|
||||||
|
|
||||||
|
let start = self.chars.peek().unwrap().0;
|
||||||
|
let mut end: usize = start;
|
||||||
|
while let Some((i, c)) = self.chars.peek() {
|
||||||
|
if !pred(*c) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
end = *i + c.len_utf8();
|
||||||
|
self.chars.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
|
||||||
|
self.chars_take_while(|c| c != '\x1B')
|
||||||
|
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
|
||||||
|
let (start_sequence, c) = self.chars.next().expect("to not be finished");
|
||||||
|
match self.chars.peek() {
|
||||||
|
None => Some(EscapeSequenceOffsets::Unknown {
|
||||||
|
start: start_sequence,
|
||||||
|
end: start_sequence + c.len_utf8(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some((_, ']')) => self.next_osc(start_sequence),
|
||||||
|
Some((_, '[')) => self.next_csi(start_sequence),
|
||||||
|
Some((i, c)) => match c {
|
||||||
|
'\x20'..='\x2F' => self.next_nf(start_sequence),
|
||||||
|
c => Some(EscapeSequenceOffsets::Unknown {
|
||||||
|
start: start_sequence,
|
||||||
|
end: i + c.len_utf8(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
|
||||||
|
let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished");
|
||||||
|
debug_assert_eq!(osc_open_char, ']');
|
||||||
|
|
||||||
|
let mut start_terminator: usize;
|
||||||
|
let mut end_sequence: usize;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
|
||||||
|
None => {
|
||||||
|
start_terminator = self.text.len();
|
||||||
|
end_sequence = start_terminator;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((_, end)) => {
|
||||||
|
start_terminator = end;
|
||||||
|
end_sequence = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.chars.next() {
|
||||||
|
Some((ti, '\x07')) => {
|
||||||
|
end_sequence = ti + '\x07'.len_utf8();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((ti, '\x1B')) => {
|
||||||
|
match self.chars.next() {
|
||||||
|
Some((i, '\\')) => {
|
||||||
|
end_sequence = i + '\\'.len_utf8();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
end_sequence = ti + '\x1B'.len_utf8();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// Repeat, since `\\`(anything) isn't a valid ST.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
// Prematurely ends.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((_, tc)) => {
|
||||||
|
panic!("this should not be reached: char {tc:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(EscapeSequenceOffsets::OSC {
|
||||||
|
start_sequence,
|
||||||
|
start_command: osc_open_index + osc_open_char.len_utf8(),
|
||||||
|
start_terminator,
|
||||||
|
end: end_sequence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
|
||||||
|
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
|
||||||
|
debug_assert_eq!(csi_open_char, '[');
|
||||||
|
|
||||||
|
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
|
||||||
|
|
||||||
|
// Keep iterating while within the range of `0x30-0x3F`.
|
||||||
|
let mut start_intermediates: usize = start_parameters;
|
||||||
|
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
|
||||||
|
start_intermediates = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep iterating while within the range of `0x20-0x2F`.
|
||||||
|
let mut start_final_byte: usize = start_intermediates;
|
||||||
|
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
|
||||||
|
start_final_byte = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the last char.
|
||||||
|
let end_of_sequence = match self.chars.next() {
|
||||||
|
None => start_final_byte,
|
||||||
|
Some((i, c)) => i + c.len_utf8(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence,
|
||||||
|
start_parameters,
|
||||||
|
start_intermediates,
|
||||||
|
start_final_byte,
|
||||||
|
end: end_of_sequence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
|
||||||
|
let (nf_open_index, nf_open_char) = self.chars.next().expect("to not be finished");
|
||||||
|
debug_assert!(matches!(nf_open_char, '\x20'..='\x2F'));
|
||||||
|
|
||||||
|
let start: usize = nf_open_index;
|
||||||
|
let mut end: usize = start;
|
||||||
|
|
||||||
|
// Keep iterating while within the range of `0x20-0x2F`.
|
||||||
|
match self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
|
||||||
|
Some((_, i)) => end = i,
|
||||||
|
None => {
|
||||||
|
return Some(EscapeSequenceOffsets::NF {
|
||||||
|
start_sequence,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the final byte.
|
||||||
|
if let Some((i, c)) = self.chars.next() {
|
||||||
|
end = i + c.len_utf8()
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(EscapeSequenceOffsets::NF {
|
||||||
|
start_sequence,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> {
|
||||||
|
type Item = EscapeSequenceOffsets;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.chars.peek() {
|
||||||
|
Some((_, '\x1B')) => self.next_sequence(),
|
||||||
|
Some((_, _)) => self.next_text(),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over ANSI/VT escape sequences within a string.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let iter = EscapeSequenceIterator::new("\x1B[33mThis is yellow text.\x1B[m");
|
||||||
|
/// ```
|
||||||
|
pub struct EscapeSequenceIterator<'a> {
|
||||||
|
text: &'a str,
|
||||||
|
offset_iter: EscapeSequenceOffsetsIterator<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EscapeSequenceIterator<'a> {
|
||||||
|
pub fn new(text: &'a str) -> EscapeSequenceIterator<'a> {
|
||||||
|
return EscapeSequenceIterator {
|
||||||
|
text,
|
||||||
|
offset_iter: EscapeSequenceOffsetsIterator::new(text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for EscapeSequenceIterator<'a> {
|
||||||
|
type Item = EscapeSequence<'a>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
use EscapeSequenceOffsets::*;
|
||||||
|
self.offset_iter.next().map(|offsets| match offsets {
|
||||||
|
Unknown { start, end } => EscapeSequence::Unknown(&self.text[start..end]),
|
||||||
|
Text { start, end } => EscapeSequence::Text(&self.text[start..end]),
|
||||||
|
NF {
|
||||||
|
start_sequence,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
} => EscapeSequence::NF {
|
||||||
|
raw_sequence: &self.text[start_sequence..end],
|
||||||
|
nf_sequence: &self.text[start..end],
|
||||||
|
},
|
||||||
|
OSC {
|
||||||
|
start_sequence,
|
||||||
|
start_command,
|
||||||
|
start_terminator,
|
||||||
|
end,
|
||||||
|
} => EscapeSequence::OSC {
|
||||||
|
raw_sequence: &self.text[start_sequence..end],
|
||||||
|
command: &self.text[start_command..start_terminator],
|
||||||
|
terminator: &self.text[start_terminator..end],
|
||||||
|
},
|
||||||
|
CSI {
|
||||||
|
start_sequence,
|
||||||
|
start_parameters,
|
||||||
|
start_intermediates,
|
||||||
|
start_final_byte,
|
||||||
|
end,
|
||||||
|
} => EscapeSequence::CSI {
|
||||||
|
raw_sequence: &self.text[start_sequence..end],
|
||||||
|
parameters: &self.text[start_parameters..start_intermediates],
|
||||||
|
intermediates: &self.text[start_intermediates..start_final_byte],
|
||||||
|
final_byte: &self.text[start_final_byte..end],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed ANSI/VT100 escape sequence.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum EscapeSequence<'a> {
|
||||||
|
Text(&'a str),
|
||||||
|
Unknown(&'a str),
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
NF {
|
||||||
|
raw_sequence: &'a str,
|
||||||
|
nf_sequence: &'a str,
|
||||||
|
},
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
OSC {
|
||||||
|
raw_sequence: &'a str,
|
||||||
|
command: &'a str,
|
||||||
|
terminator: &'a str,
|
||||||
|
},
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
CSI {
|
||||||
|
raw_sequence: &'a str,
|
||||||
|
parameters: &'a str,
|
||||||
|
intermediates: &'a str,
|
||||||
|
final_byte: &'a str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EscapeSequence<'a> {
|
||||||
|
pub fn raw(&self) -> &'a str {
|
||||||
|
use EscapeSequence::*;
|
||||||
|
match *self {
|
||||||
|
Text(raw) => raw,
|
||||||
|
Unknown(raw) => raw,
|
||||||
|
NF { raw_sequence, .. } => raw_sequence,
|
||||||
|
OSC { raw_sequence, .. } => raw_sequence,
|
||||||
|
CSI { raw_sequence, .. } => raw_sequence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::vscreen::{
|
||||||
|
EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets,
|
||||||
|
EscapeSequenceOffsetsIterator,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_text() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("text");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_text_stops_at_esc() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[ming");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_osc_with_bel() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x07");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::OSC {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_command: 2,
|
||||||
|
start_terminator: 5,
|
||||||
|
end: 6,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_osc_with_st() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x1B\\");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::OSC {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_command: 2,
|
||||||
|
start_terminator: 5,
|
||||||
|
end: 7,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_osc_thats_broken() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]ab");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::OSC {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_command: 2,
|
||||||
|
start_terminator: 4,
|
||||||
|
end: 4,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_csi() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 2,
|
||||||
|
start_final_byte: 2,
|
||||||
|
end: 3
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1;34m");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 6,
|
||||||
|
start_final_byte: 6,
|
||||||
|
end: 7
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_csi_with_intermediates() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[$m");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 2,
|
||||||
|
start_final_byte: 3,
|
||||||
|
end: 4
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters_and_intermediates() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$m");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 3,
|
||||||
|
start_final_byte: 4,
|
||||||
|
end: 5
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_csi_thats_broken() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 2,
|
||||||
|
start_final_byte: 2,
|
||||||
|
end: 2
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 3,
|
||||||
|
start_final_byte: 3,
|
||||||
|
end: 3
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 0,
|
||||||
|
start_parameters: 2,
|
||||||
|
start_intermediates: 3,
|
||||||
|
start_final_byte: 4,
|
||||||
|
end: 4
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_nf() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B($0");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::NF {
|
||||||
|
start_sequence: 0,
|
||||||
|
start: 1,
|
||||||
|
end: 4
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_parses_nf_thats_broken() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B(");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::NF {
|
||||||
|
start_sequence: 0,
|
||||||
|
start: 1,
|
||||||
|
end: 1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_offsets_iterator_iterates() {
|
||||||
|
let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[33m\x1B]OSC\x07\x1B(0");
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::CSI {
|
||||||
|
start_sequence: 4,
|
||||||
|
start_parameters: 6,
|
||||||
|
start_intermediates: 8,
|
||||||
|
start_final_byte: 8,
|
||||||
|
end: 9
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::OSC {
|
||||||
|
start_sequence: 9,
|
||||||
|
start_command: 11,
|
||||||
|
start_terminator: 14,
|
||||||
|
end: 15
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequenceOffsets::NF {
|
||||||
|
start_sequence: 15,
|
||||||
|
start: 16,
|
||||||
|
end: 18
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(iter.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_sequence_iterator_iterates() {
|
||||||
|
let mut iter = EscapeSequenceIterator::new("text\x1B[33m\x1B]OSC\x07\x1B]OSC\x1B\\\x1B(0");
|
||||||
|
assert_eq!(iter.next(), Some(EscapeSequence::Text("text")));
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequence::CSI {
|
||||||
|
raw_sequence: "\x1B[33m",
|
||||||
|
parameters: "33",
|
||||||
|
intermediates: "",
|
||||||
|
final_byte: "m",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequence::OSC {
|
||||||
|
raw_sequence: "\x1B]OSC\x07",
|
||||||
|
command: "OSC",
|
||||||
|
terminator: "\x07",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequence::OSC {
|
||||||
|
raw_sequence: "\x1B]OSC\x1B\\",
|
||||||
|
command: "OSC",
|
||||||
|
terminator: "\x1B\\",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
iter.next(),
|
||||||
|
Some(EscapeSequence::NF {
|
||||||
|
raw_sequence: "\x1B(0",
|
||||||
|
nf_sequence: "(0",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(iter.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sgr_attributes_do_not_leak_into_wrong_field() {
|
||||||
|
let mut attrs = crate::vscreen::Attributes::new();
|
||||||
|
|
||||||
|
// Bold, Dim, Italic, Underline, Foreground, Background
|
||||||
|
attrs.update(EscapeSequence::CSI {
|
||||||
|
raw_sequence: "\x1B[1;2;3;4;31;41m",
|
||||||
|
parameters: "1;2;3;4;31;41",
|
||||||
|
intermediates: "",
|
||||||
|
final_byte: "m",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(attrs.bold, "\x1B[1m");
|
||||||
|
assert_eq!(attrs.dim, "\x1B[2m");
|
||||||
|
assert_eq!(attrs.italic, "\x1B[3m");
|
||||||
|
assert_eq!(attrs.underline, "\x1B[4m");
|
||||||
|
assert_eq!(attrs.foreground, "\x1B[31m");
|
||||||
|
assert_eq!(attrs.background, "\x1B[41m");
|
||||||
|
|
||||||
|
// Bold, Bright Foreground, Bright Background
|
||||||
|
attrs.sgr_reset();
|
||||||
|
attrs.update(EscapeSequence::CSI {
|
||||||
|
raw_sequence: "\x1B[1;94;103m",
|
||||||
|
parameters: "1;94;103",
|
||||||
|
intermediates: "",
|
||||||
|
final_byte: "m",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(attrs.bold, "\x1B[1m");
|
||||||
|
assert_eq!(attrs.foreground, "\x1B[94m");
|
||||||
|
assert_eq!(attrs.background, "\x1B[103m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,13 @@ if ! command -v hyperfine > /dev/null 2>&1; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check that jq is installed.
|
||||||
|
if ! command -v jq > /dev/null 2>&1; then
|
||||||
|
echo "'jq' does not seem to be installed."
|
||||||
|
echo "You can get it here: https://jqlang.github.io/jq/download/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Check that python3 is installed.
|
# Check that python3 is installed.
|
||||||
if ! command -v python3 > /dev/null 2>&1; then
|
if ! command -v python3 > /dev/null 2>&1; then
|
||||||
echo "'python3' does not seem to be installed."
|
echo "'python3' does not seem to be installed."
|
||||||
|
@ -49,7 +56,8 @@ REPORT="$RESULT_DIR/report.md"
|
||||||
TARGET_DIR="$(get_cargo_target_dir)"
|
TARGET_DIR="$(get_cargo_target_dir)"
|
||||||
TARGET_RELEASE="${TARGET_DIR}/release/bat"
|
TARGET_RELEASE="${TARGET_DIR}/release/bat"
|
||||||
|
|
||||||
WARMUP_COUNT=3
|
: ${WARMUP_COUNT:=3}
|
||||||
|
: ${RUN_COUNT:=10}
|
||||||
|
|
||||||
# Determine which target to benchmark.
|
# Determine which target to benchmark.
|
||||||
BAT=''
|
BAT=''
|
||||||
|
@ -88,16 +96,28 @@ hyperfine \
|
||||||
"$(printf "%q" "$BAT") --no-config" \
|
"$(printf "%q" "$BAT") --no-config" \
|
||||||
--command-name "bat" \
|
--command-name "bat" \
|
||||||
--warmup "$WARMUP_COUNT" \
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
--export-markdown "$RESULT_DIR/startup-time.md" \
|
--export-markdown "$RESULT_DIR/startup-time.md" \
|
||||||
--export-json "$RESULT_DIR/startup-time.json"
|
--export-json "$RESULT_DIR/startup-time.json"
|
||||||
cat "$RESULT_DIR/startup-time.md" >> "$REPORT"
|
cat "$RESULT_DIR/startup-time.md" >> "$REPORT"
|
||||||
|
|
||||||
|
|
||||||
|
heading "Startup time without syntax highlighting"
|
||||||
|
hyperfine \
|
||||||
|
"$(printf "%q" "$BAT") --no-config startup-time-src/small-CpuInfo-file.cpuinfo" \
|
||||||
|
--command-name "bat … small-CpuInfo-file.cpuinfo" \
|
||||||
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
|
--export-markdown "$RESULT_DIR/startup-time-without-syntax-highlighting.md" \
|
||||||
|
--export-json "$RESULT_DIR/startup-time-without-syntax-highlighting.json"
|
||||||
|
cat "$RESULT_DIR/startup-time-without-syntax-highlighting.md" >> "$REPORT"
|
||||||
|
|
||||||
heading "Startup time with syntax highlighting"
|
heading "Startup time with syntax highlighting"
|
||||||
hyperfine \
|
hyperfine \
|
||||||
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-CpuInfo-file.cpuinfo" \
|
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-CpuInfo-file.cpuinfo" \
|
||||||
--command-name "bat … small-CpuInfo-file.cpuinfo" \
|
--command-name "bat … --color=always small-CpuInfo-file.cpuinfo" \
|
||||||
--warmup "$WARMUP_COUNT" \
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
--export-markdown "$RESULT_DIR/startup-time-with-syntax-highlighting.md" \
|
--export-markdown "$RESULT_DIR/startup-time-with-syntax-highlighting.md" \
|
||||||
--export-json "$RESULT_DIR/startup-time-with-syntax-highlighting.json"
|
--export-json "$RESULT_DIR/startup-time-with-syntax-highlighting.json"
|
||||||
cat "$RESULT_DIR/startup-time-with-syntax-highlighting.md" >> "$REPORT"
|
cat "$RESULT_DIR/startup-time-with-syntax-highlighting.md" >> "$REPORT"
|
||||||
|
@ -108,16 +128,52 @@ hyperfine \
|
||||||
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-Markdown-file.md" \
|
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-Markdown-file.md" \
|
||||||
--command-name "bat … small-Markdown-file.md" \
|
--command-name "bat … small-Markdown-file.md" \
|
||||||
--warmup "$WARMUP_COUNT" \
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
--export-markdown "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" \
|
--export-markdown "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" \
|
||||||
--export-json "$RESULT_DIR/startup-time-with-syntax-with-dependencies.json"
|
--export-json "$RESULT_DIR/startup-time-with-syntax-with-dependencies.json"
|
||||||
cat "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" >> "$REPORT"
|
cat "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" >> "$REPORT"
|
||||||
|
|
||||||
|
|
||||||
|
heading "Startup time with indeterminant syntax"
|
||||||
|
hyperfine \
|
||||||
|
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/mystery-file" \
|
||||||
|
--shell none \
|
||||||
|
--command-name 'bat … mystery-file' \
|
||||||
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
|
--export-markdown "$RESULT_DIR/startup-time-with-indeterminant-syntax.md" \
|
||||||
|
--export-json "$RESULT_DIR/startup-time-with-indeterminant-syntax.json"
|
||||||
|
cat "$RESULT_DIR/startup-time-with-indeterminant-syntax.md" >> "$REPORT"
|
||||||
|
|
||||||
|
heading "Startup time with manually set syntax"
|
||||||
|
hyperfine \
|
||||||
|
"$(printf "%q" "$BAT") --no-config --color=always --language=Dockerfile startup-time-src/mystery-file" \
|
||||||
|
--shell none \
|
||||||
|
--command-name 'bat … --language=Dockerfile mystery-file' \
|
||||||
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
|
--export-markdown "$RESULT_DIR/startup-time-with-manually-set-syntax.md" \
|
||||||
|
--export-json "$RESULT_DIR/startup-time-with-manually-set-syntax.json"
|
||||||
|
cat "$RESULT_DIR/startup-time-with-manually-set-syntax.md" >> "$REPORT"
|
||||||
|
|
||||||
|
heading "Startup time with mapped syntax"
|
||||||
|
hyperfine \
|
||||||
|
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/Containerfile" \
|
||||||
|
--shell none \
|
||||||
|
--command-name 'bat … Containerfile' \
|
||||||
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
|
--export-markdown "$RESULT_DIR/startup-time-with-mapped-syntax.md" \
|
||||||
|
--export-json "$RESULT_DIR/startup-time-with-mapped-syntax.json"
|
||||||
|
cat "$RESULT_DIR/startup-time-with-mapped-syntax.md" >> "$REPORT"
|
||||||
|
|
||||||
|
|
||||||
heading "Plain-text speed"
|
heading "Plain-text speed"
|
||||||
hyperfine \
|
hyperfine \
|
||||||
"$(printf "%q" "$BAT") --no-config --language=txt --style=plain highlighting-speed-src/numpy_test_multiarray.py" \
|
"$(printf "%q" "$BAT") --no-config --language=txt --style=plain highlighting-speed-src/numpy_test_multiarray.py" \
|
||||||
--command-name 'bat … --language=txt numpy_test_multiarray.py' \
|
--command-name 'bat … --language=txt numpy_test_multiarray.py' \
|
||||||
--warmup "$WARMUP_COUNT" \
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
--export-markdown "$RESULT_DIR/plain-text-speed.md" \
|
--export-markdown "$RESULT_DIR/plain-text-speed.md" \
|
||||||
--export-json "$RESULT_DIR/plain-text-speed.json"
|
--export-json "$RESULT_DIR/plain-text-speed.json"
|
||||||
cat "$RESULT_DIR/plain-text-speed.md" >> "$REPORT"
|
cat "$RESULT_DIR/plain-text-speed.md" >> "$REPORT"
|
||||||
|
@ -129,6 +185,7 @@ for wrap in character never; do
|
||||||
|
|
||||||
heading "Syntax highlighting speed --wrap=${wrap}: \`$filename\`"
|
heading "Syntax highlighting speed --wrap=${wrap}: \`$filename\`"
|
||||||
hyperfine --warmup "$WARMUP_COUNT" \
|
hyperfine --warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
"$(printf "%q" "$BAT") --no-config --style=full --color=always --wrap=${wrap} --terminal-width=80 '$SRC'" \
|
"$(printf "%q" "$BAT") --no-config --style=full --color=always --wrap=${wrap} --terminal-width=80 '$SRC'" \
|
||||||
--command-name "bat … ${filename}" \
|
--command-name "bat … ${filename}" \
|
||||||
--export-markdown "$RESULT_DIR/syntax-highlighting-speed-${filename}.md" \
|
--export-markdown "$RESULT_DIR/syntax-highlighting-speed-${filename}.md" \
|
||||||
|
@ -143,6 +200,7 @@ hyperfine \
|
||||||
"$(printf "%q" "$BAT") --no-config --language=txt --style=plain many-small-files/*.txt" \
|
"$(printf "%q" "$BAT") --no-config --language=txt --style=plain many-small-files/*.txt" \
|
||||||
--command-name 'bat … --language=txt *.txt' \
|
--command-name 'bat … --language=txt *.txt' \
|
||||||
--warmup "$WARMUP_COUNT" \
|
--warmup "$WARMUP_COUNT" \
|
||||||
|
--runs "$RUN_COUNT" \
|
||||||
--export-markdown "$RESULT_DIR/many-small-files-speed.md" \
|
--export-markdown "$RESULT_DIR/many-small-files-speed.md" \
|
||||||
--export-json "$RESULT_DIR/many-small-files-speed.json"
|
--export-json "$RESULT_DIR/many-small-files-speed.json"
|
||||||
cat "$RESULT_DIR/many-small-files-speed.md" >> "$REPORT"
|
cat "$RESULT_DIR/many-small-files-speed.md" >> "$REPORT"
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
FROM docker.io/alpine:latest
|
||||||
|
COPY foo /root/bar
|
||||||
|
RUN sleep 60
|
|
@ -0,0 +1,3 @@
|
||||||
|
FROM docker.io/alpine:latest
|
||||||
|
COPY foo /root/bar
|
||||||
|
RUN sleep 60
|
Binary file not shown.
|
@ -0,0 +1,30 @@
|
||||||
|
line 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
line 5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
line 20
|
||||||
|
line 21
|
||||||
|
|
||||||
|
|
||||||
|
line 24
|
||||||
|
|
||||||
|
line 26
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
line 30
|
|
@ -0,0 +1 @@
|
||||||
|
]8;;http://example.com\This is a link]8;;\n
|
|
@ -0,0 +1 @@
|
||||||
|
The header is not broken
|
|
@ -0,0 +1,53 @@
|
||||||
|
#[test]
|
||||||
|
fn all_jobs_not_missing_any_jobs() {
|
||||||
|
let yaml: serde_yaml::Value =
|
||||||
|
serde_yaml::from_reader(std::fs::File::open(".github/workflows/CICD.yml").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
let jobs = yaml.get("jobs").unwrap();
|
||||||
|
|
||||||
|
// Get all jobs that all-jobs depends on:
|
||||||
|
//
|
||||||
|
// jobs:
|
||||||
|
// all-jobs:
|
||||||
|
// needs:
|
||||||
|
// - this
|
||||||
|
// - list
|
||||||
|
// - ...
|
||||||
|
let actual = jobs
|
||||||
|
.get("all-jobs")
|
||||||
|
.unwrap()
|
||||||
|
.get("needs")
|
||||||
|
.unwrap()
|
||||||
|
.as_sequence()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get all jobs used in CI, except the ones we want to ignore:
|
||||||
|
//
|
||||||
|
// jobs:
|
||||||
|
// this: ...
|
||||||
|
// list: ...
|
||||||
|
// ...
|
||||||
|
let exceptions = [
|
||||||
|
"all-jobs", // 'all-jobs' should not reference itself
|
||||||
|
"winget", // only used when publishing a release
|
||||||
|
];
|
||||||
|
let expected = jobs
|
||||||
|
.as_mapping()
|
||||||
|
.unwrap()
|
||||||
|
.keys()
|
||||||
|
.filter_map(|k| {
|
||||||
|
if exceptions.contains(&k.as_str().unwrap_or_default()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(k)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Make sure they match
|
||||||
|
assert_eq!(
|
||||||
|
*actual, expected,
|
||||||
|
"`all-jobs` should depend on all other jobs"
|
||||||
|
);
|
||||||
|
}
|
|
@ -208,6 +208,105 @@ fn line_range_multiple() {
|
||||||
.stdout("line 1\nline 2\nline 4\n");
|
.stdout("line 1\nline 2\nline 4\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn squeeze_blank() {
|
||||||
|
bat()
|
||||||
|
.arg("empty_lines.txt")
|
||||||
|
.arg("--squeeze-blank")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("line 1\n\nline 5\n\nline 20\nline 21\n\nline 24\n\nline 26\n\nline 30\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn squeeze_blank_line_numbers() {
|
||||||
|
bat()
|
||||||
|
.arg("empty_lines.txt")
|
||||||
|
.arg("--squeeze-blank")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("--number")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(" 1 line 1\n 2 \n 5 line 5\n 6 \n 20 line 20\n 21 line 21\n 22 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 30 line 30\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn squeeze_limit() {
|
||||||
|
bat()
|
||||||
|
.arg("empty_lines.txt")
|
||||||
|
.arg("--squeeze-blank")
|
||||||
|
.arg("--squeeze-limit=2")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("line 1\n\n\nline 5\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\nline 30\n");
|
||||||
|
|
||||||
|
bat()
|
||||||
|
.arg("empty_lines.txt")
|
||||||
|
.arg("--squeeze-blank")
|
||||||
|
.arg("--squeeze-limit=5")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("line 1\n\n\n\nline 5\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn squeeze_limit_line_numbers() {
|
||||||
|
bat()
|
||||||
|
.arg("empty_lines.txt")
|
||||||
|
.arg("--squeeze-blank")
|
||||||
|
.arg("--squeeze-limit=2")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("--number")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(" 1 line 1\n 2 \n 3 \n 5 line 5\n 6 \n 7 \n 20 line 20\n 21 line 21\n 22 \n 23 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 28 \n 30 line 30\n");
|
||||||
|
|
||||||
|
bat()
|
||||||
|
.arg("empty_lines.txt")
|
||||||
|
.arg("--squeeze-blank")
|
||||||
|
.arg("--squeeze-limit=5")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("--number")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(" 1 line 1\n 2 \n 3 \n 4 \n 5 line 5\n 6 \n 7 \n 8 \n 9 \n 10 \n 20 line 20\n 21 line 21\n 22 \n 23 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 28 \n 29 \n 30 line 30\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_themes_with_colors() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let default_theme_chunk = "Monokai Extended Light\x1B[0m (default)";
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let default_theme_chunk = "Monokai Extended\x1B[0m (default)";
|
||||||
|
|
||||||
|
bat()
|
||||||
|
.arg("--color=always")
|
||||||
|
.arg("--list-themes")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("DarkNeon").normalize())
|
||||||
|
.stdout(predicate::str::contains(default_theme_chunk).normalize())
|
||||||
|
.stdout(predicate::str::contains("Output the square of a number.").normalize());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_themes_without_colors() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let default_theme_chunk = "Monokai Extended Light (default)";
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let default_theme_chunk = "Monokai Extended (default)";
|
||||||
|
|
||||||
|
bat()
|
||||||
|
.arg("--color=never")
|
||||||
|
.arg("--list-themes")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("DarkNeon").normalize())
|
||||||
|
.stdout(predicate::str::contains(default_theme_chunk).normalize());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg_attr(any(not(feature = "git"), target_os = "windows"), ignore)]
|
#[cfg_attr(any(not(feature = "git"), target_os = "windows"), ignore)]
|
||||||
fn short_help() {
|
fn short_help() {
|
||||||
|
@ -936,6 +1035,18 @@ fn env_var_bat_paging() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_set_terminal_title() {
|
||||||
|
bat()
|
||||||
|
.arg("--paging=always")
|
||||||
|
.arg("--set-terminal-title")
|
||||||
|
.arg("test.txt")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("\u{1b}]0;bat: test.txt\x07hello world\n")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diagnostic_sanity_check() {
|
fn diagnostic_sanity_check() {
|
||||||
bat()
|
bat()
|
||||||
|
@ -1163,6 +1274,20 @@ fn bom_stripped_when_no_color_and_not_loop_through() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test for https://github.com/sharkdp/bat/issues/2541
|
||||||
|
#[test]
|
||||||
|
fn no_broken_osc_emit_with_line_wrapping() {
|
||||||
|
bat()
|
||||||
|
.arg("--color=always")
|
||||||
|
.arg("--decorations=never")
|
||||||
|
.arg("--wrap=character")
|
||||||
|
.arg("--terminal-width=40")
|
||||||
|
.arg("regression_tests/issue_2541.txt")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::function(|s: &str| s.lines().count() == 1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_print_file_named_cache() {
|
fn can_print_file_named_cache() {
|
||||||
bat_with_config()
|
bat_with_config()
|
||||||
|
@ -1381,6 +1506,61 @@ fn header_full_binary() {
|
||||||
.stderr("");
|
.stderr("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "git"))]
|
||||||
|
fn header_narrow_terminal() {
|
||||||
|
bat()
|
||||||
|
.arg("--terminal-width=30")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(
|
||||||
|
"\
|
||||||
|
─────┬────────────────────────
|
||||||
|
│ File: this-file-path-is
|
||||||
|
│ -really-long-and-would-
|
||||||
|
│ have-broken-the-layout-
|
||||||
|
│ of-the-header.txt
|
||||||
|
─────┼────────────────────────
|
||||||
|
1 │ The header is not broke
|
||||||
|
│ n
|
||||||
|
─────┴────────────────────────
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn header_very_narrow_terminal() {
|
||||||
|
bat()
|
||||||
|
.arg("--terminal-width=10")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(
|
||||||
|
"\
|
||||||
|
──────────
|
||||||
|
File: this
|
||||||
|
-file-path
|
||||||
|
-is-really
|
||||||
|
-long-and-
|
||||||
|
would-have
|
||||||
|
-broken-th
|
||||||
|
e-layout-o
|
||||||
|
f-the-head
|
||||||
|
er.txt
|
||||||
|
──────────
|
||||||
|
The header
|
||||||
|
is not br
|
||||||
|
oken
|
||||||
|
──────────
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "git")] // Expected output assumes git is enabled
|
#[cfg(feature = "git")] // Expected output assumes git is enabled
|
||||||
fn header_default() {
|
fn header_default() {
|
||||||
|
@ -1613,7 +1793,7 @@ fn do_not_panic_regression_tests() {
|
||||||
] {
|
] {
|
||||||
bat()
|
bat()
|
||||||
.arg("--color=always")
|
.arg("--color=always")
|
||||||
.arg(&format!("regression_tests/{}", filename))
|
.arg(&format!("regression_tests/{filename}"))
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
}
|
}
|
||||||
|
@ -1626,7 +1806,7 @@ fn do_not_detect_different_syntax_for_stdin_and_files() {
|
||||||
let cmd_for_file = bat()
|
let cmd_for_file = bat()
|
||||||
.arg("--color=always")
|
.arg("--color=always")
|
||||||
.arg("--map-syntax=*.js:Markdown")
|
.arg("--map-syntax=*.js:Markdown")
|
||||||
.arg(&format!("--file-name={}", file))
|
.arg(&format!("--file-name={file}"))
|
||||||
.arg("--style=plain")
|
.arg("--style=plain")
|
||||||
.arg(file)
|
.arg(file)
|
||||||
.assert()
|
.assert()
|
||||||
|
@ -1636,7 +1816,7 @@ fn do_not_detect_different_syntax_for_stdin_and_files() {
|
||||||
.arg("--color=always")
|
.arg("--color=always")
|
||||||
.arg("--map-syntax=*.js:Markdown")
|
.arg("--map-syntax=*.js:Markdown")
|
||||||
.arg("--style=plain")
|
.arg("--style=plain")
|
||||||
.arg(&format!("--file-name={}", file))
|
.arg(&format!("--file-name={file}"))
|
||||||
.pipe_stdin(Path::new(EXAMPLES_DIR).join(file))
|
.pipe_stdin(Path::new(EXAMPLES_DIR).join(file))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.assert()
|
.assert()
|
||||||
|
@ -1655,7 +1835,7 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() {
|
||||||
bat()
|
bat()
|
||||||
.arg("--color=always")
|
.arg("--color=always")
|
||||||
.arg("--map-syntax=*.invalid-syntax:InvalidSyntax")
|
.arg("--map-syntax=*.invalid-syntax:InvalidSyntax")
|
||||||
.arg(&format!("--file-name={}", file))
|
.arg(&format!("--file-name={file}"))
|
||||||
.arg("--style=plain")
|
.arg("--style=plain")
|
||||||
.arg(file)
|
.arg(file)
|
||||||
.assert()
|
.assert()
|
||||||
|
@ -1728,6 +1908,25 @@ fn show_all_with_caret_notation() {
|
||||||
.assert()
|
.assert()
|
||||||
.stdout("hello·world^J\n├──┤^M^@^G^H^[")
|
.stdout("hello·world^J\n├──┤^M^@^G^H^[")
|
||||||
.stderr("");
|
.stderr("");
|
||||||
|
|
||||||
|
bat()
|
||||||
|
.arg("--show-all")
|
||||||
|
.arg("--nonprintable-notation=caret")
|
||||||
|
.arg("control_characters.txt")
|
||||||
|
.assert()
|
||||||
|
.stdout("^@^A^B^C^D^E^F^G^H├─┤^J\n^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\^]^^^_^?")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn show_all_with_unicode() {
|
||||||
|
bat()
|
||||||
|
.arg("--show-all")
|
||||||
|
.arg("--nonprintable-notation=unicode")
|
||||||
|
.arg("control_characters.txt")
|
||||||
|
.assert()
|
||||||
|
.stdout("␀␁␂␃␄␅␆␇␈├─┤␊\n␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␡")
|
||||||
|
.stderr("");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1834,7 +2033,7 @@ fn ansi_passthrough_emit() {
|
||||||
.arg("--paging=never")
|
.arg("--paging=never")
|
||||||
.arg("--color=never")
|
.arg("--color=never")
|
||||||
.arg("--terminal-width=80")
|
.arg("--terminal-width=80")
|
||||||
.arg(format!("--wrap={}", wrapping))
|
.arg(format!("--wrap={wrapping}"))
|
||||||
.arg("--decorations=always")
|
.arg("--decorations=always")
|
||||||
.arg("--style=plain")
|
.arg("--style=plain")
|
||||||
.write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n")
|
.write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n")
|
||||||
|
@ -1845,6 +2044,62 @@ fn ansi_passthrough_emit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines.
|
||||||
|
// This also helps ensure that escape sequences are counted as part of the visible characters when wrapping.
|
||||||
|
#[test]
|
||||||
|
fn ansi_sgr_emitted_when_wrapped() {
|
||||||
|
bat()
|
||||||
|
.arg("--paging=never")
|
||||||
|
.arg("--color=never")
|
||||||
|
.arg("--terminal-width=20")
|
||||||
|
.arg("--wrap=character")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("--style=plain")
|
||||||
|
.write_stdin("\x1B[33mColor...............Also color.\n")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("\x1B[33m\x1B[33mColor...............\n\x1B[33mAlso color.\n")
|
||||||
|
// FIXME: ~~~~~~~~ should not be emitted twice.
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines.
|
||||||
|
// This also helps ensure that escape sequences are counted as part of the visible characters when wrapping.
|
||||||
|
#[test]
|
||||||
|
fn ansi_hyperlink_emitted_when_wrapped() {
|
||||||
|
bat()
|
||||||
|
.arg("--paging=never")
|
||||||
|
.arg("--color=never")
|
||||||
|
.arg("--terminal-width=20")
|
||||||
|
.arg("--wrap=character")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("--style=plain")
|
||||||
|
.write_stdin("\x1B]8;;http://example.com/\x1B\\Hyperlinks..........Wrap across lines.\n")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("\x1B]8;;http://example.com/\x1B\\\x1B]8;;http://example.com/\x1B\\Hyperlinks..........\x1B]8;;\x1B\\\n\x1B]8;;http://example.com/\x1B\\Wrap across lines.\n")
|
||||||
|
// FIXME: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ should not be emitted twice.
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that multiple ANSI sequence SGR attributes are combined when emitted on wrapped lines.
|
||||||
|
#[test]
|
||||||
|
fn ansi_sgr_joins_attributes_when_wrapped() {
|
||||||
|
bat()
|
||||||
|
.arg("--paging=never")
|
||||||
|
.arg("--color=never")
|
||||||
|
.arg("--terminal-width=20")
|
||||||
|
.arg("--wrap=character")
|
||||||
|
.arg("--decorations=always")
|
||||||
|
.arg("--style=plain")
|
||||||
|
.write_stdin("\x1B[33mColor. \x1B[1mBold.........Also bold and color.\n")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("\x1B[33m\x1B[33mColor. \x1B[1m\x1B[33m\x1B[1mBold.........\n\x1B[33m\x1B[1mAlso bold and color.\n")
|
||||||
|
// FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice.
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ignored_suffix_arg() {
|
fn ignored_suffix_arg() {
|
||||||
bat()
|
bat()
|
||||||
|
|
|
@ -30,8 +30,7 @@ fn no_duplicate_extensions() {
|
||||||
for extension in &syntax.file_extensions {
|
for extension in &syntax.file_extensions {
|
||||||
assert!(
|
assert!(
|
||||||
KNOWN_EXCEPTIONS.contains(&extension.as_str()) || extensions.insert(extension),
|
KNOWN_EXCEPTIONS.contains(&extension.as_str()) || extensions.insert(extension),
|
||||||
"File extension / pattern \"{}\" appears twice in the syntax set",
|
"File extension / pattern \"{extension}\" appears twice in the syntax set"
|
||||||
extension
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
[38;2;249;38;114mimport[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116m../imported-file[0m[38;2;230;219;116m"[0m[38;2;248;248;242m [0m[38;2;248;248;242m;[0m
|
[38;2;249;38;114mimport[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116m../imported-file[0m[38;2;230;219;116m"[0m[38;2;248;248;242m [0m[38;2;248;248;242m;[0m
|
||||||
|
|
||||||
[38;2;117;113;94m#[0m[38;2;117;113;94m With Comments ![0m
|
[38;2;117;113;94m#[0m[38;2;117;113;94m With Comments ![0m
|
||||||
[38;2;249;38;114mdef[0m[38;2;248;248;242m [0m[38;2;166;226;46mweird[0m[38;2;248;248;242m([0m[3;38;2;253;151;31m$a[0m[38;2;248;248;242m; [0m[3;38;2;253;151;31m$b[0m[38;2;248;248;242m; [0m[3;38;2;253;151;31m$c[0m[38;2;248;248;242m)[0m[38;2;248;248;242m:[0m
|
[38;2;249;38;114mdef[0m[38;2;248;248;242m [0m[38;2;166;226;46mweird[0m[38;2;248;248;242m([0m[3;38;2;253;151;31m$a[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[3;38;2;253;151;31m$b[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[3;38;2;253;151;31m$c[0m[38;2;248;248;242m)[0m[38;2;248;248;242m:[0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m[ [0m[38;2;255;255;255m$a[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$b[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$c[0m[38;2;248;248;242m ][0m[38;2;248;248;242m | [0m[38;2;102;217;239mtranspose[0m[38;2;248;248;242m | [0m[38;2;249;38;114mreduce[0m[38;2;248;248;242m .[0m[38;2;248;248;242m[][0m[38;2;248;248;242m[][0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$item[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255ma[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$[0m[38;2;255;255;255mb[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$[0m[38;2;255;255;255mc[0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;102;217;239mtranspose[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;249;38;114mreduce[0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mitem[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m[][0m[38;2;248;248;242m;[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m;[0m
|
||||||
[38;2;248;248;242m . + [0m[38;2;255;255;255m$item[0m[38;2;248;248;242m.property[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m+[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mitem[0m[38;2;248;248;242m.[0m[38;2;248;248;242mproperty[0m
|
||||||
[38;2;248;248;242m )[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m)[0m
|
||||||
[38;2;248;248;242m;[0m
|
[38;2;248;248;242m;[0m
|
||||||
|
|
||||||
[38;2;248;248;242m. | weird [0m[38;2;248;248;242m(.a; .b; .c)[0m[38;2;248;248;242m |[0m
|
[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m weird [0m[38;2;248;248;242m([0m[38;2;248;248;242m.[0m[38;2;248;248;242ma[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242mb[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242mc[0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m
|
||||||
|
|
||||||
[38;2;248;248;242m([0m
|
[38;2;248;248;242m([0m
|
||||||
|
|
||||||
[38;2;249;38;114mif[0m[38;2;248;248;242m [0m[38;2;248;248;242m(. | [0m[38;2;102;217;239mcontains[0m[38;2;248;248;242m([0m[38;2;230;219;116m"[0m[38;2;230;219;116mnever[0m[38;2;230;219;116m"[0m[38;2;248;248;242m)[0m[38;2;248;248;242m )[0m[38;2;248;248;242m [0m[38;2;249;38;114mthen[0m
|
[38;2;249;38;114mif[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;102;217;239mcontains[0m[38;2;248;248;242m([0m[38;2;230;219;116m"[0m[38;2;230;219;116mnever[0m[38;2;230;219;116m"[0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114mthen[0m
|
||||||
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mWhy yes[0m[38;2;230;219;116m"[0m
|
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mWhy yes[0m[38;2;230;219;116m"[0m
|
||||||
[38;2;249;38;114melse[0m
|
[38;2;249;38;114melse[0m
|
||||||
[38;2;248;248;242m [0m[38;2;190;132;255m12.23[0m
|
[38;2;248;248;242m [0m[38;2;190;132;255m12.23[0m
|
||||||
[38;2;249;38;114mend[0m
|
[38;2;249;38;114mend[0m
|
||||||
|
|
||||||
[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$never[0m[38;2;248;248;242m |[0m
|
[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mnever[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m
|
||||||
|
|
||||||
[38;2;248;248;242m{[0m
|
[38;2;248;248;242m{[0m
|
||||||
[38;2;248;248;242m [0m[38;2;166;226;46mhello[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;166;226;46mhello[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;166;226;46mwhy[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mbecause[0m[38;2;230;219;116m"[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;166;226;46mwhy[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mbecause[0m[38;2;230;219;116m"[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;166;226;46mhello[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m( weird | [0m[38;2;102;217;239mascii_upcase[0m[38;2;248;248;242m )[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;166;226;46mhello[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m weird [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;102;217;239mascii_upcase[0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;166;226;46mformat_eg[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m( . | [0m[38;2;190;132;255m@json[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mMy json string [0m[38;2;190;132;255m\([0m[38;2;248;248;242m . | this | part | just | white | ascii_upcase | transpose[0m[38;2;190;132;255m)[0m[38;2;230;219;116m"[0m[38;2;248;248;242m )[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;166;226;46mformat_eg[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;190;132;255m@json[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mMy json string [0m[38;2;190;132;255m\([0m[38;2;248;248;242m . | this | part | just | white | ascii_upcase | transpose[0m[38;2;190;132;255m)[0m[38;2;230;219;116m"[0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;166;226;46mnever[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;255;255;255m$never[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;166;226;46mnever[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mnever[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mliteral_key[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m literal_value[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mliteral_key[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m literal_value[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mthis[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;190;132;255m12.1e12[0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mthis[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;190;132;255m12.1e12[0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mpart[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116malmost[0m[38;2;230;219;116m"[0m
|
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mpart[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116malmost[0m[38;2;230;219;116m"[0m
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
[38;2;248;248;242m [0m[38;2;166;226;46msimilar[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mbut not quite[0m[38;2;230;219;116m"[0m
|
[38;2;248;248;242m [0m[38;2;166;226;46msimilar[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mbut not quite[0m[38;2;230;219;116m"[0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m}[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m}[0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m}[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m}[0m
|
||||||
[38;2;248;248;242m ][0m[38;2;248;248;242m,[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m,[0m
|
||||||
[38;2;248;248;242m}[0m[38;2;248;248;242m | [0m[38;2;248;248;242m([0m
|
[38;2;248;248;242m}[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m
|
||||||
[38;2;248;248;242m [0m
|
[38;2;248;248;242m [0m
|
||||||
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m And with very basic brace matching[0m
|
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m And with very basic brace matching[0m
|
||||||
[38;2;248;248;242m [0m
|
[38;2;248;248;242m [0m
|
||||||
|
@ -47,13 +47,13 @@
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;240m][0m[38;2;248;248;242m [0m
|
[38;2;248;248;242m [0m[38;2;248;248;240m][0m[38;2;248;248;242m [0m
|
||||||
[38;2;248;248;242m [0m
|
[38;2;248;248;242m [0m
|
||||||
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m Other invalid ends[0m
|
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m Other invalid ends[0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;240m}[0m[38;2;248;248;242m [0m[38;2;248;248;240m][0m[38;2;248;248;242m )[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;240m}[0m[38;2;248;248;242m [0m[38;2;248;248;240m][0m[38;2;248;248;242m )[0m
|
||||||
|
|
||||||
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m A "valid" sequence[0m
|
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m A "valid" sequence[0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;242m{ [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m()[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;242m[][0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[][0m[38;2;248;248;242m][0m[38;2;248;248;242m ][0m[38;2;248;248;242m )[0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mgaga[0m[38;2;248;248;242m }[0m[38;2;248;248;242m ][0m[38;2;248;248;242m )[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;242m{[0m[38;2;248;248;242m [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mgaga[0m[38;2;248;248;242m [0m[38;2;248;248;242m}[0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m
|
||||||
|
|
||||||
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m A "invalid" sequence[0m
|
[38;2;248;248;242m [0m[38;2;117;113;94m#[0m[38;2;117;113;94m A "invalid" sequence[0m
|
||||||
[38;2;248;248;242m [0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;242m{ [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m()[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;242m[][0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[][0m[38;2;248;248;242m ][0m[38;2;248;248;242m [0m[38;2;248;248;240m)[0m[38;2;248;248;242m, gaga [0m[38;2;248;248;240m}[0m[38;2;248;248;242m ] )[0m
|
[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;242m{[0m[38;2;248;248;242m [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;240m)[0m[38;2;248;248;242m, gaga [0m[38;2;248;248;240m}[0m[38;2;248;248;242m ] )[0m
|
||||||
|
|
||||||
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mA string[0m[38;2;190;132;255m\[0m[38;2;190;132;255mn[0m[38;2;230;219;116m whith escaped characters [0m[38;2;190;132;255m\[0m[38;2;190;132;255m"[0m[38;2;230;219;116m because we can[0m[38;2;230;219;116m"[0m
|
[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mA string[0m[38;2;190;132;255m\[0m[38;2;190;132;255mn[0m[38;2;230;219;116m whith escaped characters [0m[38;2;190;132;255m\[0m[38;2;190;132;255m"[0m[38;2;230;219;116m because we can[0m[38;2;230;219;116m"[0m
|
||||||
[38;2;248;248;242m)[0m
|
[38;2;248;248;242m)[0m
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue