mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-20 07:04:18 +02:00
Compare commits
434 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d07fb02e86 |
||
|
|
f9fab32812 |
||
|
|
fbc7520f8c |
||
|
|
911277cd21 |
||
|
|
43157e78c7 |
||
|
|
39094354b6 |
||
|
|
1414194988 |
||
|
|
8b00800a69 |
||
|
|
5f837f4dd6 |
||
|
|
82b9c88f39 |
||
|
|
b7da9a34f3 |
||
|
|
144ea1d727 |
||
|
|
5d202fca81 |
||
|
|
fabb378368 |
||
|
|
e86ca1aa86 |
||
|
|
65ec680ea3 |
||
|
|
d15718f672 |
||
|
|
c80a935b89 |
||
|
|
21ab081dae |
||
|
|
64db5accfa |
||
|
|
8bd3a0d4aa |
||
|
|
1c68f99c2a |
||
|
|
37b0dee39f |
||
|
|
28ed785001 |
||
|
|
f8a2003bc2 |
||
|
|
df5113ddaa |
||
|
|
51593d27eb |
||
|
|
d0108b23e4 |
||
|
|
4d33e8a279 |
||
|
|
1252261d66 |
||
|
|
24e7f383b8 |
||
|
|
6c65c8f71c |
||
|
|
b8f7c0cc2d |
||
|
|
b19517097a |
||
|
|
6f91c3a765 |
||
|
|
41739c5bf9 |
||
|
|
47a936e736 |
||
|
|
593e9309b9 |
||
|
|
8b97afe7aa |
||
|
|
75e5462312 |
||
|
|
5ee1f28021 |
||
|
|
3a6fd99c85 |
||
|
|
c5d62e5987 |
||
|
|
b3cd7b1c21 |
||
|
|
e769114d8b |
||
|
|
e1213158e4 |
||
|
|
6c1d702769 |
||
|
|
d49c4a9e05 |
||
|
|
49626977ff |
||
|
|
9a83b58d51 |
||
|
|
2b127fd67e |
||
|
|
911508ce33 |
||
|
|
5b306756af |
||
|
|
abb7e8ac55 |
||
|
|
92b6c64c87 |
||
|
|
c741146db5 |
||
|
|
a74b7120bd |
||
|
|
3fa96a5bb2 |
||
|
|
7e014093cf |
||
|
|
94f9030d36 |
||
|
|
5cfb817e99 |
||
|
|
4377366c97 |
||
|
|
630b7f4423 |
||
|
|
1e87db7ab7 |
||
|
|
d1be7d6bb9 |
||
|
|
43ab2cb920 |
||
|
|
bc820c5f10 |
||
|
|
9bb95ad11d |
||
|
|
769ef4da20 |
||
|
|
8f433e7774 |
||
|
|
81a0662dbc |
||
|
|
9f43adb7f9 |
||
|
|
3d8d488f66 |
||
|
|
c6de583c46 |
||
|
|
009af7063f |
||
|
|
55f401df30 |
||
|
|
09ef7f534e |
||
|
|
120e2a92c2 |
||
|
|
09d44110f8 |
||
|
|
b9f116d629 |
||
|
|
00c7778125 |
||
|
|
826630737c |
||
|
|
dbab55a2b4 |
||
|
|
e7b434d06a |
||
|
|
fc19206029 |
||
|
|
e85de336ed |
||
|
|
909bad9c65 |
||
|
|
ceee231891 |
||
|
|
0c55a4b82a |
||
|
|
e3a06eefe3 |
||
|
|
6869837e79 |
||
|
|
ee5418b5a8 |
||
|
|
1d9153e37e |
||
|
|
d719f21f7b |
||
|
|
fb7492b0b5 |
||
|
|
d44613cf59 | ||
|
|
713f6f913c |
||
|
|
ec2daa495c |
||
|
|
fc6d644152 |
||
|
|
8995f9f07e |
||
|
|
7c245040b9 |
||
|
|
4f51ba090f | ||
|
|
60855cedcf | ||
|
|
9096d536b1 | ||
|
|
13a3d58c8b | ||
|
|
13f2c4c714 | ||
|
|
65495da5a5 | ||
|
|
f3d74850a8 |
||
|
|
290afe385d |
||
|
|
7620a67306 | ||
|
|
706c1408a7 | ||
|
|
64f90a625e | ||
|
|
83b2bdaf33 |
||
|
|
64f3fbbbc6 | ||
|
|
86293c8723 | ||
|
|
de1684edb6 | ||
|
|
3a14232105 |
||
|
|
9957dcea74 |
||
|
|
7633e46541 |
||
|
|
ee6d2418f1 |
||
|
|
c8f2408373 |
||
|
|
17c08f0261 |
||
|
|
ad69985e42 |
||
|
|
2e731d7d17 |
||
|
|
d04e671c46 |
||
|
|
5bdcd6ae9c |
||
|
|
efbdebb426 |
||
|
|
a82bc4b8d2 |
||
|
|
9e1489bbc5 |
||
|
|
1764cee1bd |
||
|
|
4d2fc26dbe |
||
|
|
7bd5ac5664 |
||
|
|
de9cf431a8 |
||
|
|
45b3db6dc4 |
||
|
|
8c6754ae78 |
||
|
|
94d56c01a7 |
||
|
|
84621cda81 |
||
|
|
fb755ba8dc | ||
|
|
c9ec88354a | ||
|
|
74d1ade884 | ||
|
|
b07a6728f3 | ||
|
|
2fa1994c76 | ||
|
|
72362769de |
||
|
|
0c8cb42664 |
||
|
|
76d9d0bbda |
||
|
|
c077ff05e4 |
||
|
|
b5f2c6ff39 | ||
|
|
e75f814294 | ||
|
|
fc8266d1c9 |
||
|
|
76bb4e72bc | ||
|
|
7c371a6852 | ||
|
|
b10f8a9485 |
||
|
|
ab5148f237 |
||
|
|
86c850282e |
||
|
|
357f0e7e71 | ||
|
|
7ded00eda0 |
||
|
|
6f72ce778d |
||
|
|
2ebf86b265 | ||
|
|
efe1b57696 | ||
|
|
0c93a4cfe9 | ||
|
|
99c86a4aff |
||
|
|
c44faf2b09 | ||
|
|
3f528ba5cb | ||
|
|
d702a1fab6 | ||
|
|
3e7dfb3c71 | ||
|
|
fb89303e85 | ||
|
|
7338933de5 | ||
|
|
afbdfa6746 | ||
|
|
33f0f108d8 | ||
|
|
026ae72742 |
||
|
|
f45a502383 |
||
|
|
f4a94112f4 |
||
|
|
aa67a59d5b |
||
|
|
65cbe80e9d |
||
|
|
a6023a5234 |
||
|
|
db492bc0cf |
||
|
|
e94ee92db4 |
||
|
|
b369678794 |
||
|
|
97b4395e25 |
||
|
|
543fa2290d |
||
|
|
abbbbf01ed | ||
|
|
4a5410fd0b | ||
|
|
0f82aa5950 |
||
|
|
d138ce23e4 |
||
|
|
750ebf653c | ||
|
|
62b6405685 | ||
|
|
3629002d5c | ||
|
|
6e09af2f8b | ||
|
|
786933e507 |
||
|
|
5a1a21b8ab |
||
|
|
716ff15779 |
||
|
|
cc22760e5a | ||
|
|
cab6666ffb | ||
|
|
d5dbe3084b |
||
|
|
2177297871 |
||
|
|
3b4451f8fa |
||
|
|
ef7432c762 | ||
|
|
b5443b75b6 |
||
|
|
77a6d48381 | ||
|
|
71b6e037a7 | ||
|
|
04804ae650 |
||
|
|
4a9763a783 | ||
|
|
82e4b97f92 | ||
|
|
d502fcdd6d |
||
|
|
5262234846 | ||
|
|
fd307757d7 | ||
|
|
405a2c9c8f | ||
|
|
9e97d454f0 | ||
|
|
84277cc31e |
||
|
|
eadfe97335 | ||
|
|
47a4d6d7ca | ||
|
|
b09f250370 |
||
|
|
8228d31451 | ||
|
|
81c30bfb77 | ||
|
|
20b7c5c33f | ||
|
|
0ebf5aca87 |
||
|
|
d1c17f2eb6 | ||
|
|
c155b6f98d | ||
|
|
2007aa0c32 | ||
|
|
e722b4f92e |
||
|
|
7e44095e63 |
||
|
|
ed3d1ea9af | ||
|
|
2b1167f19b | ||
|
|
fb40974616 | ||
|
|
0ef31d1f17 | ||
|
|
0d1300c218 |
||
|
|
bbac67dd29 |
||
|
|
ca844b35e9 | ||
|
|
abeb5c3757 | ||
|
|
66255219fa | ||
|
|
6508ec6ac6 | ||
|
|
00ec281b9b | ||
|
|
f8785fbc3e |
||
|
|
876f390ac6 | ||
|
|
b726840246 | ||
|
|
69f4808214 | ||
|
|
25d771778a | ||
|
|
1a3624d011 |
||
|
|
72d753c52b |
||
|
|
023a9d2079 |
||
|
|
02a395ad13 |
||
|
|
2684727eaa |
||
|
|
0fbc95cb71 |
||
|
|
02c2d6709a |
||
|
|
9c90f8ba7a |
||
|
|
746d4dadda |
||
|
|
3beed4ad48 |
||
|
|
cea3bcdc3f |
||
|
|
c6bc6f8183 |
||
|
|
618ecaf71f | ||
|
|
371f1d2ace | ||
|
|
6823e271a0 | ||
|
|
fc726011d1 | ||
|
|
8833b6b401 | ||
|
|
ee0d32d3de | ||
|
|
6d77483ad5 | ||
|
|
5ad7dcfa34 | ||
|
|
6300b6a24f |
||
|
|
84227937ae | ||
|
|
0ebd727a32 | ||
|
|
f7da0b028d | ||
|
|
62725fca5f | ||
|
|
006ec6f3c0 | ||
|
|
7bd08e35d1 | ||
|
|
32cc6d5893 |
||
|
|
3c92cff865 |
||
|
|
9eca2fe5db | ||
|
|
3f49100307 |
||
|
|
e84fce6f90 |
||
|
|
b0449dc6bf |
||
|
|
07dc5c6bf4 | ||
|
|
85ab38d892 | ||
|
|
7aa111e727 | ||
|
|
b359fd8e5b | ||
|
|
4376162914 | ||
|
|
ffc30d1243 | ||
|
|
a811788b31 |
||
|
|
1471b1d97b |
||
|
|
3ac087ae41 |
||
|
|
b216a63c64 |
||
|
|
22baa455b5 |
||
|
|
743998a75f |
||
|
|
808ad7ff30 |
||
|
|
e8b1c9e801 |
||
|
|
62e82461cb |
||
|
|
cdbca5c53b |
||
|
|
49f2f8a3dd | ||
|
|
8051c3169a |
||
|
|
4e0d497347 |
||
|
|
a4ac910e86 | ||
|
|
9ac67ec562 | ||
|
|
d5d3d20451 |
||
|
|
123c809630 |
||
|
|
d1d36a961a |
||
|
|
e7c5daa9b7 |
||
|
|
187214e39e |
||
|
|
a5aa822d12 |
||
|
|
767b62d493 |
||
|
|
feb20d8c5c | ||
|
|
22693c09ba | ||
|
|
06f771f64d | ||
|
|
a72ba4a7a9 |
||
|
|
e32ce77c0c | ||
|
|
b6342633cd |
||
|
|
25f736564d |
||
|
|
3223fec5ee | ||
|
|
4d4c7b6a5d |
||
|
|
388deac079 |
||
|
|
a92e974ac2 |
||
|
|
0fce79f8ed |
||
|
|
42816d3d09 |
||
|
|
2a304b17f2 |
||
|
|
07c715656f |
||
|
|
7232bdd898 |
||
|
|
49f151a605 |
||
|
|
071680800a |
||
|
|
2a6d09554b |
||
|
|
663926cc0e |
||
|
|
cb57ac1d5d | ||
|
|
ca15279386 | ||
|
|
978debe6ae | ||
|
|
4e1876ac88 | ||
|
|
1d529de260 |
||
|
|
32ff6c0e3c |
||
|
|
2e1e0006b2 | ||
|
|
5d8448c442 | ||
|
|
127124eded | ||
|
|
e5d359b935 | ||
|
|
1db4b409bc | ||
|
|
0c75e34007 |
||
|
|
c632407a25 | ||
|
|
b1a08af00a | ||
|
|
1ebb80bb14 |
||
|
|
931a5fc13c | ||
|
|
c33c146a17 | ||
|
|
6d07e78613 |
||
|
|
dff8da40a5 | ||
|
|
5ca8461dd8 |
||
|
|
e24d86c900 | ||
|
|
ddeb81b249 | ||
|
|
e35dd5e30c | ||
|
|
9236f0d4a7 |
||
|
|
151d014f5d |
||
|
|
30b7c5febc |
||
|
|
d2b5614f1a | ||
|
|
95fdddd68e | ||
|
|
b0153c7e4c |
||
|
|
5a9cde733b |
||
|
|
2a2a4cdda5 | ||
|
|
fd9e36277f | ||
|
|
504639b6a6 |
||
|
|
4f078e9511 | ||
|
|
4c413d7e40 |
||
|
|
dc6af5ad6c | ||
|
|
1b8062e0af | ||
|
|
bc04e1ac00 | ||
|
|
be79c924f6 | ||
|
|
f757389df9 | ||
|
|
baffd1ea4d | ||
|
|
09d73c2f03 |
||
|
|
d9b5e49551 | ||
|
|
a67e7d184a | ||
|
|
e507343f5d |
||
|
|
5a2d054ace | ||
|
|
3597e344cb | ||
|
|
590bb44585 |
||
|
|
e8dcde4955 |
||
|
|
55079b8e77 | ||
|
|
64e2c7b799 |
||
|
|
0b277c7515 |
||
|
|
95e6b0520f |
||
|
|
d446e50813 |
||
|
|
509d450585 | ||
|
|
2f2fe4dd92 | ||
|
|
a59182b6cc | ||
|
|
ed1ba6beed | ||
|
|
06537c7218 |
||
|
|
a45c19647f |
||
|
|
5dd9457cf4 |
||
|
|
39b1d19d66 |
||
|
|
a9fa0c7f1a | ||
|
|
d68088f94f | ||
|
|
ad92ce6765 | ||
|
|
0c24ea0cd4 | ||
|
|
2030d305c6 | ||
|
|
3a5c0ce59b |
||
|
|
a594b0013e |
||
|
|
54464da510 |
||
|
|
3e3403806c | ||
|
|
04e61080d2 | ||
|
|
2812a0ce99 | ||
|
|
ed3e4baac5 |
||
|
|
055758dba8 |
||
|
|
623cf67d2a |
||
|
|
7a3a565427 | ||
|
|
22255bf80c |
||
|
|
bcdfd5e9a8 | ||
|
|
7b3f7a6470 | ||
|
|
23d7301b53 |
||
|
|
95fab99e82 | ||
|
|
6719acb4f2 | ||
|
|
0d00e60af0 | ||
|
|
6ad91a440c |
||
|
|
94c0e99693 | ||
|
|
74c37193e3 | ||
|
|
7460cdac6d | ||
|
|
07e2ee86f5 |
||
|
|
6810693655 | ||
|
|
7f8cbb01e5 | ||
|
|
a9f2a4c132 | ||
|
|
15583fc6d0 | ||
|
|
ece174e1fc | ||
|
|
ee65478930 | ||
|
|
3d77af1fa3 | ||
|
|
8dc480e10f |
||
|
|
79d5f9f4ed | ||
|
|
9ffe459c1f |
||
|
|
2d0d621e3d | ||
|
|
8ae19fd32e | ||
|
|
70cea2245d | ||
|
|
135698b5d9 | ||
|
|
4b0c947030 | ||
|
|
ee7c5c1e61 | ||
|
|
535b75dc83 | ||
|
|
7a809d8bb8 |
||
|
|
4304e2e610 | ||
|
|
5215493720 | ||
|
|
410e96ef7f | ||
|
|
0879116a45 | ||
|
|
a052e48e50 | ||
|
|
87ddb14da1 |
||
|
|
495c3e97d9 | ||
|
|
265ed66d7d | ||
|
|
91c7a412f0 |
79 changed files with 7549 additions and 2607 deletions
|
|
@ -1,36 +0,0 @@
|
||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
docker:
|
|
||||||
- image: rust:1.31
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
# Load cargo target from cache if possible.
|
|
||||||
# Multiple caches are used to increase the chance of a cache hit.
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-cargo-cache-{{ arch }}-{{ .Branch }}
|
|
||||||
- v1-cargo-cache-{{ arch }}
|
|
||||||
|
|
||||||
# Show versions
|
|
||||||
- run: rustc --version && cargo --version
|
|
||||||
|
|
||||||
# Build
|
|
||||||
- run: cargo build
|
|
||||||
- run: cargo build --features logging
|
|
||||||
- run: cargo build --no-default-features
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
- run: cargo test
|
|
||||||
- run: cargo test --no-default-features
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
key: v1-cargo-cache-{{ arch }}-{{ .Branch }}
|
|
||||||
paths:
|
|
||||||
- target
|
|
||||||
- /usr/local/cargo
|
|
||||||
- save_cache:
|
|
||||||
key: v1-cargo-cache-{{ arch }}
|
|
||||||
paths:
|
|
||||||
- target
|
|
||||||
- /usr/local/cargo
|
|
||||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.md eol=lf
|
||||||
|
*.expected eol=lf
|
||||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
92
.github/workflows/ci.yml
vendored
Normal file
92
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "v*.x"
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 3 * * 2'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: run tests
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest, windows-latest, windows-11-arm]
|
||||||
|
toolchain: [stable, 1.88.0] # MSRV
|
||||||
|
include:
|
||||||
|
- platform: windows-latest
|
||||||
|
exe_suffix: .exe
|
||||||
|
- platform: windows-11-arm
|
||||||
|
exe_suffix: .exe
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
- run: mkdir artifacts
|
||||||
|
- name: Build with default features
|
||||||
|
run: |
|
||||||
|
cargo build --locked
|
||||||
|
cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}}
|
||||||
|
- name: Build with logging and Rustls with webpki roots
|
||||||
|
run: |
|
||||||
|
cargo build --locked --features logging,rustls-with-webpki-roots --no-default-features
|
||||||
|
cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-logging-rustls-webpki${{ matrix.exe_suffix}}
|
||||||
|
- name: Build with native TLS backend
|
||||||
|
run: |
|
||||||
|
# expects runners have the proper Native SSL library
|
||||||
|
cargo build --locked --features native-tls --no-default-features
|
||||||
|
cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}}
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }}
|
||||||
|
path: artifacts/
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --locked -- --test-threads 1
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
name: run clippy lints
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: 1.88.0 # MSRV
|
||||||
|
components: clippy
|
||||||
|
- name: run clippy lints
|
||||||
|
run: cargo clippy --locked --all-targets --features logging
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: run rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
components: rustfmt
|
||||||
|
- name: run rustfmt
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
docs:
|
||||||
|
name: build docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- run: ./scripts/get-mdbook.sh
|
||||||
|
- name: Setup toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked
|
||||||
|
- name: Ensure that docs can be built
|
||||||
|
run: ./mdbook build docs
|
||||||
|
- name: Generate usage string
|
||||||
|
run: cargo run --locked -- --help > docs/src/usage-actual.txt
|
||||||
|
- name: Ensure that usage string is up to date
|
||||||
|
run: diff docs/src/usage{,-actual}.txt
|
||||||
169
.github/workflows/release.yml
vendored
Normal file
169
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*" # push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Create release for tag
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
source ./scripts/upload-asset.sh
|
||||||
|
# Create: <token> <repo> <tag>
|
||||||
|
create_release ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} "Tealdeer version ${GITHUB_REF#refs/*/v}.\n\nFor the full changelog, see https://github.com/tealdeer-rs/tealdeer/blob/main/CHANGELOG.md.\n\nBinaries were generated automatically in CI, and are therefore unsigned. For a fully trusted release, please build from source."
|
||||||
|
|
||||||
|
upload-completions:
|
||||||
|
needs:
|
||||||
|
- create-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target: ["bash", "fish", "zsh"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Upload completion
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
source ./scripts/upload-asset.sh
|
||||||
|
# Upload: <token> <repo> <tag> <file> <name>
|
||||||
|
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} completion/${{ matrix.target }}_tealdeer completions_${{ matrix.target }}
|
||||||
|
|
||||||
|
upload-license:
|
||||||
|
needs:
|
||||||
|
- create-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target: ["MIT", "APACHE"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Upload license
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
source ./scripts/upload-asset.sh
|
||||||
|
# Upload: <token> <repo> <tag> <file> <name>
|
||||||
|
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} LICENSE-${{ matrix.target }} LICENSE-${{ matrix.target }}.txt
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: "x86_64"
|
||||||
|
libc: "musl"
|
||||||
|
- arch: "aarch64"
|
||||||
|
libc: "musl"
|
||||||
|
- arch: "i686"
|
||||||
|
libc: "musl"
|
||||||
|
- arch: "armv7"
|
||||||
|
libc: "musleabihf"
|
||||||
|
- arch: "arm"
|
||||||
|
libc: "musleabi"
|
||||||
|
- arch: "arm"
|
||||||
|
libc: "musleabihf"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Pull Docker image
|
||||||
|
run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }}
|
||||||
|
- name: Build in Docker
|
||||||
|
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} cargo build --locked --release
|
||||||
|
- name: Strip binary
|
||||||
|
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}"
|
||||||
|
path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr"
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: "x86_64"
|
||||||
|
- arch: "aarch64"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Setup toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
targets: "${{ matrix.arch }}-apple-darwin"
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked --release --target ${{ matrix.arch }}-apple-darwin
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: "tealdeer-macos-${{ matrix.arch }}"
|
||||||
|
path: "target/${{ matrix.arch }}-apple-darwin/release/tldr"
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: "x86_64"
|
||||||
|
os: windows-latest
|
||||||
|
- arch: "aarch64"
|
||||||
|
os: windows-11-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Setup toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
targets: "${{ matrix.arch }}-pc-windows-msvc"
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked --release --target ${{ matrix.arch }}-pc-windows-msvc
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: "tealdeer-windows-${{ matrix.arch }}-msvc"
|
||||||
|
path: "target/${{ matrix.arch }}-pc-windows-msvc/release/tldr.exe"
|
||||||
|
|
||||||
|
upload-release:
|
||||||
|
needs:
|
||||||
|
- create-release
|
||||||
|
- build-linux
|
||||||
|
- build-macos
|
||||||
|
- build-windows
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- linux-x86_64-musl
|
||||||
|
- linux-aarch64-musl
|
||||||
|
- linux-i686-musl
|
||||||
|
- linux-armv7-musleabihf
|
||||||
|
- linux-arm-musleabi
|
||||||
|
- linux-arm-musleabihf
|
||||||
|
- macos-x86_64
|
||||||
|
- macos-aarch64
|
||||||
|
- windows-x86_64-msvc
|
||||||
|
- windows-aarch64-msvc
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/download-artifact@v8
|
||||||
|
- name: Upload binary
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
source ./scripts/upload-asset.sh
|
||||||
|
|
||||||
|
# Move/rename file
|
||||||
|
mkdir out && cd out
|
||||||
|
if [[ "${{ matrix.target }}" == *windows* ]]; then
|
||||||
|
src="../tealdeer-${{ matrix.target }}/tldr.exe"
|
||||||
|
filename="tealdeer-${{ matrix.target }}.exe"
|
||||||
|
else
|
||||||
|
src="../tealdeer-${{ matrix.target }}/tldr"
|
||||||
|
filename="tealdeer-${{ matrix.target }}"
|
||||||
|
fi
|
||||||
|
cp $src $filename
|
||||||
|
|
||||||
|
# Create checksum
|
||||||
|
sha256sum "$filename" > "$filename.sha256"
|
||||||
|
|
||||||
|
# Upload: <token> <repo> <tag> <file> <name>
|
||||||
|
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename $filename
|
||||||
|
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename.sha256 $filename.sha256
|
||||||
7
.readthedocs.yaml
Normal file
7
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-26.04
|
||||||
|
commands:
|
||||||
|
- ./scripts/get-mdbook.sh
|
||||||
|
- ./mdbook build docs --dest-dir $READTHEDOCS_OUTPUT/html
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
language: rust
|
|
||||||
os: osx
|
|
||||||
rust:
|
|
||||||
- 1.31.0
|
|
||||||
- stable
|
|
||||||
cache: cargo
|
|
||||||
script: cargo test
|
|
||||||
672
CHANGELOG.md
672
CHANGELOG.md
|
|
@ -10,6 +10,480 @@ Possible log types:
|
||||||
- `[removed]` for deprecated features removed in this release.
|
- `[removed]` for deprecated features removed in this release.
|
||||||
- `[fixed]` for any bug fixes.
|
- `[fixed]` for any bug fixes.
|
||||||
- `[security]` to invite users to upgrade in case of vulnerabilities.
|
- `[security]` to invite users to upgrade in case of vulnerabilities.
|
||||||
|
- `[docs]` for documentation changes.
|
||||||
|
- `[chore]` for maintenance work.
|
||||||
|
|
||||||
|
### [v1.5.1][v1.5.1], [v1.6.2][v1.6.2], [v1.7.3][v1.7.3] (2026-01-25)
|
||||||
|
|
||||||
|
Today I am releasing three patch updates for outdated versions of tealdeer.
|
||||||
|
They are minimal patches for Linux distributions that ship old versions of
|
||||||
|
tealdeer which recently broke due to an upstream change. If you can choose
|
||||||
|
freely which version of tealdeer to use, I recommend using the latest version of
|
||||||
|
tealdeer, 1.8.1. For more details, see the "Notes to package maintainers"
|
||||||
|
section below.
|
||||||
|
|
||||||
|
All three updates contain only a single change compared to their respective
|
||||||
|
previous versions which changes the `ARCHIVE_URL` constant used for updating the
|
||||||
|
page cache. The reason for this change is that the upstream tldr-pages
|
||||||
|
repository shut down the domain that clients were previously required to use.
|
||||||
|
|
||||||
|
Note that this issue is already fixed in tealdeer 1.8.0 where we introduced a
|
||||||
|
config file option for changing the URL used at runtime. The versions 1.8.0 and
|
||||||
|
1.8.1 also use the new domain of the tldr-pages archive by default, so no action
|
||||||
|
is needed for users of those versions.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- [fixed] Update `ARCHIVE_URL`
|
||||||
|
|
||||||
|
#### Notes to package maintainers
|
||||||
|
|
||||||
|
I have _not_ updated the lockfile for any of these releases, so the locked
|
||||||
|
dependency versions are still the same as they were for the previous release in
|
||||||
|
the respective v1.x series. Updating the lockfile for tealdeer 1.5.0 to remove
|
||||||
|
any `cargo audit` warnings while also maintaining compatibility with Rust 1.54
|
||||||
|
also brings larger changes through transitive dependencies, which contradicts my
|
||||||
|
plan to make this update easy to plug into existing build pipelines.
|
||||||
|
|
||||||
|
If you want to build / distribute tealdeer v1.5.1, v1.6.2, or v1.7.3, please use
|
||||||
|
an up to date Rust toolchain to permit updates to newer versions of (transitive)
|
||||||
|
dependencies. Do not use the lockfile, instead update to the newest available
|
||||||
|
dependency versions.
|
||||||
|
|
||||||
|
For the same reason, there are no artifacts attached to the GitHub releases of
|
||||||
|
these versions.
|
||||||
|
|
||||||
|
### [v1.8.1][v1.8.1] (2025-11-11)
|
||||||
|
|
||||||
|
This patch release tweaks the enabled features for ureq, the library we use to
|
||||||
|
perform HTTP requests when updating the cache. In particular, support for socks
|
||||||
|
proxies is now enabled.
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [added] Enable ureq's socks-proxy feature ([#451])
|
||||||
|
|
||||||
|
### [v1.8.0][v1.8.0] (2025-10-03)
|
||||||
|
|
||||||
|
One year and one day have passed since tealdeer version 1.7.0 was released, so
|
||||||
|
it's time for an update! Tealdeer 1.8 comes with a complete rewrite of the page
|
||||||
|
cache and contains many long awaited improvements around it.
|
||||||
|
|
||||||
|
Firstly, tealdeer now supports language-specific downloads. This means that only
|
||||||
|
the pages matching the configured languages are downloaded when updating the
|
||||||
|
cache. The languages used for searching pages can be configured separately to
|
||||||
|
the ones used for updating, so it is possible to download pages in languages
|
||||||
|
that are not usually queried.
|
||||||
|
|
||||||
|
Next to configuring which languages are used for searching, it is now also
|
||||||
|
possible to specify which platforms are used in the config file. Importantly,
|
||||||
|
the default behavior for page search has changed so that all platforms are
|
||||||
|
searched if no page is found for the platform that tealdeer is running on. To
|
||||||
|
restore the behavior of tealdeer 1.7, users should set
|
||||||
|
```toml
|
||||||
|
[search]
|
||||||
|
platforms = ["current", "common"]
|
||||||
|
```
|
||||||
|
in their config file.
|
||||||
|
|
||||||
|
Coming back to updating, the default build configuration of tealdeer now
|
||||||
|
includes multiple TLS backends. This means that tealdeer does not have to be
|
||||||
|
rebuilt to try out a different TLS backend. The used backend can be chosen in
|
||||||
|
the config file. By default, tealdeer comes with support for rustls using webpki
|
||||||
|
certificates or system certificates. Native TLS is supported, but not enabled by
|
||||||
|
default to avoid build troubles with OpenSSL and musl.
|
||||||
|
|
||||||
|
For details, please refer to the [user documentation].
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [added] Resolve paths in config `[directories]` relative to the config directory ([#306])
|
||||||
|
- [added] Add `common` platform to CLI ([#401])
|
||||||
|
- [added] Add configuration option for `archive_source` ([#337])
|
||||||
|
- [added] Allows configuring TLS backend ([#386])
|
||||||
|
- [added] Add args: `--edit-page` and `--edit-patch` ([#388])
|
||||||
|
- [added] Add an option to specify a custom config file to be used ([#422])
|
||||||
|
- [added] Upload binaries from build step as artifact ([#423])
|
||||||
|
- [added] Add `search.languages` and `updates.download_languages` settings ([#430])
|
||||||
|
- [added] Add `search.platforms` config option and search all platforms by default ([#435])
|
||||||
|
- [added] Add `display.show_title` option to display command titles in output ([#439])
|
||||||
|
- [chore] Various test improvements ([#399])
|
||||||
|
- [chore] Add tests for osx/macos alias ([#407])
|
||||||
|
- [chore] Move most of `main` to `try_main` ([#400])
|
||||||
|
- [chore] Only create a single temporary directory in integration tests ([#411])
|
||||||
|
- [chore] Replace reqwest with ureq ([#417])
|
||||||
|
- [chore] Introduce Language struct ([#425])
|
||||||
|
- [chore] Cache rewrite ([#416])
|
||||||
|
- [chore] Allow references in `Config` ([#429])
|
||||||
|
- [docs] Highlight code examples in user docs ([#440])
|
||||||
|
- [removed] Remove native-tls from default feature set ([#436])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Christoph Loy][@beatbrot]
|
||||||
|
- [Erick Guan][@erickguan]
|
||||||
|
- [@MHS-0][@MHS-0]
|
||||||
|
- [Matěj Kafka][@MatejKafka]
|
||||||
|
- [Nachiket Kanore][@nachiketkanore]
|
||||||
|
- [Niklas Mohrin][@niklasmohrin]
|
||||||
|
- [Predrag Minic][@mipedja]
|
||||||
|
- [@hex1c][@hex1c]
|
||||||
|
- [lyj][@lengyijun]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
#### Notes to package maintainers
|
||||||
|
|
||||||
|
1. The MSRV has been bumped to 1.85.
|
||||||
|
2. Consider whether you want to include the `native-tls` feature in your build
|
||||||
|
of tealdeer. The feature is disabled for the binaries in the GitHub release
|
||||||
|
because we target musl, but it might work out of the box for your
|
||||||
|
distribution.
|
||||||
|
3. We have added the `ignore-online-tests` feature to automatically mark all
|
||||||
|
tests that require an internet connection as skipped, so you can use this
|
||||||
|
feature instead of maintaining a list of these tests yourself.
|
||||||
|
|
||||||
|
### [v1.7.2][v1.7.2] (2025-03-18)
|
||||||
|
|
||||||
|
This patch release updates the `zip` dependency to mitigate a potential security
|
||||||
|
vulnerability. A successful attack against tealdeer users would require
|
||||||
|
manipulation of the tldr pages archive downloaded during an update. As the
|
||||||
|
archive is downloaded from a trusted source (the tldr-pages organization), it
|
||||||
|
seems very unlikely that running a version of tealdeer prior to 1.7.2 poses a
|
||||||
|
security risk. Nevertheless, it cannot hurt to rule out any chance of an attack
|
||||||
|
by updating tealdeer to version 1.7.2.
|
||||||
|
|
||||||
|
For more details, please see https://github.com/advisories/GHSA-94vh-gphv-8pm8.
|
||||||
|
|
||||||
|
- [security] Require `zip >= 2.3.0`
|
||||||
|
- [chore] Run CI on backport branches and on dispatch
|
||||||
|
|
||||||
|
### [v1.7.1][v1.7.1] (2024-11-14)
|
||||||
|
|
||||||
|
This patch release updates the `yansi` dependency to version 1, so that the
|
||||||
|
previous versions of `yansi` can be removed from the package sets of Linux
|
||||||
|
distributions. This change should not impact the behavior of tealdeer.
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Blair Noctis][@nc7s]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
### [v1.7.0][v1.7.0] (2024-10-02)
|
||||||
|
|
||||||
|
It's been 24 months since the last release, time for tealdeer 1.7.0! Thanks to
|
||||||
|
16 individual contributors, a few nice changes and features are included in
|
||||||
|
this release.
|
||||||
|
|
||||||
|
One change is that you can **query multiple platforms at once**. For example:
|
||||||
|
|
||||||
|
tldr --platform openbsd --platform linux df
|
||||||
|
|
||||||
|
This will show the `df` page for OpenBSD (if available), followed by Linux (if
|
||||||
|
available), with fallback to the current platform on which tealdeer runs.
|
||||||
|
|
||||||
|
What's that `openbsd` thing up there? Yes, there's now **support for the BSD
|
||||||
|
platforms `freebsd`, `netbsd` and `openbsd`**.
|
||||||
|
|
||||||
|
And since we're already talking about platform support: Our **binary releases
|
||||||
|
now include builds for ARM64 (aka `aarch64`) on macOS (Apple Silicon, M1/M2/M3)
|
||||||
|
and Linux**. _(Keep in mind that binary releases are generated in CI and are
|
||||||
|
unsigned. For a trusted build, please compile from source.)_
|
||||||
|
|
||||||
|
There's also a breaking change for the folks using [custom pages and
|
||||||
|
patches](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html): These
|
||||||
|
files now use a `.md` extension. Old files will continue to work, but will
|
||||||
|
result a deprecation warning being printed when used.
|
||||||
|
|
||||||
|
On a personal note, this will be the last release from me
|
||||||
|
([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For
|
||||||
|
details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376).
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [added] Allow querying multiple platforms ([#300])
|
||||||
|
- [added] Add BSD platform support ([#354])
|
||||||
|
- [added] Allow building with native-tls in addition to rustls ([#303])
|
||||||
|
- [changed] Change custom page files to use a `.md` file extension ([#322])
|
||||||
|
- [changed] Update to clap v4 for doing command line parsing ([#298])
|
||||||
|
- [changed] Performance optimization in LineIterator ([#314])
|
||||||
|
- [changed] Performance optimizations by tweaking Cargo flags ([#355])
|
||||||
|
- [changed] Include completions in published crate ([#333])
|
||||||
|
- [changed] Minimal supported Rust version is now 1.75 ([#298])
|
||||||
|
- [fixed] Fix bash/zsh/fish completions when cache is empty ([#327], [#331])
|
||||||
|
- [docs] Publish docs only when tagging a release ([#362])
|
||||||
|
- [docs] List Scoop and Debian packages ([#305], [#315])
|
||||||
|
- [docs] Add "Tips and Tricks" chapter to user manual ([#342])
|
||||||
|
- [docs] Various docs improvements ([#293])
|
||||||
|
- [chore] Improvements to CI workflows ([#324])
|
||||||
|
- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336])
|
||||||
|
- [chore] Dependency updates
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Adam Henley][@adamazing]
|
||||||
|
- [Andrea Frigido][@frisoft]
|
||||||
|
- [Blair Noctis][@nc7s]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Felix Yan][@felixonmars]
|
||||||
|
- [Iliia Maleki][@iliya-malecki]
|
||||||
|
- [JJ Style][@jj-style]
|
||||||
|
- [K.B.Dharun Krishna][@kbdharun]
|
||||||
|
- [Linus Walker][@Walker-00]
|
||||||
|
- [Mohit Raj][@agrmohit]
|
||||||
|
- [Nicolai Fröhlich][@nifr]
|
||||||
|
- [Niklas Mohrin][@niklasmohrin]
|
||||||
|
- [@qknogxxb][@qknogxxb]
|
||||||
|
- [@tveness][@tveness]
|
||||||
|
- [Y.D.X.][@YDX-2147483647]
|
||||||
|
- [Zacchary Dempsey-Plante][@zedseven]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.6.1][v1.6.1] (2022-10-24)
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [fixed] Fix path source for custom pages dir ([#297])
|
||||||
|
- [chore] Update dependendencies ([#299])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Cyrus Yip][@CyrusYip]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.6.0][v1.6.0] (2022-10-02)
|
||||||
|
|
||||||
|
It's been 9 months since the last release already! This is not a huge update
|
||||||
|
feature-wise, but it still contains a few nice new improvements and a few
|
||||||
|
bugfixes, contributed by 11 different people. The most important new feature is
|
||||||
|
probably the option to override the cache directory through the config file.
|
||||||
|
The `TEALDEER_CACHE_DIR` env variable is now deprecated.
|
||||||
|
|
||||||
|
A note to packagers: Shell completions have been moved to the `completion/`
|
||||||
|
subdirectory! Packaging scripts might need to be updated.
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [added] Allow overriding cache directory through config ([#276])
|
||||||
|
- [added] Add `--no-auto-update` CLI flag ([#257])
|
||||||
|
- [added] Show note about auto-updates when cache is missing ([#254])
|
||||||
|
- [added] Add support for android platform ([#274])
|
||||||
|
- [added] Add custom pages to list output ([#285])
|
||||||
|
- [fixed] Cache: Return error if HTTP client cannot be created ([#247])
|
||||||
|
- [fixed] Handle cache download errors ([#253])
|
||||||
|
- [fixed] Do not page output of `tldr --update` ([#231])
|
||||||
|
- [fixed] Create macOS release builds with bundled root certificates ([#272])
|
||||||
|
- [fixed] Clean up and fix shell completions ([#262])
|
||||||
|
- [deprecated] The `TEALDEER_CACHE_DIR` env variable is now deprecated ([#276])
|
||||||
|
- [removed] The `--config-path` command was removed, use `--show-paths` instead ([#290])
|
||||||
|
- [removed] The `-o/--os` command was removed, use `-p/--platform` instead ([#290])
|
||||||
|
- [removed] The `-m/--markdown` command was removed, use `-r/--raw` instead ([#290])
|
||||||
|
- [chore] Move shell completion scripts to their own directory ([#259])
|
||||||
|
- [chore] Update dependencies ([#271], [#287], [#291])
|
||||||
|
- [chore] Use anyhow for error handling ([#249])
|
||||||
|
- [chore] Switch to Rust 2021 edition ([#284])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [@bagohart][@bagohart]
|
||||||
|
- [@cyqsimon][@cyqsimon]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Danny Mösch][@SimplyDanny]
|
||||||
|
- [Evan Lloyd New-Schmidt][@newsch]
|
||||||
|
- [Hans Gaiser][@hgaiser]
|
||||||
|
- [Kian-Meng Ang][@kianmeng]
|
||||||
|
- [Marcin Puc][@tranzystorek-io]
|
||||||
|
- [Niklas Mohrin][@niklasmohrin]
|
||||||
|
- [Olav de Haas][@Olavhaasie]
|
||||||
|
- [Simon Perdrisat][@gagarine]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.5.0][v1.5.0] (2021-12-31)
|
||||||
|
|
||||||
|
This is quite a big release with many new features. In the 15 months since the
|
||||||
|
last release, 59 pull requests from 16 different contributors were merged!
|
||||||
|
|
||||||
|
The highlights:
|
||||||
|
|
||||||
|
- **Custom pages and patches**: You can now create your own local-only tldr
|
||||||
|
pages. But not just that, you can also extend existing upstream pages with
|
||||||
|
your own examples. For more details, see
|
||||||
|
[the docs](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html).
|
||||||
|
- **Change argument parsing from docopt to clap**: We replaced docopt.rs as
|
||||||
|
argument parsing library with clap v3, resulting in almost 1 MiB smaller
|
||||||
|
binaries and a 22% speed increase when rendering a tldr page.
|
||||||
|
- **Multi-language support**: You can now override the language with `-L/--language`.
|
||||||
|
- **A new `--show-paths` command**: By running `tldr --show-paths`, you can list
|
||||||
|
the currently used config dir, cache dir, upstream pages dir and custom pages dir.
|
||||||
|
- **Compliance with the tldr client spec v1.5**: We renamed `-o/--os` to
|
||||||
|
`-p/--platform` and implemented transparent lowercasing of the page names.
|
||||||
|
- **Docs**: The README based documentation has reached its limits. There are
|
||||||
|
now new mdbook based docs over at
|
||||||
|
[tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/), we hope these
|
||||||
|
make using tealdeer easier. Of course, documentation improvements are
|
||||||
|
welcome! Also, if you're confused about how to use a certain feature, feel
|
||||||
|
free to open an issue, this way we can improve the docs.
|
||||||
|
|
||||||
|
Note that the MSRV (Minimal Supported Rust Version) of the project
|
||||||
|
[changed][i190]:
|
||||||
|
|
||||||
|
> When publishing a tealdeer release, the Rust version required to build it
|
||||||
|
> should be stable for at least a month.
|
||||||
|
|
||||||
|
#### Changes:
|
||||||
|
|
||||||
|
- [added] Support custom pages and patches ([#142][i142])
|
||||||
|
- [added] Multi-language support ([#125][i125], [#161][i161])
|
||||||
|
- [added] Add support for ANSI code and RGB colors ([#148][i148])
|
||||||
|
- [added] Implement new `--show-paths` command ([#162][i162])
|
||||||
|
- [added] Support for italic text styling ([#197][i197])
|
||||||
|
- [added] Allow SunOS platform override ([#176][i176])
|
||||||
|
- [added] Automatically lowercase page names before lookup ([#227][i227])
|
||||||
|
- [added] Add "macos" alias for "osx" ([#215][i215])
|
||||||
|
- [fixed] Consider only standalone command names for styling ([#157][i157])
|
||||||
|
- [fixed] Fixed and improved zsh completions ([#168][i168])
|
||||||
|
- [fixed] Create cache directory path if it does not exist ([#174][i174])
|
||||||
|
- [fixed] Use default style if user-defined style is missing ([#210][i210])
|
||||||
|
- [changed] Switch from docopt to clap for argument parsing ([#108][i108])
|
||||||
|
- [changed] Switch from OpenSSL to Rustls ([#187][i187])
|
||||||
|
- [changed] Performance improvements ([#187][i187])
|
||||||
|
- [changed] Send all progress logging messages to stderr ([#171][i171])
|
||||||
|
- [changed] Rename `-o/--os` to `-p/--platform` ([#217][i217])
|
||||||
|
- [changed] Rename `-m/--markdown` to `-r/--raw` ([#108][i108])
|
||||||
|
- [deprecated] The `--config-path` command is deprecated, use `--show-paths` instead ([#162][i162])
|
||||||
|
- [deprecated] The `-o/--os` command is deprecated, use `-p/--platform` instead ([#217][i217])
|
||||||
|
- [deprecated] The `-m/--markdown` command is deprecated, use `-r/--raw` instead ([#108][i108])
|
||||||
|
- [docs] New docs at [tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/)
|
||||||
|
- [docs] Add comparative benchmarks with hyperfine ([#163][i163], [README](https://github.com/tealdeer-rs/tealdeer#goals))
|
||||||
|
- [chore] Download tldr pages archive from their website, not from GitHub ([#213][i213])
|
||||||
|
- [chore] Bump MSRV to 1.54 and change MSRV policy ([#190][i190])
|
||||||
|
- [chore] The `master` branch was renamed to `main`
|
||||||
|
- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240])
|
||||||
|
- [chore] Update all dependencies
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [@bl-ue][@bl-ue]
|
||||||
|
- [Cameron Tod][@cam8001]
|
||||||
|
- [Dalton][@dmaahs2017]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Danny Mösch][@SimplyDanny]
|
||||||
|
- [Marcin Puc][@tranzystorek-io]
|
||||||
|
- [Michael Cho][@cho-m]
|
||||||
|
- [MS_Y][@black7375]
|
||||||
|
- [Niklas Mohrin][@niklasmohrin]
|
||||||
|
- [Rithvik Vibhu][@rithvikvibhu]
|
||||||
|
- [rnd][@0ndorio]
|
||||||
|
- [Sondre Nilsen][@sondr3]
|
||||||
|
- [Tomás Farías Santana][@tomasfarias]
|
||||||
|
- [Tsvetomir Bonev][@invakid404]
|
||||||
|
- [@tveness][@tveness]
|
||||||
|
- [ギャラ][@laxect]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
Last but not least, [Niklas Mohrin][@niklasmohrin] has joined the project as
|
||||||
|
co-maintainer. Thank you for your help!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.4.1][v1.4.1] (2020-09-04)
|
||||||
|
|
||||||
|
- [fixed] Syntax error in zsh completion file ([#138][i138])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Bruno A. Muciño][@mucinoab]
|
||||||
|
- [Francesco][@BachoSeven]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.4.0][v1.4.0] (2020-09-03)
|
||||||
|
|
||||||
|
- [added] Configurable automatic cache updates ([#115][i115])
|
||||||
|
- [added] Improved color detection and support for `--color` argument and
|
||||||
|
`NO_COLOR` env variable ([#111][i111])
|
||||||
|
- [changed] Make `--list` option comply with official spec ([#112][i112])
|
||||||
|
- [changed] Move cache age warning to stderr ([#113][i113])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Atul Bhosale][@Atul9]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Danny Mösch][@SimplyDanny]
|
||||||
|
- [Ilaï Deutel][@ilai-deutel]
|
||||||
|
- [Kornel][@kornelski]
|
||||||
|
- [@LovecraftianHorror][@LovecraftianHorror]
|
||||||
|
- [@michaeldel][@michaeldel]
|
||||||
|
- [Niklas Mohrin][@niklasmohrin]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.3.0][v1.3.0] (2020-02-28)
|
||||||
|
|
||||||
|
- [added] New config option for compact output mode ([#89][i89])
|
||||||
|
- [added] New -m/--markdown parameter for raw rendering ([#95][i95])
|
||||||
|
- [added] Provide zsh autocompletion ([#86][i86])
|
||||||
|
- [changed] Require at least Rust 1.39 to build (previous: 1.32)
|
||||||
|
- [changed] Switch to GitHub actions, CI testing now covers Windows as well ([#99][i99])
|
||||||
|
- [changed] Tweak the "outdated cache" warning message ([#97][i97])
|
||||||
|
- [changed] General maintenance: Upgrade dependencies, fix linter warnings
|
||||||
|
- [fixed] Fix Fish autocompletion on macOS ([#87][i87])
|
||||||
|
- [fixed] Fix compilation on Windows by disabling pager ([#99][i99])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Bruno Heridet][@Delapouite]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Hugo Locurcio][@Calinou]
|
||||||
|
- [Isak Johansson][@Plommonsorbet]
|
||||||
|
- [James Doyle][@james2doyle]
|
||||||
|
- [Jesús Trinidad Díaz Ramírez][@jesdazrez]
|
||||||
|
- [@korrat][@korrat]
|
||||||
|
- [Marc-André Renaud][@ma-renaud]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
|
### [v1.2.0][v1.2.0] (2019-08-10)
|
||||||
|
|
||||||
|
- [added] Add Windows support ([#77][i77])
|
||||||
|
- [added] Add support for spaces in commands ([#75][i75])
|
||||||
|
- [added] Add support for Fish-based autocompletion ([#71][i71])
|
||||||
|
- [added] Add pager support ([#44][i44])
|
||||||
|
- [added] Print detected OS with `-v` / `--version` ([#57][i57])
|
||||||
|
- [changed] OS detection: Treat BSDs as "osx" ([#58][i58])
|
||||||
|
- [changed] Move from curl to reqwest ([#61][i61])
|
||||||
|
- [changed] Move to Rust 2018, require Rust 1.32 ([#69][i69] / [#84][i84])
|
||||||
|
- [fixed] Add (back) support for proxies ([#68][i68])
|
||||||
|
|
||||||
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Bar Hatsor][@Bassets]
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
- [Gabriel Martinez][@mystal]
|
||||||
|
- [Ivan Smirnov][@aldanor]
|
||||||
|
- [Jan Christian Grünhage][@jcgruenhage]
|
||||||
|
- [Jonathan Dahan][@jedahan]
|
||||||
|
- [Juan D. Vega][@jdvr]
|
||||||
|
- [Natalie Pendragon][@natpen]
|
||||||
|
- [Raphael Das Gupta][@das-g]
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
|
||||||
### [v1.1.0][v1.1.0] (2018-10-22)
|
### [v1.1.0][v1.1.0] (2018-10-22)
|
||||||
|
|
@ -20,8 +494,9 @@ Possible log types:
|
||||||
- [changed] Require at least Rust 1.28 to build (previous: 1.19)
|
- [changed] Require at least Rust 1.28 to build (previous: 1.19)
|
||||||
- [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47])
|
- [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
|
- [Danilo Bargen][@dbrgn]
|
||||||
- [@equal-l2][@equal-l2]
|
- [@equal-l2][@equal-l2]
|
||||||
- [Jonathan Dahan][@jedahan]
|
- [Jonathan Dahan][@jedahan]
|
||||||
- [Lukas Bergdoll][@Voultapher]
|
- [Lukas Bergdoll][@Voultapher]
|
||||||
|
|
@ -52,15 +527,198 @@ Thanks!
|
||||||
|
|
||||||
- First crates.io release
|
- First crates.io release
|
||||||
|
|
||||||
|
[user documentation]: https://docs.tealdeer.org
|
||||||
|
|
||||||
|
[@0ndorio]: https://github.com/0ndorio
|
||||||
|
[@adamazing]: https://github.com/adamazing
|
||||||
|
[@agrmohit]: https://github.com/agrmohit
|
||||||
|
[@aldanor]: https://github.com/aldanor
|
||||||
|
[@Atul9]: https://github.com/Atul9
|
||||||
|
[@BachoSeven]: https://github.com/BachoSeven
|
||||||
|
[@bagohart]: https://github.com/bagohart
|
||||||
|
[@Bassets]: https://github.com/Bassets
|
||||||
|
[@black7375]: https://github.com/black7375
|
||||||
|
[@bl-ue]: https://github.com/bl-ue
|
||||||
|
[@Calinou]: https://github.com/Calinou
|
||||||
|
[@cam8001]: https://github.com/cam8001
|
||||||
|
[@cho-m]: https://github.com/cho-m
|
||||||
|
[@cyqsimon]: https://github.com/cyqsimon
|
||||||
|
[@CyrusYip]: https://github.com/CyrusYip
|
||||||
|
[@das-g]: https://github.com/das-g
|
||||||
|
[@dbrgn]: https://github.com/dbrgn
|
||||||
|
[@Delapouite]: https://github.com/Delapouite
|
||||||
|
[@dmaahs2017]: https://github.com/dmaahs2017
|
||||||
[@equal-l2]: https://github.com/equal-l2
|
[@equal-l2]: https://github.com/equal-l2
|
||||||
|
[@felixonmars]: https://github.com/felixonmars
|
||||||
|
[@frisoft]: https://github.com/frisoft
|
||||||
|
[@gagarine]: https://github.com/gagarine
|
||||||
|
[@hgaiser]: https://github.com/hgaiser
|
||||||
|
[@ilai-deutel]: https://github.com/ilai-deutel
|
||||||
|
[@iliya-malecki]: https://github.com/iliya-malecki
|
||||||
|
[@invakid404]: https://github.com/invakid404
|
||||||
|
[@james2doyle]: https://github.com/james2doyle
|
||||||
|
[@jcgruenhage]: https://github.com/jcgruenhage
|
||||||
|
[@jdvr]: https://github.com/jdvr
|
||||||
[@jedahan]: https://github.com/jedahan
|
[@jedahan]: https://github.com/jedahan
|
||||||
|
[@jesdazrez]: https://github.com/jesdazrez
|
||||||
|
[@jj-style]: https://github.com/jj-style
|
||||||
|
[@kbdharun]: https://github.com/kbdharun
|
||||||
|
[@kianmeng]: https://github.com/kianmeng
|
||||||
|
[@kornelski]: https://github.com/kornelski
|
||||||
|
[@korrat]: https://github.com/korrat
|
||||||
|
[@laxect]: https://github.com/laxect
|
||||||
|
[@LovecraftianHorror]: https://github.com/LovecraftianHorror
|
||||||
|
[@ma-renaud]: https://github.com/ma-renaud
|
||||||
|
[@michaeldel]: https://github.com/michaeldel
|
||||||
|
[@mucinoab]: https://github.com/mucinoab
|
||||||
|
[@mystal]: https://github.com/mystal
|
||||||
|
[@natpen]: https://github.com/natpen
|
||||||
|
[@nc7s]: https://github.com/nc7s
|
||||||
|
[@newsch]: https://github.com/newsch
|
||||||
|
[@nifr]: https://github.com/nifr
|
||||||
|
[@niklasmohrin]: https://github.com/niklasmohrin
|
||||||
|
[@Olavhaasie]: https://github.com/Olavhaasie
|
||||||
|
[@Plommonsorbet]: https://github.com/Plommonsorbet
|
||||||
|
[@qknogxxb]: https://github.com/qknogxxb
|
||||||
|
[@rithvikvibhu]: https://github.com/rithvikvibhu
|
||||||
|
[@SimplyDanny]: https://github.com/SimplyDanny
|
||||||
|
[@sondr3]: https://github.com/sondr3
|
||||||
|
[@tomasfarias]: https://github.com/tomasfarias
|
||||||
|
[@tranzystorek-io]: https://github.com/tranzystorek-io
|
||||||
|
[@tveness]: https://github.com/tveness
|
||||||
[@Voultapher]: https://github.com/Voultapher
|
[@Voultapher]: https://github.com/Voultapher
|
||||||
|
[@Walker-00]: https://github.com/Walker-00
|
||||||
|
[@YDX-2147483647]: https://github.com/YDX-2147483647
|
||||||
|
[@zedseven]: https://github.com/zedseven
|
||||||
|
[@beatbrot]: https://github.com/beatbrot
|
||||||
|
[@erickguan]: https://github.com/erickguan
|
||||||
|
[@MHS-0]: https://github.com/MHS-0
|
||||||
|
[@MatejKafka]: https://github.com/MatejKafka
|
||||||
|
[@nachiketkanore]: https://github.com/nachiketkanore
|
||||||
|
[@mipedja]: https://github.com/mipedja
|
||||||
|
[@hex1c]: https://github.com/hex1c
|
||||||
|
[@lengyijun]: https://github.com/lengyijun
|
||||||
|
|
||||||
[v1.0.0]: https://github.com/dbrgn/tealdeer/compare/v0.4.0...v1.0.0
|
[v1.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.0
|
||||||
[v1.1.0]: https://github.com/dbrgn/tealdeer/compare/v1.0.0...v1.1.0
|
[v1.1.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.0.0...v1.1.0
|
||||||
|
[v1.2.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.1.0...v1.2.0
|
||||||
|
[v1.3.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.2.0...v1.3.0
|
||||||
|
[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0
|
||||||
|
[v1.4.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.0...v1.4.1
|
||||||
|
[v1.5.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.1...v1.5.0
|
||||||
|
[v1.5.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.5.1
|
||||||
|
[v1.6.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.6.0
|
||||||
|
[v1.6.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.0...v1.6.1
|
||||||
|
[v1.6.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.6.2
|
||||||
|
[v1.7.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.7.0
|
||||||
|
[v1.7.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.0...v1.7.1
|
||||||
|
[v1.7.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.1...v1.7.2
|
||||||
|
[v1.7.3]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.7.3
|
||||||
|
[v1.8.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.8.0
|
||||||
|
[v1.8.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.8.0...v1.8.1
|
||||||
|
|
||||||
[i34]: https://github.com/dbrgn/tealdeer/issues/34
|
[i34]: https://github.com/tealdeer-rs/tealdeer/issues/34
|
||||||
[i43]: https://github.com/dbrgn/tealdeer/issues/43
|
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43
|
||||||
[i47]: https://github.com/dbrgn/tealdeer/issues/47
|
[i44]: https://github.com/tealdeer-rs/tealdeer/issues/44
|
||||||
[i48]: https://github.com/dbrgn/tealdeer/issues/48
|
[i47]: https://github.com/tealdeer-rs/tealdeer/issues/47
|
||||||
|
[i48]: https://github.com/tealdeer-rs/tealdeer/issues/48
|
||||||
|
[i57]: https://github.com/tealdeer-rs/tealdeer/issues/57
|
||||||
|
[i58]: https://github.com/tealdeer-rs/tealdeer/issues/58
|
||||||
|
[i61]: https://github.com/tealdeer-rs/tealdeer/issues/61
|
||||||
|
[i68]: https://github.com/tealdeer-rs/tealdeer/issues/68
|
||||||
|
[i69]: https://github.com/tealdeer-rs/tealdeer/issues/69
|
||||||
|
[i71]: https://github.com/tealdeer-rs/tealdeer/issues/71
|
||||||
|
[i75]: https://github.com/tealdeer-rs/tealdeer/issues/75
|
||||||
|
[i77]: https://github.com/tealdeer-rs/tealdeer/issues/77
|
||||||
|
[i84]: https://github.com/tealdeer-rs/tealdeer/issues/84
|
||||||
|
[i86]: https://github.com/tealdeer-rs/tealdeer/issues/86
|
||||||
|
[i87]: https://github.com/tealdeer-rs/tealdeer/issues/87
|
||||||
|
[i89]: https://github.com/tealdeer-rs/tealdeer/issues/89
|
||||||
|
[i95]: https://github.com/tealdeer-rs/tealdeer/issues/95
|
||||||
|
[i97]: https://github.com/tealdeer-rs/tealdeer/issues/97
|
||||||
|
[i99]: https://github.com/tealdeer-rs/tealdeer/issues/99
|
||||||
|
[i108]: https://github.com/tealdeer-rs/tealdeer/pull/108
|
||||||
|
[i111]: https://github.com/tealdeer-rs/tealdeer/issues/111
|
||||||
|
[i112]: https://github.com/tealdeer-rs/tealdeer/issues/112
|
||||||
|
[i113]: https://github.com/tealdeer-rs/tealdeer/issues/113
|
||||||
|
[i115]: https://github.com/tealdeer-rs/tealdeer/issues/115
|
||||||
|
[i125]: https://github.com/tealdeer-rs/tealdeer/pull/125
|
||||||
|
[i138]: https://github.com/tealdeer-rs/tealdeer/issues/138
|
||||||
|
[i142]: https://github.com/tealdeer-rs/tealdeer/pull/142
|
||||||
|
[i148]: https://github.com/tealdeer-rs/tealdeer/pull/148
|
||||||
|
[i157]: https://github.com/tealdeer-rs/tealdeer/pull/157
|
||||||
|
[i161]: https://github.com/tealdeer-rs/tealdeer/pull/161
|
||||||
|
[i162]: https://github.com/tealdeer-rs/tealdeer/pull/162
|
||||||
|
[i163]: https://github.com/tealdeer-rs/tealdeer/pull/163
|
||||||
|
[i168]: https://github.com/tealdeer-rs/tealdeer/pull/168
|
||||||
|
[i171]: https://github.com/tealdeer-rs/tealdeer/pull/171
|
||||||
|
[i174]: https://github.com/tealdeer-rs/tealdeer/pull/174
|
||||||
|
[i176]: https://github.com/tealdeer-rs/tealdeer/pull/176
|
||||||
|
[i187]: https://github.com/tealdeer-rs/tealdeer/pull/187
|
||||||
|
[i190]: https://github.com/tealdeer-rs/tealdeer/issues/190
|
||||||
|
[i197]: https://github.com/tealdeer-rs/tealdeer/pull/197
|
||||||
|
[i210]: https://github.com/tealdeer-rs/tealdeer/pull/210
|
||||||
|
[i213]: https://github.com/tealdeer-rs/tealdeer/pull/213
|
||||||
|
[i215]: https://github.com/tealdeer-rs/tealdeer/pull/215
|
||||||
|
[i217]: https://github.com/tealdeer-rs/tealdeer/pull/217
|
||||||
|
[i227]: https://github.com/tealdeer-rs/tealdeer/pull/227
|
||||||
|
[#231]: https://github.com/tealdeer-rs/tealdeer/pull/231
|
||||||
|
[i240]: https://github.com/tealdeer-rs/tealdeer/pull/240
|
||||||
|
[#247]: https://github.com/tealdeer-rs/tealdeer/pull/247
|
||||||
|
[#249]: https://github.com/tealdeer-rs/tealdeer/pull/249
|
||||||
|
[#253]: https://github.com/tealdeer-rs/tealdeer/pull/253
|
||||||
|
[#254]: https://github.com/tealdeer-rs/tealdeer/pull/254
|
||||||
|
[#257]: https://github.com/tealdeer-rs/tealdeer/pull/257
|
||||||
|
[#259]: https://github.com/tealdeer-rs/tealdeer/pull/259
|
||||||
|
[#262]: https://github.com/tealdeer-rs/tealdeer/pull/262
|
||||||
|
[#271]: https://github.com/tealdeer-rs/tealdeer/pull/271
|
||||||
|
[#272]: https://github.com/tealdeer-rs/tealdeer/pull/272
|
||||||
|
[#274]: https://github.com/tealdeer-rs/tealdeer/pull/274
|
||||||
|
[#276]: https://github.com/tealdeer-rs/tealdeer/pull/276
|
||||||
|
[#284]: https://github.com/tealdeer-rs/tealdeer/pull/284
|
||||||
|
[#285]: https://github.com/tealdeer-rs/tealdeer/pull/285
|
||||||
|
[#287]: https://github.com/tealdeer-rs/tealdeer/pull/287
|
||||||
|
[#290]: https://github.com/tealdeer-rs/tealdeer/pull/290
|
||||||
|
[#291]: https://github.com/tealdeer-rs/tealdeer/pull/291
|
||||||
|
[#293]: https://github.com/tealdeer-rs/tealdeer/pull/293
|
||||||
|
[#297]: https://github.com/tealdeer-rs/tealdeer/pull/297
|
||||||
|
[#298]: https://github.com/tealdeer-rs/tealdeer/pull/298
|
||||||
|
[#299]: https://github.com/tealdeer-rs/tealdeer/pull/299
|
||||||
|
[#300]: https://github.com/tealdeer-rs/tealdeer/pull/300
|
||||||
|
[#303]: https://github.com/tealdeer-rs/tealdeer/pull/303
|
||||||
|
[#305]: https://github.com/tealdeer-rs/tealdeer/pull/305
|
||||||
|
[#306]: https://github.com/tealdeer-rs/tealdeer/pull/306
|
||||||
|
[#314]: https://github.com/tealdeer-rs/tealdeer/pull/314
|
||||||
|
[#315]: https://github.com/tealdeer-rs/tealdeer/pull/315
|
||||||
|
[#322]: https://github.com/tealdeer-rs/tealdeer/pull/322
|
||||||
|
[#324]: https://github.com/tealdeer-rs/tealdeer/pull/324
|
||||||
|
[#327]: https://github.com/tealdeer-rs/tealdeer/pull/327
|
||||||
|
[#331]: https://github.com/tealdeer-rs/tealdeer/pull/331
|
||||||
|
[#333]: https://github.com/tealdeer-rs/tealdeer/pull/333
|
||||||
|
[#336]: https://github.com/tealdeer-rs/tealdeer/pull/336
|
||||||
|
[#337]: https://github.com/tealdeer-rs/tealdeer/pull/337
|
||||||
|
[#342]: https://github.com/tealdeer-rs/tealdeer/pull/342
|
||||||
|
[#354]: https://github.com/tealdeer-rs/tealdeer/pull/354
|
||||||
|
[#355]: https://github.com/tealdeer-rs/tealdeer/pull/355
|
||||||
|
[#362]: https://github.com/tealdeer-rs/tealdeer/pull/362
|
||||||
|
[#386]: https://github.com/tealdeer-rs/tealdeer/pull/386
|
||||||
|
[#388]: https://github.com/tealdeer-rs/tealdeer/pull/388
|
||||||
|
[#389]: https://github.com/tealdeer-rs/tealdeer/pull/389
|
||||||
|
[#399]: https://github.com/tealdeer-rs/tealdeer/pull/399
|
||||||
|
[#400]: https://github.com/tealdeer-rs/tealdeer/pull/400
|
||||||
|
[#401]: https://github.com/tealdeer-rs/tealdeer/pull/401
|
||||||
|
[#407]: https://github.com/tealdeer-rs/tealdeer/pull/407
|
||||||
|
[#411]: https://github.com/tealdeer-rs/tealdeer/pull/411
|
||||||
|
[#416]: https://github.com/tealdeer-rs/tealdeer/pull/416
|
||||||
|
[#417]: https://github.com/tealdeer-rs/tealdeer/pull/417
|
||||||
|
[#422]: https://github.com/tealdeer-rs/tealdeer/pull/422
|
||||||
|
[#423]: https://github.com/tealdeer-rs/tealdeer/pull/423
|
||||||
|
[#425]: https://github.com/tealdeer-rs/tealdeer/pull/425
|
||||||
|
[#426]: https://github.com/tealdeer-rs/tealdeer/pull/426
|
||||||
|
[#429]: https://github.com/tealdeer-rs/tealdeer/pull/429
|
||||||
|
[#430]: https://github.com/tealdeer-rs/tealdeer/pull/430
|
||||||
|
[#435]: https://github.com/tealdeer-rs/tealdeer/pull/435
|
||||||
|
[#436]: https://github.com/tealdeer-rs/tealdeer/pull/436
|
||||||
|
[#439]: https://github.com/tealdeer-rs/tealdeer/pull/439
|
||||||
|
[#440]: https://github.com/tealdeer-rs/tealdeer/pull/440
|
||||||
|
[#451]: https://github.com/tealdeer-rs/tealdeer/pull/451
|
||||||
|
|
|
||||||
2427
Cargo.lock
generated
2427
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
65
Cargo.toml
65
Cargo.toml
|
|
@ -1,46 +1,61 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["Danilo Bargen <mail@dbrgn.ch>"]
|
authors = [
|
||||||
|
"Danilo Bargen <mail@dbrgn.ch>",
|
||||||
|
"Niklas Mohrin <dev@niklasmohrin.de>",
|
||||||
|
]
|
||||||
description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support."
|
description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support."
|
||||||
homepage = "https://github.com/dbrgn/tealdeer/"
|
homepage = "https://github.com/tealdeer-rs/tealdeer/"
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
name = "tealdeer"
|
name = "tealdeer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/dbrgn/tealdeer/"
|
repository = "https://github.com/tealdeer-rs/tealdeer/"
|
||||||
version = "1.1.0"
|
documentation = "https://docs.tealdeer.org"
|
||||||
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "/bash_tealdeer"]
|
version = "1.8.1"
|
||||||
edition = "2018"
|
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
|
||||||
|
rust-version = "1.88" # MSRV
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tldr"
|
name = "tldr"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.10.2"
|
anyhow = "1"
|
||||||
clippy = { version = "0.0.174", optional = true }
|
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false }
|
||||||
docopt = "0.8.1"
|
env_logger = { version = "0.11", optional = true }
|
||||||
env_logger = { version = "0.5", optional = true }
|
etcetera = "0.11.0"
|
||||||
flate2 = "1.0"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = "1.0.21"
|
serde = "1.0.21"
|
||||||
serde_derive = "1.0.21"
|
serde_derive = "1.0.21"
|
||||||
tar = "0.4.14"
|
ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] }
|
||||||
time = "0.1.38"
|
toml = "1"
|
||||||
toml = "0.4.6"
|
yansi = "1"
|
||||||
walkdir = "2.0.1"
|
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
|
||||||
xdg = "2.1.0"
|
|
||||||
reqwest = { version = "0.9.5", optional = true }
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
|
pager = "0.16"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "0.10"
|
assert_cmd = "2.0.1"
|
||||||
escargot = "0.4"
|
escargot = "0.5"
|
||||||
predicates = "1.0"
|
predicates = "3.1.2"
|
||||||
tempdir = "^0.3"
|
tempfile = "3.1.0"
|
||||||
utime = "0.2.0"
|
filetime = "0.2.10"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["networking"]
|
# native-tls is not enabled by default, because it is difficult to build for musl
|
||||||
|
default = ["rustls-with-webpki-roots", "rustls-with-native-roots"]
|
||||||
logging = ["env_logger"]
|
logging = ["env_logger"]
|
||||||
networking = ["reqwest"]
|
|
||||||
|
# At least one of variants for `ureq` HTTP client must be selected.
|
||||||
|
native-tls = ["ureq/native-tls", "ureq/platform-verifier"]
|
||||||
|
rustls-with-webpki-roots = ["ureq/rustls"] # ureq uses WebPKI roots by default
|
||||||
|
rustls-with-native-roots = ["ureq/rustls", "ureq/platform-verifier"]
|
||||||
|
|
||||||
|
ignore-online-tests = []
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Copyright (C) 2015-2018 Danilo Bargen and contributors
|
Copyright (C) 2015-2021 Danilo Bargen and contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
||||||
204
README.md
204
README.md
|
|
@ -1,20 +1,28 @@
|
||||||
# tealdeer
|
# tealdeer
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|Crate|Linux|macOS|
|
|Crate|CI (Linux/macOS/Windows)|
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|
|
||||||
|[![Crates.io][crates-io-badge]][crates-io]|[![Circle CI][circle-ci-badge]][circle-ci]|[![Travis CI][travis-ci-badge]][travis-ci]|
|
|[![Crates.io][crates-io-badge]][crates-io]|[![GitHub CI][github-actions-badge]][github-actions]|
|
||||||
|
|
||||||
A very fast implementation of [tldr](https://github.com/tldr-pages/tldr) in
|
A very fast implementation of [tldr](https://github.com/tldr-pages/tldr) in
|
||||||
Rust: Simplified, example based and community-driven man pages.
|
Rust: Simplified, example based and community-driven man pages.
|
||||||
|
|
||||||
<img src="screenshot-default.png" alt="Screenshot of tldr command" width="600">
|
<img src="docs/src/screenshot-default.png" alt="Screenshot of tldr command" width="600">
|
||||||
|
|
||||||
If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :)
|
If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :)
|
||||||
|
|
||||||
In case you're in a hurry and just want to quickly try tealdeer, you can find static
|
In case you're in a hurry and just want to quickly try tealdeer, you can find static
|
||||||
binaries on the [GitHub releases page](https://github.com/dbrgn/tealdeer/releases/)!
|
binaries on the [GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases/)!
|
||||||
|
|
||||||
|
|
||||||
|
## Docs (Installing, Usage, Configuration)
|
||||||
|
|
||||||
|
User documentation is available at <https://docs.tealdeer.org>!
|
||||||
|
|
||||||
|
The docs are generated using [mdbook](https://rust-lang.github.io/mdBook/index.html).
|
||||||
|
They can be edited through the markdown files in the `docs/src/` directory.
|
||||||
|
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
@ -23,103 +31,17 @@ High level project goals:
|
||||||
|
|
||||||
- [x] Download and cache pages
|
- [x] Download and cache pages
|
||||||
- [x] Don't require a network connection for anything besides updating the cache
|
- [x] Don't require a network connection for anything besides updating the cache
|
||||||
- [x] Command line interface similar or equivalent to the [NodeJS client][tldr-node-client]
|
- [x] Comply with the [tldr client specification][client-spec]
|
||||||
|
- [x] Advanced highlighting and configuration
|
||||||
- [x] Be fast
|
- [x] Be fast
|
||||||
|
|
||||||
A tool like `tldr` should be as frictionless as possible to use. It should be
|
A tool like `tldr` should be as frictionless as possible to use and show the
|
||||||
easy to invoke (just `tldr tar`, not using another subcommand like `tldr find
|
output as fast as possible.
|
||||||
tar`) and it should show the output as fast as possible.
|
|
||||||
|
|
||||||
tealdeer reaches these goals. During a (highly non-scientific) test (see
|
|
||||||
[#38](https://github.com/dbrgn/tealdeer/issues/38) for details), I tested the
|
|
||||||
invocation speed of `tldr <command>` for a few of the existing clients:
|
|
||||||
|
|
||||||
| Client | Times (ms) | Avg of 5 (ms) |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| [Tealdeer](https://github.com/dbrgn/tealdeer/) | `15/11/5/5/11` | `9.4` (100%) |
|
|
||||||
| [C client](https://github.com/tldr-pages/tldr-cpp-client) | `11/5/12/11/15` | `10.8` (115%) |
|
|
||||||
| [Bash client](https://github.com/pepa65/tldr-bash-client) | `15/19/22/25/24` | `21.0` (223%) |
|
|
||||||
| [Go client by k3mist](https://github.com/k3mist/tldr/) | `98/96/100/95/101` | `98.8` (1'051%) |
|
|
||||||
| [Python client](https://github.com/lord63/tldr.py) | `152/148/151/158/140` | `149.8` (1'594%) |
|
|
||||||
| [NodeJS client](https://github.com/tldr-pages/tldr-node-client) | `169/171/170/170/170` | `170.0` (1'809%) |
|
|
||||||
|
|
||||||
tealdeer was the winner here, although the C client and the Bash client are in
|
|
||||||
the same speed class. Interpreted languages are clearly much slower to invoke,
|
|
||||||
a delay of 170 milliseconds is definitely noticeable and increases friction for
|
|
||||||
the user.
|
|
||||||
|
|
||||||
These are the clients I tried but failed to compile or run:
|
|
||||||
[Haskell client](https://github.com/psibi/tldr-hs),
|
|
||||||
[Ruby client](https://github.com/YellowApple/tldrb),
|
|
||||||
[Perl client](https://github.com/skaji/perl-tldr),
|
|
||||||
[Go client by anoopengineer](https://github.com/anoopengineer/tldr/),
|
|
||||||
[PHP client](https://github.com/BrainMaestro/tldr-php).
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Development
|
||||||
|
|
||||||
tldr [options] <command>
|
Creating a debug build with logging enabled:
|
||||||
tldr [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-h --help Show this screen
|
|
||||||
-v --version Show version information
|
|
||||||
-l --list List all commands in the cache
|
|
||||||
-f --render <file> Render a specific markdown file
|
|
||||||
-o --os <type> Override the operating system [linux, osx, sunos]
|
|
||||||
-u --update Update the local cache
|
|
||||||
-c --clear-cache Clear the local cache
|
|
||||||
-q --quiet Suppress informational messages
|
|
||||||
--config-path Show config file path
|
|
||||||
--seed-config Create a basic config
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ tldr tar
|
|
||||||
$ tldr --list
|
|
||||||
|
|
||||||
To control the cache:
|
|
||||||
|
|
||||||
$ tldr --update
|
|
||||||
$ tldr --clear-cache
|
|
||||||
|
|
||||||
To render a local file (for testing):
|
|
||||||
|
|
||||||
$ tldr --render /path/to/file.md
|
|
||||||
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
### Static Binaries (Linux)
|
|
||||||
|
|
||||||
Static binary builds (currently for Linux only) are available on the
|
|
||||||
[GitHub releases page](https://github.com/dbrgn/tealdeer/releases).
|
|
||||||
Simply download the binary for your platform and run it!
|
|
||||||
|
|
||||||
Builds for other platforms are planned.
|
|
||||||
|
|
||||||
### Cargo Install (any platform)
|
|
||||||
|
|
||||||
Build and install the tool via cargo...
|
|
||||||
|
|
||||||
$ cargo install tealdeer
|
|
||||||
|
|
||||||
### From Package Manager
|
|
||||||
|
|
||||||
tealdeer has been added to a few package managers:
|
|
||||||
|
|
||||||
- Arch Linux AUR: [`tealdeer`](https://aur.archlinux.org/packages/tealdeer/)
|
|
||||||
or [`tealdeer-git`](https://aur.archlinux.org/packages/tealdeer-git/)
|
|
||||||
- macOS Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer)
|
|
||||||
- Nix: [`tealdeer`](https://nixos.org/nixos/packages.html#tealdeer)
|
|
||||||
- Void Linux XBPS: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer)
|
|
||||||
|
|
||||||
### From Source (any platform)
|
|
||||||
|
|
||||||
tealdeer requires at least Rust 1.31.
|
|
||||||
|
|
||||||
Debug build with logging enabled:
|
|
||||||
|
|
||||||
$ cargo build --features logging
|
$ cargo build --features logging
|
||||||
|
|
||||||
|
|
@ -131,82 +53,32 @@ To enable the log output, set the `RUST_LOG` env variable:
|
||||||
|
|
||||||
$ export RUST_LOG=tldr=debug
|
$ export RUST_LOG=tldr=debug
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The tldr command can be customized with a config file called `config.toml`.
|
|
||||||
Creating the config file can be done manually or with the help of tldr:
|
|
||||||
|
|
||||||
$ tldr --seed-config
|
|
||||||
|
|
||||||
The configuration file path follows OS conventions. It can be queried with the following command:
|
|
||||||
|
|
||||||
$ tldr --config-path
|
|
||||||
|
|
||||||
### Style
|
|
||||||
|
|
||||||
Using the config file, the style (e.g. colors or underlines) can be customized.
|
|
||||||
|
|
||||||
Possible styles:
|
|
||||||
|
|
||||||
- `description`: The initial description text
|
|
||||||
- `command_name`: The command name as part of the example code
|
|
||||||
- `example_text`: The text that describes an example
|
|
||||||
- `example_code`: The example itself, except the `command_name` and `example_variable`
|
|
||||||
- `example_variable`: The variables in the example
|
|
||||||
|
|
||||||
Currently supported attributes:
|
|
||||||
|
|
||||||
- `foreground` (color string, see below)
|
|
||||||
- `background` (color string, see below)
|
|
||||||
- `underline` (`true` or `false`)
|
|
||||||
- `bold` (`true` or `false`)
|
|
||||||
|
|
||||||
The currently supported colors are:
|
|
||||||
|
|
||||||
- `black`
|
|
||||||
- `red`
|
|
||||||
- `green`
|
|
||||||
- `yellow`
|
|
||||||
- `blue`
|
|
||||||
- `purple`
|
|
||||||
- `cyan`
|
|
||||||
- `white`
|
|
||||||
|
|
||||||
Example customization:
|
|
||||||
|
|
||||||
<img src="screenshot-custom.png" alt="Screenshot of customized version" width="600">
|
|
||||||
|
|
||||||
|
|
||||||
## Autocompletion
|
|
||||||
|
|
||||||
- *Bash*: copy `bash_tealdeer` to `/usr/share/bash-completion/completions/tldr`
|
|
||||||
- *Fish*: copy `fish_tealdeer` to `~/.config/fish/completions/tldr.fish`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
To run tests:
|
To run tests:
|
||||||
|
|
||||||
$ cargo test
|
$ cargo test
|
||||||
|
|
||||||
(Note that integration tests are a bit slow, since they invoke `cargo build` in different configurations.)
|
|
||||||
|
|
||||||
To run lints:
|
To run lints:
|
||||||
|
|
||||||
$ rustup component add clippy
|
$ rustup component add clippy
|
||||||
$ cargo clean && cargo clippy
|
$ cargo clean && cargo clippy
|
||||||
|
|
||||||
|
|
||||||
## Build Flags
|
### AI Policy
|
||||||
|
|
||||||
tealdeer knows the following feature flags:
|
Using AI is generally discouraged. However, if it is used as part of a contribution, the contributor MUST:
|
||||||
|
|
||||||
- `logging`: This enables logging support through [env_logger](https://docs.rs/env_logger/*/env_logger/)
|
1. Clearly mark what parts (if any) of a contribution were created with the help of AI tools. This includes issue and pull request comments.
|
||||||
- `networking`: This enables support for updating the cache from the internet
|
2. Check all output of AI tools before sharing it with others in the tealdeer project.
|
||||||
|
3. Not post slop, spam, or low quality contributions. This includes pull request descriptions and comments with excessive text and markdown flair.
|
||||||
|
4. Leave small or easy tasks to new contributors who want to learn without the use of AI. This is to maintain the presence of the `good-first-issue` tag.
|
||||||
|
5. Be respectful of everyone's time: *maintainers and other contributors will be reviewing your PRs.*
|
||||||
|
|
||||||
By default, only the `networking` feature is enabled. To build tealdeer without
|
|
||||||
networking support, use the `--no-default-features` Cargo flag.
|
## MSRV (Minimally Supported Rust Version)
|
||||||
|
|
||||||
|
When publishing a tealdeer release, the Rust version required to build it
|
||||||
|
should be stable for at least a month. The current MSRV can always be found in
|
||||||
|
the `rust-version` field in `Cargo.toml`.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
@ -225,15 +97,13 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
|
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
|
||||||
be dual licensed as above, without any additional terms or conditions.
|
be dual licensed as above, without any additional terms or conditions.
|
||||||
|
|
||||||
Thanks to @SShrike for coming up with the name "tealdeer"!
|
Thanks to @severen for coming up with the name "tealdeer"!
|
||||||
|
|
||||||
|
|
||||||
[tldr-node-client]: https://github.com/tldr-pages/tldr-node-client
|
[client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md
|
||||||
|
|
||||||
<!-- Badges -->
|
<!-- Badges -->
|
||||||
[circle-ci]: https://circleci.com/gh/dbrgn/tealdeer/tree/master
|
[github-actions]: https://github.com/tealdeer-rs/tealdeer/actions?query=branch%3Amain
|
||||||
[circle-ci-badge]: https://circleci.com/gh/dbrgn/tealdeer/tree/master.svg?style=shield
|
[github-actions-badge]: https://github.com/tealdeer-rs/tealdeer/actions/workflows/ci.yml/badge.svg?branch=main
|
||||||
[travis-ci]: https://travis-ci.org/dbrgn/tealdeer
|
|
||||||
[travis-ci-badge]: https://travis-ci.org/dbrgn/tealdeer.svg?branch=master
|
|
||||||
[crates-io]: https://crates.io/crates/tealdeer
|
[crates-io]: https://crates.io/crates/tealdeer
|
||||||
[crates-io-badge]: https://img.shields.io/crates/v/tealdeer.svg
|
[crates-io-badge]: https://img.shields.io/crates/v/tealdeer.svg
|
||||||
|
|
|
||||||
12
RELEASING.md
12
RELEASING.md
|
|
@ -7,12 +7,16 @@ Run linting:
|
||||||
Set variables:
|
Set variables:
|
||||||
|
|
||||||
$ export VERSION=X.Y.Z
|
$ export VERSION=X.Y.Z
|
||||||
$ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA
|
$ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1
|
||||||
|
|
||||||
Update version numbers:
|
Update version numbers:
|
||||||
|
|
||||||
$ vim Cargo.toml
|
$ vim Cargo.toml
|
||||||
$ cargo update
|
$ cargo update -p tealdeer
|
||||||
|
|
||||||
|
Update docs:
|
||||||
|
|
||||||
|
$ cargo run -- --help > docs/src/usage.txt
|
||||||
|
|
||||||
Update changelog:
|
Update changelog:
|
||||||
|
|
||||||
|
|
@ -28,6 +32,4 @@ Publish:
|
||||||
$ cargo publish
|
$ cargo publish
|
||||||
$ git push && git push --tags
|
$ git push && git push --tags
|
||||||
|
|
||||||
Create release binaries:
|
Then publish the release on GitHub.
|
||||||
|
|
||||||
$ ./release-build.sh
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# tealdeer bash completion
|
|
||||||
|
|
||||||
_tealdeer()
|
|
||||||
{
|
|
||||||
local cur prev words cword
|
|
||||||
_init_completion || return
|
|
||||||
|
|
||||||
case $prev in
|
|
||||||
-h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|--config-path|--seed-config|-q|--quiet)
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
-f|--render)
|
|
||||||
_filedir
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
-o|--os)
|
|
||||||
COMPREPLY=( $(compgen -W 'linux osx sunos' -- "${cur}") )
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ $cur == -* ]]; then
|
|
||||||
COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) )
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
COMPREPLY=( $(compgen -W '$( tldr -l | tr -d , )' -- "${cur}") )
|
|
||||||
}
|
|
||||||
|
|
||||||
complete -F _tealdeer tldr
|
|
||||||
35
completion/bash_tealdeer
Normal file
35
completion/bash_tealdeer
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# tealdeer bash completion
|
||||||
|
|
||||||
|
_tealdeer()
|
||||||
|
{
|
||||||
|
local cur prev words cword
|
||||||
|
_init_completion || return
|
||||||
|
|
||||||
|
case $prev in
|
||||||
|
-h|--help|-v|--version|-l|--list|-u|--update|--no-auto-update|-c|--clear-cache|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet)
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
-f|--render)
|
||||||
|
_filedir
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
-p|--platform)
|
||||||
|
COMPREPLY=( $(compgen -W 'linux macos sunos windows android freebsd netbsd openbsd' -- "${cur}") )
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--color)
|
||||||
|
COMPREPLY=( $(compgen -W 'always auto never' -- "${cur}") )
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ $cur == -* ]]; then
|
||||||
|
COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) )
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if tldrlist=$(tldr -l 2>/dev/null); then
|
||||||
|
COMPREPLY=( $(compgen -W '$( echo "$tldrlist" | tr -d , )' -- "${cur}") )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _tealdeer tldr
|
||||||
34
completion/fish_tealdeer
Normal file
34
completion/fish_tealdeer
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#
|
||||||
|
# Completions for the tealdeer implementation of tldr
|
||||||
|
# https://github.com/tealdeer-rs/tealdeer/
|
||||||
|
#
|
||||||
|
|
||||||
|
complete -c tldr -s h -l help -d 'Print the help message' -f
|
||||||
|
complete -c tldr -s v -l version -d 'Show version information' -f
|
||||||
|
complete -c tldr -s l -l list -d 'List all commands in the cache' -f
|
||||||
|
complete -c tldr -l edit-page -d 'Edit custom page with `EDITOR`' -f
|
||||||
|
complete -c tldr -l edit-patch -d 'Edit custom patch with `EDITOR`' -f
|
||||||
|
complete -c tldr -s f -l render -d 'Render a specific markdown file' -r
|
||||||
|
complete -c tldr -s p -l platform -d 'Override the operating system' -xa 'linux macos sunos windows android freebsd netbsd openbsd common'
|
||||||
|
complete -c tldr -s L -l language -d 'Override the language' -x
|
||||||
|
complete -c tldr -s u -l update -d 'Update the local cache' -f
|
||||||
|
complete -c tldr -l no-auto-update -d 'If auto update is configured, disable it for this run' -f
|
||||||
|
complete -c tldr -s c -l clear-cache -d 'Clear the local cache' -f
|
||||||
|
complete -c tldr -s L -l config-path -d 'Override config file location' -r
|
||||||
|
complete -c tldr -s L -l override-config -d 'Override config values after reading config file' -x
|
||||||
|
complete -c tldr -l pager -d 'Use a pager to page output' -f
|
||||||
|
complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it' -f
|
||||||
|
complete -c tldr -s q -l quiet -d 'Suppress informational messages' -f
|
||||||
|
complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer' -f
|
||||||
|
complete -c tldr -l seed-config -d 'Create a basic config' -f
|
||||||
|
complete -c tldr -l color -d 'Controls when to use color' -xa 'always auto never'
|
||||||
|
complete -c tldr -l short-options -d 'Display the short variants of placeholders' -f
|
||||||
|
complete -c tldr -l long-options -d 'Display the long variants of placeholders' -f
|
||||||
|
|
||||||
|
function __tealdeer_entries
|
||||||
|
if set entries (tldr --list 2>/dev/null)
|
||||||
|
string replace -a -i -r "\,\s" "\n" $entries
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
complete -f -c tldr -a '(__tealdeer_entries)'
|
||||||
58
completion/zsh_tealdeer
Normal file
58
completion/zsh_tealdeer
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
#compdef tldr
|
||||||
|
|
||||||
|
_applications() {
|
||||||
|
local -a commands
|
||||||
|
if commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}); then
|
||||||
|
_describe -t commands 'command' commands
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_tealdeer() {
|
||||||
|
local I="-h --help -v --version"
|
||||||
|
integer ret=1
|
||||||
|
local -a args
|
||||||
|
|
||||||
|
args+=(
|
||||||
|
"($I -l --list)"{-l,--list}"[List all commands in the cache]"
|
||||||
|
"($I)--edit-page[Edit custom page with EDITOR]"
|
||||||
|
"($I)--edit-patch[Edit custom patch with EDITOR]"
|
||||||
|
"($I -f --render)"{-f,--render}"[Render a specific markdown file]:file:_files"
|
||||||
|
"($I -p --platform)"{-p,--platform}'[Override the operating system]:platform:((
|
||||||
|
linux
|
||||||
|
macos
|
||||||
|
sunos
|
||||||
|
windows
|
||||||
|
android
|
||||||
|
freebsd
|
||||||
|
netbsd
|
||||||
|
openbsd
|
||||||
|
common
|
||||||
|
))'
|
||||||
|
"($I -L --language)"{-L,--language}"[Override the language settings]:lang"
|
||||||
|
"($I -u --update)"{-u,--update}"[Update the local cache]"
|
||||||
|
"($I)--no-auto-update[If auto update is configured, disable it for this run]"
|
||||||
|
"($I -c --clear-cache)"{-c,--clear-cache}"[Clear the local cache]"
|
||||||
|
"($I)--config-path[Override config file location]"
|
||||||
|
"($I)--override-config[Override config values after reading config file]"
|
||||||
|
"($I)--pager[Use a pager to page output]"
|
||||||
|
"($I -r --raw)"{-r,--raw}"[Display the raw markdown instead of rendering it]"
|
||||||
|
"($I -q --quiet)"{-q,--quiet}"[Suppress informational messages]"
|
||||||
|
"($I)--show-paths[Show file and directory paths used by tealdeer]"
|
||||||
|
"($I)--seed-config[Create a basic config]"
|
||||||
|
"($I)--color[Controls when to use color]:when:((
|
||||||
|
always
|
||||||
|
auto
|
||||||
|
never
|
||||||
|
))"
|
||||||
|
"($I)--short-options[Display the short variants of placeholders]"
|
||||||
|
"($I)--long-options[Display the long variants of placeholders]"
|
||||||
|
'(- *)'{-h,--help}'[Display help]'
|
||||||
|
'(- *)'{-v,--version}'[Show version information]'
|
||||||
|
'1: :_applications'
|
||||||
|
)
|
||||||
|
|
||||||
|
_arguments $args[@] && ret=0
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
_tealdeer
|
||||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
book
|
||||||
8
docs/README.md
Normal file
8
docs/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Tealdeer Docs
|
||||||
|
|
||||||
|
To build the docs, install [mdbook](https://github.com/rust-lang/mdBook).
|
||||||
|
|
||||||
|
You can build the HTML with `mdbook build`.
|
||||||
|
|
||||||
|
To serve the docs on `localhost:3000` and watch for changes, use `mdbook
|
||||||
|
serve`.
|
||||||
5
docs/book.toml
Normal file
5
docs/book.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[book]
|
||||||
|
authors = ["Danilo Bargen", "Niklas Mohrin"]
|
||||||
|
language = "en"
|
||||||
|
src = "src"
|
||||||
|
title = "Tealdeer User Manual"
|
||||||
14
docs/src/SUMMARY.md
Normal file
14
docs/src/SUMMARY.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
[Introduction](./intro.md)
|
||||||
|
|
||||||
|
- [Installing](./installing.md)
|
||||||
|
- [Usage](./usage.md)
|
||||||
|
- [Custom Pages and Patches](./usage_custom_pages.md)
|
||||||
|
- [Configuration](./config.md)
|
||||||
|
- [Section: \[display\]](./config_display.md)
|
||||||
|
- [Section: \[style\]](./config_style.md)
|
||||||
|
- [Section: \[search\]](./config_search.md)
|
||||||
|
- [Section: \[updates\]](./config_updates.md)
|
||||||
|
- [Section: \[directories\]](./config_directories.md)
|
||||||
|
- [Tips and Tricks](./tips_and_tricks.md)
|
||||||
72
docs/src/config.md
Normal file
72
docs/src/config.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Tealdeer can be customized with a config file in [TOML
|
||||||
|
format](https://toml.io/) called `config.toml`.
|
||||||
|
|
||||||
|
## Configfile Path
|
||||||
|
|
||||||
|
The configuration file path follows OS conventions (e.g.
|
||||||
|
`$XDG_CONFIG_HOME/tealdeer/config.toml` on Linux). The paths can be queried
|
||||||
|
with the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ tldr --show-paths
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating the config file can be done manually or with the help of `tldr`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ tldr --seed-config
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux, this will usually be `~/.config/tealdeer/config.toml`.
|
||||||
|
|
||||||
|
## Config Example
|
||||||
|
|
||||||
|
Here's an example configuration file. Note that this example does not contain
|
||||||
|
all possible config options. For details on the things that can be configured,
|
||||||
|
please refer to the subsections of this documentation page
|
||||||
|
([display](config_display.html), [style](config_style.html), [search](config_search.html),
|
||||||
|
[updates](config_updates.html) or [directories](config_directories.html)).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
compact = false
|
||||||
|
use_pager = true
|
||||||
|
show_title = false
|
||||||
|
|
||||||
|
[style.command_name]
|
||||||
|
foreground = "red"
|
||||||
|
|
||||||
|
[style.example_text]
|
||||||
|
foreground = "green"
|
||||||
|
|
||||||
|
[style.example_code]
|
||||||
|
foreground = "blue"
|
||||||
|
|
||||||
|
[style.example_variable]
|
||||||
|
foreground = "blue"
|
||||||
|
underline = true
|
||||||
|
|
||||||
|
[updates]
|
||||||
|
auto_update = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Override Config Directory
|
||||||
|
|
||||||
|
The directory where the configuration file resides may be overwritten by the
|
||||||
|
environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path.
|
||||||
|
Variable expansion will not be performed on the path.
|
||||||
|
|
||||||
|
## Override Config Values
|
||||||
|
|
||||||
|
Individual config values can be overridden using the `--override-config` command
|
||||||
|
line argument. The overrides take place after reading the user config file, but
|
||||||
|
before the raw config is evaluated.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ tldr --override-config "display.compact = true" tealdeer
|
||||||
|
```
|
||||||
|
|
||||||
|
Each override is of the form `<name> = <value>` where `name` is a config key and
|
||||||
|
`value` is any TOML value.
|
||||||
29
docs/src/config_directories.md
Normal file
29
docs/src/config_directories.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Section: \[directories\]
|
||||||
|
|
||||||
|
This section allows overriding some directory paths.
|
||||||
|
|
||||||
|
## `cache_dir`
|
||||||
|
|
||||||
|
Override the cache directory. Remember to use an absolute path. Variable
|
||||||
|
expansion will not be performed on the path. If the directory does not yet
|
||||||
|
exist, it will be created.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[directories]
|
||||||
|
cache_dir = "/home/myuser/.tealdeer-cache/"
|
||||||
|
```
|
||||||
|
|
||||||
|
If no `cache_dir` is specified, tealdeer will fall back to a location that
|
||||||
|
follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`.
|
||||||
|
Use `tldr --show-paths` to show the path that is being used.
|
||||||
|
|
||||||
|
## `custom_pages_dir`
|
||||||
|
|
||||||
|
Set the directory to be used to look up [custom
|
||||||
|
pages](usage_custom_pages.html). Remember to use an absolute path. Variable
|
||||||
|
expansion will not be performed on the path.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[directories]
|
||||||
|
custom_pages_dir = "/home/myuser/custom-tldr-pages/"
|
||||||
|
```
|
||||||
88
docs/src/config_display.md
Normal file
88
docs/src/config_display.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Section: \[display\]
|
||||||
|
|
||||||
|
In the `display` section you can configure the output format.
|
||||||
|
|
||||||
|
## `use_pager`
|
||||||
|
|
||||||
|
Specifies whether the pager should be used by default or not (default `false`).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
use_pager = true
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled, `less -R` is used as pager. To override the pager command used,
|
||||||
|
set the `PAGER` environment variable.
|
||||||
|
|
||||||
|
NOTE: This feature is not available on Windows.
|
||||||
|
|
||||||
|
## `compact`
|
||||||
|
|
||||||
|
Set this to enforce more compact output, where empty lines are stripped out
|
||||||
|
(default `false`).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
compact = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## `show_title`
|
||||||
|
|
||||||
|
Display the command name at the top of the page output (default `false`).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
show_title = true
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled, the command name will be displayed at the top of the output,
|
||||||
|
styled with the `command_name` style configuration.
|
||||||
|
|
||||||
|
## `indent`
|
||||||
|
|
||||||
|
Controls the indentation of the output via two sub-keys.
|
||||||
|
|
||||||
|
### `indent.base`
|
||||||
|
|
||||||
|
Specifies the number of spaces used to indent descriptions, example text, and titles (default `2`).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display.indent]
|
||||||
|
base = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### `indent.command`
|
||||||
|
|
||||||
|
Specifies the number of spaces used to indent example code lines (default `6`).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display.indent]
|
||||||
|
command = 6
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also configure both subkeys in a single line like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
indent = {
|
||||||
|
base = 2,
|
||||||
|
command = 6,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `placeholder_format`
|
||||||
|
|
||||||
|
Display the short and/or long variants of placeholders, if available.
|
||||||
|
Possible values: `"short"`, `"long"`, or `"both"` (default `"long"`).
|
||||||
|
This behavior can be overridden with the `--short-options` and `--long-options` flags.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
# Display only short variants
|
||||||
|
placeholder_format = "short"
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, when displaying the builtin page with `tldr tealdeer`, the `-f` / `--render` flag is displayed as follows:
|
||||||
|
- `-f`, if `placeholder_format = "short"`
|
||||||
|
- `--render`, if `placeholder_format = "long"`
|
||||||
|
- `[-f|--render]`, if `placeholder_format = "both"`
|
||||||
33
docs/src/config_search.md
Normal file
33
docs/src/config_search.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Section: \[search\]
|
||||||
|
|
||||||
|
This config section is used to configure the page search in the cache.
|
||||||
|
The settings apply to `tldr <page>` and `tldr --list`.
|
||||||
|
|
||||||
|
## `languages`
|
||||||
|
|
||||||
|
The list of languages that should be considered when searching.
|
||||||
|
If unspecified, the list of languages will be inferred from the `LANG` and `LANGUAGE` environment variables.
|
||||||
|
Either way, the language used can be overwritten using the `--language` command line flag.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[search]
|
||||||
|
# Show pages in German if available, otherwise show in English
|
||||||
|
languages = ["de", "en"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `platforms`
|
||||||
|
|
||||||
|
The list of platforms that should be considered when searching.
|
||||||
|
In addition to the platforms listed in the help text of the `--platform` flag, there are two special platforms available:
|
||||||
|
- `"current"`: equals the platform that tealdeer was compiled for
|
||||||
|
- `"all"`: adds all remaining platforms to the list
|
||||||
|
|
||||||
|
Tealdeer searches the platforms in order of appearance in this list.
|
||||||
|
The default list of platforms is `["current", "common", "all"]`.
|
||||||
|
The list of platforms can be overwritten using the `--platform` command line flag.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[search]
|
||||||
|
# Search for linux and common, and then search windows before trying the remaining platforms
|
||||||
|
platforms = ["linux", "common", "windows", "all"]
|
||||||
|
```
|
||||||
47
docs/src/config_style.md
Normal file
47
docs/src/config_style.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Section: \[style\]
|
||||||
|
|
||||||
|
Using the config file, the style (e.g. colors or underlines) can be customized.
|
||||||
|
|
||||||
|
<img src="screenshot-custom.png" alt="Screenshot of customized version" width="600">
|
||||||
|
|
||||||
|
## Style Targets
|
||||||
|
|
||||||
|
- `description`: The initial description text
|
||||||
|
- `command_name`: The command name as part of the example code
|
||||||
|
- `example_text`: The text that describes an example
|
||||||
|
- `example_code`: The example itself (except the `command_name` and `example_variable`)
|
||||||
|
- `example_variable`: The variables (placeholders) in the example
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- `foreground` (color string, ANSI code, or RGB, see below)
|
||||||
|
- `background` (color string, ANSI code, or RGB, see below)
|
||||||
|
- `underline` (`true` or `false`)
|
||||||
|
- `bold` (`true` or `false`)
|
||||||
|
- `italic` (`true` or `false`)
|
||||||
|
|
||||||
|
Colors can be specified in one of three ways:
|
||||||
|
|
||||||
|
- Color string (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`):
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
foreground = "green"
|
||||||
|
```
|
||||||
|
|
||||||
|
- 256 color ANSI code (*tealdeer v1.5.0+*)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
foreground = { ansi = 4 }
|
||||||
|
```
|
||||||
|
|
||||||
|
- 24-bit RGB color (*tealdeer v1.5.0+*)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
background = { rgb = { r = 255, g = 255, b = 255 } }
|
||||||
|
```
|
||||||
91
docs/src/config_updates.md
Normal file
91
docs/src/config_updates.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Section: \[updates\]
|
||||||
|
|
||||||
|
This config section contains settings related to updating the tealdeer cache.
|
||||||
|
|
||||||
|
## Automatic updates
|
||||||
|
|
||||||
|
Tealdeer can refresh the cache automatically when it is outdated. This
|
||||||
|
behavior can be configured in the `updates` section and is disabled by
|
||||||
|
default.
|
||||||
|
|
||||||
|
### `auto_update`
|
||||||
|
|
||||||
|
Specifies whether the auto-update feature should be enabled (defaults to
|
||||||
|
`false`).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[updates]
|
||||||
|
auto_update = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `auto_update_interval_hours`
|
||||||
|
|
||||||
|
Duration, since the last cache update, after which the cache will be
|
||||||
|
refreshed (defaults to 720 hours). This parameter is ignored if `auto_update`
|
||||||
|
is set to `false`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[updates]
|
||||||
|
auto_update = true
|
||||||
|
auto_update_interval_hours = 24
|
||||||
|
```
|
||||||
|
|
||||||
|
### `warn_cache_age`
|
||||||
|
|
||||||
|
Controls when a warning is printed if the cache has not been updated in a while.
|
||||||
|
By default, the warning is shown once the cache is older than 30 days. Set this
|
||||||
|
to `"never"` to silence the warning. This is useful if, for some reason, the
|
||||||
|
modification time does not reflect its actual age.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[updates]
|
||||||
|
warn_cache_age = "never"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Download configuration
|
||||||
|
|
||||||
|
### `download_languages`
|
||||||
|
|
||||||
|
The list of languages which should be downloaded when updating.
|
||||||
|
If unspecified, the languages listed in the `search.languages` setting are used.
|
||||||
|
Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default.
|
||||||
|
Either way, the language used can be overwritten using the `--language` command line flag.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[search]
|
||||||
|
languages = ["de", "en"]
|
||||||
|
|
||||||
|
[updates]
|
||||||
|
# sometimes I like to read the Italian description
|
||||||
|
download_languages = ["de", "en", "it"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `archive_source`
|
||||||
|
|
||||||
|
URL for the location of the tldr pages archive. By default the pages are
|
||||||
|
fetched from the latest `tldr-pages/tldr` GitHub release.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[updates]
|
||||||
|
archive_source = "https://my-company.example.com/tldr/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls_backend`
|
||||||
|
|
||||||
|
Specifies which TLS backend to use. Try changing this setting if you encounter certificate errors.
|
||||||
|
|
||||||
|
Available options:
|
||||||
|
- `rustls-with-native-roots` - [Rustls][rustls] (a TLS library in Rust) with native roots
|
||||||
|
- `rustls-with-webpki-roots` - Rustls with [WebPKI][rustls-webpki] roots
|
||||||
|
- `native-tls` - Native TLS
|
||||||
|
- SChannel on Windows
|
||||||
|
- Secure Transport on macOS
|
||||||
|
- OpenSSL on other platforms
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[updates]
|
||||||
|
tls_backend = "native-tls"
|
||||||
|
```
|
||||||
|
|
||||||
|
[rustls]: https://github.com/rustls/rustls
|
||||||
|
[rustls-webpki]: https://github.com/rustls/webpki
|
||||||
BIN
docs/src/deer.png
Normal file
BIN
docs/src/deer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
52
docs/src/deer.svg
Normal file
52
docs/src/deer.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 409 KiB |
74
docs/src/installing.md
Normal file
74
docs/src/installing.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Installing
|
||||||
|
|
||||||
|
There are a few different ways to install tealdeer:
|
||||||
|
|
||||||
|
- Through [package managers](#package-managers)
|
||||||
|
- Through [static binaries](#static-binaries-linux)
|
||||||
|
- Through [cargo install](#through-cargo-install)
|
||||||
|
- By [building from source](#build-from-source)
|
||||||
|
|
||||||
|
Additionally, when not using system packages, you can [manually install
|
||||||
|
autocompletions](#autocompletion).
|
||||||
|
|
||||||
|
## Package Managers
|
||||||
|
|
||||||
|
Tealdeer has been added to a few package managers:
|
||||||
|
|
||||||
|
- Arch Linux: [`tealdeer`](https://archlinux.org/packages/extra/x86_64/tealdeer/)
|
||||||
|
- Debian: [`tealdeer`](https://tracker.debian.org/tealdeer)
|
||||||
|
- Fedora: [`tealdeer`](https://src.fedoraproject.org/rpms/rust-tealdeer)
|
||||||
|
- FreeBSD: [`sysutils/tealdeer`](https://www.freshports.org/sysutils/tealdeer/)
|
||||||
|
- Funtoo: [`app-misc/tealdeer`](https://github.com/funtoo/core-kit/tree/1.4-release/app-misc/tealdeer)
|
||||||
|
- Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer)
|
||||||
|
- MacPorts: [`tealdeer`](https://ports.macports.org/port/tealdeer/)
|
||||||
|
- NetBSD: [`sysutils/tealdeer`](https://pkgsrc.se/sysutils/tealdeer)
|
||||||
|
- Nix: [`tealdeer`](https://search.nixos.org/packages?query=tealdeer)
|
||||||
|
- openSUSE: [`tealdeer`](https://software.opensuse.org/package/tealdeer?search_term=tealdeer)
|
||||||
|
- Scoop: [`tealdeer`](https://github.com/ScoopInstaller/Main/blob/master/bucket/tealdeer.json)
|
||||||
|
- Solus: [`tealdeer`](https://packages.getsol.us/shannon/t/tealdeer/)
|
||||||
|
- Void Linux: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer)
|
||||||
|
|
||||||
|
## Static Binaries (Linux)
|
||||||
|
|
||||||
|
Static binary builds (currently for Linux only) are available on the
|
||||||
|
[GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases).
|
||||||
|
Simply download the binary for your platform and run it!
|
||||||
|
|
||||||
|
## Through `cargo install`
|
||||||
|
|
||||||
|
Build and install the tool via cargo...
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo install tealdeer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build From Source
|
||||||
|
|
||||||
|
Release build:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Release build with native TLS support:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo build --release --features native-tls
|
||||||
|
```
|
||||||
|
|
||||||
|
Debug build with logging support:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo build --features logging
|
||||||
|
```
|
||||||
|
|
||||||
|
(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.)
|
||||||
|
|
||||||
|
## Autocompletion
|
||||||
|
|
||||||
|
Shell completion scripts are located in the folder `completion`.
|
||||||
|
Just copy them to their designated location:
|
||||||
|
|
||||||
|
- *Bash*: `cp completion/bash_tealdeer /usr/share/bash-completion/completions/tldr`
|
||||||
|
- *Fish*: `cp completion/fish_tealdeer ~/.config/fish/completions/tldr.fish`
|
||||||
|
- *Zsh*: `cp completion/zsh_tealdeer /usr/share/zsh/site-functions/_tldr`
|
||||||
14
docs/src/intro.md
Normal file
14
docs/src/intro.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Tealdeer: Introduction
|
||||||
|
|
||||||
|
Tealdeer is a very fast implementation of
|
||||||
|
[tldr](https://github.com/tldr-pages/tldr) in Rust: Simplified, example based
|
||||||
|
and community-driven man pages.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This documentation shows how to install, use and configure tealdeer.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [GitHub Project Page](https://github.com/tealdeer-rs/tealdeer)
|
||||||
|
- [TLDR Pages Project](https://tldr.sh/)
|
||||||
BIN
docs/src/screenshot-custom.png
Normal file
BIN
docs/src/screenshot-custom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
BIN
docs/src/screenshot-default.png
Normal file
BIN
docs/src/screenshot-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
52
docs/src/tips_and_tricks.md
Normal file
52
docs/src/tips_and_tricks.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Tips and Tricks
|
||||||
|
|
||||||
|
This page features some example use cases of Tealdeer.
|
||||||
|
|
||||||
|
## Showing a random page on shell start
|
||||||
|
|
||||||
|
To display a randomly selected page, you can invoke `tldr` twice: One time to
|
||||||
|
select a page and a second time to display this page. To randomly select a page,
|
||||||
|
we use `shuf` from the GNU coreutils:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tldr --quiet $(tldr --quiet --list | shuf -n1)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also add the above command to your `.bashrc` (or similar shell
|
||||||
|
configuration file) to display a random page every time you start a new shell
|
||||||
|
session.
|
||||||
|
|
||||||
|
## Displaying all pages with their summary
|
||||||
|
|
||||||
|
If you want to extend the output of `tldr --list` with the first line summary of
|
||||||
|
each page, you can run the following Python script:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
commands = subprocess.run(
|
||||||
|
["tldr", "--quiet", "--list"],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
).stdout.splitlines()
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
output = subprocess.run(
|
||||||
|
["tldr", "--quiet", command],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
).stdout
|
||||||
|
description = output.lstrip().split("\n\n")[0]
|
||||||
|
description = " ".join(description.split())
|
||||||
|
print(f"{command} => {description}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that there are a lot of pages and the script will run Tealdeer once for
|
||||||
|
every page, so the script may take a couple of seconds to finish.
|
||||||
|
|
||||||
|
## Extending this chapter
|
||||||
|
|
||||||
|
If you have an interesting setup with Tealdeer, feel free to share your
|
||||||
|
configuration on [our Github repository](https://github.com/tealdeer-rs/tealdeer).
|
||||||
10
docs/src/usage.md
Normal file
10
docs/src/usage.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Tealdeer is straightforward to use, through the binary named `tldr`.
|
||||||
|
|
||||||
|
You can view the available options using `tldr --help`:
|
||||||
|
|
||||||
|
<!-- Note: To update the file below, run `cargo run -- --help > docs/src/usage.txt`. -->
|
||||||
|
```
|
||||||
|
{{#include usage.txt}}
|
||||||
|
```
|
||||||
38
docs/src/usage.txt
Normal file
38
docs/src/usage.txt
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
tealdeer 1.8.1: A fast TLDR client
|
||||||
|
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
|
||||||
|
|
||||||
|
Usage: tldr [OPTIONS] [COMMAND]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[COMMAND]... The command to show (e.g. `tar` or `git log`)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-l, --list List all commands in the cache
|
||||||
|
--edit-page Edit custom page with `EDITOR`
|
||||||
|
--edit-patch Edit custom patch with `EDITOR`
|
||||||
|
-f, --render <FILE> Render a specific markdown file
|
||||||
|
-p, --platform <PLATFORM> Override the operating system, can be specified multiple times
|
||||||
|
in order of preference [possible values: linux, macos, sunos,
|
||||||
|
windows, android, freebsd, netbsd, openbsd, common]
|
||||||
|
-L, --language <LANGUAGE> Override the language
|
||||||
|
-u, --update Update the local cache
|
||||||
|
--no-auto-update If auto update is configured, disable it for this run
|
||||||
|
-c, --clear-cache Clear the local cache
|
||||||
|
--config-path <FILE> Override config file location
|
||||||
|
--override-config <OVERRIDE> Override config values after reading config file (example:
|
||||||
|
`updates.auto_update = true`)
|
||||||
|
--pager Use a pager to page output
|
||||||
|
-r, --raw Display the raw markdown instead of rendering it
|
||||||
|
-q, --quiet Suppress informational messages
|
||||||
|
--show-paths Show file and directory paths used by tealdeer
|
||||||
|
--seed-config Create a basic config
|
||||||
|
--color <WHEN> Control whether to use color [possible values: always, auto,
|
||||||
|
never]
|
||||||
|
--short-options Display the short variants of placeholders
|
||||||
|
--long-options Display the long variants of placeholders
|
||||||
|
-v, --version Print the version
|
||||||
|
-h, --help Print help
|
||||||
|
|
||||||
|
To view the user documentation, please visit https://docs.tealdeer.org.
|
||||||
|
|
||||||
|
To view usage examples, run tldr tldr or tldr tealdeer.
|
||||||
58
docs/src/usage_custom_pages.md
Normal file
58
docs/src/usage_custom_pages.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Custom Pages and Patches
|
||||||
|
|
||||||
|
> ⚠️ **Breaking change in version 1.7.0:** The file name extension for custom
|
||||||
|
> pages and patches was changed:
|
||||||
|
>
|
||||||
|
> - `<name>.page` → `<name>.page.md`
|
||||||
|
> - `<name>.patch` → `<name>.patch.md`
|
||||||
|
>
|
||||||
|
> If you have custom pages or patches, you need to rename them.
|
||||||
|
|
||||||
|
Tealdeer allows creating new custom pages, overriding existing pages as well as
|
||||||
|
extending existing pages.
|
||||||
|
|
||||||
|
The directory, where these custom pages and patches can be placed, follows OS
|
||||||
|
conventions. On Linux for instance, the default location is
|
||||||
|
`~/.local/share/tealdeer/pages/`. To print the path used on your system, simply
|
||||||
|
run `tldr --show-paths`.
|
||||||
|
|
||||||
|
The custom pages directory can be [overridden by the config
|
||||||
|
file](config_directories.html).
|
||||||
|
|
||||||
|
## Custom Pages
|
||||||
|
|
||||||
|
To document internal command line tools, or if you want to replace an existing
|
||||||
|
tldr page with one that's better suited for you, place a file with the name
|
||||||
|
`<command>.page.md` in the custom pages directory. When calling `tldr <command>`,
|
||||||
|
your custom page will be shown instead of the upstream version in the cache.
|
||||||
|
|
||||||
|
Path:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
$CUSTOM_PAGES_DIR/<command>.page.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
~/.local/share/tealdeer/pages/ufw.page.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Patches
|
||||||
|
|
||||||
|
Sometimes you don't want to fully replace an existing upstream page, but just
|
||||||
|
want to extend it with your own examples that you frequently need. In this
|
||||||
|
case, use a file called `<command>.patch.md`, it will be appended to existing
|
||||||
|
pages.
|
||||||
|
|
||||||
|
Path:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
$CUSTOM_PAGES_DIR/<command>.patch.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
~/.local/share/tealdeer/pages/ufw.patch.md
|
||||||
|
```
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
#
|
|
||||||
# Completions for the tealdeer implementation of tldr
|
|
||||||
# https://github.com/dbrgn/tealdeer/
|
|
||||||
#
|
|
||||||
|
|
||||||
complete -c tldr -s h -l help -d 'Print the help message.' -f
|
|
||||||
complete -c tldr -s v -l version -d 'Show version information.' -f
|
|
||||||
complete -c tldr -s l -l list -d 'List all commands in the cache.' -f
|
|
||||||
complete -c tldr -s f -l render -d 'Render a specific markdown file.' -r
|
|
||||||
complete -c tldr -s o -l os -d 'Override the operating system.' -xa 'linux osx sunos other'
|
|
||||||
complete -c tldr -s u -l update -d 'Update the local cache.' -f
|
|
||||||
complete -c tldr -s c -l clear-cache -d 'Clear the local cache.' -f
|
|
||||||
complete -c tldr -s q -l quiet -d 'Suppress informational messages.' -f
|
|
||||||
complete -c tldr -l config-path -d 'Show config file path.' -f
|
|
||||||
complete -c tldr -l seed-config -d 'Create a basic config.' -f
|
|
||||||
|
|
||||||
function __tealdeer_entries
|
|
||||||
tldr --list | sed -e 's/, /\n/g'
|
|
||||||
end
|
|
||||||
|
|
||||||
complete -f -c tldr -a '(__tealdeer_entries)'
|
|
||||||
42
pages/tealdeer.md
Normal file
42
pages/tealdeer.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# tldr
|
||||||
|
|
||||||
|
> This is a builtin page that shows information for your installed tealdeer version.
|
||||||
|
> More information: <https://docs.tealdeer.org>.
|
||||||
|
|
||||||
|
> This page shows tealdeer specific functionality. See tldr tldr for more examples.
|
||||||
|
|
||||||
|
- Render a local markdown file as a tldr page:
|
||||||
|
|
||||||
|
`tldr {{[-f|--render]}} {{path/to/file.md}}`
|
||||||
|
|
||||||
|
- Show the raw markdown source of a page instead of rendering it:
|
||||||
|
|
||||||
|
`tldr {{[-r|--raw]}} {{command}}`
|
||||||
|
|
||||||
|
- Show file and directory paths used by tealdeer:
|
||||||
|
|
||||||
|
`tldr --show-paths`
|
||||||
|
|
||||||
|
- Create an initial config file:
|
||||||
|
|
||||||
|
`tldr --seed-config`
|
||||||
|
|
||||||
|
- Override config file location:
|
||||||
|
|
||||||
|
`tldr --config-path <FILE>`
|
||||||
|
|
||||||
|
- Open a custom page for a command in `$EDITOR` (creates it if it doesn't exist):
|
||||||
|
|
||||||
|
`tldr --edit-page {{command}}`
|
||||||
|
|
||||||
|
- Open a custom patch for a command in `$EDITOR` (appended to the existing page):
|
||||||
|
|
||||||
|
`tldr --edit-patch {{command}}`
|
||||||
|
|
||||||
|
- Clear the local cache:
|
||||||
|
|
||||||
|
`tldr {{[-c|--clear-cache]}}`
|
||||||
|
|
||||||
|
- If auto update is configured, disable it for this run:
|
||||||
|
|
||||||
|
`tldr --no-auto-update`
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
VERSION=$(grep '^version = ' Cargo.toml | sed 's/.*"\([0-9\.]*\)".*/\1/')
|
|
||||||
GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA
|
|
||||||
|
|
||||||
declare -a targets=(
|
|
||||||
"x86_64-musl"
|
|
||||||
"i686-musl"
|
|
||||||
"armv7-musleabihf"
|
|
||||||
"arm-musleabi"
|
|
||||||
"arm-musleabihf"
|
|
||||||
)
|
|
||||||
|
|
||||||
declare -a rusttargets=(
|
|
||||||
"x86_64-unknown-linux-musl"
|
|
||||||
"i686-unknown-linux-musl"
|
|
||||||
"armv7-unknown-linux-musleabihf"
|
|
||||||
"arm-unknown-linux-musleabi"
|
|
||||||
"arm-unknown-linux-musleabihf"
|
|
||||||
)
|
|
||||||
|
|
||||||
function docker-download {
|
|
||||||
echo "==> Downloading Docker image: messense/rust-musl-cross:$1"
|
|
||||||
docker pull messense/rust-musl-cross:$1
|
|
||||||
}
|
|
||||||
|
|
||||||
function docker-build {
|
|
||||||
echo "==> Building target: $1"
|
|
||||||
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$1 cargo build --release
|
|
||||||
}
|
|
||||||
|
|
||||||
echo -e "==> Version $VERSION\n"
|
|
||||||
|
|
||||||
for target in ${targets[@]}; do docker-download $target; done
|
|
||||||
echo ""
|
|
||||||
for target in ${targets[@]}; do docker-build $target; done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
rm -rf "dist-$VERSION"
|
|
||||||
mkdir "dist-$VERSION"
|
|
||||||
|
|
||||||
for i in ${!targets[@]}; do
|
|
||||||
echo "==> Copying ${targets[$i]}"
|
|
||||||
cp "target/${rusttargets[$i]}/release/tldr" "dist-$VERSION/tldr-${targets[$i]}"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for target in ${targets[@]}; do
|
|
||||||
echo "==> Stripping $target"
|
|
||||||
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$target musl-strip -s /home/rust/src/dist-$VERSION/tldr-$target
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for target in ${targets[@]}; do
|
|
||||||
echo "==> Signing $target"
|
|
||||||
gpg -a --output "dist-$VERSION/tldr-$target.sig" --detach-sig "dist-$VERSION/tldr-$target"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Done."
|
|
||||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty file, use defaults and disregard global settings
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB |
8
scripts/get-mdbook.sh
Executable file
8
scripts/get-mdbook.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
wget -O mdbook.tar.gz https://github.com/rust-lang/mdBook/releases/download/v0.5.4/mdbook-v0.5.4-x86_64-unknown-linux-musl.tar.gz
|
||||||
|
echo "5222beabd3e37dc5be0d18ff99b79058469354db5c220153a1b92db5ba12be89 mdbook.tar.gz" > sha256sums
|
||||||
|
sha256sum --check sha256sums
|
||||||
|
tar xvf mdbook.tar.gz
|
||||||
88
scripts/upload-asset.sh
Normal file
88
scripts/upload-asset.sh
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Upload artifacts to GitHub Actions.
|
||||||
|
#
|
||||||
|
# Based on: https://gist.github.com/schell/2fe896953b6728cc3c5d8d5f9f3a17a3
|
||||||
|
#
|
||||||
|
# Requires curl and jq on PATH
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# token: GitHub API user token
|
||||||
|
# repo: GitHub username/reponame
|
||||||
|
# tag: Name of the tag for which to create a release
|
||||||
|
# description: Release description
|
||||||
|
create_release() {
|
||||||
|
# Args
|
||||||
|
token=$1
|
||||||
|
repo=$2
|
||||||
|
tag=$3
|
||||||
|
description=$4
|
||||||
|
echo "Creating release:"
|
||||||
|
echo " repo=$repo"
|
||||||
|
echo " tag=$tag"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
http_code=$(
|
||||||
|
curl -s -o create.json -w '%{http_code}' \
|
||||||
|
--header "Accept: application/vnd.github.v3+json" \
|
||||||
|
--header "Authorization: Bearer $token" \
|
||||||
|
--header "Content-Type:application/json" \
|
||||||
|
"https://api.github.com/repos/$repo/releases" \
|
||||||
|
-d '{"tag_name":"'"$tag"'","name":"'"${tag/v/Version }"'","draft":true,"body":"'"${description/\"/\\\"}"'"}'
|
||||||
|
)
|
||||||
|
if [ "$http_code" == "201" ]; then
|
||||||
|
echo "Release for tag $tag created."
|
||||||
|
else
|
||||||
|
echo "Asset upload failed with code '$http_code'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# token: GitHub API user token
|
||||||
|
# repo: GitHub username/reponame
|
||||||
|
# tag: Name of the tag for which to upload the assets
|
||||||
|
# file: Path to the asset file to upload
|
||||||
|
# name: Name to use for the uploaded asset
|
||||||
|
upload_release_file() {
|
||||||
|
# Args
|
||||||
|
token=$1
|
||||||
|
repo=$2
|
||||||
|
tag=$3
|
||||||
|
file=$4
|
||||||
|
name=$5
|
||||||
|
echo "Uploading:"
|
||||||
|
echo " repo=$repo"
|
||||||
|
echo " tag=$tag"
|
||||||
|
echo " file=$file"
|
||||||
|
echo " name=$name"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Determine upload URL of latest draft release for the specified tag
|
||||||
|
upload_url=$(
|
||||||
|
curl -s \
|
||||||
|
--header "Accept: application/vnd.github.v3+json" \
|
||||||
|
--header "Authorization: Bearer $token" \
|
||||||
|
"https://api.github.com/repos/$repo/releases" \
|
||||||
|
| jq -r '[.[] | select(.tag_name == "'"$tag"'" and .draft)][0].upload_url' \
|
||||||
|
| cut -d"{" -f'1'
|
||||||
|
)
|
||||||
|
echo "Determined upload URL: $upload_url"
|
||||||
|
http_code=$(
|
||||||
|
curl -s -o upload.json -w '%{http_code}' \
|
||||||
|
--request POST \
|
||||||
|
--header "Accept: application/vnd.github.v3+json" \
|
||||||
|
--header "Authorization: Bearer $token" \
|
||||||
|
--header "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$file" "$upload_url?name=$name"
|
||||||
|
)
|
||||||
|
if [ "$http_code" == "201" ]; then
|
||||||
|
echo "Asset $name uploaded:"
|
||||||
|
jq -r .browser_download_url upload.json
|
||||||
|
else
|
||||||
|
echo "Asset upload failed with code '$http_code':"
|
||||||
|
cat upload.json
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
648
src/cache.rs
648
src/cache.rs
|
|
@ -1,157 +1,239 @@
|
||||||
#[cfg(feature = "networking")]
|
use std::{
|
||||||
use std::borrow::Cow;
|
fs::{self, File},
|
||||||
use std::env;
|
io::{Cursor, ErrorKind, Read},
|
||||||
use std::fs;
|
path::{Path, PathBuf},
|
||||||
use std::io::Read;
|
time::{Duration, SystemTime},
|
||||||
use std::path::{Path, PathBuf};
|
};
|
||||||
|
|
||||||
#[cfg(all(unix, feature = "networking"))]
|
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||||
use std::os::unix::fs::MetadataExt;
|
use log::{debug, info};
|
||||||
|
use ureq::{
|
||||||
|
Agent,
|
||||||
|
http::StatusCode,
|
||||||
|
tls::{RootCerts, TlsConfig, TlsProvider},
|
||||||
|
};
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
use crate::{
|
||||||
use reqwest::{Client, Proxy};
|
config::{Language, TlsBackend},
|
||||||
use flate2::read::GzDecoder;
|
types::PlatformType,
|
||||||
use log::debug;
|
};
|
||||||
use tar::Archive;
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
use time;
|
|
||||||
use walkdir::{DirEntry, WalkDir};
|
|
||||||
use xdg::BaseDirectories;
|
|
||||||
|
|
||||||
use crate::error::TealdeerError::{self, CacheError, UpdateError};
|
pub static TLDR_PAGES_DIR: &str = "tldr-pages";
|
||||||
use crate::types::OsType;
|
|
||||||
|
|
||||||
/// A cache update source.
|
#[derive(Clone)]
|
||||||
#[derive(Debug)]
|
pub struct CacheConfig<'a> {
|
||||||
pub enum Source {
|
pub pages_directory: &'a Path,
|
||||||
/// Load the archive from the file system.
|
pub custom_pages_directory: Option<&'a Path>,
|
||||||
File(PathBuf),
|
pub platforms: &'a [PlatformType],
|
||||||
|
pub search_languages: &'a [Language<'a>],
|
||||||
|
pub download_languages: &'a [Language<'a>],
|
||||||
|
}
|
||||||
|
|
||||||
/// Load the archive from the network.
|
/// The directory backing this cache is checked to be populated at construction.
|
||||||
#[cfg(feature = "networking")]
|
pub struct Cache<'a> {
|
||||||
Url(Cow<'static, str>),
|
config: CacheConfig<'a>,
|
||||||
|
|
||||||
/// No source is defined.
|
|
||||||
#[cfg(not(feature = "networking"))]
|
|
||||||
None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Cache {
|
pub struct PageLookupResult {
|
||||||
/// The cache source. Either an URL or a file path.
|
pub page_path: PathBuf,
|
||||||
source: Source,
|
pub patch_path: Option<PathBuf>,
|
||||||
/// The target OS type.
|
|
||||||
os: OsType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache {
|
impl<'a> Cache<'a> {
|
||||||
pub fn new<S>(source: S, os: OsType) -> Self
|
/// Try opening a cache at the location given by `config.pages_directory`. If no directory
|
||||||
where
|
/// exists at this location, `Ok(None)` is returned.
|
||||||
S: Into<Source>,
|
pub fn open(config: CacheConfig<'a>) -> Result<Option<Self>> {
|
||||||
{
|
match config.pages_directory.metadata() {
|
||||||
Self {
|
Ok(md) => {
|
||||||
source: source.into(),
|
ensure!(
|
||||||
os,
|
md.is_dir(),
|
||||||
|
"Cache directory `{}` exists, but is not a directory.",
|
||||||
|
config.pages_directory.display(),
|
||||||
|
);
|
||||||
|
Ok(Some(Cache { config }))
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
|
||||||
|
Err(err) => Err(anyhow!(err).context(format!(
|
||||||
|
"Error getting metdata of cache directory {}",
|
||||||
|
config.pages_directory.display()
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the path to the cache directory.
|
/// Open an existing cache at `config.pages_directory` or create one if no cache resides at
|
||||||
fn get_cache_dir(&self) -> Result<PathBuf, TealdeerError> {
|
/// this location. In case of success, the return value is a tuple with the `Cache` and a
|
||||||
// Allow overriding the cache directory by setting the
|
/// boolean indicating whether the cache was newly created.
|
||||||
// $TEALDEER_CACHE_DIR env variable.
|
pub fn open_or_create(config: CacheConfig<'a>) -> Result<(Self, bool)> {
|
||||||
if let Ok(value) = env::var("TEALDEER_CACHE_DIR") {
|
if let Some(cache) = Self::open(config.clone())? {
|
||||||
let path = PathBuf::from(value);
|
return Ok((cache, false));
|
||||||
|
}
|
||||||
|
|
||||||
if path.exists() && path.is_dir() {
|
fs::create_dir_all(config.pages_directory).with_context(|| {
|
||||||
return Ok(path);
|
format!(
|
||||||
} else {
|
"Cache directory `{}` cannot be created",
|
||||||
return Err(CacheError(
|
config.pages_directory.display(),
|
||||||
"Path specified by $TEALDEER_CACHE_DIR \
|
)
|
||||||
does not exist or is not a directory."
|
})?;
|
||||||
.into(),
|
eprintln!(
|
||||||
));
|
"Successfully created cache directory `{}`.",
|
||||||
|
config.pages_directory.display(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((Cache { config }, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn age(&self) -> Result<Duration> {
|
||||||
|
let mtime = self.config.pages_directory.metadata()?.modified()?;
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(mtime)
|
||||||
|
.context("Error comparing cache mtime with current time")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_page(&self, command: &str) -> Option<PageLookupResult> {
|
||||||
|
let page_filename = format!("{command}.md");
|
||||||
|
let patch_filename = format!("{command}.patch.md");
|
||||||
|
let custom_filename = format!("{command}.page.md");
|
||||||
|
|
||||||
|
if let Some(custom_pages_dir) = self.config.custom_pages_directory {
|
||||||
|
let custom_page = custom_pages_dir.join(custom_filename);
|
||||||
|
if custom_page.is_file() {
|
||||||
|
return Some(PageLookupResult::with_page(custom_page));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let patch_path = self
|
||||||
|
.config
|
||||||
|
.custom_pages_directory
|
||||||
|
.map(|dir| dir.join(&patch_filename))
|
||||||
|
.filter(|path| path.is_file());
|
||||||
|
|
||||||
|
for &platform in self.config.platforms {
|
||||||
|
for language in self.config.search_languages {
|
||||||
|
let mut search_path = self.config.pages_directory.to_path_buf();
|
||||||
|
search_path.push(language.directory_name());
|
||||||
|
search_path.push(platform.directory_name());
|
||||||
|
search_path.push(&page_filename);
|
||||||
|
|
||||||
|
if search_path.is_file() {
|
||||||
|
return Some(
|
||||||
|
PageLookupResult::with_page(search_path).with_optional_patch(patch_path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_pages(&self) -> Result<impl IntoIterator<Item = String> + use<>> {
|
||||||
|
let mut pages = Vec::new();
|
||||||
|
|
||||||
|
let mut append_all = |directory: &Path, suffix: &str| -> Result<()> {
|
||||||
|
let Ok(file_iter) = fs::read_dir(directory) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in file_iter {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type()?.is_file() {
|
||||||
|
let mut page_path = entry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| anyhow!("Found invalid filename: {:?}", entry.path()))?;
|
||||||
|
|
||||||
|
if page_path.ends_with(suffix) {
|
||||||
|
page_path.truncate(page_path.len() - suffix.len());
|
||||||
|
pages.push(page_path);
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"Skipping page entry not ending in \".md\": {:?}",
|
||||||
|
entry.path(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
// Otherwise, fall back to $XDG_CACHE_HOME/tealdeer.
|
let mut search_path = self.config.pages_directory.to_path_buf();
|
||||||
let xdg_dirs = match BaseDirectories::with_prefix(crate::NAME) {
|
for language in self.config.search_languages {
|
||||||
Ok(dirs) => dirs,
|
search_path.push(language.directory_name());
|
||||||
Err(_) => return Err(CacheError("Could not determine XDG base directory.".into())),
|
for platform in self.config.platforms {
|
||||||
|
search_path.push(platform.directory_name());
|
||||||
|
append_all(&search_path, ".md")?;
|
||||||
|
search_path.pop();
|
||||||
|
}
|
||||||
|
search_path.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(custom_pages_dir) = self.config.custom_pages_directory {
|
||||||
|
append_all(custom_pages_dir, ".page.md")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.sort_unstable();
|
||||||
|
pages.dedup();
|
||||||
|
Ok(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn old_custom_pages_exist(&self) -> Result<bool> {
|
||||||
|
let Some(directory) = self.config.custom_pages_directory else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let Ok(file_iter) = fs::read_dir(directory) else {
|
||||||
|
return Ok(false);
|
||||||
};
|
};
|
||||||
Ok(xdg_dirs.get_cache_home())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the archive from the file system.
|
for entry in file_iter {
|
||||||
fn load_from_file(path: &Path) -> Result<Vec<u8>, TealdeerError> {
|
if let Some(extension) = entry?.path().extension()
|
||||||
let mut f = fs::File::open(path)
|
&& (extension == "page" || extension == "patch")
|
||||||
.map_err(|e| UpdateError(format!("Could not open file: {}", e)))?;
|
{
|
||||||
let mut buf: Vec<u8> = vec![];
|
return Ok(true);
|
||||||
let bytes_read = f.read_to_end(&mut buf)
|
|
||||||
.map_err(|e| UpdateError(format!("Could not read file: {}", e)))?;
|
|
||||||
debug!("{} bytes loaded from filesystem", bytes_read);
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the archive from the network.
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
fn load_from_network(url: &str) -> Result<Vec<u8>, TealdeerError> {
|
|
||||||
let mut builder = Client::builder();
|
|
||||||
if let Ok(ref host) = env::var("HTTP_PROXY") {
|
|
||||||
if let Ok(proxy) = Proxy::http(host) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(ref host) = env::var("HTTPS_PROXY") {
|
|
||||||
if let Ok(proxy) = Proxy::https(host) {
|
Ok(false)
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let client = builder.build().unwrap_or_else(|_| Client::new());
|
|
||||||
let mut resp = client.get(url).send()?;
|
|
||||||
let mut buf: Vec<u8> = vec![];
|
|
||||||
let bytes_downloaded = resp.copy_to(&mut buf)?;
|
|
||||||
debug!("{} bytes downloaded", bytes_downloaded);
|
|
||||||
Ok(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download the archive from the network or load it from a file.
|
pub fn clear(self) -> Result<()> {
|
||||||
#[cfg(feature = "networking")]
|
fs::remove_dir_all(self.config.pages_directory).with_context(|| {
|
||||||
fn load(&self) -> Result<Vec<u8>, TealdeerError> {
|
format!(
|
||||||
match self.source {
|
"Could not remove pages directory at {}",
|
||||||
Source::File(ref path) => Self::load_from_file(path),
|
self.config.pages_directory.display(),
|
||||||
Source::Url(ref url) => Self::load_from_network(url),
|
)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the archive from the file system.
|
/// Download archives for the languages in `self.config().download_languages` and replace the
|
||||||
#[cfg(not(feature = "networking"))]
|
/// pages directory with the newly downloaded pages. As not all languages might have pages
|
||||||
fn load(&self) -> Result<Vec<u8>, TealdeerError> {
|
/// available (for example, `en_US` instead of `en`), an iterator yielding all languages which
|
||||||
match self.source {
|
/// were successfully downloaded is returned.
|
||||||
Source::File(ref path) => Self::load_from_file(path),
|
pub fn update(
|
||||||
Source::None => Err(TealdeerError::UpdateError("No update source defined".into())),
|
&mut self,
|
||||||
}
|
archive_url: &str,
|
||||||
}
|
tls_backend: TlsBackend,
|
||||||
|
) -> Result<impl IntoIterator<Item = Language<'_>> + use<'_>> {
|
||||||
|
let client = Self::build_client(tls_backend);
|
||||||
|
|
||||||
/// Decompress and open the archive
|
// Download everything before deleting anything
|
||||||
fn decompress<R: Read>(&self, reader: R) -> Archive<GzDecoder<R>> {
|
let mut archives = self
|
||||||
Archive::new(GzDecoder::new(reader))
|
.config
|
||||||
}
|
.download_languages
|
||||||
|
.iter()
|
||||||
/// Update the pages cache.
|
.map(|&lang| {
|
||||||
pub fn update(&self) -> Result<(), TealdeerError> {
|
Ok((
|
||||||
// First, load the compressed data
|
lang,
|
||||||
let bytes: Vec<u8> = self.load()?;
|
Self::download(
|
||||||
|
&client,
|
||||||
// Decompress the response body into an `Archive`
|
&format!("{archive_url}/tldr-{}.zip", lang.directory_name()),
|
||||||
let mut archive = self.decompress(&bytes[..]);
|
)?
|
||||||
|
.map(|bytes| ZipArchive::new(Cursor::new(bytes)))
|
||||||
// Determine paths
|
.transpose()?,
|
||||||
let cache_dir = self.get_cache_dir()?;
|
))
|
||||||
|
})
|
||||||
// Make sure that cache directory exists
|
.collect::<Result<Vec<_>>>()?;
|
||||||
debug!("Ensure cache directory {:?} exists", &cache_dir);
|
|
||||||
fs::create_dir_all(&cache_dir)
|
|
||||||
.map_err(|e| UpdateError(format!("Could not create cache directory: {}", e)))?;
|
|
||||||
|
|
||||||
// Clear cache directory
|
// Clear cache directory
|
||||||
// Note: This is not the best solution. Ideally we would download the
|
// Note: This is not the best solution. Ideally we would download the
|
||||||
|
|
@ -159,143 +241,191 @@ impl Cache {
|
||||||
// But renaming a directory doesn't work across filesystems and Rust
|
// But renaming a directory doesn't work across filesystems and Rust
|
||||||
// does not yet offer a recursive directory copying function. So for
|
// does not yet offer a recursive directory copying function. So for
|
||||||
// now, we'll use this approach.
|
// now, we'll use this approach.
|
||||||
self.clear()?;
|
fs::remove_dir_all(self.config.pages_directory)?;
|
||||||
|
fs::create_dir(self.config.pages_directory)?;
|
||||||
|
|
||||||
// Extract archive
|
for (lang, archive) in &mut archives {
|
||||||
archive
|
if let Some(archive) = archive {
|
||||||
.unpack(&cache_dir)
|
info!("Extracting archive for {lang:?}");
|
||||||
.map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?;
|
archive.extract(self.config.pages_directory.join(lang.directory_name()))?;
|
||||||
|
} else {
|
||||||
Ok(())
|
info!("No archive found for {lang:?}");
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(unix, feature = "networking"))]
|
|
||||||
/// Return the number of seconds since the cache directory was last modified.
|
|
||||||
pub fn last_update(&self) -> Option<i64> {
|
|
||||||
if let Ok(cache_dir) = self.get_cache_dir() {
|
|
||||||
if let Ok(metadata) = fs::metadata(cache_dir.join("tldr-master")) {
|
|
||||||
let mtime = metadata.mtime();
|
|
||||||
let now = time::now_utc().to_timespec();
|
|
||||||
return Some(now.sec - mtime);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the platform directory.
|
|
||||||
#[allow(clippy::match_same_arms)]
|
|
||||||
fn get_platform_dir(&self) -> Option<&'static str> {
|
|
||||||
match self.os {
|
|
||||||
OsType::Linux => Some("linux"),
|
|
||||||
OsType::OsX => Some("osx"),
|
|
||||||
OsType::SunOs => None, // TODO: Does Rust support SunOS?
|
|
||||||
OsType::Other => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search for a page and return the path to it.
|
|
||||||
pub fn find_page(&self, name: &str) -> Option<PathBuf> {
|
|
||||||
// Build page file name
|
|
||||||
let page_filename = format!("{}.md", name);
|
|
||||||
|
|
||||||
// Get platform dir
|
|
||||||
let platforms_dir = match self.get_cache_dir() {
|
|
||||||
Ok(cache_dir) => cache_dir.join("tldr-master").join("pages"),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine platform
|
|
||||||
let platform = self.get_platform_dir();
|
|
||||||
|
|
||||||
// Search for the page in the platform specific directory
|
|
||||||
if let Some(pf) = platform {
|
|
||||||
let path = platforms_dir.join(&pf).join(&page_filename);
|
|
||||||
if path.exists() && path.is_file() {
|
|
||||||
return Some(path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If platform is not supported or if platform specific page does not exist,
|
Ok(archives
|
||||||
// look up the page in the "common" directory.
|
|
||||||
let path = platforms_dir.join("common").join(&page_filename);
|
|
||||||
|
|
||||||
// Return it if it exists, otherwise give up and return `None`
|
|
||||||
if path.exists() && path.is_file() {
|
|
||||||
Some(path)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the available pages.
|
|
||||||
pub fn list_pages(&self) -> Result<Vec<String>, TealdeerError> {
|
|
||||||
// Determine platforms directory and platform
|
|
||||||
let cache_dir = self.get_cache_dir()?;
|
|
||||||
let platforms_dir = cache_dir.join("tldr-master").join("pages");
|
|
||||||
let platform_dir = self.get_platform_dir();
|
|
||||||
|
|
||||||
// Closure that allows the WalkDir instance to traverse platform
|
|
||||||
// specific and common page directories, but not others.
|
|
||||||
let should_walk = |entry: &DirEntry| -> bool {
|
|
||||||
let file_type = entry.file_type();
|
|
||||||
let file_name = match entry.file_name().to_str() {
|
|
||||||
Some(name) => name,
|
|
||||||
None => return false,
|
|
||||||
};
|
|
||||||
if file_type.is_dir() {
|
|
||||||
if file_name == "common" {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if let Some(platform) = platform_dir {
|
|
||||||
return file_name == platform;
|
|
||||||
}
|
|
||||||
} else if file_type.is_file() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Recursively walk through common and (if applicable) platform specific directory
|
|
||||||
let mut pages = WalkDir::new(platforms_dir)
|
|
||||||
.min_depth(1) // Skip root directory
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_entry(|e| should_walk(e)) // Filter out pages for other architectures
|
.filter_map(|(lang, archive)| archive.is_some().then_some(lang)))
|
||||||
.filter_map(|e| e.ok()) // Convert results to options, filter out errors
|
|
||||||
.filter_map(|e| {
|
|
||||||
let path = e.path();
|
|
||||||
let extension = &path.extension().and_then(|s| s.to_str()).unwrap_or("");
|
|
||||||
if e.file_type().is_file() && extension == &"md" {
|
|
||||||
path.file_stem()
|
|
||||||
.and_then(|stem| stem.to_str().map(|s| s.into()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
pages.sort();
|
|
||||||
pages.dedup();
|
|
||||||
Ok(pages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the cache directory.
|
pub fn config(&self) -> &CacheConfig<'a> {
|
||||||
pub fn clear(&self) -> Result<(), TealdeerError> {
|
&self.config
|
||||||
let path = self.get_cache_dir()?;
|
}
|
||||||
if path.exists() && path.is_dir() {
|
}
|
||||||
fs::remove_dir_all(&path).map_err(|_| CacheError(format!(
|
|
||||||
"Could not remove cache directory ({}).",
|
impl PageLookupResult {
|
||||||
path.display()
|
pub fn with_page(page_path: PathBuf) -> Self {
|
||||||
)))?;
|
Self {
|
||||||
} else if path.exists() {
|
page_path,
|
||||||
return Err(CacheError(format!(
|
patch_path: None,
|
||||||
"Cache path ({}) is not a directory.",
|
}
|
||||||
path.display()
|
}
|
||||||
)));
|
|
||||||
} else {
|
pub fn with_optional_patch(mut self, patch_path: Option<PathBuf>) -> Self {
|
||||||
return Err(CacheError(format!(
|
self.patch_path = patch_path;
|
||||||
"Cache path ({}) does not exist.",
|
self
|
||||||
path.display()
|
}
|
||||||
)));
|
|
||||||
};
|
/// Create a reader that sequentially reads from the page and the
|
||||||
Ok(())
|
/// patch, as if they were concatenated.
|
||||||
|
///
|
||||||
|
/// This will return an error if either the page file or the patch file
|
||||||
|
/// cannot be opened.
|
||||||
|
pub fn reader(&self) -> Result<Box<dyn Read>> {
|
||||||
|
// Open page file
|
||||||
|
let page_file = File::open(&self.page_path)
|
||||||
|
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
|
||||||
|
|
||||||
|
// Open patch file
|
||||||
|
let patch_file_opt = match &self.patch_path {
|
||||||
|
Some(path) => Some(
|
||||||
|
File::open(path)
|
||||||
|
.with_context(|| format!("Could not open patch file at {}", path.display()))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create chained reader from file(s)
|
||||||
|
//
|
||||||
|
// Note: It might be worthwhile to create our own struct that accepts
|
||||||
|
// the page and patch files and that will read them sequentially,
|
||||||
|
// because it avoids the boxing below. However, the performance impact
|
||||||
|
// would first need to be shown to be significant using a benchmark.
|
||||||
|
Ok(if let Some(patch_file) = patch_file_opt {
|
||||||
|
Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box<dyn Read>
|
||||||
|
} else {
|
||||||
|
Box::new(page_file) as Box<dyn Read>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Language<'_> {
|
||||||
|
fn directory_name(&self) -> String {
|
||||||
|
format!("pages.{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformType {
|
||||||
|
fn directory_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PlatformType::Linux => "linux",
|
||||||
|
PlatformType::OsX => "osx",
|
||||||
|
PlatformType::SunOs => "sunos",
|
||||||
|
PlatformType::Windows => "windows",
|
||||||
|
PlatformType::Android => "android",
|
||||||
|
PlatformType::FreeBsd => "freebsd",
|
||||||
|
PlatformType::NetBsd => "netbsd",
|
||||||
|
PlatformType::OpenBsd => "openbsd",
|
||||||
|
PlatformType::Common => "common",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cache<'_> {
|
||||||
|
fn build_client(tls_backend: TlsBackend) -> Agent {
|
||||||
|
let tls_builder = match tls_backend {
|
||||||
|
#[cfg(feature = "native-tls")]
|
||||||
|
TlsBackend::NativeTls => TlsConfig::builder()
|
||||||
|
.provider(TlsProvider::NativeTls)
|
||||||
|
.root_certs(RootCerts::PlatformVerifier),
|
||||||
|
#[cfg(feature = "rustls-with-webpki-roots")]
|
||||||
|
TlsBackend::RustlsWithWebpkiRoots => TlsConfig::builder()
|
||||||
|
.provider(TlsProvider::Rustls)
|
||||||
|
.root_certs(RootCerts::WebPki),
|
||||||
|
#[cfg(feature = "rustls-with-native-roots")]
|
||||||
|
TlsBackend::RustlsWithNativeRoots => TlsConfig::builder()
|
||||||
|
.provider(TlsProvider::Rustls)
|
||||||
|
.root_certs(RootCerts::PlatformVerifier),
|
||||||
|
};
|
||||||
|
let config = Agent::config_builder()
|
||||||
|
.http_status_as_error(false) // because we want to handle them
|
||||||
|
.tls_config(tls_builder.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
config.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download the archive from the specified URL.
|
||||||
|
fn download(client: &Agent, archive_url: &str) -> Result<Option<Vec<u8>>> {
|
||||||
|
info!("Downloading archive from {archive_url}");
|
||||||
|
let response = client.get(archive_url).call();
|
||||||
|
match response {
|
||||||
|
Ok(response) if response.status().is_success() => {
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
response.into_body().into_reader().read_to_end(&mut buf)?;
|
||||||
|
debug!("{} bytes downloaded", buf.len());
|
||||||
|
Ok(Some(buf))
|
||||||
|
}
|
||||||
|
Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None),
|
||||||
|
_ => {
|
||||||
|
bail!("Could not download tldr pages from {archive_url}: {response:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unit Tests for cache module
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reader_with_patch() {
|
||||||
|
// Write test files
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let page_path = dir.path().join("test.page.md");
|
||||||
|
let patch_path = dir.path().join("test.patch.md");
|
||||||
|
{
|
||||||
|
let mut f1 = File::create(&page_path).unwrap();
|
||||||
|
f1.write_all(b"Hello\n").unwrap();
|
||||||
|
let mut f2 = File::create(&patch_path).unwrap();
|
||||||
|
f2.write_all(b"World").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chained reader from lookup result
|
||||||
|
let lr = PageLookupResult::with_page(page_path).with_optional_patch(Some(patch_path));
|
||||||
|
let mut reader = lr.reader().unwrap();
|
||||||
|
|
||||||
|
// Read into a Vec
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
reader.read_to_end(&mut buf).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(&buf, b"Hello\n\nWorld");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reader_without_patch() {
|
||||||
|
// Write test file
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let page_path = dir.path().join("test.page.md");
|
||||||
|
{
|
||||||
|
let mut f = File::create(&page_path).unwrap();
|
||||||
|
f.write_all(b"Hello\n").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chained reader from lookup result
|
||||||
|
let lr = PageLookupResult::with_page(page_path);
|
||||||
|
let mut reader = lr.reader().unwrap();
|
||||||
|
|
||||||
|
// Read into a Vec
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
reader.read_to_end(&mut buf).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(&buf, b"Hello\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
src/cli.rs
Normal file
124
src/cli.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
//! Definition of the CLI arguments and options.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{ArgGroup, Parser, builder::ArgAction};
|
||||||
|
|
||||||
|
use crate::types::{ColorOptions, PlatformType};
|
||||||
|
|
||||||
|
// Note: flag names are specified explicitly in clap attributes
|
||||||
|
// to improve readability and allow contributors to grep names like "clear-cache"
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
about = "A fast TLDR client",
|
||||||
|
version,
|
||||||
|
disable_version_flag = true,
|
||||||
|
author,
|
||||||
|
help_template = "{before-help}{name} {version}: {about-with-newline}{author-with-newline}
|
||||||
|
{usage-heading} {usage}
|
||||||
|
|
||||||
|
{all-args}{after-help}",
|
||||||
|
after_help = "To view the user documentation, please visit https://docs.tealdeer.org.
|
||||||
|
|
||||||
|
To view usage examples, run tldr tldr or tldr tealdeer.",
|
||||||
|
arg_required_else_help = true,
|
||||||
|
help_expected = true,
|
||||||
|
group = ArgGroup::new("command_or_file").args(&["command", "render"]),
|
||||||
|
)]
|
||||||
|
pub(crate) struct Cli {
|
||||||
|
/// The command to show (e.g. `tar` or `git log`)
|
||||||
|
#[arg(num_args(1..))]
|
||||||
|
pub command: Vec<String>,
|
||||||
|
|
||||||
|
/// List all commands in the cache
|
||||||
|
#[arg(short = 'l', long = "list")]
|
||||||
|
pub list: bool,
|
||||||
|
|
||||||
|
/// Edit custom page with `EDITOR`
|
||||||
|
#[arg(long, requires = "command")]
|
||||||
|
pub edit_page: bool,
|
||||||
|
|
||||||
|
/// Edit custom patch with `EDITOR`
|
||||||
|
#[arg(long, requires = "command", conflicts_with = "edit_page")]
|
||||||
|
pub edit_patch: bool,
|
||||||
|
|
||||||
|
/// Render a specific markdown file
|
||||||
|
#[arg(
|
||||||
|
short = 'f',
|
||||||
|
long = "render",
|
||||||
|
value_name = "FILE",
|
||||||
|
conflicts_with = "command"
|
||||||
|
)]
|
||||||
|
pub render: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Override the operating system, can be specified multiple times in order of preference
|
||||||
|
#[arg(
|
||||||
|
short = 'p',
|
||||||
|
long = "platform",
|
||||||
|
value_name = "PLATFORM",
|
||||||
|
action = ArgAction::Append,
|
||||||
|
)]
|
||||||
|
pub platforms: Option<Vec<PlatformType>>,
|
||||||
|
|
||||||
|
/// Override the language
|
||||||
|
#[arg(short = 'L', long = "language")]
|
||||||
|
pub language: Option<String>,
|
||||||
|
|
||||||
|
/// Update the local cache
|
||||||
|
#[arg(short = 'u', long = "update")]
|
||||||
|
pub update: bool,
|
||||||
|
|
||||||
|
/// If auto update is configured, disable it for this run
|
||||||
|
#[arg(long = "no-auto-update")]
|
||||||
|
pub no_auto_update: bool,
|
||||||
|
|
||||||
|
/// Clear the local cache
|
||||||
|
#[arg(short = 'c', long = "clear-cache")]
|
||||||
|
pub clear_cache: bool,
|
||||||
|
|
||||||
|
/// Override config file location
|
||||||
|
#[arg(long = "config-path", value_name = "FILE")]
|
||||||
|
pub config_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Override config values after reading config file (example: `updates.auto_update = true`)
|
||||||
|
#[arg(long, action = ArgAction::Append, value_name = "OVERRIDE")]
|
||||||
|
pub override_config: Vec<String>,
|
||||||
|
|
||||||
|
/// Use a pager to page output
|
||||||
|
#[arg(long = "pager", requires = "command_or_file")]
|
||||||
|
pub pager: bool,
|
||||||
|
|
||||||
|
/// Display the raw markdown instead of rendering it
|
||||||
|
#[arg(short = 'r', long = "raw", requires = "command_or_file")]
|
||||||
|
pub raw: bool,
|
||||||
|
|
||||||
|
/// Suppress informational messages
|
||||||
|
#[arg(short = 'q', long = "quiet")]
|
||||||
|
pub quiet: bool,
|
||||||
|
|
||||||
|
/// Show file and directory paths used by tealdeer
|
||||||
|
#[arg(long = "show-paths")]
|
||||||
|
pub show_paths: bool,
|
||||||
|
|
||||||
|
/// Create a basic config
|
||||||
|
#[arg(long = "seed-config")]
|
||||||
|
pub seed_config: bool,
|
||||||
|
|
||||||
|
/// Control whether to use color
|
||||||
|
#[arg(long = "color", value_name = "WHEN")]
|
||||||
|
pub color: Option<ColorOptions>,
|
||||||
|
|
||||||
|
/// Display the short variants of placeholders
|
||||||
|
#[arg(long)]
|
||||||
|
pub short_options: bool,
|
||||||
|
|
||||||
|
/// Display the long variants of placeholders
|
||||||
|
#[arg(long)]
|
||||||
|
pub long_options: bool,
|
||||||
|
|
||||||
|
/// Print the version
|
||||||
|
// Note: We override the version flag because clap uses `-V` by default,
|
||||||
|
// while TLDR specification requires `-v` to be used.
|
||||||
|
#[arg(short = 'v', long = "version", action = ArgAction::Version)]
|
||||||
|
pub version: (),
|
||||||
|
}
|
||||||
1143
src/config.rs
1143
src/config.rs
File diff suppressed because it is too large
Load diff
29
src/error.rs
29
src/error.rs
|
|
@ -1,29 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
use reqwest::Error as ReqwestError;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(clippy::pub_enum_variant_names)]
|
|
||||||
pub enum TealdeerError {
|
|
||||||
CacheError(String),
|
|
||||||
ConfigError(String),
|
|
||||||
UpdateError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
impl From<ReqwestError> for TealdeerError {
|
|
||||||
fn from(err: ReqwestError) -> Self {
|
|
||||||
TealdeerError::UpdateError(format!("HTTP error: {}", err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TealdeerError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
TealdeerError::CacheError(e) => write!(f, "CacheError: {}", e),
|
|
||||||
TealdeerError::ConfigError(e) => write!(f, "ConfigError: {}", e),
|
|
||||||
TealdeerError::UpdateError(e) => write!(f, "UpdateError: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
src/extensions.rs
Normal file
33
src/extensions.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
/// An extension trait to clear duplicates from a collection.
|
||||||
|
pub(crate) trait Dedup<T: PartialEq> {
|
||||||
|
fn clear_duplicates(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear duplicates from a collection, keep the first one seen.
|
||||||
|
///
|
||||||
|
/// For small vectors, this will be faster than a `HashSet`.
|
||||||
|
impl<T: PartialEq> Dedup<T> for Vec<T> {
|
||||||
|
fn clear_duplicates(&mut self) {
|
||||||
|
let orig = mem::replace(self, Vec::with_capacity(self.len()));
|
||||||
|
for item in orig {
|
||||||
|
if !self.contains(&item) {
|
||||||
|
self.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `str::find`, but starts searching at `start`.
|
||||||
|
pub(crate) trait FindFrom {
|
||||||
|
fn find_from(&self, needle: &Self, start: usize) -> Option<usize>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindFrom for str {
|
||||||
|
fn find_from(&self, needle: &Self, start: usize) -> Option<usize> {
|
||||||
|
self.get(start..)
|
||||||
|
.and_then(|s| s.find(needle))
|
||||||
|
.map(|i| i + start)
|
||||||
|
}
|
||||||
|
}
|
||||||
575
src/formatter.rs
575
src/formatter.rs
|
|
@ -1,78 +1,543 @@
|
||||||
//! Functions related to formatting and printing lines from a `Tokenizer`.
|
//! Functions related to formatting and printing lines from a `Tokenizer`.
|
||||||
|
|
||||||
use std::io::BufRead;
|
|
||||||
|
|
||||||
use ansi_term::{ANSIString, ANSIStrings};
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::{config::Indent, extensions::FindFrom, types::LineType};
|
||||||
use crate::tokenizer::Tokenizer;
|
|
||||||
use crate::types::LineType;
|
|
||||||
|
|
||||||
fn highlight_command<'a>(
|
#[derive(Debug, Clone, Copy, Eq)]
|
||||||
command: &'a str,
|
/// Represents a snippet from a page of a specific highlighting class.
|
||||||
example_code: &'a str,
|
pub enum PageSnippet<T> {
|
||||||
config: &Config,
|
CommandName(T),
|
||||||
parts: &mut Vec<ANSIString<'a>>,
|
Placeholder(T),
|
||||||
) {
|
PlaceholderVariants { short: T, long: T },
|
||||||
let mut code_part_end_pos = 0;
|
NormalCode(T),
|
||||||
while let Some(command_start) = example_code[code_part_end_pos..].find(&command) {
|
Description(T),
|
||||||
let code_part = &example_code[code_part_end_pos..code_part_end_pos + command_start];
|
Text(T),
|
||||||
parts.push(config.style.example_code.paint(code_part));
|
Title(T),
|
||||||
parts.push(config.style.command_name.paint(command));
|
Indent(usize),
|
||||||
|
Linebreak,
|
||||||
code_part_end_pos += command_start + command.len();
|
|
||||||
}
|
|
||||||
parts.push(
|
|
||||||
config
|
|
||||||
.style
|
|
||||||
.example_code
|
|
||||||
.paint(&example_code[code_part_end_pos..]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format and highlight code examples including variables in {{ curly braces }}.
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
fn format_code(command: &str, text: &str, config: &Config) -> String {
|
impl<T> PageSnippet<T> {
|
||||||
let mut parts = Vec::new();
|
pub fn map<F, U>(self, f: F) -> PageSnippet<U>
|
||||||
for between_variables in text.split("}}") {
|
where
|
||||||
if let Some(variable_start) = between_variables.find("{{") {
|
F: Fn(T) -> U,
|
||||||
let example_code = &between_variables[..variable_start];
|
{
|
||||||
let example_variable = &between_variables[variable_start + 2..];
|
match self {
|
||||||
|
PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)),
|
||||||
highlight_command(&command, &example_code, &config, &mut parts);
|
PageSnippet::Placeholder(s) => PageSnippet::Placeholder(f(s)),
|
||||||
parts.push(config.style.example_variable.paint(example_variable));
|
PageSnippet::PlaceholderVariants { short, long } => PageSnippet::PlaceholderVariants {
|
||||||
} else {
|
short: f(short),
|
||||||
highlight_command(&command, &between_variables, &config, &mut parts);
|
long: f(long),
|
||||||
|
},
|
||||||
|
PageSnippet::NormalCode(s) => PageSnippet::NormalCode(f(s)),
|
||||||
|
PageSnippet::Description(s) => PageSnippet::Description(f(s)),
|
||||||
|
PageSnippet::Text(s) => PageSnippet::Text(f(s)),
|
||||||
|
PageSnippet::Title(s) => PageSnippet::Title(f(s)),
|
||||||
|
PageSnippet::Indent(n) => PageSnippet::Indent(n),
|
||||||
|
PageSnippet::Linebreak => PageSnippet::Linebreak,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ANSIStrings(&parts).to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a token stream to an ANSI terminal.
|
impl<T: PartialEq<U>, U> PartialEq<PageSnippet<U>> for PageSnippet<T> {
|
||||||
pub fn print_lines<R>(tokenizer: &mut Tokenizer<R>, config: &Config)
|
fn eq(&self, other: &PageSnippet<U>) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(PageSnippet::CommandName(s), PageSnippet::CommandName(t))
|
||||||
|
| (PageSnippet::Placeholder(s), PageSnippet::Placeholder(t))
|
||||||
|
| (PageSnippet::NormalCode(s), PageSnippet::NormalCode(t))
|
||||||
|
| (PageSnippet::Description(s), PageSnippet::Description(t))
|
||||||
|
| (PageSnippet::Text(s), PageSnippet::Text(t))
|
||||||
|
| (PageSnippet::Title(s), PageSnippet::Title(t)) => s == t,
|
||||||
|
(
|
||||||
|
PageSnippet::PlaceholderVariants {
|
||||||
|
short: left_short,
|
||||||
|
long: left_long,
|
||||||
|
},
|
||||||
|
PageSnippet::PlaceholderVariants {
|
||||||
|
short: right_short,
|
||||||
|
long: right_long,
|
||||||
|
},
|
||||||
|
) => left_short == right_short && left_long == right_long,
|
||||||
|
(PageSnippet::Indent(n), PageSnippet::Indent(m)) => n == m,
|
||||||
|
(PageSnippet::Linebreak, PageSnippet::Linebreak) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageSnippet<&str> {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
use PageSnippet::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
CommandName(s) | Placeholder(s) | NormalCode(s) | Description(s) | Text(s)
|
||||||
|
| Title(s) => s.is_empty(),
|
||||||
|
PageSnippet::PlaceholderVariants { short, long } => short.is_empty() && long.is_empty(),
|
||||||
|
Indent(n) => *n == 0,
|
||||||
|
Linebreak => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the content of each line yielded by `lines` and yield `HighLightingSnippet`s accordingly.
|
||||||
|
pub fn highlight_lines<L, F, E>(
|
||||||
|
lines: L,
|
||||||
|
process_snippet: &mut F,
|
||||||
|
keep_empty_lines: bool,
|
||||||
|
show_title: bool,
|
||||||
|
indent: Indent,
|
||||||
|
) -> Result<(), E>
|
||||||
where
|
where
|
||||||
R: BufRead,
|
L: Iterator<Item = LineType>,
|
||||||
|
F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>,
|
||||||
{
|
{
|
||||||
let mut command = String::new();
|
let mut command = String::new();
|
||||||
while let Some(token) = tokenizer.next_token() {
|
for line in lines {
|
||||||
match token {
|
match line {
|
||||||
LineType::Empty => println!(),
|
LineType::Empty => {
|
||||||
|
if keep_empty_lines {
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
LineType::Title(title) => {
|
LineType::Title(title) => {
|
||||||
debug!("Ignoring title");
|
if show_title {
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
process_snippet(PageSnippet::Indent(indent.base))?;
|
||||||
|
process_snippet(PageSnippet::Title(&title))?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
} else {
|
||||||
|
debug!("Ignoring title");
|
||||||
|
}
|
||||||
// This is safe as long as the parsed title is only the command,
|
// This is safe as long as the parsed title is only the command,
|
||||||
// and tokenizer yields values in order of appearance.
|
// and the iterator yields values in order of appearance.
|
||||||
command = title;
|
command = title;
|
||||||
debug!("Detected command name: {}", &command);
|
debug!("Detected command name: {command}");
|
||||||
|
}
|
||||||
|
LineType::Description(text) => {
|
||||||
|
process_snippet(PageSnippet::Indent(indent.base))?;
|
||||||
|
process_snippet(PageSnippet::Description(&text))?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
}
|
||||||
|
LineType::ExampleText(text) => {
|
||||||
|
process_snippet(PageSnippet::Indent(indent.base))?;
|
||||||
|
process_snippet(PageSnippet::Text(&text))?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
}
|
}
|
||||||
LineType::Description(text) => println!(" {}", config.style.description.paint(text)),
|
|
||||||
LineType::ExampleText(text) => println!(" {}", config.style.example_text.paint(text)),
|
|
||||||
LineType::ExampleCode(text) => {
|
LineType::ExampleCode(text) => {
|
||||||
println!(" {}", &format_code(&command, &text, &config))
|
process_snippet(PageSnippet::Indent(indent.command))?;
|
||||||
|
highlight_code(&command, &text, process_snippet)?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
LineType::Other(text) => debug!("Unknown line type: {text:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight code examples.
|
||||||
|
/// - parse placeholders (`{{ curly braces }}`)
|
||||||
|
/// - replace escaped placeholder markers (`\{\{` and `\}\}`)
|
||||||
|
fn highlight_code<E>(
|
||||||
|
command: &str,
|
||||||
|
mut text: &str,
|
||||||
|
process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>,
|
||||||
|
) -> Result<(), E> {
|
||||||
|
// We replace escaped placeholder markers at the end so that our replacing does not interfere
|
||||||
|
// with finding the actual markers.
|
||||||
|
// NOTE: This is not optimal, as it allocates one String for each `replace`
|
||||||
|
let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Find placeholder markers and split into code and placeholder accordingly
|
||||||
|
|
||||||
|
let Some(start_marker) = find_marker(text, "{{", r"\{\{") else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let Some(mut end_marker) = find_marker(&text[start_marker + 2..], "}}", r"\}\}") else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
end_marker += start_marker + 2;
|
||||||
|
|
||||||
|
// Greedily extend matched range
|
||||||
|
while end_marker + 2 < text.len() && text.as_bytes()[end_marker + 2] == b'}' {
|
||||||
|
end_marker += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholder_content = &text[start_marker + 2..end_marker];
|
||||||
|
|
||||||
|
if start_marker > 0 {
|
||||||
|
highlight_code_segment(
|
||||||
|
command,
|
||||||
|
&replace_escaped(&text[..start_marker]),
|
||||||
|
process_snippet,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholder_content = replace_escaped(placeholder_content);
|
||||||
|
if let Some(s) = placeholder_content.strip_prefix('[')
|
||||||
|
&& let Some(s) = s.strip_suffix(']')
|
||||||
|
&& let Some((short, long)) = s.split_once('|')
|
||||||
|
{
|
||||||
|
process_snippet(PageSnippet::PlaceholderVariants { short, long })?;
|
||||||
|
} else {
|
||||||
|
process_snippet(PageSnippet::Placeholder(&placeholder_content))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
text = &text[end_marker + 2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
highlight_code_segment(command, &replace_escaped(text), process_snippet)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a "{{" (or "}}") substring that does not overlap with a preceding "\{\{" (or "\}\}").
|
||||||
|
fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option<usize> {
|
||||||
|
let mut search_start = 0;
|
||||||
|
loop {
|
||||||
|
let marker_index = s.find_from(marker, search_start)?;
|
||||||
|
|
||||||
|
let overlaps_with_prefix = (forbidden_prefix.len() <= marker_index + 1) && {
|
||||||
|
let prefix_start = marker_index + 1 - forbidden_prefix.len();
|
||||||
|
// NOTE: The indices might not be valid character offsets, so we should do this
|
||||||
|
// comparison on raw bytes. If prefix_start is indeed not a character offset than the
|
||||||
|
// comparison is guaranteed to return false because forbidden_prefix[0] definitely _is_
|
||||||
|
// the start of a (single byte, ASCII) character.
|
||||||
|
&s.as_bytes()[prefix_start..=marker_index] == forbidden_prefix.as_bytes()
|
||||||
|
};
|
||||||
|
if !overlaps_with_prefix {
|
||||||
|
return Some(marker_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next valid marker cannot include the first character of the current match
|
||||||
|
search_start = marker_index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Yields `NormalCode` and `CommandName` in alternating order according to the occurrences of
|
||||||
|
/// `command_name` in `segment`. Placeholders are not detected here, see `highlight_code`
|
||||||
|
/// instead.
|
||||||
|
fn highlight_code_segment<'a, E>(
|
||||||
|
command_name: &'a str,
|
||||||
|
mut segment: &'a str,
|
||||||
|
process_snippet: &mut impl FnMut(PageSnippet<&'a str>) -> Result<(), E>,
|
||||||
|
) -> Result<(), E> {
|
||||||
|
if !command_name.is_empty() {
|
||||||
|
let mut search_start = 0;
|
||||||
|
while let Some(match_start) = segment.find_from(command_name, search_start) {
|
||||||
|
let match_end = match_start + command_name.len();
|
||||||
|
if is_freestanding_substring(segment, (match_start, match_end)) {
|
||||||
|
process_snippet(PageSnippet::NormalCode(&segment[..match_start]))?;
|
||||||
|
process_snippet(PageSnippet::CommandName(command_name))?;
|
||||||
|
segment = &segment[match_end..];
|
||||||
|
search_start = 0;
|
||||||
|
} else {
|
||||||
|
search_start = segment[match_start..]
|
||||||
|
.char_indices()
|
||||||
|
.nth(1)
|
||||||
|
.map_or(segment.len(), |(i, _)| match_start + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process_snippet(PageSnippet::NormalCode(segment))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the characters right before and after the substring (given by half-open index interval) are whitespace (if they exist).
|
||||||
|
fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bool {
|
||||||
|
let (start, end) = substring;
|
||||||
|
// "okay" meaning <exists and is whitespace> or <doesn't exist>
|
||||||
|
let char_before_is_okay = surrounding[..start]
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.is_none_or(char::is_whitespace);
|
||||||
|
let char_after_is_okay = surrounding[end..]
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_none_or(char::is_whitespace);
|
||||||
|
char_before_is_okay && char_after_is_okay
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_freestanding_substring() {
|
||||||
|
assert!(is_freestanding_substring("I love tldr", (0, 1)));
|
||||||
|
assert!(is_freestanding_substring("I love tldr", (2, 6)));
|
||||||
|
assert!(is_freestanding_substring("I love tldr", (7, 11)));
|
||||||
|
|
||||||
|
assert!(is_freestanding_substring("tldr", (0, 4)));
|
||||||
|
assert!(is_freestanding_substring("tldr ", (0, 4)));
|
||||||
|
assert!(is_freestanding_substring(" tldr", (1, 5)));
|
||||||
|
assert!(is_freestanding_substring(" tldr ", (1, 5)));
|
||||||
|
|
||||||
|
assert!(!is_freestanding_substring("tldr", (1, 3)));
|
||||||
|
assert!(!is_freestanding_substring("tldr ", (1, 4)));
|
||||||
|
assert!(!is_freestanding_substring(" tldr", (1, 4)));
|
||||||
|
|
||||||
|
assert!(is_freestanding_substring(
|
||||||
|
" épicé ",
|
||||||
|
(1, " épicé".len()) // note the missing trailing space
|
||||||
|
));
|
||||||
|
assert!(!is_freestanding_substring(
|
||||||
|
" épicé ",
|
||||||
|
(1, " épic".len()) // note the missing trailing space and character
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<String>> {
|
||||||
|
let mut yielded = Vec::new();
|
||||||
|
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||||
|
if !snip.is_empty() {
|
||||||
|
yielded.push(snip.map(str::to_string));
|
||||||
|
}
|
||||||
|
Ok::<(), ()>(())
|
||||||
|
};
|
||||||
|
|
||||||
|
highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed");
|
||||||
|
yielded
|
||||||
|
}
|
||||||
|
|
||||||
|
mod highlight_code_segment {
|
||||||
|
use super::*;
|
||||||
|
use PageSnippet::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_highlight_code_segment() {
|
||||||
|
assert!(run("make", "").is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
&run("make", "make all CC=clang -q"),
|
||||||
|
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&run("make", " make money --always-make"),
|
||||||
|
&[
|
||||||
|
NormalCode(" "),
|
||||||
|
CommandName("make"),
|
||||||
|
NormalCode(" money --always-make")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&run("git commit", "git commit -m 'git commit'"),
|
||||||
|
&[CommandName("git commit"), NormalCode(" -m 'git commit'"),]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_i18n() {
|
||||||
|
assert_eq!(
|
||||||
|
&run("mäke", "mäke höhlenrätselbücher"),
|
||||||
|
&[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&run(
|
||||||
|
"Müll",
|
||||||
|
"1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
|
||||||
|
),
|
||||||
|
&[
|
||||||
|
NormalCode("1000 Gründe warum "),
|
||||||
|
CommandName("Müll"),
|
||||||
|
NormalCode(" heute größer ist als "),
|
||||||
|
CommandName("Müll"),
|
||||||
|
NormalCode(" früher, ärgerlich")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&run(
|
||||||
|
"übergang",
|
||||||
|
"die Zustandsübergangsfunktion übergang Änderungen",
|
||||||
|
),
|
||||||
|
&[
|
||||||
|
NormalCode("die Zustandsübergangsfunktion "),
|
||||||
|
CommandName("übergang"),
|
||||||
|
NormalCode(" Änderungen")
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_command() {
|
||||||
|
let segment = "some code";
|
||||||
|
let snippets = [NormalCode(segment)];
|
||||||
|
|
||||||
|
assert_eq!(run("", segment), snippets);
|
||||||
|
assert_eq!(run(" ", segment), snippets);
|
||||||
|
assert_eq!(run(" \t ", segment), snippets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod placeholders {
|
||||||
|
use super::*;
|
||||||
|
use PageSnippet::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_vs_escaped() {
|
||||||
|
assert_eq!(
|
||||||
|
run("ping", "ping {{example.com}}"),
|
||||||
|
[
|
||||||
|
CommandName("ping"),
|
||||||
|
NormalCode(" "),
|
||||||
|
Placeholder("example.com"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
run(
|
||||||
|
"docker inspect",
|
||||||
|
r"docker inspect --format '\{\{range.NetworkSettings.Networks\}\}\{\{.IPAddress\}\}\{\{end\}\}' {{container}}"
|
||||||
|
),
|
||||||
|
[
|
||||||
|
CommandName("docker inspect"),
|
||||||
|
NormalCode(
|
||||||
|
" --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "
|
||||||
|
),
|
||||||
|
Placeholder("container"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"),
|
||||||
|
[
|
||||||
|
CommandName("mount"),
|
||||||
|
NormalCode(r" \\"),
|
||||||
|
Placeholder("computer_name"),
|
||||||
|
NormalCode(r"\"),
|
||||||
|
Placeholder("share_name"),
|
||||||
|
NormalCode(" Z:"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(run("", r"\{"), [NormalCode(r"\{")]);
|
||||||
|
assert_eq!(run("", r"\{{a"), [NormalCode(r"\{{a")]);
|
||||||
|
assert_eq!(run("", r"\{{a}}"), [NormalCode(r"\"), Placeholder("a")]);
|
||||||
|
|
||||||
|
// Placeholder has begin marker, but no end marker
|
||||||
|
assert_eq!(run("", r"{{\}\}}"), [NormalCode("{{}}}")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn outer_precedence() {
|
||||||
|
assert_eq!(
|
||||||
|
run("git stash", "git stash show --patch {{stash@{0}}}"),
|
||||||
|
[
|
||||||
|
CommandName("git stash"),
|
||||||
|
NormalCode(" show --patch "),
|
||||||
|
Placeholder("stash@{0}"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// The following is not listed in the specification, but this is the highlighting I would expect.
|
||||||
|
assert_eq!(
|
||||||
|
run("rg", "rg {{}}}"),
|
||||||
|
[CommandName("rg"), NormalCode(" "), Placeholder("}")]
|
||||||
|
);
|
||||||
|
|
||||||
|
// And these are just to document the current behavior
|
||||||
|
assert_eq!(run("", "{{{}}}"), [Placeholder("{}")]);
|
||||||
|
assert_eq!(run("", "{{{{}}}"), [Placeholder("{{}")]);
|
||||||
|
assert_eq!(run("", "{{{}}}}"), [Placeholder("{}}")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_inside_placeholder() {
|
||||||
|
assert_eq!(
|
||||||
|
run(
|
||||||
|
"playerctl",
|
||||||
|
r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""#
|
||||||
|
),
|
||||||
|
[
|
||||||
|
CommandName("playerctl"),
|
||||||
|
NormalCode(" metadata "),
|
||||||
|
PlaceholderVariants {
|
||||||
|
short: "-f",
|
||||||
|
long: "--format"
|
||||||
|
},
|
||||||
|
NormalCode(" \""),
|
||||||
|
Placeholder("Now playing: {{artist}} - {{album}} - {{title}}"),
|
||||||
|
NormalCode("\""),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_inside_escaped() {
|
||||||
|
assert_eq!(
|
||||||
|
run("test", r"test \{\{{{var}} normal\}\}"),
|
||||||
|
[
|
||||||
|
CommandName("test"),
|
||||||
|
NormalCode(" {{"),
|
||||||
|
Placeholder("var"),
|
||||||
|
NormalCode(" normal}}"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Regression test for <https://github.com/tealdeer-rs/tealdeer/issues/473>
|
||||||
|
fn prefix_check_character_boundary() {
|
||||||
|
assert_eq!("Ä".len(), 2);
|
||||||
|
assert_eq!(run("", r"Äxx{{x}}"), [NormalCode("Äxx"), Placeholder("x")],);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod placeholder_variants {
|
||||||
|
use super::*;
|
||||||
|
use PageSnippet::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_marker() {
|
||||||
|
assert_eq!(
|
||||||
|
run("foo", "{{[short|long]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "short",
|
||||||
|
long: "long"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(run("foo", "{{short|long]}}"), [Placeholder("short|long]")]);
|
||||||
|
assert_eq!(run("foo", "{{[short|long}}"), [Placeholder("[short|long")]);
|
||||||
|
assert_eq!(run("foo", "{{[shortlong]}}"), [Placeholder("[shortlong]")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The character `[` is a valid command name
|
||||||
|
#[test]
|
||||||
|
fn command_name_interaction() {
|
||||||
|
for name in ["[", "]", "|"] {
|
||||||
|
assert_eq!(
|
||||||
|
run(name, "{{[short|long]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "short",
|
||||||
|
long: "long"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_variant() {
|
||||||
|
for name in ["[", "]", "|"] {
|
||||||
|
assert_eq!(
|
||||||
|
run(name, "{{[|long]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "",
|
||||||
|
long: "long"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
run(name, "{{[short|]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "short",
|
||||||
|
long: ""
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(run(name, "{{[|]}}"), [] as [PageSnippet::<String>; 0]);
|
||||||
}
|
}
|
||||||
LineType::Other(text) => debug!("Unknown line type: {:?}", text),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
122
src/line_iterator.rs
Normal file
122
src/line_iterator.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
//! Code to split a `BufRead` instance into an iterator of `LineType`s.
|
||||||
|
|
||||||
|
use std::io::{BufRead, Read};
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::types::LineType;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum TldrFormat {
|
||||||
|
/// Not yet clear
|
||||||
|
Undecided,
|
||||||
|
/// The original format
|
||||||
|
V1,
|
||||||
|
/// The new format (see <https://github.com/tldr-pages/tldr/pull/958>)
|
||||||
|
V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `LineIterator` is initialized with a `BufReader` instance that contains the
|
||||||
|
/// entire Tldr page. It then implements `Iterator<Item = LineType>`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LineIterator<R: BufRead> {
|
||||||
|
/// An instance of `R: BufRead`.
|
||||||
|
reader: R,
|
||||||
|
/// Whether the first line has already been processed or not.
|
||||||
|
first_line: bool,
|
||||||
|
/// Buffer for the current line. Used internally.
|
||||||
|
current_line: String,
|
||||||
|
/// The tldr page format.
|
||||||
|
format: TldrFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> LineIterator<R>
|
||||||
|
where
|
||||||
|
R: BufRead,
|
||||||
|
{
|
||||||
|
pub fn new(reader: R) -> Self {
|
||||||
|
Self {
|
||||||
|
reader,
|
||||||
|
first_line: true,
|
||||||
|
current_line: String::new(),
|
||||||
|
format: TldrFormat::Undecided,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: BufRead> Iterator for LineIterator<R> {
|
||||||
|
type Item = LineType;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<LineType> {
|
||||||
|
self.current_line.clear();
|
||||||
|
let bytes_read = self.reader.read_line(&mut self.current_line);
|
||||||
|
match bytes_read {
|
||||||
|
Ok(0) => None,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not read line from reader: {e:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
// Handle new titles
|
||||||
|
if self.first_line {
|
||||||
|
if self.current_line.starts_with('#') {
|
||||||
|
// It's the old format.
|
||||||
|
self.format = TldrFormat::V1;
|
||||||
|
} else {
|
||||||
|
// It's the new format! Drop next line.
|
||||||
|
if let Err(e) = Read::bytes(&mut self.reader)
|
||||||
|
.find(|b| matches!(b, Ok(b'\n') | Err(_)))
|
||||||
|
.transpose()
|
||||||
|
{
|
||||||
|
warn!("Could not read line from reader: {e:?}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.first_line = false;
|
||||||
|
self.format = TldrFormat::V2;
|
||||||
|
return Some(LineType::Title(self.current_line.trim_end().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.first_line = false;
|
||||||
|
|
||||||
|
// Convert line to a `LineType` instance
|
||||||
|
match self.format {
|
||||||
|
TldrFormat::V1 => Some(LineType::from_v1(&self.current_line[..])),
|
||||||
|
TldrFormat::V2 => Some(LineType::from(&self.current_line[..])),
|
||||||
|
TldrFormat::Undecided => panic!("Could not determine page format version"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::LineIterator;
|
||||||
|
use crate::types::LineType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_line_old_format() {
|
||||||
|
let input = "# The Title\n> Description\n";
|
||||||
|
let mut lines = LineIterator::new(input.as_bytes());
|
||||||
|
let title = lines.next().unwrap();
|
||||||
|
assert_eq!(title, LineType::Title("The Title".to_string()));
|
||||||
|
let description = lines.next().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
description,
|
||||||
|
LineType::Description("Description".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_line_new_format() {
|
||||||
|
let input = "The Title\n=========\n> Description\n";
|
||||||
|
let mut lines = LineIterator::new(input.as_bytes());
|
||||||
|
let title = lines.next().unwrap();
|
||||||
|
assert_eq!(title, LineType::Title("The Title".to_string()));
|
||||||
|
let description = lines.next().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
description,
|
||||||
|
LineType::Description("Description".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
706
src/main.rs
706
src/main.rs
|
|
@ -1,6 +1,6 @@
|
||||||
//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust.
|
//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust.
|
||||||
//
|
//
|
||||||
// Copyright (c) 2015-2018 tealdeer developers
|
// Copyright (c) 2015-2021 tealdeer developers
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
|
@ -10,162 +10,136 @@
|
||||||
|
|
||||||
#![deny(clippy::all)]
|
#![deny(clippy::all)]
|
||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
|
#![allow(clippy::enum_glob_use)]
|
||||||
|
#![allow(clippy::module_name_repetitions)]
|
||||||
#![allow(clippy::similar_names)]
|
#![allow(clippy::similar_names)]
|
||||||
#![allow(clippy::stutter)]
|
#![allow(clippy::struct_excessive_bools)]
|
||||||
|
#![allow(clippy::too_many_lines)]
|
||||||
|
#![allow(clippy::unnecessary_debug_formatting)]
|
||||||
|
#![allow(clippy::while_let_loop)]
|
||||||
|
|
||||||
#[cfg(feature = "logging")]
|
#[cfg(not(any(
|
||||||
extern crate env_logger;
|
feature = "native-tls",
|
||||||
|
feature = "rustls-with-webpki-roots",
|
||||||
|
feature = "rustls-with-native-roots",
|
||||||
|
)))]
|
||||||
|
compile_error!(
|
||||||
|
"at least one of the features \"native-tls\", \"rustls-with-webpki-roots\" or \"rustls-with-native-roots\" must be enabled"
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
use std::{
|
||||||
use std::borrow::Cow;
|
env,
|
||||||
use std::fs::File;
|
fs::create_dir_all,
|
||||||
use std::io::BufReader;
|
io::{self, IsTerminal},
|
||||||
use std::path::{Path, PathBuf};
|
path::Path,
|
||||||
use std::process;
|
process::{Command, ExitCode},
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
use anyhow::{Context, Result, anyhow};
|
||||||
use ansi_term::Color;
|
use cache::CacheConfig;
|
||||||
use docopt::Docopt;
|
use clap::Parser;
|
||||||
use serde_derive::Deserialize;
|
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
|
||||||
|
use log::debug;
|
||||||
|
use types::PlatformType;
|
||||||
|
|
||||||
mod cache;
|
mod cache;
|
||||||
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
pub mod extensions;
|
||||||
mod formatter;
|
mod formatter;
|
||||||
mod tokenizer;
|
mod line_iterator;
|
||||||
|
mod output;
|
||||||
mod types;
|
mod types;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use crate::cache::{Cache, Source};
|
use crate::{
|
||||||
use crate::config::{get_config_path, make_default_config, Config};
|
cache::{Cache, PageLookupResult, TLDR_PAGES_DIR},
|
||||||
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
|
cli::Cli,
|
||||||
use crate::formatter::print_lines;
|
config::{
|
||||||
use crate::tokenizer::Tokenizer;
|
Config, PathWithSource, PlaceholderFormat, get_config_dir, make_default_config,
|
||||||
use crate::types::OsType;
|
supported_tls_backends_string,
|
||||||
|
},
|
||||||
|
output::print_page,
|
||||||
|
types::ColorOptions,
|
||||||
|
utils::{print_error, print_warning},
|
||||||
|
};
|
||||||
|
|
||||||
const NAME: &str = "tealdeer";
|
const NAME: &str = "tealdeer";
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
static TEALDEER_PAGE: &str =
|
||||||
const USAGE: &str = "
|
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md"));
|
||||||
tealdeer, a fast tldr implementation written in Rust.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
tldr [options] <command>
|
|
||||||
tldr [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-h --help Show this screen
|
|
||||||
-v --version Show version information
|
|
||||||
-l --list List all commands in the cache
|
|
||||||
-f --render <file> Render a specific markdown file
|
|
||||||
-o --os <type> Override the operating system [linux, osx, sunos]
|
|
||||||
-u --update Update the local cache from the network
|
|
||||||
-U --update-from <src> Update the local cache from the specified URL or path
|
|
||||||
-c --clear-cache Clear the local cache
|
|
||||||
-q --quiet Suppress informational messages
|
|
||||||
--config-path Show config file path
|
|
||||||
--seed-config Create a basic config
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ tldr tar
|
|
||||||
$ tldr --list
|
|
||||||
|
|
||||||
To control the cache:
|
|
||||||
|
|
||||||
$ tldr --update
|
|
||||||
$ tldr --clear-cache
|
|
||||||
|
|
||||||
You can also manually specify the archive to be used for updating the cache:
|
|
||||||
|
|
||||||
$ tldr --update-from https://github.com/tldr-pages/tldr/archive/master.tar.gz
|
|
||||||
$ tldr --update-from master.tar.gz
|
|
||||||
|
|
||||||
To render a local file (for testing):
|
|
||||||
|
|
||||||
$ tldr --render /path/to/file.md
|
|
||||||
";
|
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz";
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
const MAX_CACHE_AGE: i64 = 2_592_000; // 30 days
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Args {
|
|
||||||
arg_command: Option<String>,
|
|
||||||
flag_help: bool,
|
|
||||||
flag_version: bool,
|
|
||||||
flag_list: bool,
|
|
||||||
flag_render: Option<String>,
|
|
||||||
flag_os: Option<OsType>,
|
|
||||||
flag_update: bool,
|
|
||||||
flag_update_from: Option<String>,
|
|
||||||
flag_clear_cache: bool,
|
|
||||||
flag_quiet: bool,
|
|
||||||
flag_config_path: bool,
|
|
||||||
flag_seed_config: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print page by path
|
|
||||||
fn print_page(path: &Path) -> Result<(), String> {
|
|
||||||
// Open file
|
|
||||||
let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?;
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
|
|
||||||
// Look up config file, if none is found fall back to default config.
|
|
||||||
let config = match Config::load() {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(ConfigError(msg)) => {
|
|
||||||
eprintln!("Could not load config: {}", msg);
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Could not load config: {}", e);
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create tokenizer and print output
|
|
||||||
let mut tokenizer = Tokenizer::new(reader);
|
|
||||||
print_lines(&mut tokenizer, &config);
|
|
||||||
|
|
||||||
|
/// Clear the cache
|
||||||
|
fn clear_cache(cache: Cache, quietly: bool) -> Result<()> {
|
||||||
|
let cache_dir = cache.config().pages_directory.display();
|
||||||
|
cache.clear().context("Could not clear cache")?;
|
||||||
|
if !quietly {
|
||||||
|
eprintln!("Successfully cleared cache at `{cache_dir}`.");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check the cache for freshness
|
/// Update the cache
|
||||||
#[cfg(feature = "networking")]
|
fn update_cache(
|
||||||
fn check_cache(args: &Args, cache: &Cache) {
|
cache: &mut Cache,
|
||||||
if !args.flag_update {
|
archive_source: &str,
|
||||||
match cache.last_update() {
|
tls_backend: TlsBackend,
|
||||||
Some(ago) if ago > MAX_CACHE_AGE => {
|
quietly: bool,
|
||||||
if args.flag_quiet {
|
) -> Result<()> {
|
||||||
return;
|
let downloaded_languages = cache
|
||||||
}
|
.update(archive_source, tls_backend)
|
||||||
println!(
|
.context("Could not update cache")?;
|
||||||
"{}",
|
if !quietly {
|
||||||
Color::Red.paint(format!(
|
eprintln!("Successfully updated cache.");
|
||||||
"Cache wasn't updated for more than {} days.\n\
|
eprint!("Pages for the following languages were downloaded: ");
|
||||||
You should probably run `tldr --update` soon.",
|
let language_strings: Vec<_> = downloaded_languages
|
||||||
MAX_CACHE_AGE / 24 / 3600
|
.into_iter()
|
||||||
))
|
.map(|lang| lang.0)
|
||||||
);
|
.collect();
|
||||||
}
|
if language_strings.is_empty() {
|
||||||
None => {
|
eprintln!("(none)");
|
||||||
eprintln!("Cache not found. Please run `tldr --update`.");
|
} else {
|
||||||
process::exit(1);
|
eprintln!("{}", language_strings.join(", "));
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check the cache for freshness
|
/// Show file paths
|
||||||
///
|
fn show_paths(config: &Config) {
|
||||||
/// No-op when networking support is disabled.
|
let config_dir = {
|
||||||
#[cfg(not(feature = "networking"))]
|
let (mut path, source) = get_config_dir();
|
||||||
fn check_cache(_args: &Args, _cache: &Cache) {
|
path.push(""); // Trailing path separator
|
||||||
// No-op when no networking support is enabled.
|
match path.to_str() {
|
||||||
|
Some(path) => format!("{path} ({source})"),
|
||||||
|
None => "[Invalid]".to_string(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let config_path = config.file_path.to_string();
|
||||||
|
let cache_dir = config.directories.cache_dir.to_string();
|
||||||
|
let pages_dir = {
|
||||||
|
let mut path = config.directories.cache_dir.path.clone();
|
||||||
|
path.push(TLDR_PAGES_DIR);
|
||||||
|
path.push(""); // Trailing path separator
|
||||||
|
path.display().to_string()
|
||||||
|
};
|
||||||
|
let custom_pages_dir = match config.directories.custom_pages_dir {
|
||||||
|
Some(ref path_with_source) => path_with_source.to_string(),
|
||||||
|
None => "[None]".to_string(),
|
||||||
|
};
|
||||||
|
println!("Config dir: {config_dir}");
|
||||||
|
println!("Config path: {config_path}");
|
||||||
|
println!("Cache dir: {cache_dir}");
|
||||||
|
println!("Pages dir: {pages_dir}");
|
||||||
|
println!("Custom pages dir: {custom_pages_dir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_config(path: Option<&Path>) -> Result<()> {
|
||||||
|
let config_file_path = make_default_config(path).context("Could not create seed config")?;
|
||||||
|
eprintln!(
|
||||||
|
"Successfully created seed config file here: {}",
|
||||||
|
config_file_path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "logging")]
|
#[cfg(feature = "logging")]
|
||||||
|
|
@ -176,233 +150,295 @@ fn init_log() {
|
||||||
#[cfg(not(feature = "logging"))]
|
#[cfg(not(feature = "logging"))]
|
||||||
fn init_log() {}
|
fn init_log() {}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> {
|
||||||
fn get_os() -> OsType {
|
create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?;
|
||||||
OsType::Linux
|
|
||||||
|
let custom_page_path = custom_pages_dir.join(file_name);
|
||||||
|
let Some(custom_page_path) = custom_page_path.to_str() else {
|
||||||
|
return Err(anyhow!("`custom_page_path.to_str()` failed"));
|
||||||
|
};
|
||||||
|
let Ok(editor) = env::var("EDITOR") else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"To edit a custom page, please set the `EDITOR` environment variable."
|
||||||
|
));
|
||||||
|
};
|
||||||
|
println!("Editing {custom_page_path:?}");
|
||||||
|
|
||||||
|
let status = Command::new(&editor).arg(custom_page_path).status()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(anyhow!("{editor} exit with code {:?}", status.code()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos",
|
fn main() -> ExitCode {
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "netbsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "dragonfly"))]
|
|
||||||
fn get_os() -> OsType {
|
|
||||||
OsType::OsX
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux",
|
|
||||||
target_os = "macos",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "netbsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "dragonfly")))]
|
|
||||||
fn get_os() -> OsType {
|
|
||||||
OsType::Other
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
fn get_url_source(source: &str) -> Source {
|
|
||||||
Source::Url(Cow::Owned(source.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "networking"))]
|
|
||||||
fn get_url_source(_source: &str) -> Source {
|
|
||||||
eprintln!("tealdeer has been compiled without networking support,");
|
|
||||||
eprintln!("cannot update the cache from a network URL.");
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "networking")]
|
|
||||||
fn get_default_source() -> Source {
|
|
||||||
Source::Url(Cow::Borrowed(ARCHIVE_URL))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "networking"))]
|
|
||||||
fn get_default_source() -> Source {
|
|
||||||
Source::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
init_log();
|
init_log();
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
let args: Args = Docopt::new(USAGE)
|
let args = Cli::parse();
|
||||||
.and_then(|d| d.deserialize())
|
|
||||||
.unwrap_or_else(|e| e.exit());
|
|
||||||
|
|
||||||
// Show version and exit
|
// Determine the usage of styles
|
||||||
if args.flag_version {
|
let enable_styles = match args.color.unwrap_or_default() {
|
||||||
let os = get_os();
|
// Attempt to use styling if instructed
|
||||||
println!("{} v{} ({})", NAME, VERSION, os);
|
ColorOptions::Always => {
|
||||||
process::exit(0);
|
yansi::enable(); // disable yansi's automatic detection for ANSI support on Windows
|
||||||
}
|
true
|
||||||
|
}
|
||||||
// Specify target OS
|
// Enable styling if:
|
||||||
let os: OsType = match args.flag_os {
|
// * NO_COLOR env var isn't set: https://no-color.org/
|
||||||
Some(os) => os,
|
// * The output stream is stdout (not being piped)
|
||||||
None => get_os(),
|
ColorOptions::Auto => env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(),
|
||||||
|
// Disable styling
|
||||||
|
ColorOptions::Never => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate cache source
|
try_main(args, enable_styles).unwrap_or_else(|error| {
|
||||||
let cache_source: Source = match args.flag_update_from {
|
print_error(enable_styles, &error);
|
||||||
Some(ref src) if src.starts_with("http") => get_url_source(src),
|
ExitCode::FAILURE
|
||||||
Some(ref src) => Source::File(PathBuf::from(src)),
|
})
|
||||||
None => get_default_source(),
|
}
|
||||||
|
|
||||||
|
fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
||||||
|
// Look up config file, if none is found fall back to default config.
|
||||||
|
debug!("Loading config");
|
||||||
|
let config_loader = match &args.config_path {
|
||||||
|
Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), &args.override_config)
|
||||||
|
.context("Could not read config from given path")?,
|
||||||
|
_ => ConfigLoader::read_default_path(&args.override_config)
|
||||||
|
.context("Could not read config from default path")?,
|
||||||
|
};
|
||||||
|
let mut config = config_loader.load()?;
|
||||||
|
|
||||||
|
// Override styles if needed
|
||||||
|
if !enable_styles {
|
||||||
|
config.style = StyleConfig::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.display.placeholder_format = match (args.short_options, args.long_options) {
|
||||||
|
(false, false) => config.display.placeholder_format, // keep old value
|
||||||
|
(true, false) => PlaceholderFormat::Short,
|
||||||
|
(false, true) => PlaceholderFormat::Long,
|
||||||
|
(true, true) => PlaceholderFormat::Both,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize cache
|
let custom_pages_dir = config
|
||||||
let cache = Cache::new(cache_source, os);
|
.directories
|
||||||
|
.custom_pages_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(PathWithSource::path);
|
||||||
|
|
||||||
// Clear cache, pass through
|
// Note: According to the TLDR client spec, page names must be transparently
|
||||||
if args.flag_clear_cache {
|
// lowercased before lookup:
|
||||||
cache.clear().unwrap_or_else(|e| {
|
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
|
||||||
match e {
|
let command = args.command.join("-").to_lowercase();
|
||||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
|
|
||||||
eprintln!("Could not delete cache: {}", msg)
|
if args.edit_patch || args.edit_page {
|
||||||
}
|
let file_name = if args.edit_patch {
|
||||||
};
|
format!("{command}.patch.md")
|
||||||
process::exit(1);
|
} else {
|
||||||
});
|
format!("{command}.page.md")
|
||||||
if !args.flag_quiet {
|
};
|
||||||
println!("Successfully deleted cache.");
|
|
||||||
}
|
custom_pages_dir
|
||||||
|
.context("To edit custom pages/patches, please specify a custom pages directory.")
|
||||||
|
.and_then(|custom_pages_dir| spawn_editor(custom_pages_dir, &file_name))?;
|
||||||
|
|
||||||
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cache, pass through
|
// Show various paths
|
||||||
if args.flag_update || args.flag_update_from.is_some() {
|
if args.show_paths {
|
||||||
cache.update().unwrap_or_else(|e| {
|
show_paths(&config);
|
||||||
match e {
|
|
||||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
|
|
||||||
eprintln!("Could not update cache: {}", msg)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
process::exit(1);
|
|
||||||
});
|
|
||||||
if !args.flag_quiet {
|
|
||||||
println!("Successfully updated cache.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show config file and path, pass through
|
|
||||||
if args.flag_config_path {
|
|
||||||
match get_config_path() {
|
|
||||||
Ok(config_file_path) => {
|
|
||||||
println!("Config path is: {}", config_file_path.to_str().unwrap());
|
|
||||||
}
|
|
||||||
Err(ConfigError(msg)) => {
|
|
||||||
eprintln!("Could not look up config_path: {}", msg);
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Unknown error");
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a basic config and exit
|
// Create a basic config and exit
|
||||||
if args.flag_seed_config {
|
if args.seed_config {
|
||||||
match make_default_config() {
|
create_config(args.config_path.as_deref())?;
|
||||||
Ok(config_file_path) => {
|
return Ok(ExitCode::SUCCESS);
|
||||||
println!(
|
}
|
||||||
"Successfully created seed config file here: {}",
|
|
||||||
config_file_path.to_str().unwrap()
|
// If a local file was passed in, render it and exit
|
||||||
);
|
if let Some(file) = args.render {
|
||||||
process::exit(0);
|
let reader = PageLookupResult::with_page(file).reader()?;
|
||||||
}
|
print_page(reader, args.raw, enable_styles, args.pager, &config)?;
|
||||||
Err(ConfigError(msg)) => {
|
return Ok(ExitCode::SUCCESS);
|
||||||
eprintln!("Could not create seed config: {}", msg);
|
}
|
||||||
process::exit(1);
|
|
||||||
}
|
// The tealdeer page is embedded in the binary, no cache needed
|
||||||
Err(_) => {
|
if command == "tealdeer" {
|
||||||
eprintln!("Unkown error");
|
print_page(
|
||||||
process::exit(1);
|
TEALDEER_PAGE.as_bytes(),
|
||||||
}
|
args.raw,
|
||||||
|
enable_styles,
|
||||||
|
args.pager,
|
||||||
|
&config,
|
||||||
|
)?;
|
||||||
|
return Ok(ExitCode::SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(platforms) = args.platforms {
|
||||||
|
config.search.platforms = platforms;
|
||||||
|
if !config.search.platforms.contains(&PlatformType::Common) {
|
||||||
|
config.search.platforms.push(PlatformType::Common);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() {
|
||||||
|
Some(lang) => (&[Language(lang)], &[Language(lang)]),
|
||||||
|
None => (&config.search.languages, &config.updates.download_languages),
|
||||||
|
};
|
||||||
|
|
||||||
// Render local file and exit
|
let cache_config = CacheConfig {
|
||||||
if let Some(ref file) = args.flag_render {
|
pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR),
|
||||||
let path = PathBuf::from(file);
|
custom_pages_directory: config
|
||||||
if let Err(msg) = print_page(&path) {
|
.directories
|
||||||
eprintln!("{}", msg);
|
.custom_pages_dir
|
||||||
process::exit(1);
|
.as_ref()
|
||||||
} else {
|
.map(PathWithSource::path),
|
||||||
process::exit(0);
|
platforms: &config.search.platforms,
|
||||||
};
|
search_languages,
|
||||||
|
download_languages,
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.clear_cache {
|
||||||
|
if let Some(cache) = Cache::open(cache_config)? {
|
||||||
|
clear_cache(cache, args.quiet)?;
|
||||||
|
}
|
||||||
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// List cached commands and exit
|
let cache = if args.update || config.updates.auto_update && !args.no_auto_update {
|
||||||
if args.flag_list {
|
let (mut cache, was_created) = Cache::open_or_create(cache_config)?;
|
||||||
// Check cache for freshness
|
if was_created || args.update || cache.age()? >= config.updates.auto_update_interval {
|
||||||
check_cache(&args, &cache);
|
let result = update_cache(
|
||||||
|
&mut cache,
|
||||||
|
config.updates.archive_source,
|
||||||
|
config.updates.tls_backend,
|
||||||
|
args.quiet,
|
||||||
|
);
|
||||||
|
|
||||||
// Get list of pages
|
if let Err(e) = result {
|
||||||
let pages = cache.list_pages().unwrap_or_else(|e| {
|
print_error(enable_styles, &e);
|
||||||
match e {
|
|
||||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
|
eprintln!();
|
||||||
eprintln!("Could not get list of pages: {}", msg)
|
eprintln!(
|
||||||
}
|
"Note: Update errors are often caused by unexpected or missing TLS certificates."
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"You are currently using the following TLS backend: {}",
|
||||||
|
config.updates.tls_backend,
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"Try changing the updates.tls_backend setting in the config file, for example:"
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(" [updates]");
|
||||||
|
eprintln!(" tls_backend = \"rustls-with-native-roots\"");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"This build of tealdeer has support for the following options: {}",
|
||||||
|
supported_tls_backends_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
}
|
}
|
||||||
process::exit(1);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Print pages
|
cache
|
||||||
println!("{}", pages.join(", "));
|
} else if args.list || !command.is_empty() {
|
||||||
process::exit(0);
|
// Cache is needed for these commands to work
|
||||||
|
let Some(cache) = Cache::open(cache_config)? else {
|
||||||
|
if !args.quiet {
|
||||||
|
print_error(
|
||||||
|
enable_styles,
|
||||||
|
&anyhow::anyhow!(
|
||||||
|
"Page cache not found. Please run `tldr --update` to download the cache."
|
||||||
|
),
|
||||||
|
);
|
||||||
|
println!("\nNote: You can optionally enable automatic cache updates by adding the");
|
||||||
|
println!("following config to your config file:\n");
|
||||||
|
println!(" [updates]");
|
||||||
|
println!(" auto_update = true\n");
|
||||||
|
println!("The path to your config file can be looked up with `tldr --show-paths`.");
|
||||||
|
println!("To create an initial config file, use `tldr --seed-config`.\n");
|
||||||
|
println!("You can find more tips and tricks in our docs:\n");
|
||||||
|
println!(" https://docs.tealdeer.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_cache_age) = config.updates.warn_cache_age {
|
||||||
|
let age = cache.age()?;
|
||||||
|
if age > max_cache_age && !args.quiet {
|
||||||
|
print_warning(
|
||||||
|
enable_styles,
|
||||||
|
&format!(
|
||||||
|
"The cache hasn't been updated for {} days.\n\
|
||||||
|
You should probably run `tldr --update` soon.",
|
||||||
|
age.as_secs() / 24 / 3600
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache
|
||||||
|
} else {
|
||||||
|
// There is nothing left to do
|
||||||
|
return Ok(ExitCode::SUCCESS);
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.list {
|
||||||
|
for page in cache.list_pages()? {
|
||||||
|
println!("{page}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show command from cache
|
// Show command from cache
|
||||||
if let Some(ref command) = args.arg_command {
|
if !command.is_empty() {
|
||||||
// Check cache for freshness
|
// TODO: Remove this check 1 year after version 1.7.0 was released
|
||||||
check_cache(&args, &cache);
|
if cache.old_custom_pages_exist()? {
|
||||||
|
print_warning(
|
||||||
// Search for command in cache
|
enable_styles,
|
||||||
if let Some(path) = cache.find_page(&command) {
|
&format!(
|
||||||
if let Err(msg) = print_page(&path) {
|
"Custom pages using the old naming convention were found in {}.\n\
|
||||||
eprintln!("{}", msg);
|
Please rename them to follow the new convention:\n\
|
||||||
process::exit(1);
|
- `<name>.page` → `<name>.page.md`\n\
|
||||||
} else {
|
- `<name>.patch` → `<name>.patch.md`",
|
||||||
process::exit(0);
|
cache
|
||||||
}
|
.config()
|
||||||
} else {
|
.custom_pages_directory
|
||||||
if !args.flag_quiet {
|
.expect("Old custom pages can only exist in custom pages directory")
|
||||||
println!("Page {} not found in cache", &command);
|
.display(),
|
||||||
println!("Try updating with `tldr --update`, or submit a pull request to:");
|
),
|
||||||
println!("https://github.com/tldr-pages/tldr");
|
);
|
||||||
}
|
|
||||||
process::exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(result) = cache.find_page(&command) else {
|
||||||
|
if !args.quiet {
|
||||||
|
print_warning(
|
||||||
|
enable_styles,
|
||||||
|
&format!(
|
||||||
|
"Page `{command}` not found in cache.\n\
|
||||||
|
Try updating with `tldr --update`, or submit a pull request to:\n\
|
||||||
|
https://github.com/tldr-pages/tldr"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
};
|
||||||
|
|
||||||
|
print_page(
|
||||||
|
result.reader()?,
|
||||||
|
args.raw,
|
||||||
|
enable_styles,
|
||||||
|
args.pager,
|
||||||
|
&config,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some flags can be run without a command.
|
Ok(ExitCode::SUCCESS)
|
||||||
if !(args.flag_update || args.flag_clear_cache || args.flag_config_path) {
|
|
||||||
eprintln!("{}", USAGE);
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use docopt::{Docopt, Error};
|
|
||||||
use crate::{Args, OsType, USAGE};
|
|
||||||
|
|
||||||
fn test_helper(argv: &[&str]) -> Result<Args, Error> {
|
|
||||||
Docopt::new(USAGE).and_then(|d| d.argv(argv.iter()).deserialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_docopt_os_case_insensitive() {
|
|
||||||
let argv = vec!["cp", "--os", "LiNuX"];
|
|
||||||
let os = test_helper(&argv).unwrap().flag_os.unwrap();
|
|
||||||
assert_eq!(OsType::Linux, os);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_docopt_expect_error() {
|
|
||||||
let argv = vec!["cp", "--os", "lindows"];
|
|
||||||
assert!(!test_helper(&argv).is_ok());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
116
src/output.rs
Normal file
116
src/output.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
//! Functions for printing pages to the terminal
|
||||||
|
|
||||||
|
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use yansi::Paint;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, PlaceholderFormat, StyleConfig},
|
||||||
|
formatter::{PageSnippet, highlight_lines},
|
||||||
|
line_iterator::LineIterator,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Set up display pager
|
||||||
|
///
|
||||||
|
/// SAFETY: this function may be called multiple times
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn configure_pager(_: bool) {
|
||||||
|
use std::sync::Once;
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
INIT.call_once(|| pager::Pager::with_default_pager("less -R").setup());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn configure_pager(enable_styles: bool) {
|
||||||
|
use crate::utils::print_warning;
|
||||||
|
print_warning(enable_styles, "--pager flag not available on Windows!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print page by path
|
||||||
|
pub fn print_page(
|
||||||
|
reader: impl Read,
|
||||||
|
enable_markdown: bool,
|
||||||
|
enable_styles: bool,
|
||||||
|
use_pager: bool,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
let reader = BufReader::new(reader);
|
||||||
|
|
||||||
|
// Configure pager if applicable
|
||||||
|
if use_pager || config.display.use_pager {
|
||||||
|
configure_pager(enable_styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock stdout only once, this improves performance considerably
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut handle = stdout.lock();
|
||||||
|
|
||||||
|
if enable_markdown {
|
||||||
|
// Print the raw markdown of the file.
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line.context("Error while reading from a page")?;
|
||||||
|
writeln!(handle, "{line}").context("Could not write to stdout")?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Closure that processes a page snippet and writes it to stdout
|
||||||
|
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||||
|
if snip.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
print_snippet(
|
||||||
|
&mut handle,
|
||||||
|
snip,
|
||||||
|
&config.style,
|
||||||
|
config.display.placeholder_format,
|
||||||
|
)
|
||||||
|
.context("Failed to print snippet")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Print highlighted lines
|
||||||
|
highlight_lines(
|
||||||
|
LineIterator::new(reader),
|
||||||
|
&mut process_snippet,
|
||||||
|
!config.display.compact,
|
||||||
|
config.display.show_title,
|
||||||
|
config.display.indent,
|
||||||
|
)
|
||||||
|
.context("Could not write to stdout")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done outputting data, flush stdout now!
|
||||||
|
handle.flush().context("Could not flush stdout")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_snippet(
|
||||||
|
writer: &mut impl Write,
|
||||||
|
snip: PageSnippet<&str>,
|
||||||
|
style: &StyleConfig,
|
||||||
|
placeholder_format: PlaceholderFormat,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
use PageSnippet::*;
|
||||||
|
|
||||||
|
match snip {
|
||||||
|
CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)),
|
||||||
|
Placeholder(s) => write!(writer, "{}", s.paint(style.example_variable)),
|
||||||
|
PlaceholderVariants { short, long } => match placeholder_format {
|
||||||
|
PlaceholderFormat::Short => write!(writer, "{}", short.paint(style.example_code)),
|
||||||
|
PlaceholderFormat::Long => write!(writer, "{}", long.paint(style.example_code)),
|
||||||
|
PlaceholderFormat::Both => {
|
||||||
|
write!(
|
||||||
|
writer,
|
||||||
|
"{}",
|
||||||
|
format!("[{short}|{long}]").paint(style.example_code)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)),
|
||||||
|
Description(s) => write!(writer, "{}", s.paint(style.description)),
|
||||||
|
Text(s) => write!(writer, "{}", s.paint(style.example_text)),
|
||||||
|
Indent(n) => write!(writer, "{:n$}", ' '),
|
||||||
|
Linebreak => writeln!(writer),
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/tokenizer.rs
112
src/tokenizer.rs
|
|
@ -1,112 +0,0 @@
|
||||||
//! Code to tokenize a `BufRead` instance into an iterator of `LineType`s.
|
|
||||||
|
|
||||||
use std::io::BufRead;
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
use crate::types::LineType;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum TldrFormat {
|
|
||||||
/// Not yet clear
|
|
||||||
Undecided,
|
|
||||||
/// The original format
|
|
||||||
V1,
|
|
||||||
/// The new format (see https://github.com/tldr-pages/tldr/pull/958)
|
|
||||||
V2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A tokenizer is initialized with a `BufReader` instance that contains the
|
|
||||||
/// entire Tldr page. It then returns tokens as `Option<LineType>`.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Tokenizer<R: BufRead> {
|
|
||||||
/// An instance of `R: BufRead`.
|
|
||||||
reader: R,
|
|
||||||
/// Whether the first line has already been tokenized or not.
|
|
||||||
first_line: bool,
|
|
||||||
/// Buffer for the current line. Used internally.
|
|
||||||
current_line: String,
|
|
||||||
/// The tldr page format.
|
|
||||||
format: TldrFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R> Tokenizer<R>
|
|
||||||
where
|
|
||||||
R: BufRead,
|
|
||||||
{
|
|
||||||
pub fn new(reader: R) -> Self {
|
|
||||||
Self {
|
|
||||||
reader,
|
|
||||||
first_line: true,
|
|
||||||
current_line: String::new(),
|
|
||||||
format: TldrFormat::Undecided,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_token(&mut self) -> Option<LineType> {
|
|
||||||
self.current_line.clear();
|
|
||||||
let bytes_read = self.reader.read_line(&mut self.current_line);
|
|
||||||
match bytes_read {
|
|
||||||
Ok(0) => None,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Could not read line from token reader: {:?}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Ok(_) => {
|
|
||||||
// Handle new titles
|
|
||||||
if self.first_line && !self.current_line.starts_with('#') {
|
|
||||||
// It's the new format! Drop next line.
|
|
||||||
// (Hmm, is there a way to do this without an allocation?)
|
|
||||||
let mut devnull = String::new();
|
|
||||||
if let Err(e) = self.reader.read_line(&mut devnull) {
|
|
||||||
warn!("Could not read line from token reader: {:?}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.first_line = false;
|
|
||||||
self.format = TldrFormat::V2;
|
|
||||||
return Some(LineType::Title(self.current_line.trim_right().to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.first_line {
|
|
||||||
// Clear `first_line` flag
|
|
||||||
self.first_line = false;
|
|
||||||
|
|
||||||
// It's the old format.
|
|
||||||
self.format = TldrFormat::V1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert line to a `LineType` instance
|
|
||||||
match self.format {
|
|
||||||
TldrFormat::V1 => Some(LineType::from_v1(&self.current_line[..])),
|
|
||||||
TldrFormat::V2 => Some(LineType::from(&self.current_line[..])),
|
|
||||||
TldrFormat::Undecided => panic!("Could not determine page format version"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::Tokenizer;
|
|
||||||
use crate::types::LineType;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_first_line_old_format() {
|
|
||||||
let input = "# The Title\n\n";
|
|
||||||
let mut tokenizer = Tokenizer::new(input.as_bytes());
|
|
||||||
let title = tokenizer.next_token().unwrap();
|
|
||||||
assert_eq!(title, LineType::Title("The Title".to_string()));
|
|
||||||
let empty = tokenizer.next_token().unwrap();
|
|
||||||
assert_eq!(empty, LineType::Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_first_line_new_format() {
|
|
||||||
let input = "The Title\n=========\n\n";
|
|
||||||
let mut tokenizer = Tokenizer::new(input.as_bytes());
|
|
||||||
let title = tokenizer.next_token().unwrap();
|
|
||||||
assert_eq!(title, LineType::Title("The Title".to_string()));
|
|
||||||
let empty = tokenizer.next_token().unwrap();
|
|
||||||
assert_eq!(empty, LineType::Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
187
src/types.rs
187
src/types.rs
|
|
@ -1,30 +1,131 @@
|
||||||
//! Types used in the client.
|
//! Shared types used in tealdeer.
|
||||||
|
|
||||||
use std::fmt;
|
use std::{fmt, str};
|
||||||
|
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum OsType {
|
pub enum PlatformType {
|
||||||
Linux,
|
Linux,
|
||||||
OsX,
|
OsX,
|
||||||
|
Windows,
|
||||||
SunOs,
|
SunOs,
|
||||||
Other,
|
Android,
|
||||||
|
FreeBsd,
|
||||||
|
NetBsd,
|
||||||
|
OpenBsd,
|
||||||
|
Common,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for OsType {
|
impl fmt::Display for PlatformType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
OsType::Linux => write!(f, "Linux"),
|
Self::Linux => write!(f, "Linux"),
|
||||||
OsType::OsX => write!(f, "macOS / BSD"),
|
Self::OsX => write!(f, "macOS / BSD"),
|
||||||
OsType::SunOs => write!(f, "SunOS"),
|
Self::Windows => write!(f, "Windows"),
|
||||||
OsType::Other => write!(f, "Unknown OS"),
|
Self::SunOs => write!(f, "SunOS"),
|
||||||
|
Self::Android => write!(f, "Android"),
|
||||||
|
Self::FreeBsd => write!(f, "FreeBSD"),
|
||||||
|
Self::NetBsd => write!(f, "NetBSD"),
|
||||||
|
Self::OpenBsd => write!(f, "OpenBSD"),
|
||||||
|
Self::Common => write!(f, "Common"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl clap::ValueEnum for PlatformType {
|
||||||
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
|
&[
|
||||||
|
Self::Linux,
|
||||||
|
Self::OsX,
|
||||||
|
Self::SunOs,
|
||||||
|
Self::Windows,
|
||||||
|
Self::Android,
|
||||||
|
Self::FreeBsd,
|
||||||
|
Self::NetBsd,
|
||||||
|
Self::OpenBsd,
|
||||||
|
Self::Common,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_possible_value<'a>(&self) -> Option<clap::builder::PossibleValue> {
|
||||||
|
match self {
|
||||||
|
Self::Linux => Some(clap::builder::PossibleValue::new("linux")),
|
||||||
|
Self::OsX => Some(clap::builder::PossibleValue::new("macos").alias("osx")),
|
||||||
|
Self::Windows => Some(clap::builder::PossibleValue::new("windows")),
|
||||||
|
Self::SunOs => Some(clap::builder::PossibleValue::new("sunos")),
|
||||||
|
Self::Android => Some(clap::builder::PossibleValue::new("android")),
|
||||||
|
Self::FreeBsd => Some(clap::builder::PossibleValue::new("freebsd")),
|
||||||
|
Self::NetBsd => Some(clap::builder::PossibleValue::new("netbsd")),
|
||||||
|
Self::OpenBsd => Some(clap::builder::PossibleValue::new("openbsd")),
|
||||||
|
Self::Common => Some(clap::builder::PossibleValue::new("common")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformType {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "dragonfly"))]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::OsX
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::Windows
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::Android
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::FreeBsd
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "netbsd")]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::NetBsd
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "openbsd")]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::OpenBsd
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "openbsd",
|
||||||
|
target_os = "dragonfly",
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "android",
|
||||||
|
)))]
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::Other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum ColorOptions {
|
||||||
|
Always,
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
Never,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum LineType {
|
pub enum LineType {
|
||||||
Empty,
|
Empty,
|
||||||
|
|
@ -36,28 +137,24 @@ pub enum LineType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a str> for LineType {
|
impl<'a> From<&'a str> for LineType {
|
||||||
/// Convert a string slice to a LineType. Newlines and trailing whitespace are trimmed.
|
/// Convert a string slice to a `LineType`. Newlines and trailing whitespace are trimmed.
|
||||||
fn from(line: &'a str) -> Self {
|
fn from(line: &'a str) -> Self {
|
||||||
let trimmed: &str = line.trim_right();
|
let trimmed: &str = line.trim_end();
|
||||||
let mut chars = trimmed.chars();
|
let mut chars = trimmed.chars();
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
None => LineType::Empty,
|
None => Self::Empty,
|
||||||
Some('#') => LineType::Title(
|
Some('#') => Self::Title(
|
||||||
trimmed
|
trimmed
|
||||||
.trim_left_matches(|chr: char| chr == '#' || chr.is_whitespace())
|
.trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace())
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
Some('>') => LineType::Description(
|
Some('>') => Self::Description(
|
||||||
trimmed
|
trimmed
|
||||||
.trim_left_matches(|chr: char| chr == '>' || chr.is_whitespace())
|
.trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace())
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
Some(' ') => LineType::ExampleCode(
|
Some(' ') => Self::ExampleCode(trimmed.trim_start_matches(char::is_whitespace).into()),
|
||||||
trimmed
|
Some(_) => Self::ExampleText(trimmed.into()),
|
||||||
.trim_left_matches(|chr: char| chr.is_whitespace())
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
_ => LineType::ExampleText(trimmed.into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,32 +166,60 @@ impl LineType {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
let mut chars = trimmed.chars();
|
let mut chars = trimmed.chars();
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
None => LineType::Empty,
|
None => Self::Empty,
|
||||||
Some('#') => LineType::Title(
|
Some('#') => Self::Title(
|
||||||
trimmed
|
trimmed
|
||||||
.trim_left_matches(|chr: char| chr == '#' || chr.is_whitespace())
|
.trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace())
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
Some('>') => LineType::Description(
|
Some('>') => Self::Description(
|
||||||
trimmed
|
trimmed
|
||||||
.trim_left_matches(|chr: char| chr == '>' || chr.is_whitespace())
|
.trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace())
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
Some('-') => LineType::ExampleText(
|
Some('-') => Self::ExampleText(
|
||||||
trimmed
|
trimmed
|
||||||
.trim_left_matches(|chr: char| chr == '-' || chr.is_whitespace())
|
.trim_start_matches(|chr: char| chr == '-' || chr.is_whitespace())
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
Some('`') if chars.last() == Some('`') => LineType::ExampleCode(
|
Some('`') if chars.last() == Some('`') => Self::ExampleCode(
|
||||||
trimmed
|
trimmed
|
||||||
.trim_matches(|chr: char| chr == '`' || chr.is_whitespace())
|
.trim_matches(|chr: char| chr == '`' || chr.is_whitespace())
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
_ => LineType::Other(trimmed.into()),
|
Some(_) => Self::Other(trimmed.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The reason why a certain path (e.g. config path or cache dir) was chosen.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum PathSource {
|
||||||
|
/// OS convention (e.g. XDG on Linux)
|
||||||
|
OsConvention,
|
||||||
|
/// Env variable (TEALDEER_*)
|
||||||
|
EnvVar,
|
||||||
|
/// Config file
|
||||||
|
ConfigFile,
|
||||||
|
/// CLI argument override
|
||||||
|
Cli,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PathSource {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::OsConvention => "OS convention",
|
||||||
|
Self::EnvVar => "env variable",
|
||||||
|
Self::ConfigFile => "config file",
|
||||||
|
Self::Cli => "command line argument",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::LineType;
|
use super::LineType;
|
||||||
|
|
|
||||||
21
src/utils.rs
Normal file
21
src/utils.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
use yansi::{Color, Paint};
|
||||||
|
|
||||||
|
/// Print a warning to stderr. If `enable_styles` is true, then a yellow
|
||||||
|
/// message will be printed.
|
||||||
|
pub fn print_warning(enable_styles: bool, message: &str) {
|
||||||
|
print_msg(enable_styles, message, "Warning: ", Color::Yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print an anyhow error to stderr. If `enable_styles` is true, then a red
|
||||||
|
/// message will be printed.
|
||||||
|
pub fn print_error(enable_styles: bool, error: &anyhow::Error) {
|
||||||
|
print_msg(enable_styles, &format!("{error:?}"), "Error: ", Color::Red);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_msg(enable_styles: bool, message: &str, prefix: &'static str, color: Color) {
|
||||||
|
if enable_styles {
|
||||||
|
eprintln!("{}{}", prefix.paint(color), message.paint(color));
|
||||||
|
} else {
|
||||||
|
eprintln!("{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
36
tests/cache/pages.en/common/git-checkout.md
vendored
Normal file
36
tests/cache/pages.en/common/git-checkout.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# git checkout
|
||||||
|
|
||||||
|
> Checkout a branch or paths to the working tree.
|
||||||
|
> More information: <https://git-scm.com/docs/git-checkout>.
|
||||||
|
|
||||||
|
- Create and switch to a new branch:
|
||||||
|
|
||||||
|
`git checkout -b {{branch_name}}`
|
||||||
|
|
||||||
|
- Create and switch to a new branch based on a specific reference (branch, remote/branch, tag are examples of valid references):
|
||||||
|
|
||||||
|
`git checkout -b {{branch_name}} {{reference}}`
|
||||||
|
|
||||||
|
- Switch to an existing local branch:
|
||||||
|
|
||||||
|
`git checkout {{branch_name}}`
|
||||||
|
|
||||||
|
- Switch to the previously checked out branch:
|
||||||
|
|
||||||
|
`git checkout -`
|
||||||
|
|
||||||
|
- Switch to an existing remote branch:
|
||||||
|
|
||||||
|
`git checkout --track {{remote_name}}/{{branch_name}}`
|
||||||
|
|
||||||
|
- Discard all unstaged changes in the current directory (see `git reset` for more undo-like commands):
|
||||||
|
|
||||||
|
`git checkout .`
|
||||||
|
|
||||||
|
- Discard unstaged changes to a given file:
|
||||||
|
|
||||||
|
`git checkout {{path/to/file}}`
|
||||||
|
|
||||||
|
- Replace a file in the current directory with the version of it committed in a given branch:
|
||||||
|
|
||||||
|
`git checkout {{branch_name}} -- {{path/to/file}}`
|
||||||
|
|
@ -26,3 +26,7 @@
|
||||||
- Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
- Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
`inkscape {{filename.svg}} --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit`
|
`inkscape {{filename.svg}} --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit`
|
||||||
|
|
||||||
|
- Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
`inkscape --use-inkscape=v3.0 file`
|
||||||
|
|
@ -27,3 +27,7 @@ Export an SVG document to PDF, converting all texts to paths:
|
||||||
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
inkscape {{filename.svg}} --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
inkscape {{filename.svg}} --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
32
tests/cache/pages.en/common/playerctl.md
vendored
Normal file
32
tests/cache/pages.en/common/playerctl.md
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# playerctl
|
||||||
|
|
||||||
|
> Control media players via MPRIS.
|
||||||
|
> More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
- Toggle play:
|
||||||
|
|
||||||
|
`playerctl play-pause`
|
||||||
|
|
||||||
|
- Skip to the next track:
|
||||||
|
|
||||||
|
`playerctl next`
|
||||||
|
|
||||||
|
- Go back to the previous track:
|
||||||
|
|
||||||
|
`playerctl previous`
|
||||||
|
|
||||||
|
- List all players:
|
||||||
|
|
||||||
|
`playerctl {{[-l|--list-all]}}`
|
||||||
|
|
||||||
|
- Send a command to a specific player:
|
||||||
|
|
||||||
|
`playerctl {{[-p|--player]}} {{player_name}} {{play-pause|next|previous|...}}`
|
||||||
|
|
||||||
|
- Send a command to all players:
|
||||||
|
|
||||||
|
`playerctl {{[-a|--all-players]}} {{play-pause|next|previous|...}}`
|
||||||
|
|
||||||
|
- Display metadata about the current track:
|
||||||
|
|
||||||
|
`playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}"`
|
||||||
11
tests/cache/pages.en/common/which.md
vendored
Normal file
11
tests/cache/pages.en/common/which.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# which
|
||||||
|
|
||||||
|
> Locate a program in the user's path.
|
||||||
|
|
||||||
|
- Search the PATH environment variable and display the location of any matching executables:
|
||||||
|
|
||||||
|
`which {{executable}}`
|
||||||
|
|
||||||
|
- If there are multiple executables which match, display all:
|
||||||
|
|
||||||
|
`which -a {{executable}}`
|
||||||
37
tests/cache/pages.ja/common/apt.md
vendored
Normal file
37
tests/cache/pages.ja/common/apt.md
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# apt
|
||||||
|
|
||||||
|
> Debian系ディストリビューションで使われるパッケージ管理システムです。
|
||||||
|
> Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
|
||||||
|
> 詳しくはこちら: <https://manned.org/apt.8>
|
||||||
|
|
||||||
|
- 利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):
|
||||||
|
|
||||||
|
`sudo apt update`
|
||||||
|
|
||||||
|
- 指定されたパッケージの検索:
|
||||||
|
|
||||||
|
`apt search {{パッケージ}}`
|
||||||
|
|
||||||
|
- パッケージの情報を出力:
|
||||||
|
|
||||||
|
`apt show {{パッケージ}}`
|
||||||
|
|
||||||
|
- パッケージのインストール、または利用可能な最新バージョンに更新:
|
||||||
|
|
||||||
|
`sudo apt install {{パッケージ}}`
|
||||||
|
|
||||||
|
- パッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):
|
||||||
|
|
||||||
|
`sudo apt remove {{パッケージ}}`
|
||||||
|
|
||||||
|
- インストールされている全てのパッケージを最新のバージョンにアップグレード:
|
||||||
|
|
||||||
|
`sudo apt upgrade`
|
||||||
|
|
||||||
|
- インストールできるすべてのパッケージを表示:
|
||||||
|
|
||||||
|
`apt list`
|
||||||
|
|
||||||
|
- インストールされた全てのパッケージを表示(依存関係も表示):
|
||||||
|
|
||||||
|
`apt list --installed`
|
||||||
3
tests/custom-pages/inkscape-v2.patch.md
Normal file
3
tests/custom-pages/inkscape-v2.patch.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Custom inkscape entry
|
||||||
|
|
||||||
|
My Inkscape example
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
An SVG (Scalable Vector Graphics) editing program.
|
|
||||||
Use -z to not open the GUI and only process files in the console.
|
|
||||||
|
|
||||||
[32mOpen an SVG file in the Inkscape GUI:[0m
|
|
||||||
|
|
||||||
[36minkscape [4mfilename.svg[0m[36m[0m
|
|
||||||
|
|
||||||
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
|
||||||
|
|
||||||
[36minkscape [4mfilename.svg[0m[36m -e [4mfilename.png[0m[36m[0m
|
|
||||||
|
|
||||||
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
|
||||||
|
|
||||||
[36minkscape [4mfilename.svg[0m[36m -e [4mfilename.png[0m[36m -w [4m600[0m[36m -h [4m400[0m[36m[0m
|
|
||||||
|
|
||||||
[32mExport a single object, given its ID, into a bitmap:[0m
|
|
||||||
|
|
||||||
[36minkscape [4mfilename.svg[0m[36m -i [4mid[0m[36m -e [4mobject.png[0m[36m[0m
|
|
||||||
|
|
||||||
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
|
||||||
|
|
||||||
[36minkscape [4mfilename.svg[0m[36m | inkscape | inkscape --export-pdf=[4minkscape.pdf[0m[36m | inkscape | inkscape --export-text-to-path[0m
|
|
||||||
|
|
||||||
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
|
||||||
|
|
||||||
[36minkscape [4mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
|
||||||
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
An SVG (Scalable Vector Graphics) editing program.
|
|
||||||
Use -z to not open the GUI and only process files in the console.
|
|
||||||
|
|
||||||
[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
|
||||||
|
|
||||||
inkscape [4mfilename.svg[0m
|
|
||||||
|
|
||||||
[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
|
||||||
|
|
||||||
inkscape [4mfilename.svg[0m -e [4mfilename.png[0m
|
|
||||||
|
|
||||||
[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
|
||||||
|
|
||||||
inkscape [4mfilename.svg[0m -e [4mfilename.png[0m -w [4m600[0m -h [4m400[0m
|
|
||||||
|
|
||||||
[44;30mExport a single object, given its ID, into a bitmap:[0m
|
|
||||||
|
|
||||||
inkscape [4mfilename.svg[0m -i [4mid[0m -e [4mobject.png[0m
|
|
||||||
|
|
||||||
[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
|
||||||
|
|
||||||
inkscape [4mfilename.svg[0m | inkscape | inkscape --export-pdf=[4minkscape.pdf[0m | inkscape | inkscape --export-text-to-path
|
|
||||||
|
|
||||||
[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
|
||||||
|
|
||||||
inkscape [4mfilename.svg[0m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
|
||||||
|
|
||||||
1510
tests/lib.rs
1510
tests/lib.rs
File diff suppressed because it is too large
Load diff
37
tests/rendered/apt.ja.expected
Normal file
37
tests/rendered/apt.ja.expected
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
Debian系ディストリビューションで使われるパッケージ管理システムです。
|
||||||
|
Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
|
||||||
|
詳しくはこちら: <https://manned.org/apt.8>
|
||||||
|
|
||||||
|
[32m利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):[0m
|
||||||
|
|
||||||
|
[36msudo [0m[36mapt[0m[36m update[0m
|
||||||
|
|
||||||
|
[32m指定されたパッケージの検索:[0m
|
||||||
|
|
||||||
|
[36mapt[0m[36m search [0m[4;36mパッケージ[0m
|
||||||
|
|
||||||
|
[32mパッケージの情報を出力:[0m
|
||||||
|
|
||||||
|
[36mapt[0m[36m show [0m[4;36mパッケージ[0m
|
||||||
|
|
||||||
|
[32mパッケージのインストール、または利用可能な最新バージョンに更新:[0m
|
||||||
|
|
||||||
|
[36msudo [0m[36mapt[0m[36m install [0m[4;36mパッケージ[0m
|
||||||
|
|
||||||
|
[32mパッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):[0m
|
||||||
|
|
||||||
|
[36msudo [0m[36mapt[0m[36m remove [0m[4;36mパッケージ[0m
|
||||||
|
|
||||||
|
[32mインストールされている全てのパッケージを最新のバージョンにアップグレード:[0m
|
||||||
|
|
||||||
|
[36msudo [0m[36mapt[0m[36m upgrade[0m
|
||||||
|
|
||||||
|
[32mインストールできるすべてのパッケージを表示:[0m
|
||||||
|
|
||||||
|
[36mapt[0m[36m list[0m
|
||||||
|
|
||||||
|
[32mインストールされた全てのパッケージを表示(依存関係も表示):[0m
|
||||||
|
|
||||||
|
[36mapt[0m[36m list --installed[0m
|
||||||
|
|
||||||
32
tests/rendered/inkscape-compact-no-color.expected
Normal file
32
tests/rendered/inkscape-compact-no-color.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
Open an SVG file in the Inkscape GUI:
|
||||||
|
|
||||||
|
inkscape filename.svg
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||||
|
|
||||||
|
Export a single object, given its ID, into a bitmap:
|
||||||
|
|
||||||
|
inkscape filename.svg -i id -e object.png
|
||||||
|
|
||||||
|
Export an SVG document to PDF, converting all texts to paths:
|
||||||
|
|
||||||
|
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||||
|
|
||||||
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
|
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
|
|
||||||
32
tests/rendered/inkscape-default-no-color.expected
Normal file
32
tests/rendered/inkscape-default-no-color.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
Open an SVG file in the Inkscape GUI:
|
||||||
|
|
||||||
|
inkscape filename.svg
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||||
|
|
||||||
|
Export a single object, given its ID, into a bitmap:
|
||||||
|
|
||||||
|
inkscape filename.svg -i id -e object.png
|
||||||
|
|
||||||
|
Export an SVG document to PDF, converting all texts to paths:
|
||||||
|
|
||||||
|
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||||
|
|
||||||
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
|
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
|
|
||||||
32
tests/rendered/inkscape-default.expected
Normal file
32
tests/rendered/inkscape-default.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||||
|
|
||||||
|
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||||
|
|
||||||
|
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m[36m -w [0m[4;36m600[0m[36m -h [0m[4;36m400[0m
|
||||||
|
|
||||||
|
[32mExport a single object, given its ID, into a bitmap:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||||
|
|
||||||
|
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-pdf=[0m[4;36minkscape.pdf[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-text-to-path[0m
|
||||||
|
|
||||||
|
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||||
|
|
||||||
|
[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||||
|
|
||||||
36
tests/rendered/inkscape-patched-no-color.expected
Normal file
36
tests/rendered/inkscape-patched-no-color.expected
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
Open an SVG file in the Inkscape GUI:
|
||||||
|
|
||||||
|
inkscape filename.svg
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||||
|
|
||||||
|
Export a single object, given its ID, into a bitmap:
|
||||||
|
|
||||||
|
inkscape filename.svg -i id -e object.png
|
||||||
|
|
||||||
|
Export an SVG document to PDF, converting all texts to paths:
|
||||||
|
|
||||||
|
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||||
|
|
||||||
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
|
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
|
|
||||||
|
Custom inkscape entry
|
||||||
|
|
||||||
|
My Inkscape example
|
||||||
|
|
||||||
32
tests/rendered/inkscape-with-config.expected
Normal file
32
tests/rendered/inkscape-with-config.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
||||||
|
|
||||||
|
[1minkscape[0m [3;4mfilename.svg[0m
|
||||||
|
|
||||||
|
[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||||
|
|
||||||
|
[1minkscape[0m [3;4mfilename.svg[0m -e [3;4mfilename.png[0m
|
||||||
|
|
||||||
|
[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||||
|
|
||||||
|
[1minkscape[0m [3;4mfilename.svg[0m -e [3;4mfilename.png[0m -w [3;4m600[0m -h [3;4m400[0m
|
||||||
|
|
||||||
|
[44;30mExport a single object, given its ID, into a bitmap:[0m
|
||||||
|
|
||||||
|
[1minkscape[0m [3;4mfilename.svg[0m -i [3;4mid[0m -e [3;4mobject.png[0m
|
||||||
|
|
||||||
|
[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||||
|
|
||||||
|
[1minkscape[0m [3;4mfilename.svg[0m | [1minkscape[0m | [1minkscape[0m --export-pdf=[3;4minkscape.pdf[0m | [1minkscape[0m | [1minkscape[0m --export-text-to-path
|
||||||
|
|
||||||
|
[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||||
|
|
||||||
|
[1minkscape[0m [3;4mfilename.svg[0m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
[44;30mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||||
|
|
||||||
|
[1minkscape[0m --use-inkscape=v3.0 file
|
||||||
|
|
||||||
34
tests/rendered/inkscape-with-title-no-color.expected
Normal file
34
tests/rendered/inkscape-with-title-no-color.expected
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
inkscape
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
Open an SVG file in the Inkscape GUI:
|
||||||
|
|
||||||
|
inkscape filename.svg
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||||
|
|
||||||
|
Export a single object, given its ID, into a bitmap:
|
||||||
|
|
||||||
|
inkscape filename.svg -i id -e object.png
|
||||||
|
|
||||||
|
Export an SVG document to PDF, converting all texts to paths:
|
||||||
|
|
||||||
|
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||||
|
|
||||||
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
|
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
|
|
||||||
34
tests/rendered/inkscape-with-title.expected
Normal file
34
tests/rendered/inkscape-with-title.expected
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
[36minkscape[0m
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||||
|
|
||||||
|
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||||
|
|
||||||
|
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m[36m -w [0m[4;36m600[0m[36m -h [0m[4;36m400[0m
|
||||||
|
|
||||||
|
[32mExport a single object, given its ID, into a bitmap:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||||
|
|
||||||
|
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-pdf=[0m[4;36minkscape.pdf[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-text-to-path[0m
|
||||||
|
|
||||||
|
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||||
|
|
||||||
|
[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||||
|
|
||||||
32
tests/rendered/playerctl-both.expected
Normal file
32
tests/rendered/playerctl-both.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
Control media players via MPRIS.
|
||||||
|
More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
[32mToggle play:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m play-pause[0m
|
||||||
|
|
||||||
|
[32mSkip to the next track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m next[0m
|
||||||
|
|
||||||
|
[32mGo back to the previous track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m previous[0m
|
||||||
|
|
||||||
|
[32mList all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m[-l|--list-all][0m
|
||||||
|
|
||||||
|
[32mSend a command to a specific player:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m[-p|--player][0m[36m [0m[4;36mplayer_name[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mSend a command to all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m[-a|--all-players][0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mDisplay metadata about the current track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m metadata [0m[36m[-f|--format][0m[36m "[0m[4;36mNow playing: {{artist}} - {{album}} - {{title}}[0m[36m"[0m
|
||||||
|
|
||||||
32
tests/rendered/playerctl-long.expected
Normal file
32
tests/rendered/playerctl-long.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
Control media players via MPRIS.
|
||||||
|
More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
[32mToggle play:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m play-pause[0m
|
||||||
|
|
||||||
|
[32mSkip to the next track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m next[0m
|
||||||
|
|
||||||
|
[32mGo back to the previous track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m previous[0m
|
||||||
|
|
||||||
|
[32mList all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m--list-all[0m
|
||||||
|
|
||||||
|
[32mSend a command to a specific player:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m--player[0m[36m [0m[4;36mplayer_name[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mSend a command to all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m--all-players[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mDisplay metadata about the current track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m metadata [0m[36m--format[0m[36m "[0m[4;36mNow playing: {{artist}} - {{album}} - {{title}}[0m[36m"[0m
|
||||||
|
|
||||||
32
tests/rendered/playerctl-short.expected
Normal file
32
tests/rendered/playerctl-short.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
Control media players via MPRIS.
|
||||||
|
More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
[32mToggle play:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m play-pause[0m
|
||||||
|
|
||||||
|
[32mSkip to the next track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m next[0m
|
||||||
|
|
||||||
|
[32mGo back to the previous track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m previous[0m
|
||||||
|
|
||||||
|
[32mList all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m-l[0m
|
||||||
|
|
||||||
|
[32mSend a command to a specific player:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m-p[0m[36m [0m[4;36mplayer_name[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mSend a command to all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m-a[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mDisplay metadata about the current track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m metadata [0m[36m-f[0m[36m "[0m[4;36mNow playing: {{artist}} - {{album}} - {{title}}[0m[36m"[0m
|
||||||
|
|
||||||
|
|
@ -3,6 +3,9 @@ foreground = "green"
|
||||||
underline = false
|
underline = false
|
||||||
bold = false
|
bold = false
|
||||||
|
|
||||||
|
[style.command_name]
|
||||||
|
bold = true
|
||||||
|
|
||||||
[style.description]
|
[style.description]
|
||||||
underline = false
|
underline = false
|
||||||
bold = false
|
bold = false
|
||||||
|
|
@ -15,3 +18,4 @@ underline = false
|
||||||
[style.example_variable]
|
[style.example_variable]
|
||||||
underline = true
|
underline = true
|
||||||
bold = false
|
bold = false
|
||||||
|
italic = true
|
||||||
Loading…
Add table
Add a link
Reference in a new issue