Compare commits

...

412 commits

Author SHA1 Message Date
Niklas Mohrin
37b0dee39f
Support XDG directory spec on macOS by switching app_dirs2 to etcetera (#467)
Closes #311

Similar to how `bat` does it, we now have a global struct which holds
all the queried system directories. We prefer the XDG directories, but
use the "native" directories as a fallback. This should ensure that when
MacOs users upgrade from an older version, their existing config and
cache are still used. After deleting the old directories, the new ones
should be used by tealdeer automatically.
2026-08-06 00:37:37 +02:00
Niklas Mohrin
28ed785001
Fix 1.97 clippy lints 2026-07-26 23:52:43 +02:00
Nikolaos Karaolidis
f8a2003bc2
Add updates.warn_cache_age config option (#492)
This is useful when the cache is managed externally, e.g. provisioned
from a Nix store path or by a package manager, where the directory's
mtime doesn't reflect the cache's real age, causing a spurious warning
on every invocation. Today the only way to silence it is `--quiet`,
which must be passed every call and hides all other output too.
2026-07-07 14:24:06 +02:00
RedHare
df5113ddaa
Added AI Policy to README (#489)
Addresses #479, and adds a section for an AI policy based on what is
outlined by @niklasmohrin in that issue. The AI Policy is located below
the "Development" section.
2026-07-04 23:05:15 +02:00
dependabot[bot]
51593d27eb
Bump actions/checkout from 6 to 7 (#493) 2026-07-04 22:59:46 +02:00
xtqqczze
d0108b23e4
Fix clippy lints on all targets (#481) 2026-05-07 15:07:27 +02:00
xtqqczze
4d33e8a279
Remove tldr-c entry from benchmark results in README.md (#480)
`tldr-c-client` is unmaintained.
2026-05-06 16:36:37 +02:00
Ellis Clayton
1252261d66
Support tilde (~) expansion on config paths (#476)
Allows directories in the configuration file (cache & custom pages) to
be relative to the user's home directory by expanding the common `~`
path prefix notation. Works for the current user only (`~diferentUser/`
syntax is not supported, and will cause an error if attempted).

Works for Linux/Unix (via `HOME` env var) and Windows (via `USERPROFILE`
env var).

Examples (assuming a user called "foo" on a Linux system):

```
~/my/custom-pages # /home/foo/custom-pages
~ # /home/foo
~bar/cache # error
```
2026-05-03 21:47:29 +02:00
Niklas Mohrin
24e7f383b8
Fix Rust 1.95 clippy lints 2026-04-17 22:08:42 +02:00
Niklas Mohrin
6c65c8f71c
Fix off-boundary string access in formatter (#474)
Closes #473

This regression was introduced in
593e9309b9 (#414) and leads to a panic
when trying to display pages where characters line up like in the issue
or the test. I checked other places and found that a similar panic could
occur when parsing language strings, so I added code to ignore them
instead.

- Add regression test
- Fix prefix check
- Skip non-ASCII locales
2026-04-17 22:01:45 +02:00
Pavel Timofeev
b8f7c0cc2d
Add display.indent config option (#471)
Fixes #394.
2026-04-17 21:59:34 +02:00
Pavel Timofeev
b19517097a
Add builtin tldr tealdeer page (#472)
Fixes #218.
2026-04-14 00:31:31 +02:00
dependabot[bot]
6f91c3a765
Bump actions/download-artifact from 7 to 8 (#469)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 7 to 8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>v8 - What's new</h2>
<h3>Direct downloads</h3>
<p>To support direct uploads in <code>actions/upload-artifact</code>,
the action will no longer attempt to unzip all downloaded files.
Instead, the action checks the <code>Content-Type</code> header ahead of
unzipping and skips non-zipped files. Callers wishing to download a
zipped file as-is can also set the new <code>skip-decompress</code>
parameter to <code>false</code>.</p>
<h3>Enforced checks (breaking)</h3>
<p>A previous release introduced digest checks on the download. If a
download hash didn't match the expected hash from the server, the action
would log a warning. Callers can now configure the behavior on mismatch
with the <code>digest-mismatch</code> parameter. To be secure by
default, we are now defaulting the behavior to <code>error</code> which
will fail the workflow run.</p>
<h3>ESM</h3>
<p>To support new versions of the @actions/* packages, we've upgraded
the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Don't attempt to un-zip non-zipped downloads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/460">actions/download-artifact#460</a></li>
<li>Add a setting to specify what to do on hash mismatch and default it
to <code>error</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/461">actions/download-artifact#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v7...v8.0.0">https://github.com/actions/download-artifact/compare/v7...v8.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="70fc10c6e5"><code>70fc10c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/461">#461</a>
from actions/danwkennedy/digest-mismatch-behavior</li>
<li><a
href="f258da9a50"><code>f258da9</code></a>
Add change docs</li>
<li><a
href="ccc058e5fb"><code>ccc058e</code></a>
Fix linting issues</li>
<li><a
href="bd7976ba57"><code>bd7976b</code></a>
Add a setting to specify what to do on hash mismatch and default it to
<code>error</code></li>
<li><a
href="ac21fcf45e"><code>ac21fcf</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/460">#460</a>
from actions/danwkennedy/download-no-unzip</li>
<li><a
href="15999bff51"><code>15999bf</code></a>
Add note about package bumps</li>
<li><a
href="974686ed50"><code>974686e</code></a>
Bump the version to <code>v8</code> and add release notes</li>
<li><a
href="fbe48b1d27"><code>fbe48b1</code></a>
Update test names to make it clearer what they do</li>
<li><a
href="96bf374a61"><code>96bf374</code></a>
One more test fix</li>
<li><a
href="b8c4819ef5"><code>b8c4819</code></a>
Fix skip decompress test</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/v7...v8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=7&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 14:53:06 +01:00
dependabot[bot]
41739c5bf9
Bump actions/upload-artifact from 6 to 7 (#468)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 6 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 What's new</h2>
<h3>Direct Uploads</h3>
<p>Adds support for uploading single files directly (unzipped). Callers
can set the new <code>archive</code> parameter to <code>false</code> to
skip zipping the file during upload. Right now, we only support single
files. The action will fail if the glob passed resolves to multiple
files. The <code>name</code> parameter is also ignored with this
setting. Instead, the name of the artifact will be the name of the
uploaded file.</p>
<h3>ESM</h3>
<p>To support new versions of the <code>@actions/*</code> packages,
we've upgraded the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Add proxy integration test by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
<li>Upgrade the module to ESM and bump dependencies by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/762">actions/upload-artifact#762</a></li>
<li>Support direct file uploads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/764">actions/upload-artifact#764</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Link"><code>@​Link</code></a>- made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v6...v7.0.0">https://github.com/actions/upload-artifact/compare/v6...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bbbca2ddaa"><code>bbbca2d</code></a>
Support direct file uploads (<a
href="https://redirect.github.com/actions/upload-artifact/issues/764">#764</a>)</li>
<li><a
href="589182c5a4"><code>589182c</code></a>
Upgrade the module to ESM and bump dependencies (<a
href="https://redirect.github.com/actions/upload-artifact/issues/762">#762</a>)</li>
<li><a
href="47309c993a"><code>47309c9</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/754">#754</a>
from actions/Link-/add-proxy-integration-tests</li>
<li><a
href="02a8460834"><code>02a8460</code></a>
Add proxy integration test</li>
<li>See full diff in <a
href="https://github.com/actions/upload-artifact/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 14:52:53 +01:00
Niklas Mohrin
47a936e736
Suggest trying different TLS backend when update fails (#465)
Closes #453

- Add note about changing tls_backend setting
- impl Display for TlsBackend
- Remove trailing slash in default archive source to make URL in error
look nicer
2026-02-21 00:32:23 +01:00
Niklas Mohrin
593e9309b9
Placeholder escaping (#414)
Closes #402

This adds special handling for escaped placeholders as required by the
client spec. The added tests include examples from current pages that
rely on this behavior.

The text replacements use `str::replace` which constructs a new
allocated `String`. I considered using a custom `replace_inplace` method
on `&mut str` (which works if the replacement string is at most as long
as the pattern to be replaced), but decided against it because I think
that the performance improvement is not significant enough to justify
adding `unsafe` code. It is also possible to avoid `unsafe` by
re-checking UTF-8 validity after all modifications, but the code still
felt a bit out of place for tealdeer. We can always add these
optimizations later if we want to.
2026-02-20 23:44:36 +01:00
Niklas Mohrin
8b97afe7aa
Add workflow_dispatch trigger for GitHub Pages workflow 2026-01-25 18:39:52 +01:00
Niklas Mohrin
75e5462312
Update CHANGELOG.md 2026-01-25 18:29:32 +01:00
dependabot[bot]
5ee1f28021
Bump actions/upload-artifact from 5 to 6 (#456)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 18:55:53 +01:00
dependabot[bot]
3a6fd99c85
Bump actions/download-artifact from 6 to 7 (#457)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 18:55:41 +01:00
dependabot[bot]
c5d62e5987
Bump actions/checkout from 5 to 6 (#454)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 20:32:38 +01:00
Niklas Mohrin
b3cd7b1c21
Release v1.8.1 2025-11-11 22:27:52 +01:00
Niklas Mohrin
e769114d8b
Enable ureq's socks-proxy feature (#451) 2025-11-11 17:33:19 +01:00
dependabot[bot]
e1213158e4
Bump actions/upload-artifact from 4 to 5 (#447)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 00:19:23 +01:00
dependabot[bot]
6c1d702769
Bump actions/download-artifact from 5 to 6 (#448)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 00:18:45 +01:00
Niklas Mohrin
d49c4a9e05
Release v1.8.0 2025-10-03 22:59:18 +02:00
Niklas Mohrin
49626977ff
Run cargo +nightly clippy --fix and formatting (#442) 2025-10-03 22:44:39 +02:00
Niklas Mohrin
9a83b58d51
Bump MSRV to 1.85 and update dependencies (#441)
The latest version of base64ct requires 1.85, and I don't want to think about whether older versions of crypto libraries are safe.
2025-10-03 22:38:02 +02:00
Niklas Mohrin
2b127fd67e
Highlight code examples in user docs (#440)
* Highlight code examples in user docs
* Add missing quotes to archive_source example
2025-10-03 20:59:22 +02:00
Niklas Mohrin
911508ce33
Add search.platforms config option and search all platforms by default (#435) 2025-10-03 20:15:34 +02:00
hex1c
5b306756af
Add display.show_title option to display command titles in output (#439) 2025-10-03 19:53:25 +02:00
Niklas Mohrin
abb7e8ac55
Remove native-tls from default feature set (#436) 2025-09-22 16:06:36 +02:00
Niklas Mohrin
92b6c64c87
Merge pull request #430 from niklasmohrin/language-config
Add `search.languages` and `updates.download_languages` settings
2025-09-22 00:05:24 +02:00
Niklas Mohrin
c741146db5
Add updates.download_languages setting 2025-09-22 00:00:09 +02:00
Niklas Mohrin
a74b7120bd
Add search.languages setting 2025-09-21 23:59:50 +02:00
Niklas Mohrin
3fa96a5bb2
Add test config::test::language::with_encoding 2025-09-21 23:59:13 +02:00
Niklas Mohrin
7e014093cf
Move existing tests from config module into test submodule 2025-09-21 23:59:13 +02:00
Niklas Mohrin
94f9030d36
Move Language related functionality into config module 2025-09-21 23:59:05 +02:00
dependabot[bot]
5cfb817e99
Bump actions/download-artifact from 4 to 5 (#433)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2025-09-03 17:17:51 +02:00
dependabot[bot]
4377366c97
Bump actions/checkout from 4 to 5 (#434)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 16:52:36 +02:00
Niklas Mohrin
630b7f4423
Allow references in Config (#429)
For #251, I want to use the `Language<'_>` type inside `Config`. The languages will either refer to values read from the config file, or to static strings from `get_languages_from_env`, so just using `Language<'static>` is not an option.

Instead, some input for the `Config` needs to be persisted in the main function for the duration of the program so that the config can reference it. At first I was hoping that this input would be the `contents` string from `RawConfig::load`, but as it turns out you cannot (in general) deserialize strings from toml without having to alter them, for example when they contain escapes like `\n`. Thus, the toml parser seemingly doesn't even try and just throws an error when deserializing into a borrowed string (even if it could in theory just return the correct substring from the input). Given that `RawConfig` should stay static then, the raw config itself is the next best thing to keep alive and have the config reference into.

While this change might seem a bit drastic for little benefit, I am actually pretty happy with it because I want to unify the configuration anyways at some point so that the CLI arguments, environment variables, and the config file are merged at the beginning of the program and then only a single config is used for the everything (no more `enable_styles` everywhere!). At this time, the `Config` would have references into `Cli` anyways, and having the `ConfigLoader` as an entity for this merging also seems natural.
2025-08-05 23:46:49 +02:00
Niklas Mohrin
1e87db7ab7
Cache rewrite (#416) 2025-08-01 16:03:01 +02:00
Niklas Mohrin
d1be7d6bb9
Introduce Language struct (#425)
* Remove unneeded Clone bound on Dedup
* Introduce Language struct to represent language strings
* Move language directory name logic into own method on Language type
2025-06-06 23:25:30 +02:00
Niklas Mohrin
43ab2cb920
Bump MSRV to 1.80 (#426) 2025-06-06 23:13:47 +02:00
Niklas Mohrin
bc820c5f10
Upload binaries from build step as artifact (#423) 2025-06-06 22:52:44 +02:00
MHS-0
9bb95ad11d
Add an option to specify a custom config file to be used (#422)
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2025-05-31 21:43:39 +02:00
Niklas Mohrin
769ef4da20
Merge branch 'v1.7.x' 2025-03-18 23:51:51 +01:00
Niklas Mohrin
8f433e7774
Release v1.7.2 2025-03-18 23:38:12 +01:00
Niklas Mohrin
81a0662dbc
Run CI on backport branches and on dispatch 2025-03-18 23:04:24 +01:00
Niklas Mohrin
9f43adb7f9
Require zip >= 2.3.0 2025-03-18 22:58:26 +01:00
Erick Guan
3d8d488f66
Replace reqwest with ureq (#417)
ureq is a blocking HTTP client. ureq is simpler than reqwest. This
brings:
- smaller binary size
- cleaner configuration interfaces
2025-03-09 22:06:47 +01:00
Nachiket Kanore
c6de583c46
Only create a single temporary directory in integration tests (#411) 2025-03-02 12:03:09 +01:00
Niklas Mohrin
009af7063f
Move most of main to try_main (#400)
Currently, `main` uses `print_error` and `process::exit` in several
places. These could be unified by propagating errors via `Result`s.
There is one place left that manually calls `print_error`, because the
error should be followed by help instructions printed without any
highlighting.
2025-02-26 23:04:41 +01:00
lyj
55f401df30
Add args: --edit-page and --edit-patch (#388)
Fix https://github.com/tealdeer-rs/tealdeer/issues/383

Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2025-02-26 22:03:18 +01:00
Erick Guan
09ef7f534e
Allow configuring TLS backend (#386) 2025-02-26 00:27:44 +01:00
Predrag Minic
120e2a92c2
Add configuration option for archive_source (#337)
This allows for specifying a custom archive URL as an alternative approach to Custom Pages.

Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2025-01-12 12:24:02 +01:00
Christoph Loy
09d44110f8
Add tests for osx/macos alias (#407)
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2025-01-12 12:04:22 +01:00
Christoph Loy
b9f116d629
Add common platform to CLI (#401) 2025-01-12 11:50:32 +01:00
Matěj Kafka
00c7778125
Resolve paths in config [directories] relative to the config directory (#395) 2025-01-05 00:17:44 +01:00
Niklas Mohrin
826630737c
Merge pull request #399 from niklasmohrin/tests
Various test improvements
2025-01-03 00:36:56 +01:00
Niklas Mohrin
dbab55a2b4
Use only one test thread
Otherwise the builds made by `TestEnv::command` will conflict with each
other. This manifested itself on Windows with

> linking with `link.exe` failed: exit code: 1104
> LINK : fatal error LNK1104: cannot open file '...'
2025-01-02 23:55:36 +01:00
Niklas Mohrin
e7b434d06a
Prefer config file over TEALDEER_CACHE_DIR in integration tests 2025-01-02 23:55:36 +01:00
Niklas Mohrin
fc19206029
Add ignore-online-tests feature
Since these tests require internet access, they are undesirable in
reproducible builds [1] (and in general, I guess). The overall goal is
to shrink this list to a minimum. At some point we might remove the
feature again and ignore online tests by default. Before putting much
thought into refactoring the remaining tests, we should wait until we
have introduced the `updates.archive_url` option. Then, we could run a
local webserver that serves a known file. Until then, having the feature
already helps discourage using `--update` in tests that don't need it
and allows for quicker test execution locally if wanted.

- [1]: edf04b75c1/pkgs/by-name/te/tealdeer/package.nix (L34-L44)
2025-01-02 23:55:36 +01:00
Niklas Mohrin
e85de336ed
Structure test fixtures like page cache 2025-01-02 23:55:35 +01:00
Niklas Mohrin
909bad9c65
Fix feature selection in integration tests 2025-01-02 23:55:35 +01:00
Niklas Mohrin
ceee231891
Merge pull request #398 from niklasmohrin/dependencies
Update dependencies to hush dependabot
2025-01-01 19:33:59 +01:00
Niklas Mohrin
0c55a4b82a
Run cargo update --recursive -p rustls 2025-01-01 19:22:25 +01:00
Niklas Mohrin
e3a06eefe3
Run cargo update -p hashbrown 2025-01-01 19:21:36 +01:00
Niklas Mohrin
6869837e79
Run cargo update -p url to bump idna 2025-01-01 19:21:04 +01:00
Niklas Mohrin
ee5418b5a8
Run cargo clippy --fix (Rust 1.83) (#397) 2025-01-01 19:07:40 +01:00
Niklas Mohrin
1d9153e37e
Release v1.7.1 2024-11-14 22:11:34 +01:00
Niklas Mohrin
d719f21f7b
Bump futures-util (0.3.30 is yanked) 2024-11-14 21:59:23 +01:00
Blair Noctis
fb7492b0b5
Upgrade yansi: 0.5.1 -> 1.0.1 (#389)
- Adapt to `thing.paint(style)` API; was `style.paint(thing)`
- Remove `yansi::Paint::enable_windows_ascii()` in style usage decision;
  removed in yansi commit b186eb5bfb, which introduced "automatic"
  support for Windows: "If support is not available, styling is disabled
  and no styling sequences are emitted", fitting the `Auto` option
- Respect `--color=always` even if we know it won't work

---------

Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2024-11-14 16:07:30 +01:00
Danilo Bargen
d44613cf59 Release v1.7.0 2024-10-02 23:02:32 +02:00
Danilo Bargen
713f6f913c
Run cargo update (#381) 2024-10-02 22:44:22 +02:00
Niklas Mohrin
ec2daa495c
Replace atty with std::io::IsTerminal (#380) 2024-10-02 20:32:17 +00:00
Danilo Bargen
fc6d644152
Update all URLs to point to new GitHub organization (#379)
See https://github.com/tealdeer-rs/tealdeer/issues/376 for details.
2024-09-30 11:27:19 +02:00
Danilo Bargen
8995f9f07e
Merge pull request #374 from dbrgn/updates-20240812
Update Dependencies
2024-08-13 00:15:16 +02:00
Danilo Bargen
7c245040b9
Command line help: Add hint about platform overrides (#375)
- Rename PLATFORMS to PLATFORM
- Add hint about specifying platform multiple times
2024-08-13 00:14:19 +02:00
Danilo Bargen
4f51ba090f Upgrade zip: 0.6.6 → 2.1.6 2024-08-12 23:09:55 +02:00
Danilo Bargen
60855cedcf Upgrade toml: 0.5.11 → 0.8.19 2024-08-12 23:02:28 +02:00
Danilo Bargen
9096d536b1 Upgrade reqwest: 0.11.27 → 0.12.5 2024-08-12 23:02:22 +02:00
Danilo Bargen
13a3d58c8b Upgrade predicates: 2.1.5 → 3.1.2 2024-08-12 22:57:32 +02:00
Danilo Bargen
13f2c4c714 Upgrade env_logger: 0.10 → 0.11 2024-08-12 22:54:58 +02:00
Danilo Bargen
65495da5a5 Run cargo update 2024-08-12 22:54:03 +02:00
Danilo Bargen
f3d74850a8
Make clippy happy (#373) 2024-08-06 21:46:40 +00:00
Danilo Bargen
290afe385d
Merge pull request #368 from dbrgn/365-apple-silicon-builds
CI: Create release builds for Apple silicon (ARM)
2024-06-17 01:28:56 +02:00
Danilo Bargen
7620a67306 CI: Set name for every workflow 2024-06-17 01:07:29 +02:00
Danilo Bargen
706c1408a7 CI: Don't publish docs for 0.x test releases 2024-06-17 01:02:26 +02:00
Danilo Bargen
64f90a625e CI: Create release builds for Apple silicon (ARM) 2024-06-17 00:59:42 +02:00
Danilo Bargen
83b2bdaf33
Merge pull request #298 from dbrgn/clap-v4
Migrate to Clap v4, bump MSRV to 1.75
2024-06-17 00:44:53 +02:00
Danilo Bargen
64f3fbbbc6 Apply clippy suggestions 2024-06-17 00:35:04 +02:00
Danilo Bargen
86293c8723 Bump MSRV to 1.75 2024-06-17 00:35:04 +02:00
Danilo Bargen
de1684edb6 Update to clap v4 2024-06-17 00:35:04 +02:00
Adam Henley
3a14232105
Generate docs only upon release (#362)
Signed-off-by: Adam Henley <adamazing@gmail.com>
2024-06-06 17:17:20 +02:00
dependabot[bot]
9957dcea74
Bump peaceiris/actions-mdbook from 1 to 2 (#360)
Bumps [peaceiris/actions-mdbook](https://github.com/peaceiris/actions-mdbook) from 1 to 2.
- [Release notes](https://github.com/peaceiris/actions-mdbook/releases)
- [Changelog](https://github.com/peaceiris/actions-mdbook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-mdbook/compare/v1...v2)

---
updated-dependencies:
- dependency-name: peaceiris/actions-mdbook
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 18:51:53 +02:00
dependabot[bot]
7633e46541
Bump peaceiris/actions-gh-pages from 3 to 4 (#359)
Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4.
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peaceiris/actions-gh-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 18:50:44 +02:00
K.B.Dharun Krishna
ee6d2418f1
Add BSD platform support and update workflows (#354) 2024-04-10 16:05:51 +02:00
Linus Walker
c8f2408373
Performance optimization with cargo flags (#355) 2024-03-03 18:35:09 +01:00
Zacchary Dempsey-Plante
17c08f0261
Change custom page files to use a .md extension (#322)
* Change custom page files to use a `.md` extension.
* Add a breaking change warning to the documentation for the usage of custom pages.
* Add a temporary check for old custom pages to assist in the migration to the new naming convention.

Co-authored-by: Danilo Bargen <mail@dbrgn.ch>
2024-02-15 19:27:56 +00:00
Zacchary Dempsey-Plante
ad69985e42
Update the Nix package link in the documentation (#353) 2024-02-11 20:58:06 +00:00
Danilo Bargen
2e731d7d17
Bump MSRV to 1.64 (#352) 2024-02-11 15:20:11 +01:00
Nicolai Fröhlich
d04e671c46
Add release build target "linux-aarch64-musl" for platform "linux/arm64" (#351) 2024-01-17 23:30:03 +01:00
Felix Yan
5bdcd6ae9c
Update Arch Linux package URL in installing.md (#348)
The old URL returns 404 now.
2023-11-21 12:34:22 +01:00
Niklas Mohrin
efbdebb426
Update Github actions badge (#346) 2023-10-21 14:51:16 +02:00
Niklas Mohrin
a82bc4b8d2
Add "Tips and Tricks" chapter to user manual (#342) 2023-10-02 21:06:09 +02:00
JJ Style
9e1489bbc5
Allow querying multiple platforms (#300) 2023-08-27 21:52:20 +00:00
Andrea Frigido
1764cee1bd
Cargo.toml: Update license field following SPDX 2.1 license expression standard (#336) 2023-08-13 19:13:06 +00:00
Niklas Mohrin
4d2fc26dbe
Include completion/* in published crate (#333) 2023-07-10 18:22:20 +02:00
K.B.Dharun Krishna
7bd5ac5664
Update CI workflows (#324) 2023-07-06 23:50:14 +02:00
Iliia Maleki
de9cf431a8
Completions: Add checks for wrong tldr --list output (#327) 2023-07-06 16:22:50 +02:00
Adam Henley
45b3db6dc4
Guard Fish completion when cache is empty (#331)
Signed-off-by: Adam Henley <adamazing@gmail.com>
2023-07-06 16:20:46 +02:00
qknogxxb
8c6754ae78
Line iterator: Improve test functions (#316) 2023-07-05 23:33:39 +02:00
Danilo Bargen
94d56c01a7
Allow building with native-tls (#303)
Right now we only support building with Rustls. However, there are quite
a few architectures that aren't yet supported by "ring" (the crypto
library used by Rustls), for example MIPS, PowerPC or SPARC.

To offer an alternative, I added the "native-tls" feature that can be
used instead of "native-roots". When used, the native TLS stack is used
instead of Rustls (i.e. SChannel on Windows, Secure Transport on macOS
and OpenSSL otherwise).
2023-02-14 18:21:07 +01:00
Danilo Bargen
84621cda81
Merge pull request #293 from dbrgn/documentation-fixes
Documentation fixes: Some leftovers, fixes and improvements.
2023-02-12 19:07:28 +01:00
Danilo Bargen
fb755ba8dc Docs: Consistent capitalization of tealdeer
Capitalize at the start of a sentence, lowercase otherwise.
2023-02-12 18:54:05 +01:00
Danilo Bargen
c9ec88354a Docs: Remove section about overriding cache dir
The env var `TEALDEER_CACHE_DIR` is deprecated. The new way to configure
the cache dir is documented in the "[directories]" section.
2023-02-12 18:49:39 +01:00
Danilo Bargen
74d1ade884 Docs: Mention building with bundled roots 2023-02-12 18:49:39 +01:00
Danilo Bargen
b07a6728f3 Docs: Remove note about OpenSSL
OpenSSL is not used anymore since version 1.5.0.
2023-02-12 18:49:39 +01:00
Danilo Bargen
2fa1994c76 CHANGELOG: Fix typo 2023-02-12 18:49:39 +01:00
qknogxxb
72362769de
Line iterator: Drop empty line without allocating (#314)
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
2023-02-12 17:45:53 +00:00
Blair Noctis
0c8cb42664
Docs: Add Debian to packaged list (#315) 2023-02-10 22:28:45 +00:00
tveness
76d9d0bbda
Obey 1.67 clippy lints (#313) 2023-02-07 21:56:41 +01:00
Y.D.X
c077ff05e4
Docs: Fix typo (irregular space → regular space) (#310)
It was a no-break space (U+00A0), which causes mdBook to ignore the line break and [render wrongly](https://dbrgn.github.io/tealdeer/installing.html#package-managers).

Instead, a regular space (U+0020) should be used.
2023-01-11 10:22:49 +00:00
Danilo Bargen
b5f2c6ff39 Upgrade env_logger: 0.9 → 0.10 2022-12-24 15:03:41 +01:00
Danilo Bargen
e75f814294 Run cargo update 2022-12-24 15:03:41 +01:00
Mohit Raj
fc8266d1c9
Docs: Add scoop package manager as supported installation method (#305) 2022-12-18 23:40:27 +01:00
Danilo Bargen
76bb4e72bc Update CHANGELOG
Forgot to add myself 🙃
2022-10-24 15:04:24 +02:00
Danilo Bargen
7c371a6852 Release v1.6.1 2022-10-24 14:42:37 +02:00
Danilo Bargen
b10f8a9485
Run cargo update (#299) 2022-10-22 20:24:57 +00:00
Danilo Bargen
ab5148f237
Fix path source for custom pages dir (#297) 2022-10-22 22:00:29 +02:00
Cyrus Yip
86c850282e
Docs: improve grammar (#296) 2022-10-11 12:08:41 +00:00
Danilo Bargen
357f0e7e71 Release v1.6.0 2022-10-02 00:36:58 +02:00
Danilo Bargen
7ded00eda0
Run cargo update (#291) 2022-10-01 22:23:26 +00:00
Danilo Bargen
6f72ce778d
Merge pull request #290 from dbrgn/remove-deprecated-commands
Remove deprecated commands
2022-10-02 00:05:00 +02:00
Danilo Bargen
2ebf86b265 Remove deprecated -m/--markdown command 2022-10-01 23:54:37 +02:00
Danilo Bargen
efe1b57696 Remove deprecated -o/--os command 2022-10-01 23:53:32 +02:00
Danilo Bargen
0c93a4cfe9 Remove deprecated --config-path command 2022-10-01 23:52:03 +02:00
Danilo Bargen
99c86a4aff
Merge pull request #276 from dbrgn/cache-dir-config
Allow overriding cache directory through config
2022-10-01 12:32:41 +02:00
Danilo Bargen
c44faf2b09 Lazy creation of cache directory 2022-10-01 12:18:44 +02:00
Danilo Bargen
3f528ba5cb Impl From<RawX> for more config types
- StyleConfig
- DisplayConfig
- UpdatesConfig
2022-10-01 12:18:34 +02:00
Danilo Bargen
d702a1fab6 Cache: Add pages_dir helper method
Additionally, rename `cache_dir` to `pages_dir` where terminology was
wrong.
2022-10-01 12:18:34 +02:00
Danilo Bargen
3e7dfb3c71 Cache: Make URL a parameter, not a field 2022-10-01 12:18:33 +02:00
Danilo Bargen
fb89303e85 Move custom_pages_dir business logic from RawConfig to Config
This also introduces the path source information for that directory,
which was previously unknown.
2022-10-01 12:18:30 +02:00
Danilo Bargen
7338933de5 Add integration test for cache_dir 2022-10-01 12:13:40 +02:00
Danilo Bargen
afbdfa6746 Bring back path source for cache directory 2022-10-01 12:13:40 +02:00
Danilo Bargen
33f0f108d8 Allow overriding cache directory through config
- Config: Add `cache_dir` to `[directories]` section
- Logic to handle legacy env var, config override and default cache dir
  selection is now in the `Config::try_from(RawConfig)` impl
- Deprecate old `TEALDEER_CACHE_DIR` env variable
- Update docs

Co-authored-by: Hans Gaiser <hansg91@gmail.com>
2022-10-01 12:13:39 +02:00
Danilo Bargen
026ae72742
Switch from ansi_term to yansi (#288)
ansi_term is not actively maintained anymore (see
https://rustsec.org/advisories/RUSTSEC-2021-0139). Replace it with a
suitable alternative.

I looked at termcolor, owo-colors and yansi and picked yansi:

- It is very simple
- It does not do terminal color support checking (we already do that in
  tealdeer)
- Its color enum type is almost identical to the one from ansi_term, so
  migrating is easy

The only change from a config API point of view is that "purple" is now
renamed to "magenta", but "purple" still works.
2022-09-27 23:55:50 +02:00
Danilo Bargen
f45a502383
Run cargo update (#287) 2022-09-24 20:54:01 +00:00
Olav de Haas
f4a94112f4
Add custom pages to list output (#285)
Fixes #205.
2022-09-24 21:27:35 +02:00
Niklas Mohrin
aa67a59d5b
Merge pull request #284 from niklasmohrin/edition-and-lints
Switch to 2021 Edition and maintenance
2022-08-21 23:18:39 +02:00
Niklas Mohrin
65cbe80e9d
Add comment to empty rustfmt.toml 2022-08-21 22:39:02 +02:00
Niklas Mohrin
a6023a5234
Small fixes in ci.yml
- Add `profile: minimal`
- Install `rustfmt` when installing the toolchain, not afterwards
- Use stable toolchain for `rustfmt`
- Replace `override` with `default`. I think this better reflects the
  "use what we just installed" intent.
2022-08-21 22:39:02 +02:00
Niklas Mohrin
db492bc0cf
Upgrade to 2021 edition of Rust
To do so, we need to bump the MSRV to 1.56 (released 10/2021). While at
it, I actually bumped it all the way to 1.62, where the latest version,
(1.62.1) has been released on July 19, which is just more than one month
ago. Additionally, I removed `clippy.toml` and instead specified the
MSRV in `Cargo.toml`. This will also print an error when trying to
compile with an older compiler.
2022-08-21 22:32:07 +02:00
Niklas Mohrin
e94ee92db4
cargo clippy --fix 2022-08-21 21:53:10 +02:00
bagohart
b369678794
added missing fish completion for language (#280) 2022-07-18 10:01:03 +00:00
Kian-Meng Ang
97b4395e25
Fix typos (#277) 2022-06-20 12:35:17 +00:00
Danilo Bargen
543fa2290d
Merge pull request #274 from newsch/android-platform
Add support for Android platform
2022-06-14 23:11:08 +02:00
Evan Lloyd New-Schmidt
abbbbf01ed Add PlatformType::current for android target 2022-06-14 15:50:18 -04:00
Evan Lloyd New-Schmidt
4a5410fd0b Add support for android platform
tldr added android-specific commands under a new 'android' platform
last year: <https://github.com/tldr-pages/tldr/issues/5462>
2022-06-02 12:36:20 -04:00
Danilo Bargen
0f82aa5950
Create macOS release builds with bundled root certificates (#272)
As reported in https://github.com/dbrgn/tealdeer/issues/244, some users
on macOS had problems with `rustls-tls-native-roots`. Since we did not
find the root cause of this, we'll build macOS release builds with
`rustls-tls-webpki-roots` instead.

Fixes #244.
2022-05-06 10:43:30 +00:00
Danilo Bargen
d138ce23e4
Merge pull request #271 from dbrgn/updates
Updates
2022-05-03 22:54:56 +02:00
Danilo Bargen
750ebf653c CI: Bump rustfmt toolchain to 1.60 2022-05-03 22:31:58 +02:00
Danilo Bargen
62b6405685 Fix clap deprecation warnings 2022-05-03 22:10:18 +02:00
Danilo Bargen
3629002d5c Upgrade zip: 0.5 → 0.6 2022-05-03 21:49:44 +02:00
Danilo Bargen
6e09af2f8b Run cargo update 2022-05-03 21:47:15 +02:00
Simon Perdrisat
786933e507
Docs: Add MacPorts in the list of package managers (#270) 2022-04-09 15:10:22 +00:00
Danilo Bargen
5a1a21b8ab
Add note about auto-updates when cache is missing (#254) 2022-02-06 22:53:48 +01:00
Danilo Bargen
716ff15779
Merge pull request #262 from dbrgn/fix-completions
Clean up shell completions
2022-01-22 01:31:51 +01:00
Danilo Bargen
cc22760e5a Align help text and completion descriptions 2022-01-21 09:15:52 +01:00
Danilo Bargen
cab6666ffb Fix completions: Remove -p for pager 2022-01-21 09:14:30 +01:00
cyqsimon
d5dbe3084b
Add no-auto-update flag (#257)
* Added `no-auto-update`
* Updated usage.txt
* Add `no-auto-update` to completion scripts
* Apply review suggestions
2022-01-21 09:12:10 +01:00
Danny Moesch
2177297871
Move shell completion scripts to their own directory (#259)
* Move shell completion scripts to their own directory
* Remove completion scripts from published Cargo package
2022-01-20 09:10:18 +01:00
Danny Moesch
3b4451f8fa
Move definition of CLI arguments and options into own file (#258) 2022-01-19 23:49:23 +01:00
cyqsimon
ef7432c762 Enforce default rustfmt settings (#257) 2022-01-16 20:16:58 +01:00
Danilo Bargen
b5443b75b6
Merge pull request #253 from dbrgn/handle-download-errors
Handle cache download errors. Fixes #252.
2022-01-09 21:24:22 +01:00
Danilo Bargen
77a6d48381 Pages download: Check return status 2022-01-09 21:14:30 +01:00
Danilo Bargen
71b6e037a7 When decompressing ZIP fails, propagate error
Do not unwrap. Instead, propagate the error with a anyhow context.
2022-01-09 21:14:30 +01:00
Danilo Bargen
04804ae650
Merge pull request #231 from tranzystorek-io/fix-pager
Move pager configuration near render logic. Fixes #169.
2022-01-09 15:13:57 +01:00
Marcin Puc
4a9763a783 Ensure that pager configuration is run at most once 2022-01-09 13:08:10 +01:00
Marcin Puc
82e4b97f92 Move pager configuration near render logic 2022-01-09 01:27:42 +01:00
Danilo Bargen
d502fcdd6d
Merge pull request #249 from dbrgn/anyhow
Use anyhow for error handling
2022-01-09 01:13:43 +01:00
Danilo Bargen
5262234846 Remove error module and clean up 2022-01-09 01:04:24 +01:00
Danilo Bargen
fd307757d7 Port cache and output modules to anyhow results 2022-01-09 01:04:07 +01:00
Danilo Bargen
405a2c9c8f Port config module to anyhow results 2022-01-09 01:04:07 +01:00
Danilo Bargen
9e97d454f0 Add anyhow dependency 2022-01-08 22:08:29 +01:00
Danilo Bargen
84277cc31e
Cache: Return error if HTTP client cannot be created (#247)
Apparently `Client::new` can panic (see #244), so let's return the error instead.
2022-01-08 22:01:36 +01:00
Danilo Bargen
eadfe97335 CHANGELOG: Mention the switch to Rustls 2022-01-02 20:51:44 +01:00
Danilo Bargen
47a4d6d7ca Docs: Remove note about patch concatenation (#245) 2022-01-02 02:04:52 +01:00
Danilo Bargen
b09f250370
Use clap 3 stable (#243) 2022-01-01 15:10:20 +01:00
Danilo Bargen
8228d31451 upload-asset script: Change release name 2022-01-01 14:43:58 +01:00
Danilo Bargen
81c30bfb77 Release v1.5.0 2021-12-31 23:04:51 +01:00
Danilo Bargen
20b7c5c33f Run cargo update 2021-12-31 18:53:56 +01:00
Danilo Bargen
0ebf5aca87
Merge pull request #240 from dbrgn/ci-release
Release with GitHub Workflow
2021-12-31 18:28:54 +01:00
Danilo Bargen
d1c17f2eb6 Remove release-build.sh
We don't need it anymore now that we have a CI pipeline for release
binaries.
2021-12-31 18:20:17 +01:00
Danilo Bargen
c155b6f98d CI: Clean up release workflow and upload script
- Clean up scripts
- Create draft release before uploading assets
- Find release for current tag, don't simply pick the latest
- Add Windows builds
- Add checksums for binaries
2021-12-31 17:56:03 +01:00
alstjr7375
2007aa0c32 CI: Add initial release workflow 2021-12-31 00:29:28 +01:00
Danilo Bargen
e722b4f92e
Rename main branch (#239) 2021-12-30 22:39:27 +01:00
Danilo Bargen
7e44095e63
Merge pull request #238 from dbrgn/181-page-patch-chain
Concatenate page and patch bytes before processing
2021-12-30 22:11:12 +01:00
Danilo Bargen
ed3d1ea9af Always add empty line before concatenating patch file 2021-12-30 21:09:37 +01:00
Danilo Bargen
2b1167f19b Update custom patches docs 2021-12-30 00:12:51 +01:00
Danilo Bargen
fb40974616 Cache: Add tests for PageLookupResult::reader
Co-authored-by: Dalton Maahs <dalton.maahs@yourbind.com>
2021-12-30 00:01:31 +01:00
Danilo Bargen
0ef31d1f17 Concatenate page and patch bytes before processing
This way a patch file does not need a well-formed header anymore.

Fixes #181.
2021-12-30 00:01:31 +01:00
Danilo Bargen
0d1300c218
Merge pull request #236 from dbrgn/custom-pages-in-show-paths
Custom pages in show paths
2021-12-29 22:38:05 +01:00
Danilo Bargen
bbac67dd29
Merge pull request #237 from dbrgn/improved-docs
Improve docs.

Fixes #235, refs #218.
2021-12-24 21:56:17 +01:00
Danilo Bargen
ca844b35e9 Docs: Document custom pages and patches 2021-12-20 01:05:24 +01:00
Danilo Bargen
abeb5c3757 Docs: Add directories config section 2021-12-20 01:05:24 +01:00
Danilo Bargen
66255219fa Docs: Improve structure of config docs 2021-12-20 01:05:21 +01:00
Danilo Bargen
6508ec6ac6 Ensure that default custom_pages_dir ends with trailing slash 2021-12-20 00:26:19 +01:00
Danilo Bargen
00ec281b9b Include custom pages directory in --show-paths command 2021-12-20 00:25:48 +01:00
Danilo Bargen
f8785fbc3e
Merge pull request #233 from dbrgn/updates
Update dependencies
2021-12-11 19:22:00 +01:00
Danilo Bargen
876f390ac6 Clap: Enable colored error messages 2021-12-11 18:00:19 +01:00
Danilo Bargen
b726840246 Run cargo update 2021-12-11 18:00:19 +01:00
Danilo Bargen
69f4808214 Clap: Hide deprecated -o/--os argument from help 2021-12-11 18:00:19 +01:00
Danilo Bargen
25d771778a Update clap to 3.0.0-rc.4 2021-12-11 17:36:28 +01:00
Danilo Bargen
1a3624d011
Introduce command_or_file arg group (#230)
This allows to say "this command line parameter requires either a
command or a raw file".

Fixes #219.
2021-12-05 20:45:35 +01:00
Danilo Bargen
72d753c52b
Add "macos" alias for "osx" (#215)
The Apple computer operating system was renamed from "OS X" to "macOS"
in 2016. We should accept "macos" instead of "osx" in all our CLI APIs,
and the docs should also mention this alias instead of "osx".

For backwards compatibility and for compatibility with the tldr client
specification, we should still accept "osx" though.
2021-12-05 16:38:35 +01:00
Danilo Bargen
023a9d2079
Lowercase page names before lookup (#227) 2021-12-05 14:31:03 +01:00
Danilo Bargen
02a395ad13
Remove last traces of short -p pager flag (#228)
I missed some occurrences in #217.
2021-12-05 14:15:49 +01:00
Danilo Bargen
2684727eaa
Add helper functions: print_warning / print_error (#223)
These functions prepend a consistend prefix and they also use colors
(when enabled).

Messages are written to stderr.
2021-12-05 14:00:59 +01:00
Danilo Bargen
0fbc95cb71
Rename -o / --os to -p / --platform (#217)
This makes it compliant with the client spec.

The `-p` flag was previously used for the pager, using the pager now
requires using the long flag (`--pager`). Alternatively the pager can be
configured through the config file.

This commit also renames OsType to PlatformType.
2021-12-05 13:28:58 +01:00
Danilo Bargen
02c2d6709a
Run cargo update (#226)
This fixes a vulnerability in tokio (RUSTSEC-2021-0124).
2021-12-04 21:47:58 +01:00
Danilo Bargen
9c90f8ba7a
Fix clippy lints added in 1.57 (#225) 2021-12-04 21:34:06 +01:00
Danilo Bargen
746d4dadda
Fix clippy warnings (#222)
- Silence warning about excessive bools
- Implement recommendation about unneeded Option in return type
2021-10-24 23:30:10 +02:00
Marcin Puc
3beed4ad48
Simplify clap derive definition (#221)
- Use `possible_values`
- Change `render` field type to `PathBuf`
- Add comment about explicit field names
2021-10-24 23:17:43 +02:00
Danilo Bargen
cea3bcdc3f
Update benchmarks (#216)
New numbers with clap instead of docopt (current master branch). This
change improved performance considerably.
2021-10-18 00:21:43 +02:00
Danilo Bargen
c6bc6f8183
Merge pull request #108 from dbrgn/clap
Argument parsing: Switch from docopt to clap

Fixes #106.
2021-10-17 22:35:55 +02:00
Danilo Bargen
618ecaf71f Add @niklasmohrin to crate authors 2021-10-17 22:23:35 +02:00
Danilo Bargen
371f1d2ace Help: Add a link to the online docs 2021-10-17 22:22:45 +02:00
Danilo Bargen
6823e271a0 Disable clap colors for now
May be re-introduced if clap adds a `DisableColoredHelp` setting.

Relevant discussion:

- https://github.com/clap-rs/clap/pull/2845#issuecomment-945185102
- https://github.com/dbrgn/tealdeer/pull/108#issuecomment-945182471
2021-10-17 22:10:08 +02:00
Danilo Bargen
fc726011d1 Move free-standing get_os function to OsType::current 2021-10-17 21:16:50 +02:00
Danilo Bargen
8833b6b401 Rename -m / --markdown to -r / --raw
That flag name is clearer: We output raw page data without processing.
2021-10-17 21:12:08 +02:00
Danilo Bargen
ee0d32d3de CI: Ensure that usage string is always up to date 2021-10-17 21:12:08 +02:00
Danilo Bargen
6d77483ad5 Implement Default for ColorOptions 2021-10-17 21:12:08 +02:00
Danilo Bargen
5ad7dcfa34 Argument parsing: Switch from docopt to clap 2021-10-17 21:12:08 +02:00
Danilo Bargen
6300b6a24f
Merge pull request #214 from dbrgn/remove-udeps
Remove unused dependencies
2021-10-17 19:48:05 +02:00
Danilo Bargen
84227937ae Remove some optional zip features
By getting rid of the bzip2 and time features, we reduce the number of
dependencies. The official tldr pages archive uses deflate compression,
so that should be sufficient.
2021-10-16 20:40:40 +02:00
Danilo Bargen
0ebd727a32 Remove tar dependency
It's unused since #213.
2021-10-16 20:36:53 +02:00
Danilo Bargen
f7da0b028d Merge pull request #213 from invakid404/use-proper-tldr-archive
Download tldr pages from proper archive URL.
2021-10-16 20:10:00 +02:00
invakid404
62725fca5f When clearing cache, only delete pages, not entire cache dir
Squashed commits:

- chore: wipe only pages instead of entire cache dir
- fix: only delete dir if it exists
- chore: try to make variable names less confusing
- chore: display error as well
2021-10-16 20:09:32 +02:00
invakid404
006ec6f3c0 Switch to recommended tldr-pages archive URL
Instead of downloading the GitHub repository archive, download the pages
from the official URL instead.

Squashed commits:

- chore: use recommended tldr-pages archive url
- chore: extract pages dir to constant
- chore(deps): install zip crate
- feat: use zip crate to unpack archive
- chore(deps): drop flate2
2021-10-16 20:09:32 +02:00
Danilo Bargen
7bd08e35d1 Refactor cache freshness checks
- Create explicit CacheFreshness enum
- Do not exit inside `check_cache` function, this prevents further
  actions if the cache isn't found

Co-authored-by: invakid404 <invakid404@riseup.net>
2021-10-16 20:09:16 +02:00
rnd
32cc6d5893
Use default style if user-defined style is missing (#210)
Changes the serde configuration of the `RawConfig` struct to utilize
a custom `Default` implementation to initialize new objects. This
implementation contains the default style set as it was previously
provided through the implementation of `RawConfig::new()`.

This allows to utilize the default configuration until the user
overwrites the related `RawStyleConfig` field, e.g. the description
highlighting.

Fixes #149.
2021-10-16 17:38:07 +02:00
Danny Mösch
3c92cff865
Add test for hightlighting with empty command (#178) 2021-10-16 17:20:07 +02:00
Danilo Bargen
9eca2fe5db Update copyright year 2021-10-16 13:06:28 +02:00
Tsvetomir Bonev
3f49100307
Docs: Add funtoo package to list (#208) 2021-10-03 16:10:30 +02:00
Danilo Bargen
e84fce6f90
Use tree-style imports (#204) 2021-09-13 09:06:21 +02:00
Danilo Bargen
b0449dc6bf
Merge pull request #203 from dbrgn/update-deps
Update dependencies
2021-09-12 23:29:06 +02:00
Danilo Bargen
07dc5c6bf4 Upgrade predicates and assert_cmd: 1 → 2 2021-09-12 21:12:00 +02:00
Danilo Bargen
85ab38d892 Remove dev dependency on remove_dir_all
It's a transitive dependency of the `tempdir` crate. It was pinned to a
specific version to achieve compatibility with Rust 1.39, but we now run
on 1.53.
2021-09-12 21:06:06 +02:00
Danilo Bargen
7aa111e727 Upgrade reqwest: 0.10 → 0.11
This increases the release binary size by 144 KiB, but it reduces the
number of transitive dependencies from 152 to 140.
2021-09-12 20:56:24 +02:00
Danilo Bargen
b359fd8e5b Upgrade pager: 0.15 → 0.16 2021-09-12 20:42:10 +02:00
Danilo Bargen
4376162914 Upgrade env_logger: 0.7 → 0.9 2021-09-12 20:40:08 +02:00
Danilo Bargen
ffc30d1243 Run cargo update 2021-09-12 20:35:00 +02:00
Danilo Bargen
a811788b31
Merge pull request #189 from niklasmohrin/simplify_highlight_commands
Refactor formatting code
2021-09-12 20:31:45 +02:00
Danny Mösch
1471b1d97b
Create cache directory path if it does not exist (#174)
Fixes #167
2021-09-12 20:28:22 +02:00
Niklas Mohrin
3ac087ae41
Final touches for formatting refactor
- Rename: `yield_snippet` => `process_snippet`
- Rename: `HighlightingSnippet` => `PageSnippet`
2021-09-08 15:02:25 +02:00
Niklas Mohrin
b216a63c64
Add integration test for non-ascii page rendering
Although this did not fail on old master, it is still an important case
to test. Actually, this test doesn't even break when introducing some
errors that fail i18n unit tests, unless the command name itself is
non-ascii.
2021-09-08 14:44:10 +02:00
Niklas Mohrin
22baa455b5
Update inkscape-default.expected 2021-09-08 14:44:09 +02:00
Niklas Mohrin
743998a75f
Reorder functions in formatter.rs
As someone reading this file for the first time, I would want to see the
only public and most general function first and find the specifics
further down instead of having to look for the "module entry" first.
2021-09-08 14:44:09 +02:00
Niklas Mohrin
808ad7ff30
Extract output logic for pages into own module
This moves `print_page` from `main.rs` and `print_snippet` from
`formatter.rs` into a new file `output.rs`. To decompose `print_snippet`
from `print_lines`, the latter now takes the `yield_snippet` callable as
an argument (similar to how the helper methods got a hold of it). In
order for this to work, callers have to provide a function that is
generic over all possible snippet lifetimes.
2021-09-08 14:40:32 +02:00
Niklas Mohrin
e8b1c9e801
Rename Tokenizer to LineIterator
This also replaces the `next_token` method with the `next` method from
the `Iterator` trait.
2021-09-08 14:39:57 +02:00
Niklas Mohrin
62e82461cb
Refactor most of formatter.rs
This includes
- adding the `HighlightingSnippet` enum as a common ground for
  highlighting and printing code to communicate
- decomposing `print_lines` accordingly
- clearing up `highlight_code_segment` (previously `highlight_command`)
- adding unit tests (now that they can reason about
  `HighlightingSnippet`s instead of having to output on the integration
  test level
2021-09-08 14:39:51 +02:00
Niklas Mohrin
cdbca5c53b
Add FindFrom extension trait to str to start searching at a byte offset.
This also moves the contents of `dedup.rs` into a shared module `extensions`.
2021-09-08 14:38:31 +02:00
Danilo Bargen
49f2f8a3dd Mention italic styling in docs 2021-09-06 17:02:53 +02:00
Wesley Klop
8051c3169a
Implement support for italics (#197)
Closes #146.
2021-09-06 17:01:55 +02:00
Danilo Bargen
4e0d497347
Merge pull request #201 from tranzystorek-io/refactor-misc
Add various small refactorings
2021-08-29 16:53:04 +02:00
Marcin Puc
a4ac910e86 Add various small refactorings 2021-08-29 16:12:24 +02:00
Marcin Puc
9ac67ec562 Bump CI toolchain to 1.53 2021-08-29 16:11:46 +02:00
Niklas Mohrin
d5d3d20451
Run clippy on stable with msrv setting instead of on msrv (#200) 2021-08-17 19:40:35 +02:00
Niklas Mohrin
123c809630
Merge pull request #163 from niklasmohrin/hyperfine 2021-07-30 15:46:03 +02:00
Niklas Mohrin
d1d36a961a
Update README.md to include updated benchmark results 2021-07-30 15:31:46 +02:00
Niklas Mohrin
e7c5daa9b7
Add Dockerfile for benchmarks 2021-07-30 15:31:17 +02:00
Rithvik Vibhu
187214e39e
Remove AUR links for tealdeer and tealdeer-bin (#196)
The package is now in the community repos.
2021-07-19 13:57:16 +02:00
bl-ue
a5aa822d12
README: Update @SShrike → @severen (#193) 2021-06-22 13:31:51 +02:00
Danilo Bargen
767b62d493
Merge pull request #187 from sondr3/performance-fixes
- Use unbuffered I/O for faster printing to stdout
- Replace OpenSSL with Rustls
2021-05-26 09:40:58 +02:00
Sondre Nilsen
feb20d8c5c Add WriteError type, convert print_lines to return a result 2021-05-25 16:38:28 +02:00
Sondre Nilsen
22693c09ba Use rustls for TLS to avoid dynamic linking 2021-05-25 16:38:00 +02:00
Sondre Nilsen
06f771f64d Use unbuffered IO for faster printing to stdout 2021-05-25 16:37:57 +02:00
Danilo Bargen
a72ba4a7a9
Merge pull request #191 from dbrgn/msrv
Update MSRV policy
2021-05-13 14:45:05 +02:00
Danilo Bargen
e32ce77c0c CI: Bump Rust version 2021-05-13 14:31:22 +02:00
Danny Mösch
b6342633cd
Use which instead of tar as reference tldr page in tests (#192)
The page for `tar` changes pretty frequently. `which` was last updated in 2018 and is a rather simple command overall. So it is less likely that the reference in the repository has to be updated as often as the one for `tar`.
2021-05-13 14:12:11 +02:00
Cameron Tod
25f736564d
Docs: Fix packet/package typo (#185) 2021-05-13 00:25:41 +02:00
Danilo Bargen
3223fec5ee Update MSRV policy
As discussed in #190.
2021-05-13 00:06:45 +02:00
Dalton
4d4c7b6a5d
Fix clippy warnings (#188)
* Fix clippy warnings
* Update toolchain for CI workflows fmt and clippy
2021-05-11 13:28:26 +02:00
Dalton
388deac079
Send all progress logging messages to stderr (#171)
- Change logging messages to go to stderr
- Updated tests to expect these messages in stderr instead of stdout
- Changed test_markdown_rendering to no longer rely on the tldr pages repo
2021-05-10 09:17:13 +02:00
Danny Mösch
a92e974ac2
Update expected output for 'tar' tldr page (#186) 2021-05-09 18:05:50 +02:00
Dalton
0fce79f8ed
Add unit tests for custom pages / patches (#183) 2021-04-23 20:06:44 +02:00
ギャラ
42816d3d09
Fix order of derive helper attributes (#179) 2021-04-19 15:09:07 +02:00
Danny Mösch
2a304b17f2
Fix clippy warnings (#173) 2021-04-18 19:27:25 +02:00
Dalton
07c715656f
Support custom pages and patches (#142)
Allow adding, overriding and extending custom pages.

Fixes #98.
2021-04-18 19:22:41 +02:00
Danny Mösch
7232bdd898
Allow SunOS override (#176)
Fixes #82.
2021-04-14 20:55:27 +02:00
Danny Mösch
49f151a605
Remove 'extern crate' declarations (#175)
Cargo.toml specifies Rust Edition 2018 which does not require 'extern crate' anymore.
2021-04-10 21:33:48 +02:00
Michael Cho
071680800a
zsh-completion: fix issue with [.md, add basic -L completion, remove extra _files args (#168)
- Use array `commands=(...)` and `_describe` to deal with '[.md' & empty cache scenario.  Fixes #166
- Hide `tldr --list` stderr (`2>/dev/null`) which breaks completion with empty cache
- Remove `sed` since #112 changed commas to newlines
- Add new `sed`-equivalent replacement (`:` -> `\:`) using native [ZSH `${name//pattern/repl}`](http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion) since colon is special character in ZSH completions
- Add basic support for `-L, --language` flag from #125. In future, can consider adding extra completions maybe based on caches `pages.{lang}` folders.
- Remove extraneous completion of file names for positional arguments (i.e. `'*:file:_files'`)
2021-03-21 15:55:23 +01:00
Niklas Mohrin
2a6d09554b
Update tar-markdown.expected (#164) 2021-02-16 18:13:38 +01:00
Danilo Bargen
663926cc0e
Merge pull request #162 from dbrgn/show-dirs
Implement new --show-dirs command
2021-02-05 00:32:20 +01:00
Danilo Bargen
cb57ac1d5d Require Rust 1.41 (was 1.40)
This is required for Result::map_or_else.

Additionally, run clippy with 1.41 as well. Otherwise it reports
suggestions that are not compatible with 1.41.
2021-02-05 00:12:57 +01:00
Danilo Bargen
ca15279386 Show reason for choice of a certain cache/config dir 2021-02-04 23:53:14 +01:00
Danilo Bargen
978debe6ae Implement new --show-dirs command
The command prints out the different directories (config, cache, etc)
used by tealdeer.
2021-01-30 21:32:39 +01:00
Danilo Bargen
4e1876ac88 Fix typo in code comment 2021-01-29 22:14:37 +01:00
Danny Mösch
1d529de260
Consider only standalone command names for styling (#157) 2021-01-29 21:22:52 +01:00
Danilo Bargen
32ff6c0e3c
Merge pull request #161 from dbrgn/multilanguage-improvements
Language support improvements
2021-01-29 21:14:53 +01:00
Danilo Bargen
2e1e0006b2 Avoid cloning in Dedup::clear_duplicates
Co-authored-by: Niklas Mohrin <niklas.mohrin@gmail.com>
2021-01-29 14:23:15 +01:00
Danilo Bargen
5d8448c442 Require Rust 1.40 (was 1.39) 2021-01-29 12:16:38 +01:00
Danilo Bargen
127124eded Optimize get_languages function
- Replace `Result` parameters with `Option`
- Replace `HashSet` for deduplication with linear search based approach
- Avoid intermediate allocations

This reduces the instruction count in release mode by almost 50%.
2021-01-29 12:05:01 +01:00
Danilo Bargen
e5d359b935 Language tests: Group into module 2021-01-29 00:08:23 +01:00
Niklas Mohrin
1db4b409bc Add support for multiple languages (#125)
The implementation follows the tldr-pages client spec:
https://github.com/tldr-pages/tldr/blob/master/CLIENT-SPECIFICATION.md#language

Fixes #96
2021-01-29 00:00:10 +01:00
tveness
0c75e34007
Fix tar markdown rendering test (#159)
tldr-pages repository changed file
https://github.com/tldr-pages/tldr/blob/master/pages/common/tar.md
and so test was failing
2021-01-13 23:32:02 +01:00
Danilo Bargen
c632407a25 README: Move up link to docs 2020-11-15 00:03:48 +01:00
Danilo Bargen
b1a08af00a Link to docs in Cargo.toml 2020-11-15 00:03:15 +01:00
Danilo Bargen
1ebb80bb14
Merge pull request #155 from dbrgn/docs-cache-config
Document location of cache directory. Fixes #151.
2020-11-15 00:02:49 +01:00
Danilo Bargen
931a5fc13c Docs: Document location of cache directory 2020-11-15 00:00:15 +01:00
Danilo Bargen
c33c146a17 Docs: Add README 2020-11-14 23:55:49 +01:00
Danilo Bargen
6d07e78613
Remove xdg dependency (#152)
It's not being used anymore.
2020-11-08 13:15:52 +01:00
Danilo Bargen
dff8da40a5 Docs: Add screenshot to intro page 2020-11-01 01:52:01 +01:00
Danilo Bargen
5ca8461dd8
Merge pull request #150 from dbrgn/mdbook
The README has gotten really long, it's getting hard to read. Instead, extract user docs into an mdbook page deployed through GH pages.
2020-11-01 01:49:37 +01:00
Danilo Bargen
e24d86c900 Deploy docs through CI 2020-11-01 01:40:04 +01:00
Danilo Bargen
ddeb81b249 Add mdbook documentation 2020-11-01 01:39:47 +01:00
Danilo Bargen
e35dd5e30c README: Simplify color docs 2020-11-01 00:26:19 +01:00
Tomás Farías Santana
9236f0d4a7
Add support for ANSI code and RGB colors (#148)
Fixes #147.
2020-11-01 00:17:21 +01:00
Danilo Bargen
151d014f5d
Fix expected output for tar test (#144) 2020-10-13 09:45:21 +02:00
Danilo Bargen
30b7c5febc
Release v1.4.1 (#140) 2020-09-04 12:25:37 +02:00
Bruno A. Muciño
d2b5614f1a Replace deprecated tempdir crate with tempfile crate (#139)
Fixes #134.
2020-09-04 11:59:10 +02:00
Danilo Bargen
95fdddd68e Fix mixed indentation in zsh completion file 2020-09-04 11:35:36 +02:00
Francesco
b0153c7e4c
Fix zsh completion for --color argument (#138)
Fixes #137.
2020-09-04 11:34:07 +02:00
Danilo Bargen
5a9cde733b
Release v1.4.0 (#136) 2020-09-03 23:21:25 +02:00
Danilo Bargen
2a2a4cdda5 README: Add links to Fedora and NetBSD packages 2020-09-03 23:06:47 +02:00
Danilo Bargen
fd9e36277f Disable clippy::too_many_lines warning 2020-09-03 22:46:53 +02:00
Danilo Bargen
504639b6a6
Run cargo update (#133)
This includes a security fix of the bumpalo crate:

https://rustsec.org/advisories/RUSTSEC-2020-0006
2020-09-03 08:09:38 +02:00
Danilo Bargen
4f078e9511 README: Update usage section
The color option was missing.

Fixes #135
2020-08-31 17:17:19 +02:00
Danilo Bargen
4c413d7e40
Merge pull request #111 from LovecraftianHorror/better-color-detection
Improved color detection with --color argument and NO_COLOR env variable.

Fixes #81.
2020-08-31 00:31:54 +02:00
Lovecraftian Horror
dc6af5ad6c Add integration tests for the auto/never color options 2020-08-31 00:08:54 +02:00
Lovecraftian Horror
1b8062e0af Switch --color always to force styling 2020-08-31 00:08:54 +02:00
Lovecraftian Horror
bc04e1ac00 Disable styling for --color auto if the output is being piped 2020-08-31 00:08:54 +02:00
Lovecraftian Horror
be79c924f6 Disable styling for --color auto when NO_COLOR is set 2020-08-31 00:08:54 +02:00
Lovecraftian Horror
f757389df9 Add flag for specifying color preference 2020-08-31 00:08:54 +02:00
Lovecraftian Horror
baffd1ea4d Apply enable_styles config to stale cache warning
This change makes the color for the "stale cache" warning follow
`enable_styles`, like the rest of the styling.
2020-08-30 21:17:11 +02:00
Danilo Bargen
09d73c2f03
Merge pull request #119 from Atul9/add-cargo-fmt-to-github-actions
Add cargo fmt to github-actions config
2020-08-30 20:04:10 +02:00
Danilo Bargen
d9b5e49551 CI: Always run rustfmt on fixed Rust version
Otherwise a new release of rustfmt may break CI without any change to
the code.
2020-08-30 19:56:33 +02:00
Atul Bhosale
a67e7d184a CI: Add cargo fmt step 2020-08-30 19:56:15 +02:00
Danilo Bargen
e507343f5d
CI: Run clippy with Rust 1.46 (#132) 2020-08-30 19:55:38 +02:00
Danilo Bargen
5a2d054ace Update Cargo.lock
Should have been part of 590bb44.
2020-08-30 18:48:29 +02:00
Danilo Bargen
3597e344cb Apply rustfmt to codebase
Co-authored-by: Atul Bhosale <atul1bhosale@gmail.com>
Co-authored-by: Lovecraftian Horror <LovecraftianHorror@pm.me>
2020-08-30 18:39:29 +02:00
Kornel
590bb44585
Switch to maintained app_dirs2 (#124)
The app_dirs crate is no longer actively maintained. The app_dirs2 crate is a drop-in replacement.
2020-08-30 18:37:27 +02:00
Danny Mösch
e8dcde4955
Mention the environment variable TEALDEER_CONFIG_DIR in the README (#131) 2020-08-30 15:37:33 +02:00
Danilo Bargen
55079b8e77 Document requirement for libssl-dev
Closes #130
2020-08-21 21:37:35 +02:00
Niklas Mohrin
64e2c7b799
Update outdated expected output for the markdown test (#128)
Fixes #126.
2020-08-11 21:04:48 +02:00
Danilo Bargen
0b277c7515
Add test for pager warnings on Windows (#122) 2020-07-18 01:06:58 +02:00
Danilo Bargen
95e6b0520f
Upgrade assert_cmd and escargot (#123) 2020-07-18 00:24:29 +02:00
Danilo Bargen
d446e50813
Merge pull request #115 from ilai-deutel/automatic-updates
Implement automatic cache updates
2020-06-26 16:59:28 +02:00
Ilaï Deutel
509d450585 Add instructions for configuring auto updates to README 2020-05-31 17:12:57 -07:00
Ilaï Deutel
2f2fe4dd92 Fix serde default for updates.auto_update_interval_hours 2020-05-31 17:12:57 -07:00
Ilaï Deutel
a59182b6cc Use filetime instead of utime to set modification times in tests 2020-05-31 17:12:57 -07:00
Ilaï Deutel
ed1ba6beed Implement automatic cache updates 2020-05-31 17:12:57 -07:00
Danilo Bargen
06537c7218
Tests: Allow dead code for feature selection (#118)
These methods might be useful in the future, so I don't want to remove them even if they're unused. The warnings clutter diffs in CI though.
2020-05-08 22:40:34 +02:00
michaeldel
a45c19647f
Move cache age warning to stderr (#114)
Fixes #113
2020-04-16 21:42:02 +02:00
michaeldel
5dd9457cf4
Make list option comply with official spec (#112)
Separate page list with newlines, not with commas.

From the official spec:

> Additional decoration MAY be printed if the standard output is a TTY.
> If not, then the output MUST not contain any additional decorations.
> For example a page list MUST be formatted with 1 page name per line
> (to enable easy manipulation using standard CLI tools such as grep etc.).
2020-04-16 11:56:14 +02:00
Danilo Bargen
39b1d19d66
Fix integration tests (#110)
The tar tldr page was updated.
2020-04-10 23:49:39 +02:00
Danilo Bargen
a9fa0c7f1a README: Add link to openSUSE package 2020-02-28 12:29:29 +01:00
Danilo Bargen
d68088f94f README: Add links to FreeBSD and Solus packages 2020-02-28 12:20:52 +01:00
Danilo Bargen
ad92ce6765 Update license copyright year 2020-02-28 01:50:02 +01:00
Danilo Bargen
0c24ea0cd4 release-build script: Include "linux" in binary name 2020-02-28 01:44:45 +01:00
Danilo Bargen
2030d305c6 release-build script: Copy completions and licenses 2020-02-28 01:42:54 +01:00
Danilo Bargen
3a5c0ce59b
Release v1.3.0 2020-02-28 01:14:39 +01:00
Danilo Bargen
a594b0013e
Fix clippy warnings (#105) 2020-02-28 01:11:10 +01:00
Danilo Bargen
54464da510
Merge pull request #104 from dbrgn/updates
Update dependencies, bump MSRV to 1.39
2020-02-28 00:47:02 +01:00
Danilo Bargen
3e3403806c Require Rust 1.39 (was 1.36) 2020-02-28 00:38:53 +01:00
Danilo Bargen
04e61080d2 Upgrade reqwest
This reduces the dependency tree from 333 to 188 entries, mostly because
some reqwest features were disabled :)
2020-02-28 00:20:37 +01:00
Danilo Bargen
2812a0ce99 Run cargo update 2020-02-28 00:20:37 +01:00
Danilo Bargen
ed3e4baac5
Fix integration test (#103)
It broke because the `tar` tldr page was changed upstream :)
2020-02-28 00:20:17 +01:00
Jesús Trinidad Díaz Ramírez
055758dba8
Fix clippy warnings (#101) 2020-02-06 15:43:00 +01:00
Danilo Bargen
623cf67d2a
CI: Run on pull requests (#102)
* CI: Run on pull requests

* fixup! CI: Run on pull requests
2020-02-06 09:13:49 +01:00
Danilo Bargen
7a3a565427 README: List tealdeer-bin AUR package 2020-01-23 11:34:55 +01:00
Danilo Bargen
22255bf80c
Merge pull request #99 from dbrgn/github-actions
CI: Switch to GitHub Actions
2020-01-22 23:55:19 +01:00
Danilo Bargen
bcdfd5e9a8 Disable pager feature on Windows 2020-01-22 23:44:27 +01:00
Danilo Bargen
7b3f7a6470 CI: Switch to GitHub Actions 2020-01-22 23:44:27 +01:00
Danilo Bargen
23d7301b53
Merge pull request #95 from Plommonsorbet/render_as_markdown_option
Add -m / --markdown option for raw rendering
2020-01-22 23:06:03 +01:00
Danilo Bargen
95fab99e82 Add -m command to README and completion files 2020-01-22 22:55:59 +01:00
Isak Johansson
6719acb4f2 Add -m / --markdown option for raw rendering
New option to render as markdown, not with ANSI escape codes.
This is useful for when you want to open it up in a pager which doesn't
support ANSI escape codes by default. In my use case it allows me to
open it inside vim without having to use an addon to convert ANSI escape
codes to colours.
2020-01-22 22:54:11 +01:00
Hugo Locurcio
0d00e60af0 Tweak the outdated cache warning message (#97)
This makes it display in yellow (instead of red) as it's
more of a *warning* message than an *error* message.
2020-01-07 09:16:40 +01:00
Danilo Bargen
6ad91a440c
Update dependencies, increase MSRV to 1.36 (#92) 2019-11-27 10:58:21 +01:00
Bruno Heridet
94c0e99693 Fix typo in error message for flag_seed_config (#91) 2019-10-29 13:54:06 +01:00
Danilo Bargen
74c37193e3 Use non-deprecated trim functions 2019-10-04 01:18:36 +02:00
Danilo Bargen
7460cdac6d Remove unused import 2019-10-04 01:18:36 +02:00
Danilo Bargen
07e2ee86f5
Merge pull request #89 from korrat/add-compact-output
Add compact output option. Fixes #88.
2019-10-04 00:52:43 +02:00
Danilo Bargen
6810693655 Document compact output option in README 2019-10-03 23:56:07 +02:00
Korrat
7f8cbb01e5 Suppress empty lines for compact output 2019-10-03 11:55:07 +02:00
Korrat
a9f2a4c132 Add config field for compact output 2019-10-03 11:47:20 +02:00
James Doyle
15583fc6d0 Use string replace instead of sed (#87)
It seems like this was broken on my machine (OSX, 10.14.6) when using the completions. It would strip the `, ` but it would not add a proper newline. Instead it added nothing. So it just become a giant single line. Basically, it didn’t work.

With this change, the string gets split properly. Since this is fish shell, the `string replace` command is guaranteed to be there.
2019-08-28 00:51:35 +02:00
Marc-André Renaud
ece174e1fc Add zsh autocompletion (#86) 2019-08-27 09:11:06 +02:00
Danilo Bargen
ee65478930 CHANGELOG: Add missing reference 2019-08-10 00:31:34 +02:00
Danilo Bargen
3d77af1fa3 CHANGELOG: Fix contributors 2019-08-10 00:31:10 +02:00
Danilo Bargen
8dc480e10f
Release v1.2.0 2019-08-10 00:21:32 +02:00
Danilo Bargen
79d5f9f4ed Cargo.toml: Add more includes 2019-08-10 00:18:52 +02:00
Danilo Bargen
9ffe459c1f
Merge pull request #84 from dbrgn/updates
Update dependencies
2019-07-29 01:10:11 +02:00
Danilo Bargen
2d0d621e3d CI: Bump cache key to speed up tests 2019-07-28 17:48:42 +02:00
Danilo Bargen
8ae19fd32e Upgrade env_logger 2019-07-28 17:48:42 +02:00
Danilo Bargen
70cea2245d Upgrade docopt 2019-07-28 17:48:42 +02:00
Danilo Bargen
135698b5d9 Upgrade cargo-toml 2019-07-28 17:48:42 +02:00
Danilo Bargen
4b0c947030 Require Rust 1.32 (was 1.31) 2019-07-28 17:48:42 +02:00
Danilo Bargen
ee7c5c1e61 Run cargo update 2019-07-28 17:48:42 +02:00
Danilo Bargen
535b75dc83 Remove clippy from dependencies 2019-07-28 16:31:11 +02:00
Danilo Bargen
7a809d8bb8
Merge pull request #44 from jdvr/master
Add pager flag to page the output using pager crate.

Fixes #36.
2019-03-11 12:11:26 +01:00
Danilo Bargen
4304e2e610 Paging: Fix bug that I introduced during merge 2019-03-11 11:12:28 +01:00
Danilo Bargen
5215493720 Don't override display config when styles are disabled 2019-03-11 11:12:28 +01:00
Danilo Bargen
410e96ef7f Ensure that all config keys are optional
Without this change, a config without a [display] section
would fail to parse.
2019-03-11 11:12:22 +01:00
jdvr
0879116a45 Add pager support 2019-03-11 10:52:12 +01:00
Danilo Bargen
a052e48e50 Update README 2019-03-10 18:29:11 +01:00
Danilo Bargen
87ddb14da1
Merge pull request #77 from mystal/windows-support
Add Windows support
2019-03-10 18:27:51 +01:00
Gabriel Martinez
495c3e97d9 Support colors on Windows using ansi_term
We try to enable ANSI color support on Windows using the new APIs.
If that succeeds, we print colors as usual. If it fails, we disable
styles completely.
2019-02-09 21:44:10 -08:00
Jason Liquorish
265ed66d7d Add Windows support 2019-02-05 09:32:13 -08:00
Jonathan Dahan
91c7a412f0 Add support for spaces in commands (#75)
For example "git commit".

Fixes #74.
2019-01-14 09:28:48 +01:00
73 changed files with 7356 additions and 2358 deletions

View file

@ -1,34 +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 tests
- run: cargo test
- 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
View file

@ -0,0 +1,4 @@
* text=auto
*.md eol=lf
*.expected eol=lf

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

93
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,93 @@
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]
toolchain: [stable, 1.87.0] # MSRV
include:
- platform: windows-latest
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
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 --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 --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 -- --test-threads 1
clippy:
name: run clippy lints
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: clippy
- name: run clippy lints
run: cargo clippy --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
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: '0.4.4'
- name: Setup toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Build
run: cargo build
- name: Ensure that docs can be built
run: cd docs && mdbook build
- name: Generate usage string
run: cargo run -- --help > docs/src/usage-actual.txt
- name: Ensure that usage string is up to date
run: diff docs/src/usage{,-actual}.txt

25
.github/workflows/gh-pages.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: GitHub Pages
on:
push:
tags:
- "v[1-9]*" # push events matching `v` followed by anything larger than 0, e.g. v1.0, v20.15.10
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: '0.4.4'
- run: cd docs && mdbook build
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/book

160
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,160 @@
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 --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 --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: windows-latest
steps:
- uses: actions/checkout@v7
- name: Setup toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Build
run: cargo build --release --target x86_64-pc-windows-msvc
- uses: actions/upload-artifact@v7
with:
name: "tealdeer-windows-x86_64-msvc"
path: "target/x86_64-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
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

View file

@ -1,7 +0,0 @@
language: rust
os: osx
rust:
- 1.31.0
- stable
cache: cargo
script: cargo test

View file

@ -10,6 +10,480 @@ Possible log types:
- `[removed]` for deprecated features removed in this release.
- `[fixed]` for any bug fixes.
- `[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)
@ -20,8 +494,9 @@ Possible log types:
- [changed] Require at least Rust 1.28 to build (previous: 1.19)
- [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]
- [Jonathan Dahan][@jedahan]
- [Lukas Bergdoll][@Voultapher]
@ -52,15 +527,198 @@ Thanks!
- First crates.io release
[user documentation]: https://tealdeer-rs.github.io/tealdeer/
[@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
[@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
[@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
[@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.1.0]: https://github.com/dbrgn/tealdeer/compare/v1.0.0...v1.1.0
[v1.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.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
[i43]: https://github.com/dbrgn/tealdeer/issues/43
[i47]: https://github.com/dbrgn/tealdeer/issues/47
[i48]: https://github.com/dbrgn/tealdeer/issues/48
[i34]: https://github.com/tealdeer-rs/tealdeer/issues/34
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43
[i44]: https://github.com/tealdeer-rs/tealdeer/issues/44
[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

2509
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,61 @@
[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."
homepage = "https://github.com/dbrgn/tealdeer/"
license = "MIT/Apache-2.0"
homepage = "https://github.com/tealdeer-rs/tealdeer/"
license = "MIT OR Apache-2.0"
name = "tealdeer"
readme = "README.md"
repository = "https://github.com/dbrgn/tealdeer/"
version = "1.1.0"
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "/bash_tealdeer"]
edition = "2018"
repository = "https://github.com/tealdeer-rs/tealdeer/"
documentation = "https://tealdeer-rs.github.io/tealdeer/"
version = "1.8.1"
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
rust-version = "1.87" # MSRV
edition = "2021"
[[bin]]
name = "tldr"
path = "src/main.rs"
[dependencies]
ansi_term = "0.10.2"
clippy = { version = "0.0.174", optional = true }
docopt = "0.8.1"
env_logger = { version = "0.5", optional = true }
flate2 = "1.0"
anyhow = "1"
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false }
env_logger = { version = "0.11", optional = true }
etcetera = "0.11.0"
log = "0.4"
serde = "1.0.21"
serde_derive = "1.0.21"
tar = "0.4.14"
time = "0.1.38"
toml = "0.4.6"
walkdir = "2.0.1"
xdg = "2.1.0"
reqwest = "0.9.5"
ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] }
toml = "0.8.19"
yansi = "1"
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
[target.'cfg(not(windows))'.dependencies]
pager = "0.16"
[dev-dependencies]
assert_cmd = "0.10"
escargot = "0.3"
predicates = "1.0"
tempdir = "^0.3"
utime = "0.2.0"
assert_cmd = "2.0.1"
escargot = "0.5"
predicates = "3.1.2"
tempfile = "3.1.0"
filetime = "0.2.10"
[features]
# 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"]
# 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]
strip = true
opt-level = 3
lto = true
codegen-units = 1

View file

@ -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
this software and associated documentation files (the "Software"), to deal in

228
README.md
View file

@ -1,20 +1,28 @@
# tealdeer
![teal deer](deer.png)
![teal deer](docs/src/deer.png)
|Crate|Linux|macOS|
|:---:|:---:|:---:|
|[![Crates.io][crates-io-badge]][crates-io]|[![Circle CI][circle-ci-badge]][circle-ci]|[![Travis CI][travis-ci-badge]][travis-ci]|
|Crate|CI (Linux/macOS/Windows)|
|:---:|:---:|
|[![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
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 :)
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://tealdeer-rs.github.io/tealdeer/>!
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
@ -23,103 +31,40 @@ High level project goals:
- [x] Download and cache pages
- [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] Command line interface similar or equivalent to the [NodeJS client][node-gh]
- [x] Comply with the [tldr client specification][client-spec]
- [x] Advanced highlighting and configuration
- [x] Be fast
A tool like `tldr` should be as frictionless as possible to use. It should be
easy to invoke (just `tldr tar`, not using another subcommand like `tldr find
tar`) and it should show the output as fast as possible.
A tool like `tldr` should be as frictionless as possible to use and 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:
We think that `tealdeer` reaches these goals. We put together a (more or less)
reproducible benchmark that compiles a handful of clients from source and
measures the execution times on a cold disk cache. The benchmarking is run in a
Docker container using sharkdp's [`hyperfine`][hyperfine-gh]
([Dockerfile][benchmark-dockerfile]).
| 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%) |
| Client (50 runs, 17.10.2021) | Programming Language | Mean in ms | Deviation in ms | Comments |
| :---: | :---: | :---: | :---: | :---: |
| [`outfieldr`][outfieldr-gh] | Zig | 9.1 | 0.5 | no user configuration |
| `tealdeer` | Rust | 13.2 | 0.5 | |
| [`fast-tldr`][fast-tldr-gh] | Haskell | 17.0 | 0.6 | no example highlighting |
| [`tldr-hs`][hs-gh] | Haskell | 25.1 | 0.5 | no example highlighting |
| [`tldr-bash`][bash-gh] | Bash | 30.0 | 0.8 | |
| [`tldr-python-client`][python-gh] | Python | 87.0 | 2.4 | |
| [`tldr-node-client`][node-gh] | JavaScript / NodeJS | 407.1 | 12.9 | |
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.
As you can see, `tealdeer` is one of the fastest of the tested clients.
However, we strive for useful features and code quality over raw performance,
even if that means that we don't come out on top in this friendly competition.
That said, we are still optimizing the code, for example when the `outfieldr`
developers [suggested to switch][outfieldr-comment-tls] to a native TLS
implementation instead of the native libraries.
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).
## Development
## 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
-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:
Creating a debug build with logging enabled:
$ cargo build --features logging
@ -131,60 +76,6 @@ To enable the log output, set the `RUST_LOG` env variable:
$ 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:
$ cargo test
@ -195,6 +86,23 @@ To run lints:
$ cargo clean && cargo clippy
### AI Policy
Using AI is generally discouraged. However, if it is used as part of a contribution, the contributor MUST:
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.
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.*
## 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.
## License
Licensed under either of
@ -211,15 +119,23 @@ 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
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
[node-gh]: https://github.com/tldr-pages/tldr-node-client
[hs-gh]: https://github.com/psibi/tldr-hs
[fast-tldr-gh]: https://github.com/gutjuri/fast-tldr
[bash-gh]: https://4e4.win/tldr
[outfieldr-gh]: https://gitlab.com/ve-nt/outfieldr
[python-gh]: https://github.com/tldr-pages/tldr-python-client
[benchmark-dockerfile]: https://github.com/tealdeer-rs/tealdeer/blob/main/benchmarks/Dockerfile
[client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md
[hyperfine-gh]: https://github.com/sharkdp/hyperfine
[outfieldr-comment-tls]: https://github.com/tealdeer-rs/tealdeer/issues/129#issuecomment-833596765
<!-- Badges -->
[circle-ci]: https://circleci.com/gh/dbrgn/tealdeer/tree/master
[circle-ci-badge]: https://circleci.com/gh/dbrgn/tealdeer/tree/master.svg?style=shield
[travis-ci]: https://travis-ci.org/dbrgn/tealdeer
[travis-ci-badge]: https://travis-ci.org/dbrgn/tealdeer.svg?branch=master
[github-actions]: https://github.com/tealdeer-rs/tealdeer/actions?query=branch%3Amain
[github-actions-badge]: https://github.com/tealdeer-rs/tealdeer/actions/workflows/ci.yml/badge.svg?branch=main
[crates-io]: https://crates.io/crates/tealdeer
[crates-io-badge]: https://img.shields.io/crates/v/tealdeer.svg

View file

@ -7,12 +7,16 @@ Run linting:
Set variables:
$ export VERSION=X.Y.Z
$ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA
$ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1
Update version numbers:
$ vim Cargo.toml
$ cargo update
$ cargo update -p tealdeer
Update docs:
$ cargo run -- --help > docs/src/usage.txt
Update changelog:
@ -28,6 +32,4 @@ Publish:
$ cargo publish
$ git push && git push --tags
Create release binaries:
$ ./release-build.sh
Then publish the release on GitHub.

View file

@ -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

139
benchmarks/Dockerfile Normal file
View file

@ -0,0 +1,139 @@
# Benchmark Dockerfile for tealdeer
#
# To run the benchmarks, execute
#
# docker build --pull -t tldr-benchmark .
# docker run --privileged --rm -it tldr-benchmark
#
# as root in the directory of this Dockerfile. This will build the compared
# clients and benchmark them with `hyperfine` at the end.
#
# The `--privileged` flag is needed to drop the disk caches before every run. If
# you want to test with hot caches or don't want to use this flag, you will have
# to remove the `--prepare` line from the `hyperfine` command at the end of this
# file and rebuild the image.
################################################################################
FROM rust AS tealdeer-builder
WORKDIR /build
RUN git clone https://github.com/tealdeer-rs/tealdeer.git \
&& cd tealdeer \
&& cargo build --release \
&& mkdir /build-outputs \
&& cp target/release/tldr /build-outputs/tealdeer
################################################################################
FROM ubuntu:latest AS tldr-c-builder
WORKDIR /build
RUN apt-get update && apt-get install -y build-essential git && rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/tldr-pages/tldr-c-client.git \
&& cd tldr-c-client \
&& DEBIAN_FRONTEND=noninteractive ./deps.sh \
&& make \
&& mkdir /build-outputs /deps \
&& cp tldr /build-outputs/tldr-c \
&& cp deps.sh /deps/tldr-c-deps.sh
################################################################################
FROM haskell AS haskell-builder
WORKDIR /build
RUN git clone https://github.com/psibi/tldr-hs.git \
&& cd tldr-hs \
&& stack build --install-ghc
RUN git clone https://github.com/gutjuri/fast-tldr \
&& cd fast-tldr \
&& stack build --install-ghc
RUN mkdir /build-outputs \
&& find tldr-hs/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/tldr-hs \; \
&& find fast-tldr/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/fast-tldr \;
################################################################################
FROM node:slim AS node-builder
WORKDIR /build-outputs
RUN npm install tldr \
&& cp $(which node) . \
&& echo './node -- ./node_modules/.bin/tldr "$@"' > tldr-node \
&& chmod +x tldr-node
################################################################################
FROM euantorano/zig:0.8.0 AS zig-builder
WORKDIR /build
RUN apk add git \
&& git clone https://gitlab.com/ve-nt/outfieldr.git \
&& cd outfieldr \
&& git submodule init \
&& git submodule update \
&& zig build -Drelease-safe \
&& mkdir /build-outputs \
&& cp bin/tldr /build-outputs/outfieldr
################################################################################
FROM ubuntu:latest AS benchmark
ENV LANG="en_US.UTF-8"
WORKDIR /deps
RUN apt-get update && apt-get install -y wget unzip python3 python3-venv && rm -rf /var/lib/apt/lists/*
COPY --from=tldr-c-builder /deps/* ./
RUN for file in *; do DEBIAN_FRONTEND=noninteractive sh $file; done
WORKDIR /clients
COPY --from=tealdeer-builder /build-outputs/* ./
COPY --from=tldr-c-builder /build-outputs/* ./
COPY --from=haskell-builder /build-outputs/* ./
RUN wget -qO tldr-bash https://4e4.win/tldr && chmod +x tldr-bash
COPY --from=node-builder /build-outputs/node /build-outputs/tldr-node ./
COPY --from=node-builder /build-outputs/node_modules/ ./node_modules/
COPY --from=zig-builder /build-outputs/* ./
# python is really hard to isolate in a package, using pyinstaller didn't really work either, so for now we just use it like this
RUN python3 -m venv tldr-python \
&& cd tldr-python \
&& bash -c 'source bin/activate; pip install wheel; pip install tldr; deactivate' \
&& cd .. \
&& echo '#!/bin/bash' > tldr-python.bash \
&& echo 'source tldr-python/bin/activate; tldr $@' >> tldr-python.bash \
&& chmod +x tldr-python.bash
# Update all the individual caches
RUN bash -c 'mkdir -p /caches/{tealdeer,tldr-c,tldr-hs,fast-tldr,tldr-bash,tldr-node,tldr-python,outfieldr/.local/share}' \
&& TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer -u \
&& TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c -u \
&& XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs -u \
&& XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr -u \
&& XDG_DATA_HOME=/caches/tldr-bash ./tldr-bash -u \
&& HOME=/caches/tldr-node ./tldr-node -u \
&& HOME=/caches/tldr-python ./tldr-python.bash -u \
&& HOME=/caches/outfieldr ./outfieldr -u
WORKDIR /tools
RUN wget -q https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && dpkg -i hyperfine_1.11.0_amd64.deb
ENV PAGE="tar"
WORKDIR /clients
CMD hyperfine \
--warmup 10 \
--runs 50 \
--prepare 'sync; echo 3 | tee /proc/sys/vm/drop_caches' \
"TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer $PAGE" \
"TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c $PAGE" \
"XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs $PAGE" \
"XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr $PAGE" \
"XDG_DATA_HOME=/caches/tldr-bash TLDR_LESS=0 ./tldr-bash $PAGE" \
"HOME=/caches/tldr-python ./tldr-python.bash $PAGE" \
"HOME=/caches/outfieldr ./outfieldr $PAGE" \
"HOME=/caches/tldr-node ./tldr-node $PAGE"

35
completion/bash_tealdeer Normal file
View 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

28
completion/fish_tealdeer Normal file
View file

@ -0,0 +1,28 @@
#
# 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 -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'
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 -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'
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)'

51
completion/zsh_tealdeer Normal file
View file

@ -0,0 +1,51 @@
#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 -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
))'
"($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)--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
))"
'(- *)'{-h,--help}'[Display help]'
'(- *)'{-v,--version}'[Show version information]'
'1: :_applications'
)
_arguments $args[@] && ret=0
return ret
}
_tealdeer

1
docs/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
book

8
docs/README.md Normal file
View 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`.

6
docs/book.toml Normal file
View file

@ -0,0 +1,6 @@
[book]
authors = ["Danilo Bargen"]
language = "en"
multilingual = false
src = "src"
title = "Tealdeer User Manual"

14
docs/src/SUMMARY.md Normal file
View 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)

59
docs/src/config.md Normal file
View file

@ -0,0 +1,59 @@
# 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.

View 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/"
```

View file

@ -0,0 +1,71 @@
# 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,
}
```

33
docs/src/config_search.md Normal file
View 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
View 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 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 } }
```

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

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
View 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
View 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.
![Screenshot](screenshot-default.png)
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/)

View file

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before After
Before After

View 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
View 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}}
```

33
docs/src/usage.txt Normal file
View file

@ -0,0 +1,33 @@
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
--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]
-v, --version Print the version
-h, --help Print help
To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.
To view usage examples, run tldr tldr or tldr tealdeer.

View 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
```

View file

@ -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
View file

@ -0,0 +1,42 @@
# tldr
> This is a builtin page that shows information for your installed tealdeer version.
> More information: <https://tealdeer-rs.github.io/tealdeer/>.
> This page shows tealdeer specific functionality. See tldr tldr for more examples.
- Render a local markdown file as a tldr page:
`tldr --render {{path/to/file.md}}`
- Show the raw markdown source of a page instead of rendering it:
`tldr --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 --clear-cache`
- If auto update is configured, disable it for this run:
`tldr --no-auto-update`

View file

@ -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
View file

@ -0,0 +1 @@
# Empty file, use defaults and disregard global settings

88
scripts/upload-asset.sh Normal file
View 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
}

View file

@ -1,106 +1,240 @@
use std::env;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::{
fs::{self, File},
io::{Cursor, ErrorKind, Read},
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use anyhow::{anyhow, bail, ensure, Context, Result};
use log::{debug, info};
use ureq::{
http::StatusCode,
tls::{RootCerts, TlsConfig, TlsProvider},
Agent,
};
use zip::ZipArchive;
use reqwest::{Client, Proxy};
use flate2::read::GzDecoder;
use log::debug;
use tar::Archive;
use time;
use walkdir::{DirEntry, WalkDir};
use xdg::BaseDirectories;
use crate::{
config::{Language, TlsBackend},
types::PlatformType,
};
use crate::error::TealdeerError::{self, CacheError, UpdateError};
use crate::types::OsType;
pub static TLDR_PAGES_DIR: &str = "tldr-pages";
pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master";
#[derive(Debug)]
pub struct Cache {
url: String,
os: OsType,
#[derive(Clone)]
pub struct CacheConfig<'a> {
pub pages_directory: &'a Path,
pub custom_pages_directory: Option<&'a Path>,
pub platforms: &'a [PlatformType],
pub search_languages: &'a [Language<'a>],
pub download_languages: &'a [Language<'a>],
}
impl Cache {
pub fn new<S>(url: S, os: OsType) -> Self
where
S: Into<String>,
{
Self {
url: url.into(),
os,
/// The directory backing this cache is checked to be populated at construction.
pub struct Cache<'a> {
config: CacheConfig<'a>,
}
#[derive(Debug)]
pub struct PageLookupResult {
pub page_path: PathBuf,
pub patch_path: Option<PathBuf>,
}
impl<'a> Cache<'a> {
/// Try opening a cache at the location given by `config.pages_directory`. If no directory
/// exists at this location, `Ok(None)` is returned.
pub fn open(config: CacheConfig<'a>) -> Result<Option<Self>> {
match config.pages_directory.metadata() {
Ok(md) => {
ensure!(
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.
fn get_cache_dir(&self) -> Result<PathBuf, TealdeerError> {
// Allow overriding the cache directory by setting the
// $TEALDEER_CACHE_DIR env variable.
if let Ok(value) = env::var("TEALDEER_CACHE_DIR") {
let path = PathBuf::from(value);
/// Open an existing cache at `config.pages_directory` or create one if no cache resides at
/// this location. In case of success, the return value is a tuple with the `Cache` and a
/// boolean indicating whether the cache was newly created.
pub fn open_or_create(config: CacheConfig<'a>) -> Result<(Self, bool)> {
if let Some(cache) = Self::open(config.clone())? {
return Ok((cache, false));
}
if path.exists() && path.is_dir() {
return Ok(path);
} else {
return Err(CacheError(
"Path specified by $TEALDEER_CACHE_DIR \
does not exist or is not a directory."
.into(),
));
fs::create_dir_all(config.pages_directory).with_context(|| {
format!(
"Cache directory `{}` cannot be created",
config.pages_directory.display(),
)
})?;
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>> {
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 xdg_dirs = match BaseDirectories::with_prefix(crate::NAME) {
Ok(dirs) => dirs,
Err(_) => return Err(CacheError("Could not determine XDG base directory.".into())),
let mut search_path = self.config.pages_directory.to_path_buf();
for language in self.config.search_languages {
search_path.push(language.directory_name());
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())
}
/// Download the archive
fn download(&self) -> 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);
for entry in file_iter {
if let Some(extension) = entry?.path().extension() {
if extension == "page" || extension == "patch" {
return Ok(true);
}
}
}
if let Ok(ref host) = env::var("HTTPS_PROXY") {
if let Ok(proxy) = Proxy::https(host) {
builder = builder.proxy(proxy);
}
}
let client = builder.build().unwrap_or_else(|_| Client::new());
let mut resp = client.get(&self.url).send()?;
let mut buf: Vec<u8> = vec![];
let bytes_downloaded = resp.copy_to(&mut buf)?;
debug!("{} bytes downloaded", bytes_downloaded);
Ok(buf)
Ok(false)
}
/// Decompress and open the archive
fn decompress<R: Read>(&self, reader: R) -> Archive<GzDecoder<R>> {
Archive::new(GzDecoder::new(reader))
pub fn clear(self) -> Result<()> {
fs::remove_dir_all(self.config.pages_directory).with_context(|| {
format!(
"Could not remove pages directory at {}",
self.config.pages_directory.display(),
)
})
}
/// Update the pages cache.
pub fn update(&self) -> Result<(), TealdeerError> {
// First, download the compressed data
let bytes: Vec<u8> = self.download()?;
/// Download archives for the languages in `self.config().download_languages` and replace the
/// pages directory with the newly downloaded pages. As not all languages might have pages
/// available (for example, `en_US` instead of `en`), an iterator yielding all languages which
/// were successfully downloaded is returned.
pub fn update(
&mut self,
archive_url: &str,
tls_backend: TlsBackend,
) -> Result<impl IntoIterator<Item = Language<'_>>> {
let client = Self::build_client(tls_backend);
// Decompress the response body into an `Archive`
let mut archive = self.decompress(&bytes[..]);
// Determine paths
let cache_dir = self.get_cache_dir()?;
// Make sure that cache directory exists
debug!("Ensure cache directory {:?} exists", &cache_dir);
fs::create_dir_all(&cache_dir)
.map_err(|e| UpdateError(format!("Could not create cache directory: {}", e)))?;
// Download everything before deleting anything
let mut archives = self
.config
.download_languages
.iter()
.map(|&lang| {
Ok((
lang,
Self::download(
&client,
&format!("{archive_url}/tldr-{}.zip", lang.directory_name()),
)?
.map(|bytes| ZipArchive::new(Cursor::new(bytes)))
.transpose()?,
))
})
.collect::<Result<Vec<_>>>()?;
// Clear cache directory
// Note: This is not the best solution. Ideally we would download the
@ -108,143 +242,191 @@ impl Cache {
// But renaming a directory doesn't work across filesystems and Rust
// does not yet offer a recursive directory copying function. So for
// 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
archive
.unpack(&cache_dir)
.map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?;
Ok(())
}
#[cfg(unix)]
/// 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);
for (lang, archive) in &mut archives {
if let Some(archive) = archive {
info!("Extracting archive for {lang:?}");
archive.extract(self.config.pages_directory.join(lang.directory_name()))?;
} else {
info!("No archive found for {lang:?}");
}
}
// If platform is not supported or if platform specific page does not exist,
// 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
Ok(archives
.into_iter()
.filter_entry(|e| should_walk(e)) // Filter out pages for other architectures
.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)
.filter_map(|(lang, archive)| archive.is_some().then_some(lang)))
}
/// Delete the cache directory.
pub fn clear(&self) -> Result<(), TealdeerError> {
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 ({}).",
path.display()
)))?;
} else if path.exists() {
return Err(CacheError(format!(
"Cache path ({}) is not a directory.",
path.display()
)));
} else {
return Err(CacheError(format!(
"Cache path ({}) does not exist.",
path.display()
)));
};
Ok(())
pub fn config(&self) -> &CacheConfig<'a> {
&self.config
}
}
impl PageLookupResult {
pub fn with_page(page_path: PathBuf) -> Self {
Self {
page_path,
patch_path: None,
}
}
pub fn with_optional_patch(mut self, patch_path: Option<PathBuf>) -> Self {
self.patch_path = patch_path;
self
}
/// Create a reader that sequentially reads from the page and the
/// 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");
}
}

112
src/cli.rs Normal file
View file

@ -0,0 +1,112 @@
//! Definition of the CLI arguments and options.
use std::path::PathBuf;
use clap::{builder::ArgAction, ArgGroup, Parser};
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://tealdeer-rs.github.io/tealdeer/.
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", requires = "command_or_file")]
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>,
/// 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>,
/// 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: (),
}

File diff suppressed because it is too large Load diff

View file

@ -1,26 +0,0 @@
use std::fmt;
use reqwest::Error as ReqwestError;
#[derive(Debug)]
#[allow(clippy::pub_enum_variant_names)]
pub enum TealdeerError {
CacheError(String),
ConfigError(String),
UpdateError(String),
}
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
View 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)
}
}

View file

@ -1,78 +1,459 @@
//! Functions related to formatting and printing lines from a `Tokenizer`.
use std::io::BufRead;
use ansi_term::{ANSIString, ANSIStrings};
use log::debug;
use crate::config::Config;
use crate::tokenizer::Tokenizer;
use crate::types::LineType;
use crate::{config::Indent, extensions::FindFrom, types::LineType};
fn highlight_command<'a>(
command: &'a str,
example_code: &'a str,
config: &Config,
parts: &mut Vec<ANSIString<'a>>,
) {
let mut code_part_end_pos = 0;
while let Some(command_start) = example_code[code_part_end_pos..].find(&command) {
let code_part = &example_code[code_part_end_pos..code_part_end_pos + command_start];
parts.push(config.style.example_code.paint(code_part));
parts.push(config.style.command_name.paint(command));
code_part_end_pos += command_start + command.len();
}
parts.push(
config
.style
.example_code
.paint(&example_code[code_part_end_pos..]),
);
#[derive(Debug, Clone, Copy, Eq)]
/// Represents a snippet from a page of a specific highlighting class.
pub enum PageSnippet<T> {
CommandName(T),
Variable(T),
NormalCode(T),
Description(T),
Text(T),
Title(T),
Linebreak,
}
/// Format and highlight code examples including variables in {{ curly braces }}.
fn format_code(command: &str, text: &str, config: &Config) -> String {
let mut parts = Vec::new();
for between_variables in text.split("}}") {
if let Some(variable_start) = between_variables.find("{{") {
let example_code = &between_variables[..variable_start];
let example_variable = &between_variables[variable_start + 2..];
highlight_command(&command, &example_code, &config, &mut parts);
parts.push(config.style.example_variable.paint(example_variable));
} else {
highlight_command(&command, &between_variables, &config, &mut parts);
#[cfg_attr(not(test), allow(dead_code))]
impl<T> PageSnippet<T> {
pub fn map<F, U>(self, f: F) -> PageSnippet<U>
where
F: FnOnce(T) -> U,
{
match self {
PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)),
PageSnippet::Variable(s) => PageSnippet::Variable(f(s)),
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::Linebreak => PageSnippet::Linebreak,
}
}
ANSIStrings(&parts).to_string()
}
/// Print a token stream to an ANSI terminal.
pub fn print_lines<R>(tokenizer: &mut Tokenizer<R>, config: &Config)
impl<T: PartialEq<U>, U> PartialEq<PageSnippet<U>> for PageSnippet<T> {
fn eq(&self, other: &PageSnippet<U>) -> bool {
match (self, other) {
(PageSnippet::CommandName(s), PageSnippet::CommandName(t))
| (PageSnippet::Variable(s), PageSnippet::Variable(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::Linebreak, PageSnippet::Linebreak) => true,
_ => false,
}
}
}
impl PageSnippet<&str> {
pub fn is_empty(&self) -> bool {
use PageSnippet::*;
match self {
CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) | Title(s) => {
s.is_empty()
}
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
R: BufRead,
L: Iterator<Item = LineType>,
F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>,
{
let base_indent = " ".repeat(indent.base);
let command_indent = " ".repeat(indent.command);
let mut command = String::new();
while let Some(token) = tokenizer.next_token() {
match token {
LineType::Empty => println!(),
for line in lines {
match line {
LineType::Empty => {
if keep_empty_lines {
process_snippet(PageSnippet::Linebreak)?;
}
}
LineType::Title(title) => {
debug!("Ignoring title");
if show_title {
process_snippet(PageSnippet::Linebreak)?;
process_snippet(PageSnippet::Title(&base_indent))?;
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,
// and tokenizer yields values in order of appearance.
// and the iterator yields values in order of appearance.
command = title;
debug!("Detected command name: {}", &command);
debug!("Detected command name: {command}");
}
LineType::Description(text) => {
process_snippet(PageSnippet::Description(&base_indent))?;
process_snippet(PageSnippet::Description(&text))?;
process_snippet(PageSnippet::Linebreak)?;
}
LineType::ExampleText(text) => {
process_snippet(PageSnippet::Text(&base_indent))?;
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) => {
println!(" {}", &format_code(&command, &text, &config))
process_snippet(PageSnippet::NormalCode(&command_indent))?;
highlight_code(&command, &text, process_snippet)?;
process_snippet(PageSnippet::Linebreak)?;
}
LineType::Other(text) => debug!("Unknown line type: {:?}", text),
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,
)?;
}
process_snippet(PageSnippet::Variable(&replace_escaped(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`. Variables 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 variable_vs_escaped() {
assert_eq!(
run("ping", "ping {{example.com}}"),
[
CommandName("ping"),
NormalCode(" "),
Variable("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}}' "
),
Variable("container"),
],
);
assert_eq!(
run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"),
[
CommandName("mount"),
NormalCode(r" \\"),
Variable("computer_name"),
NormalCode(r"\"),
Variable("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"\"), Variable("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 "),
Variable("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(" "), Variable("}")]
);
// And these are just to document the current behavior
assert_eq!(run("", "{{{}}}"), [Variable("{}")]);
assert_eq!(run("", "{{{{}}}"), [Variable("{{}")]);
assert_eq!(run("", "{{{}}}}"), [Variable("{}}")]);
}
#[test]
fn escaped_inside_placeholder() {
assert_eq!(
run(
"playerctl",
r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""#
),
[
CommandName("playerctl"),
NormalCode(" metadata "),
Variable("[-f|--format]"),
NormalCode(" \""),
Variable("Now playing: {{artist}} - {{album}} - {{title}}"),
NormalCode("\""),
],
);
}
#[test]
fn placeholder_inside_escaped() {
assert_eq!(
run("test", r"test \{\{{{var}} normal\}\}"),
[
CommandName("test"),
NormalCode(" {{"),
Variable("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"), Variable("x")],);
}
}
println!();
}

122
src/line_iterator.rs Normal file
View 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())
);
}
}

View file

@ -1,6 +1,6 @@
//! 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
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
@ -10,138 +10,135 @@
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::enum_glob_use)]
#![allow(clippy::module_name_repetitions)]
#![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")]
extern crate env_logger;
#[cfg(not(any(
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"
);
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process;
use std::{
env,
fs::create_dir_all,
io::{self, IsTerminal},
path::Path,
process::{Command, ExitCode},
};
use ansi_term::Color;
use docopt::Docopt;
use serde_derive::Deserialize;
use anyhow::{anyhow, Context, Result};
use cache::{CacheConfig, TLDR_OLD_PAGES_DIR};
use clap::Parser;
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
use log::debug;
use types::PlatformType;
mod cache;
mod cli;
mod config;
mod error;
pub mod extensions;
mod formatter;
mod tokenizer;
mod line_iterator;
mod output;
mod types;
mod utils;
use crate::cache::Cache;
use crate::config::{get_config_path, make_default_config, Config};
use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError};
use crate::formatter::print_lines;
use crate::tokenizer::Tokenizer;
use crate::types::OsType;
use crate::{
cache::{Cache, PageLookupResult, TLDR_PAGES_DIR},
cli::Cli,
config::{
get_config_dir, make_default_config, supported_tls_backends_string, Config, PathWithSource,
},
output::print_page,
types::ColorOptions,
utils::{print_error, print_warning},
};
const NAME: &str = "tealdeer";
const VERSION: &str = env!("CARGO_PKG_VERSION");
const USAGE: &str = "
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
-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
";
const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz";
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_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);
static TEALDEER_PAGE: &str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md"));
/// 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(())
}
/// Check the cache for freshness
fn check_cache(args: &Args, cache: &Cache) {
if !args.flag_update {
match cache.last_update() {
Some(ago) if ago > MAX_CACHE_AGE => {
if args.flag_quiet {
return;
}
println!(
"{}",
Color::Red.paint(format!(
"Cache wasn't updated for more than {} days.\n\
You should probably run `tldr --update` soon.",
MAX_CACHE_AGE / 24 / 3600
))
);
}
None => {
eprintln!("Cache not found. Please run `tldr --update`.");
process::exit(1);
}
_ => {}
/// Update the cache
fn update_cache(
cache: &mut Cache,
archive_source: &str,
tls_backend: TlsBackend,
quietly: bool,
) -> Result<()> {
let downloaded_languages = cache
.update(archive_source, tls_backend)
.context("Could not update cache")?;
if !quietly {
eprintln!("Successfully updated cache.");
eprint!("Pages for the following languages were downloaded: ");
let language_strings: Vec<_> = downloaded_languages
.into_iter()
.map(|lang| lang.0)
.collect();
if language_strings.is_empty() {
eprintln!("(none)");
} else {
eprintln!("{}", language_strings.join(", "));
}
}
Ok(())
}
/// Show file paths
fn show_paths(config: &Config) {
let config_dir = {
let (mut path, source) = get_config_dir();
path.push(""); // Trailing path separator
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")]
@ -152,204 +149,296 @@ fn init_log() {
#[cfg(not(feature = "logging"))]
fn init_log() {}
#[cfg(target_os = "linux")]
fn get_os() -> OsType {
OsType::Linux
fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> {
create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?;
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",
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
}
fn main() {
fn main() -> ExitCode {
// Initialize logger
init_log();
// Parse arguments
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit());
let args = Cli::parse();
// Show version and exit
if args.flag_version {
let os = get_os();
println!("{} v{} ({})", NAME, VERSION, os);
process::exit(0);
}
// Specify target OS
let os: OsType = match args.flag_os {
Some(os) => os,
None => get_os(),
// Determine the usage of styles
let enable_styles = match args.color.unwrap_or_default() {
// Attempt to use styling if instructed
ColorOptions::Always => {
yansi::enable(); // disable yansi's automatic detection for ANSI support on Windows
true
}
// Enable styling if:
// * NO_COLOR env var isn't set: https://no-color.org/
// * The output stream is stdout (not being piped)
ColorOptions::Auto => env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(),
// Disable styling
ColorOptions::Never => false,
};
// Initialize cache
let cache = Cache::new(ARCHIVE_URL, os);
try_main(args, enable_styles).unwrap_or_else(|error| {
print_error(enable_styles, &error);
ExitCode::FAILURE
})
}
// Clear cache, pass through
if args.flag_clear_cache {
cache.clear().unwrap_or_else(|e| {
match e {
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
eprintln!("Could not delete cache: {}", msg)
}
};
process::exit(1);
});
if !args.flag_quiet {
println!("Successfully deleted cache.");
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()).context("Could not read config from given path")?
}
_ => {
ConfigLoader::read_default_path().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();
}
// Update cache, pass through
if args.flag_update {
cache.update().unwrap_or_else(|e| {
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.");
}
let custom_pages_dir = config
.directories
.custom_pages_dir
.as_ref()
.map(PathWithSource::path);
// Note: According to the TLDR client spec, page names must be transparently
// lowercased before lookup:
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
let command = args.command.join("-").to_lowercase();
if args.edit_patch || args.edit_page {
let file_name = if args.edit_patch {
format!("{command}.patch.md")
} else {
format!("{command}.page.md")
};
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);
}
// 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);
}
}
// Show various paths
if args.show_paths {
show_paths(&config);
}
// Create a basic config and exit
if args.flag_seed_config {
match make_default_config() {
Ok(config_file_path) => {
println!(
"Successfully created seed config file here: {}",
config_file_path.to_str().unwrap()
);
process::exit(0);
}
Err(ConfigError(msg)) => {
eprintln!("Could not create seed config: {}", msg);
process::exit(1);
}
Err(_) => {
eprintln!("Unkown error");
process::exit(1);
}
if args.seed_config {
create_config(args.config_path.as_deref())?;
return Ok(ExitCode::SUCCESS);
}
// If a local file was passed in, render it and exit
if let Some(file) = args.render {
let reader = PageLookupResult::with_page(file).reader()?;
print_page(reader, args.raw, enable_styles, args.pager, &config)?;
return Ok(ExitCode::SUCCESS);
}
// The tealdeer page is embedded in the binary, no cache needed
if command == "tealdeer" {
print_page(
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
if let Some(ref file) = args.flag_render {
let path = PathBuf::from(file);
if let Err(msg) = print_page(&path) {
eprintln!("{}", msg);
process::exit(1);
} else {
process::exit(0);
};
let cache_config = CacheConfig {
pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR),
custom_pages_directory: config
.directories
.custom_pages_dir
.as_ref()
.map(PathWithSource::path),
platforms: &config.search.platforms,
search_languages,
download_languages,
};
// TODO: remove in tealdeer 1.9
let old_config = CacheConfig {
pages_directory: &config.directories.cache_dir.path().join(TLDR_OLD_PAGES_DIR),
..cache_config
};
if let Ok(Some(old_cache)) = Cache::open(old_config) {
old_cache.clear()?;
eprintln!("Cleared pages from old cache location.");
}
// List cached commands and exit
if args.flag_list {
// Check cache for freshness
check_cache(&args, &cache);
if args.clear_cache {
if let Some(cache) = Cache::open(cache_config)? {
clear_cache(cache, args.quiet)?;
}
return Ok(ExitCode::SUCCESS);
}
// Get list of pages
let pages = cache.list_pages().unwrap_or_else(|e| {
match e {
CacheError(msg) | ConfigError(msg) | UpdateError(msg) => {
eprintln!("Could not get list of pages: {}", msg)
}
let cache = if args.update || config.updates.auto_update && !args.no_auto_update {
let (mut cache, was_created) = Cache::open_or_create(cache_config)?;
if was_created || args.update || cache.age()? >= config.updates.auto_update_interval {
let result = update_cache(
&mut cache,
config.updates.archive_source,
config.updates.tls_backend,
args.quiet,
);
if let Err(e) = result {
print_error(enable_styles, &e);
eprintln!();
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
println!("{}", pages.join(", "));
process::exit(0);
cache
} else if args.list || !command.is_empty() {
// Cache is needed for these commands to work
let Some(cache) = Cache::open(cache_config)? else {
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://tealdeer-rs.github.io/tealdeer/config_updates.html");
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
if let Some(ref command) = args.arg_command {
// Check cache for freshness
check_cache(&args, &cache);
// Search for command in cache
if let Some(path) = cache.find_page(&command) {
if let Err(msg) = print_page(&path) {
eprintln!("{}", msg);
process::exit(1);
} else {
process::exit(0);
}
} else {
if !args.flag_quiet {
println!("Page {} not found in cache", &command);
println!("Try updating with `tldr --update`, or submit a pull request to:");
println!("https://github.com/tldr-pages/tldr");
}
process::exit(1);
if !command.is_empty() {
// TODO: Remove this check 1 year after version 1.7.0 was released
if cache.old_custom_pages_exist()? {
print_warning(
enable_styles,
&format!(
"Custom pages using the old naming convention were found in {}.\n\
Please rename them to follow the new convention:\n\
- `<name>.page` `<name>.page.md`\n\
- `<name>.patch` `<name>.patch.md`",
cache
.config()
.custom_pages_directory
.expect("Old custom pages can only exist in custom pages directory")
.display(),
),
);
}
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.
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());
}
Ok(ExitCode::SUCCESS)
}

97
src/output.rs Normal file
View file

@ -0,0 +1,97 @@
//! 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, StyleConfig},
formatter::{highlight_lines, PageSnippet},
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).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,
) -> io::Result<()> {
use PageSnippet::*;
match snip {
CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)),
Variable(s) => write!(writer, "{}", s.paint(style.example_variable)),
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)),
Linebreak => writeln!(writer),
}
}

View file

@ -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);
}
}

View file

@ -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};
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[allow(dead_code)]
pub enum OsType {
pub enum PlatformType {
Linux,
OsX,
Windows,
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 {
match self {
OsType::Linux => write!(f, "Linux"),
OsType::OsX => write!(f, "macOS / BSD"),
OsType::SunOs => write!(f, "SunOS"),
OsType::Other => write!(f, "Unknown OS"),
Self::Linux => write!(f, "Linux"),
Self::OsX => write!(f, "macOS / BSD"),
Self::Windows => write!(f, "Windows"),
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)]
pub enum LineType {
Empty,
@ -36,28 +137,24 @@ pub enum 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 {
let trimmed: &str = line.trim_right();
let trimmed: &str = line.trim_end();
let mut chars = trimmed.chars();
match chars.next() {
None => LineType::Empty,
Some('#') => LineType::Title(
None => Self::Empty,
Some('#') => Self::Title(
trimmed
.trim_left_matches(|chr: char| chr == '#' || chr.is_whitespace())
.trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace())
.into(),
),
Some('>') => LineType::Description(
Some('>') => Self::Description(
trimmed
.trim_left_matches(|chr: char| chr == '>' || chr.is_whitespace())
.trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace())
.into(),
),
Some(' ') => LineType::ExampleCode(
trimmed
.trim_left_matches(|chr: char| chr.is_whitespace())
.into(),
),
_ => LineType::ExampleText(trimmed.into()),
Some(' ') => Self::ExampleCode(trimmed.trim_start_matches(char::is_whitespace).into()),
Some(_) => Self::ExampleText(trimmed.into()),
}
}
}
@ -69,32 +166,60 @@ impl LineType {
let trimmed = line.trim();
let mut chars = trimmed.chars();
match chars.next() {
None => LineType::Empty,
Some('#') => LineType::Title(
None => Self::Empty,
Some('#') => Self::Title(
trimmed
.trim_left_matches(|chr: char| chr == '#' || chr.is_whitespace())
.trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace())
.into(),
),
Some('>') => LineType::Description(
Some('>') => Self::Description(
trimmed
.trim_left_matches(|chr: char| chr == '>' || chr.is_whitespace())
.trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace())
.into(),
),
Some('-') => LineType::ExampleText(
Some('-') => Self::ExampleText(
trimmed
.trim_left_matches(|chr: char| chr == '-' || chr.is_whitespace())
.trim_start_matches(|chr: char| chr == '-' || chr.is_whitespace())
.into(),
),
Some('`') if chars.last() == Some('`') => LineType::ExampleCode(
Some('`') if chars.last() == Some('`') => Self::ExampleCode(
trimmed
.trim_matches(|chr: char| chr == '`' || chr.is_whitespace())
.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)]
mod test {
use super::LineType;

21
src/utils.rs Normal file
View 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}");
}
}

View 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}}`

View file

@ -26,3 +26,7 @@
- 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`

View 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:
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

11
tests/cache/pages.en/common/which.md vendored Normal file
View 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
View 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`

View file

@ -0,0 +1,3 @@
Custom inkscape entry
My Inkscape example

View file

@ -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.
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

View file

@ -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.
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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -3,6 +3,9 @@ foreground = "green"
underline = false
bold = false
[style.command_name]
bold = true
[style.description]
underline = false
bold = false
@ -15,3 +18,4 @@ underline = false
[style.example_variable]
underline = true
bold = false
italic = true