tealdeer-1.8.0/.cargo_vcs_info.json0000644000000001360000000000100126270ustar { "git": { "sha1": "d49c4a9e05eab5018e322c03da01279040def538" }, "path_in_vcs": "" }tealdeer-1.8.0/Cargo.lock0000644000001261170000000000100106120ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.60.2", ] [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "app_dirs2" version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e" dependencies = [ "jni", "ndk-context", "winapi", "xdg", ] [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "assert_cmd" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", "doc-comment", "libc", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bstr" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", "clap_lex", "terminal_size", ] [[package]] name = "clap_derive" version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "pem-rfc7468", "zeroize", ] [[package]] name = "derive_arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ "errno-dragonfly", "libc", "winapi", ] [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.1", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "escargot" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf" dependencies = [ "log", "serde", "serde_json", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" [[package]] name = "flate2" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "libz-rs-sys", "miniz_oxide", ] [[package]] name = "float-cmp" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "indexmap" version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jni" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", "windows-sys 0.45.0", ] [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "libc" version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", "redox_syscall", ] [[package]] name = "libz-rs-sys" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework 2.11.1", "security-framework-sys", "tempfile", ] [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "pager" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880" dependencies = [ "errno 0.2.8", "libc", ] [[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "predicates" version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", "float-cmp", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno 0.3.14", "libc", "linux-raw-sys", "windows-sys 0.61.1", ] [[package]] name = "rustls" version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", "security-framework 3.5.1", ] [[package]] name = "rustls-pemfile" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "zeroize", ] [[package]] name = "rustls-platform-verifier" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", "windows-sys 0.59.0", ] [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.1", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework" version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_spanned" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tealdeer" version = "1.8.0" dependencies = [ "anyhow", "app_dirs2", "assert_cmd", "clap", "env_logger", "escargot", "filetime", "log", "pager", "predicates", "serde", "serde_derive", "tempfile", "toml", "ureq", "yansi", "zip", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.61.1", ] [[package]] name = "terminal_size" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", "windows-sys 0.60.2", ] [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "toml_write", "winnow", ] [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" dependencies = [ "base64", "der", "flate2", "log", "native-tls", "percent-encoding", "rustls", "rustls-pemfile", "rustls-pki-types", "rustls-platform-verifier", "ureq-proto", "utf-8", "webpki-root-certs", "webpki-roots", ] [[package]] name = "ureq-proto" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ "base64", "http", "httparse", "log", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ "wasip2", ] [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "webpki-root-certs" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.61.1", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.4", ] [[package]] name = "windows-sys" version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "xdg" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zip" version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" dependencies = [ "arbitrary", "crc32fast", "flate2", "indexmap", "memchr", "zopfli", ] [[package]] name = "zlib-rs" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] tealdeer-1.8.0/Cargo.toml0000644000000053070000000000100106320ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.85" name = "tealdeer" version = "1.8.0" authors = [ "Danilo Bargen ", "Niklas Mohrin ", ] build = false include = [ "/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support." homepage = "https://github.com/tealdeer-rs/tealdeer/" documentation = "https://tealdeer-rs.github.io/tealdeer/" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/tealdeer-rs/tealdeer/" [features] default = [ "rustls-with-webpki-roots", "rustls-with-native-roots", ] ignore-online-tests = [] logging = ["env_logger"] native-tls = [ "ureq/native-tls", "ureq/platform-verifier", ] rustls-with-native-roots = [ "ureq/rustls", "ureq/platform-verifier", ] rustls-with-webpki-roots = ["ureq/rustls"] [[bin]] name = "tldr" path = "src/main.rs" [[test]] name = "lib" path = "tests/lib.rs" [dependencies.anyhow] version = "1" [dependencies.app_dirs] version = "2" package = "app_dirs2" [dependencies.clap] version = "4" features = [ "std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help", ] default-features = false [dependencies.env_logger] version = "0.11" optional = true [dependencies.log] version = "0.4" [dependencies.serde] version = "1.0.21" [dependencies.serde_derive] version = "1.0.21" [dependencies.toml] version = "0.8.19" [dependencies.ureq] version = "3.0.8" features = ["gzip"] default-features = false [dependencies.yansi] version = "1" [dependencies.zip] version = "5.1.1" features = ["deflate"] default-features = false [dev-dependencies.assert_cmd] version = "2.0.1" [dev-dependencies.escargot] version = "0.5" [dev-dependencies.filetime] version = "0.2.10" [dev-dependencies.predicates] version = "3.1.2" [dev-dependencies.tempfile] version = "3.1.0" [target."cfg(not(windows))".dependencies.pager] version = "0.16" [profile.release] opt-level = 3 lto = true codegen-units = 1 strip = true tealdeer-1.8.0/Cargo.toml.orig000064400000000000000000000036141046102023000143120ustar 00000000000000[package] authors = [ "Danilo Bargen ", "Niklas Mohrin ", ] description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support." homepage = "https://github.com/tealdeer-rs/tealdeer/" license = "MIT OR Apache-2.0" name = "tealdeer" readme = "README.md" repository = "https://github.com/tealdeer-rs/tealdeer/" documentation = "https://tealdeer-rs.github.io/tealdeer/" version = "1.8.0" include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] rust-version = "1.85" edition = "2021" [[bin]] name = "tldr" path = "src/main.rs" [dependencies] anyhow = "1" app_dirs = { version = "2", package = "app_dirs2" } clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false } env_logger = { version = "0.11", optional = true } log = "0.4" serde = "1.0.21" serde_derive = "1.0.21" ureq = { version = "3.0.8", default-features = false, features = ["gzip"] } 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 = "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 tealdeer-1.8.0/LICENSE-APACHE000064400000000000000000000227731046102023000133560ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS tealdeer-1.8.0/LICENSE-MIT000064400000000000000000000020671046102023000130600ustar 00000000000000Copyright (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 the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tealdeer-1.8.0/README.md000064400000000000000000000125671046102023000127110ustar 00000000000000# tealdeer ![teal deer](docs/src/deer.png) |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. Screenshot of tldr command 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/tealdeer-rs/tealdeer/releases/)! ## Docs (Installing, Usage, Configuration) User documentation is available at ! 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 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][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 and show the output as fast as possible. 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 (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-c`][c-gh] | C | 38.4 | 1.0 | | | [`tldr-python-client`][python-gh] | Python | 87.0 | 2.4 | | | [`tldr-node-client`][node-gh] | JavaScript / NodeJS | 407.1 | 12.9 | | 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. ## Development Creating a debug build with logging enabled: $ cargo build --features logging Release build without logging: $ cargo build --release To enable the log output, set the `RUST_LOG` env variable: $ export RUST_LOG=tldr=debug To run tests: $ cargo test To run lints: $ rustup component add clippy $ cargo clean && cargo clippy ## 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 * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ### Contribution 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 @severen for coming up with the name "tealdeer"! [node-gh]: https://github.com/tldr-pages/tldr-node-client [c-gh]: https://github.com/tldr-pages/tldr-c-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 [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 tealdeer-1.8.0/completion/bash_tealdeer000064400000000000000000000014401046102023000162740ustar 00000000000000# 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 tealdeer-1.8.0/completion/fish_tealdeer000064400000000000000000000030421046102023000163100ustar 00000000000000# # 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)' tealdeer-1.8.0/completion/zsh_tealdeer000064400000000000000000000031731046102023000161700ustar 00000000000000#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 tealdeer-1.8.0/src/cache.rs000064400000000000000000000347151046102023000136310ustar 00000000000000use std::{ fs::{self, File}, io::{BufReader, Cursor, ErrorKind, Read}, path::{Path, PathBuf}, time::{Duration, SystemTime}, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use log::{debug, info}; use ureq::{ http::StatusCode, tls::{RootCerts, TlsConfig, TlsProvider}, Agent, }; use zip::ZipArchive; use crate::{ config::{Language, TlsBackend}, types::PlatformType, }; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; #[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>], } /// 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, } 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> { 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() ))), } } /// 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)); } 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 { 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 { 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> { 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(()) }; 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 { let Some(directory) = self.config.custom_pages_directory else { return Ok(false); }; let Ok(file_iter) = fs::read_dir(directory) else { return Ok(false); }; for entry in file_iter { if let Some(extension) = entry?.path().extension() { if extension == "page" || extension == "patch" { return Ok(true); } } } Ok(false) } 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(), ) }) } /// 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>> { let client = Self::build_client(tls_backend); // 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::>>()?; // Clear cache directory // Note: This is not the best solution. Ideally we would download the // archive to a temporary directory and then swap the two directories. // 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. fs::remove_dir_all(self.config.pages_directory)?; fs::create_dir(self.config.pages_directory)?; 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:?}"); } } Ok(archives .into_iter() .filter_map(|(lang, archive)| archive.is_some().then_some(lang))) } 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) -> Self { self.patch_path = patch_path; self } /// Create a buffered 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>> { // 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(BufReader::new(if let Some(patch_file) = patch_file_opt { Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box } else { Box::new(page_file) as Box })) } } 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>> { 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 = 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"); } } tealdeer-1.8.0/src/cli.rs000064400000000000000000000064031046102023000133260ustar 00000000000000//! 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/.", 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, /// 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, /// 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>, /// Override the language #[arg(short = 'L', long = "language")] pub language: Option, /// 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, /// 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, /// 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: (), } tealdeer-1.8.0/src/config.rs000064400000000000000000000635621046102023000140350ustar 00000000000000use std::{ env, fmt, fs::{self, File}, io::{ErrorKind, Write}, path::{Path, PathBuf}, sync::LazyLock, time::Duration, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use app_dirs::{get_app_root, AppDataType}; use clap::ValueEnum; use serde::Serialize as _; use serde_derive::{Deserialize, Serialize}; use yansi::{Color, Style}; use crate::{ extensions::Dedup as _, types::{PathSource, PlatformType}, }; pub const CONFIG_FILE_NAME: &str = "config.toml"; pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days const DEFAULT_UPDATE_INTERVAL_HOURS: u64 = MAX_CACHE_AGE.as_secs() / 3600; // 30 days const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[ #[cfg(feature = "native-tls")] RawTlsBackend::NativeTls, #[cfg(feature = "rustls-with-webpki-roots")] RawTlsBackend::RustlsWithWebpkiRoots, #[cfg(feature = "rustls-with-native-roots")] RawTlsBackend::RustlsWithNativeRoots, ]; fn default_underline() -> bool { false } fn default_bold() -> bool { false } fn default_italic() -> bool { false } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] pub enum RawColor { Black, Red, Green, Yellow, Blue, Magenta, Purple, // Backwards compatibility with ansi_term (until tealdeer 1.5.0) Cyan, White, Ansi(u8), Rgb { r: u8, g: u8, b: u8 }, } impl From for Color { fn from(raw_color: RawColor) -> Self { match raw_color { RawColor::Black => Self::Black, RawColor::Red => Self::Red, RawColor::Green => Self::Green, RawColor::Yellow => Self::Yellow, RawColor::Blue => Self::Blue, RawColor::Magenta | RawColor::Purple => Self::Magenta, RawColor::Cyan => Self::Cyan, RawColor::White => Self::White, RawColor::Ansi(num) => Self::Fixed(num), RawColor::Rgb { r, g, b } => Self::Rgb(r, g, b), } } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] struct RawStyle { pub foreground: Option, pub background: Option, #[serde(default = "default_underline")] pub underline: bool, #[serde(default = "default_bold")] pub bold: bool, #[serde(default = "default_italic")] pub italic: bool, } #[allow(clippy::derivable_impls)] // Explicitly control defaults impl Default for RawStyle { fn default() -> Self { Self { foreground: None, background: None, underline: false, bold: false, italic: false, } } } impl From for Style { fn from(raw_style: RawStyle) -> Self { let mut style = Self::default(); if let Some(foreground) = raw_style.foreground { style = style.fg(Color::from(foreground)); } if let Some(background) = raw_style.background { style = style.bg(Color::from(background)); } if raw_style.underline { style = style.underline(); } if raw_style.bold { style = style.bold(); } if raw_style.italic { style = style.italic(); } style } } #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawStyleConfig { #[serde(default)] pub description: RawStyle, #[serde(default)] pub command_name: RawStyle, #[serde(default)] pub example_text: RawStyle, #[serde(default)] pub example_code: RawStyle, #[serde(default)] pub example_variable: RawStyle, } impl From<&RawStyleConfig> for StyleConfig { fn from(raw_style_config: &RawStyleConfig) -> Self { Self { command_name: raw_style_config.command_name.into(), description: raw_style_config.description.into(), example_text: raw_style_config.example_text.into(), example_code: raw_style_config.example_code.into(), example_variable: raw_style_config.example_variable.into(), } } } #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawDisplayConfig { #[serde(default)] pub compact: bool, #[serde(default)] pub use_pager: bool, #[serde(default)] pub show_title: bool, } impl From<&RawDisplayConfig> for DisplayConfig { fn from(raw_display_config: &RawDisplayConfig) -> Self { Self { compact: raw_display_config.compact, use_pager: raw_display_config.use_pager, show_title: raw_display_config.show_title, } } } /// Serde doesn't support default values yet (tracking issue: /// ), so we need to wrap /// `DEFAULT_UPDATE_INTERVAL_HOURS` in a function to be able to use /// `#[serde(default = ...)]` const fn default_auto_update_interval_hours() -> u64 { DEFAULT_UPDATE_INTERVAL_HOURS } fn default_archive_source() -> String { "https://github.com/tldr-pages/tldr/releases/latest/download/".to_owned() } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct RawUpdatesConfig { #[serde(default)] pub auto_update: bool, #[serde(default = "default_auto_update_interval_hours")] pub auto_update_interval_hours: u64, #[serde(default = "default_archive_source")] pub archive_source: String, #[serde(default)] pub tls_backend: RawTlsBackend, #[serde(default)] pub download_languages: Option>, } impl Default for RawUpdatesConfig { fn default() -> Self { Self { auto_update: false, auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, archive_source: default_archive_source(), tls_backend: RawTlsBackend::default(), download_languages: None, } } } #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawDirectoriesConfig { #[serde(default)] pub cache_dir: Option, #[serde(default)] pub custom_pages_dir: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] enum RawPlatformType { Current, All, MacOs, // alias for Platform(PlatformType::OsX) #[serde(untagged)] Platform(PlatformType), } impl RawPlatformType { pub fn flatten(raw_platforms: impl IntoIterator) -> Vec { let mut flattened = Vec::new(); for raw_platform in raw_platforms { match raw_platform { RawPlatformType::Current => flattened.push(PlatformType::current()), RawPlatformType::Platform(platform) => flattened.push(platform), RawPlatformType::MacOs => flattened.push(PlatformType::OsX), RawPlatformType::All => flattened.extend(PlatformType::value_variants()), } } flattened.clear_duplicates(); flattened } } #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawSearchConfig { pub languages: Option>, pub platforms: Option>, } impl<'a> From<&'a RawSearchConfig> for SearchConfig<'a> { fn from(raw_search_config: &'a RawSearchConfig) -> Self { let languages = raw_search_config .languages .as_ref() .map_or_else(get_languages_from_env, |langs| { langs.iter().map(|lang| Language(lang)).collect() }); let platforms = if let Some(raw_platforms) = raw_search_config.platforms.as_ref() { RawPlatformType::flatten(raw_platforms.iter().copied()) } else { RawPlatformType::flatten([ RawPlatformType::Current, RawPlatformType::Platform(PlatformType::Common), RawPlatformType::All, ]) }; Self { languages, platforms, } } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(default)] struct RawConfig { style: RawStyleConfig, display: RawDisplayConfig, updates: RawUpdatesConfig, directories: RawDirectoriesConfig, search: RawSearchConfig, } impl Default for RawConfig { fn default() -> Self { let mut raw_config = RawConfig { style: RawStyleConfig::default(), display: RawDisplayConfig::default(), updates: RawUpdatesConfig::default(), directories: RawDirectoriesConfig::default(), search: RawSearchConfig::default(), }; // Set default config raw_config.style.example_text.foreground = Some(RawColor::Green); raw_config.style.command_name.foreground = Some(RawColor::Cyan); raw_config.style.example_code.foreground = Some(RawColor::Cyan); raw_config.style.example_variable.foreground = Some(RawColor::Cyan); raw_config.style.example_variable.underline = true; raw_config } } #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct StyleConfig { pub description: Style, pub command_name: Style, pub example_text: Style, pub example_code: Style, pub example_variable: Style, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct DisplayConfig { pub compact: bool, pub use_pager: bool, pub show_title: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct UpdatesConfig<'a> { pub auto_update: bool, pub auto_update_interval: Duration, pub archive_source: &'a str, pub tls_backend: TlsBackend, pub download_languages: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PathWithSource { pub path: PathBuf, pub source: PathSource, } impl PathWithSource { pub fn path(&self) -> &Path { &self.path } } impl fmt::Display for PathWithSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} ({})", self.path.display(), self.source) } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct DirectoriesConfig { pub cache_dir: PathWithSource, pub custom_pages_dir: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct SearchConfig<'a> { pub languages: Vec>, pub platforms: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Language<'a>(pub &'a str); fn get_languages<'a>( env_lang: Option<&'a str>, env_language: Option<&'a str>, ) -> Vec> { // Language list according to // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language let Some(env_lang) = env_lang else { return vec![Language("en")]; }; // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) let locales = env_language.unwrap_or("").split(':').chain([env_lang]); let mut lang_list = Vec::new(); for locale in locales { // Language plus country code (e.g. `en_US`) if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { lang_list.push(Language(&locale[..5])); } // Language code only (e.g. `en`) if locale.len() >= 2 && locale != "POSIX" { lang_list.push(Language(&locale[..2])); } } lang_list.push(Language("en")); lang_list.clear_duplicates(); lang_list } pub fn get_languages_from_env<'a>() -> Vec> { static LANG: LazyLock> = LazyLock::new(|| std::env::var("LANG").ok()); static LANGUAGE: LazyLock> = LazyLock::new(|| std::env::var("LANGUAGE").ok()); get_languages( LANG.as_ref().map(String::as_str), LANGUAGE.as_ref().map(String::as_str), ) } #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum RawTlsBackend { /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) NativeTls, /// Rustls with `WebPKI` roots. RustlsWithWebpkiRoots, /// Rustls with native roots. RustlsWithNativeRoots, } impl Default for RawTlsBackend { fn default() -> Self { *SUPPORTED_TLS_BACKENDS.first().unwrap() } } impl std::fmt::Display for RawTlsBackend { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { self.serialize(f) } } /// Allows choosing a `reqwest`'s TLS backend. Available TLS backends: #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum TlsBackend { /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) #[cfg(feature = "native-tls")] NativeTls, /// Rustls with `WebPKI` roots. #[cfg(feature = "rustls-with-webpki-roots")] RustlsWithWebpkiRoots, /// Rustls with native roots. #[cfg(feature = "rustls-with-native-roots")] RustlsWithNativeRoots, } impl TryFrom for TlsBackend { type Error = anyhow::Error; fn try_from(raw: RawTlsBackend) -> Result { match raw { #[cfg(feature = "native-tls")] RawTlsBackend::NativeTls => Ok(TlsBackend::NativeTls), #[cfg(feature = "rustls-with-webpki-roots")] RawTlsBackend::RustlsWithWebpkiRoots => Ok(TlsBackend::RustlsWithWebpkiRoots), #[cfg(feature = "rustls-with-native-roots")] RawTlsBackend::RustlsWithNativeRoots => Ok(TlsBackend::RustlsWithNativeRoots), // when compiling without all TLS backend features, we want to handle config error. #[allow(unreachable_patterns)] _ => Err(anyhow!( "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", raw, SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") )) } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Config<'a> { pub style: StyleConfig, pub display: DisplayConfig, pub updates: UpdatesConfig<'a>, pub directories: DirectoriesConfig, pub search: SearchConfig<'a>, pub file_path: PathWithSource, } impl<'a> Config<'a> { /// Convert a `RawConfig` to a high-level `Config`. /// /// For this, some values need to be converted to other types and some /// defaults need to be set (sometimes based on env variables). fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result { let style = (&raw_config.style).into(); let display = (&raw_config.display).into(); let search: SearchConfig<'a> = (&raw_config.search).into(); let updates = UpdatesConfig { auto_update: raw_config.updates.auto_update, auto_update_interval: Duration::from_secs( raw_config.updates.auto_update_interval_hours * 3600, ), archive_source: &raw_config.updates.archive_source, tls_backend: raw_config.updates.tls_backend.try_into()?, download_languages: raw_config.updates.download_languages.as_ref().map_or_else( || search.languages.clone(), |languages| languages.iter().map(|lang| Language(lang)).collect(), ), }; let relative_path_root = config_file_path .path() .parent() .context("Failed to get config directory")?; // Determine directories config. For this, we need to take some // additional factory into account, like env variables, or the // user config. let cache_dir_env_var = "TEALDEER_CACHE_DIR"; let cache_dir = if let Ok(env_var) = env::var(cache_dir_env_var) { // For backwards compatibility reasons, the cache directory can be // overridden using an env variable. This is deprecated and will be // phased out in the future. eprintln!("Warning: The ${cache_dir_env_var} env variable is deprecated, use the `cache_dir` option in the config file instead."); PathWithSource { path: PathBuf::from(env_var), source: PathSource::EnvVar, } } else if let Some(config_value) = &raw_config.directories.cache_dir { // If the user explicitly configured a cache directory, use that. PathWithSource { // Resolve possible relative path. It would be nicer to clean up the path, but Rust stdlib // does not give any method for that that does not need the paths to exist. path: relative_path_root.join(config_value), source: PathSource::ConfigFile, } } else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) { // Otherwise, fall back to the default user cache directory. PathWithSource { path: default_dir, source: PathSource::OsConvention, } } else { // If everything fails, give up bail!("Could not determine user cache directory"); }; let custom_pages_dir = raw_config .directories .custom_pages_dir .as_ref() .map(|path| PathWithSource { // Resolve possible relative path. path: relative_path_root.join(path), source: PathSource::ConfigFile, }) .or_else(|| { get_app_root(AppDataType::UserData, &crate::APP_INFO) .map(|path| { // Note: The `join("")` call ensures that there's a trailing slash PathWithSource { path: path.join("pages").join(""), source: PathSource::OsConvention, } }) .ok() }); let directories = DirectoriesConfig { cache_dir, custom_pages_dir, }; Ok(Self { style, display, updates, directories, search, file_path: config_file_path, }) } } /// The [`ConfigLoader`] is used to load a [`Config`] from a file. /// /// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the /// [`Config`]. The [`ConfigLoader`] thus offers the following flow: /// 1. Read a raw config using [`ConfigLoader::read`] or [`ConfigLoader::read_default_path`]. /// 2. Validate the contents to a [`Config`] that borrows the [`ConfigLoader`]. pub struct ConfigLoader { raw: RawConfig, path: PathWithSource, } impl ConfigLoader { fn read_internal(path: PathWithSource, allow_not_found: bool) -> Result { match fs::read_to_string(&path.path) { Ok(content) => Ok(Self { raw: toml::from_str(&content).with_context(|| { format!( "Could not parse config file contents as toml from {}.", path.path.display() ) })?, path, }), Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { raw: RawConfig::default(), path, }), Err(e) => Err(e).context(format!( "Could not read config file contents from {}.", path.path().display() )), } } /// Create a loader that uses the config at `path`. pub fn read(path: PathBuf) -> Result { Self::read_internal( PathWithSource { path, source: PathSource::Cli, }, false, ) } /// Create a loader that uses the default config file location. If no file is present at the default location, the /// default configuration is used. pub fn read_default_path() -> Result { let path = get_default_config_path().context("Could not determine default config path.")?; Self::read_internal(path, true) } /// Parse the read [`RawConfig`] into a [`Config`]. pub fn load(&self) -> Result> { Config::from_raw(&self.raw, self.path.clone()) .context("Could not process raw config into rich config") } } /// Return the path to the config directory. /// /// The config dir path can be overridden using the `TEALDEER_CONFIG_DIR` env /// variable. Otherwise, the user config directory is returned. /// /// Note that this function does not verify whether the directory at that /// location exists, or is a directory. pub fn get_config_dir() -> Result<(PathBuf, PathSource)> { // Allow overriding the config directory by setting the // $TEALDEER_CONFIG_DIR env variable. if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") { return Ok((PathBuf::from(value), PathSource::EnvVar)); } // Otherwise, fall back to the user config directory. let dirs = get_app_root(AppDataType::UserConfig, &crate::APP_INFO) .context("Failed to determine the user config directory")?; Ok((dirs, PathSource::OsConvention)) } /// Return the path to the config file. /// /// Note that this function does not verify whether the file at that location /// exists, or is a file. pub fn get_default_config_path() -> Result { let (config_dir, source) = get_config_dir()?; let config_file_path = config_dir.join(CONFIG_FILE_NAME); Ok(PathWithSource { path: config_file_path, source, }) } /// Create default config file. /// path: Can be specified to create the config in that path instead of /// the default path. pub fn make_default_config(path: Option<&Path>) -> Result { let config_file_path = if let Some(p) = path { p.into() } else { let (config_dir, _) = get_config_dir()?; // Ensure that config directory exists if config_dir.exists() { ensure!( config_dir.is_dir(), "Config directory could not be created: {} already exists but is not a directory", config_dir.to_string_lossy(), ); } else { fs::create_dir_all(&config_dir).context("Could not create config directory")?; } config_dir.join(CONFIG_FILE_NAME) }; // Ensure that a config file doesn't get overwritten ensure!( !config_file_path.is_file(), "A configuration file already exists at {}, no action was taken.", config_file_path.to_str().unwrap() ); // Create default config let serialized_config = toml::to_string(&RawConfig::default()).context("Failed to serialize default config")?; // Write default config let mut config_file = File::create(&config_file_path).context("Could not create config file")?; let _wc = config_file .write(serialized_config.as_bytes()) .context("Could not write to config file")?; Ok(config_file_path) } #[cfg(test)] mod test { use super::*; #[test] fn serialize_deserialize() { let raw_config = RawConfig::default(); let serialized = toml::to_string(&raw_config).unwrap(); let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); assert_eq!(raw_config, deserialized); } #[test] fn relative_path_resolution() { let mut raw_config = RawConfig::default(); raw_config.directories.cache_dir = Some("../cache".into()); raw_config.directories.custom_pages_dir = Some("../custom_pages".into()); let config = Config::from_raw( &raw_config, PathWithSource { path: PathBuf::from("/path/to/config/config.toml"), source: PathSource::OsConvention, }, ) .unwrap(); assert_eq!( config.directories.cache_dir.path(), Path::new("/path/to/config/../cache") ); assert_eq!( config.directories.custom_pages_dir.unwrap().path(), Path::new("/path/to/config/../custom_pages") ); } mod language { use super::*; #[test] fn missing_lang_env() { let lang_list = get_languages(None, Some("de:fr")); assert_eq!(lang_list, [Language("en")]); let lang_list = get_languages(None, None); assert_eq!(lang_list, [Language("en")]); } #[test] fn missing_language_env() { let lang_list = get_languages(Some("de"), None); assert_eq!(lang_list, [Language("de"), Language("en")]); } #[test] fn preference_order() { let lang_list = get_languages(Some("de"), Some("fr:cn")); assert_eq!( lang_list, [ Language("fr"), Language("cn"), Language("de"), Language("en") ] ); } #[test] fn country_code_expansion() { let lang_list = get_languages(Some("pt_BR"), None); assert_eq!( lang_list, [Language("pt_BR"), Language("pt"), Language("en")] ); } #[test] fn with_encoding() { let lang_list = get_languages(Some("de_DE.UTF-8"), None); assert_eq!( lang_list, [Language("de_DE"), Language("de"), Language("en")] ); } #[test] fn ignore_posix_and_c() { let lang_list = get_languages(Some("POSIX"), None); assert_eq!(lang_list, [Language("en")]); let lang_list = get_languages(Some("C"), None); assert_eq!(lang_list, [Language("en")]); } #[test] fn no_duplicates() { let lang_list = get_languages(Some("de"), Some("fr:de:cn:de")); assert_eq!( lang_list, [ Language("fr"), Language("de"), Language("cn"), Language("en") ] ); } } } tealdeer-1.8.0/src/extensions.rs000064400000000000000000000016371046102023000147620ustar 00000000000000use std::mem; /// An extension trait to clear duplicates from a collection. pub(crate) trait Dedup { 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 Dedup for Vec { 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; } impl FindFrom for str { fn find_from(&self, needle: &Self, start: usize) -> Option { self.get(start..) .and_then(|s| s.find(needle)) .map(|i| i + start) } } tealdeer-1.8.0/src/formatter.rs000064400000000000000000000203071046102023000145610ustar 00000000000000//! Functions related to formatting and printing lines from a `Tokenizer`. use log::debug; use crate::{extensions::FindFrom, types::LineType}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Represents a snippet from a page of a specific highlighting class. pub enum PageSnippet<'a> { CommandName(&'a str), Variable(&'a str), NormalCode(&'a str), Description(&'a str), Text(&'a str), Title(&'a str), Linebreak, } impl PageSnippet<'_> { 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( lines: L, process_snippet: &mut F, keep_empty_lines: bool, show_title: bool, ) -> Result<(), E> where L: Iterator, F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>, { let mut command = String::new(); for line in lines { match line { LineType::Empty => { if keep_empty_lines { process_snippet(PageSnippet::Linebreak)?; } } LineType::Title(title) => { if show_title { process_snippet(PageSnippet::Linebreak)?; process_snippet(PageSnippet::Title(&title))?; } else { debug!("Ignoring title"); } // This is safe as long as the parsed title is only the command, // and the iterator yields values in order of appearance. command = title; debug!("Detected command name: {}", &command); } LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?, LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?, LineType::ExampleCode(text) => { process_snippet(PageSnippet::NormalCode(" "))?; highlight_code(&command, &text, process_snippet)?; process_snippet(PageSnippet::Linebreak)?; } LineType::Other(text) => debug!("Unknown line type: {text:?}"), } } process_snippet(PageSnippet::Linebreak)?; Ok(()) } /// Highlight code examples including user variables in {{ curly braces }}. fn highlight_code<'a, E>( command: &'a str, text: &'a str, process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>, ) -> Result<(), E> { let variable_splits = text .split("}}") .map(|s| s.split_once("{{").unwrap_or((s, ""))); for (code_segment, variable) in variable_splits { highlight_code_segment(command, code_segment, process_snippet)?; process_snippet(PageSnippet::Variable(variable))?; } Ok(()) } /// 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>) -> 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 or let char_before_is_okay = surrounding[..start] .chars() .last() .filter(|prev_char| !prev_char.is_whitespace()) .is_none(); let char_after_is_okay = surrounding[end..] .chars() .next() .filter(|next_char| !next_char.is_whitespace()) .is_none(); char_before_is_okay && char_after_is_okay } #[cfg(test)] mod tests { use super::*; use PageSnippet::*; #[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> { let mut yielded = Vec::new(); let mut process_snippet = |snip: PageSnippet<'a>| { if !snip.is_empty() { yielded.push(snip); } Ok::<(), ()>(()) }; highlight_code_segment(cmd, segment, &mut process_snippet) .expect("highlight code segment failed"); yielded } #[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); } } tealdeer-1.8.0/src/line_iterator.rs000064400000000000000000000074631046102023000154260ustar 00000000000000//! 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 ) V2, } /// A `LineIterator` is initialized with a `BufReader` instance that contains the /// entire Tldr page. It then implements `Iterator`. #[derive(Debug)] pub struct LineIterator { /// 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 LineIterator where R: BufRead, { pub fn new(reader: R) -> Self { Self { reader, first_line: true, current_line: String::new(), format: TldrFormat::Undecided, } } } impl Iterator for LineIterator { type Item = LineType; fn next(&mut self) -> Option { 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()) ); } } tealdeer-1.8.0/src/main.rs000064400000000000000000000316411046102023000135050ustar 00000000000000//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust. // // Copyright (c) 2015-2021 tealdeer developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. All files in the project carrying such notice may not be // copied, modified, or distributed except according to those terms. #![deny(clippy::all)] #![warn(clippy::pedantic)] #![allow(clippy::enum_glob_use)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::similar_names)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_lines)] #![allow(clippy::unnecessary_debug_formatting)] #[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::{ env, fs::create_dir_all, io::{self, IsTerminal}, path::Path, process::{Command, ExitCode}, }; use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; 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; pub mod extensions; mod formatter; mod line_iterator; mod output; mod types; mod utils; use crate::{ cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, cli::Cli, config::{get_config_dir, make_default_config, Config, PathWithSource}, output::print_page, types::ColorOptions, utils::{print_error, print_warning}, }; const NAME: &str = "tealdeer"; const APP_INFO: AppInfo = AppInfo { name: NAME, author: NAME, }; /// 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(()) } /// 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 = get_config_dir().map_or_else( |e| format!("[Error: {e}]"), |(mut path, source)| { 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")] fn init_log() { env_logger::init(); } #[cfg(not(feature = "logging"))] fn init_log() {} 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(()) } fn main() -> ExitCode { // Initialize logger init_log(); // Parse arguments let args = Cli::parse(); // 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, }; try_main(args, enable_styles).unwrap_or_else(|error| { print_error(enable_styles, &error); ExitCode::FAILURE }) } fn try_main(args: Cli, enable_styles: bool) -> Result { // 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(); } 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 various paths if args.show_paths { show_paths(&config); } // Create a basic config and exit 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 path = PageLookupResult::with_page(file); print_page(&path, 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), }; 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."); } if args.clear_cache { if let Some(cache) = Cache::open(cache_config)? { clear_cache(cache, args.quiet)?; } return Ok(ExitCode::SUCCESS); } 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 { update_cache( &mut cache, config.updates.archive_source, config.updates.tls_backend, args.quiet, )?; } 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); }; let age = cache.age()?; if age > config::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 !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\ - `.page` → `.page.md`\n\ - `.patch` → `.patch.md`", cache .config() .custom_pages_directory .expect("Old custom pages can only exist in custom pages directory") .display(), ), ); } let Some(lookup_result) = cache.find_page(&command) else { if !args.quiet { print_warning( enable_styles, &format!( "Page `{}` not found in cache.\n\ Try updating with `tldr --update`, or submit a pull request to:\n\ https://github.com/tldr-pages/tldr", &command ), ); } return Ok(ExitCode::FAILURE); }; print_page(&lookup_result, args.raw, enable_styles, args.pager, &config)?; } Ok(ExitCode::SUCCESS) } tealdeer-1.8.0/src/output.rs000064400000000000000000000056671046102023000141320ustar 00000000000000//! Functions for printing pages to the terminal use std::io::{self, BufRead, Write}; use anyhow::{Context, Result}; use yansi::Paint; use crate::{ cache::PageLookupResult, 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( lookup_result: &PageLookupResult, enable_markdown: bool, enable_styles: bool, use_pager: bool, config: &Config, ) -> Result<()> { // Create reader from file(s) let reader = lookup_result.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<'_>| { 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, ) .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<'_>, style: &StyleConfig, ) -> io::Result<()> { use PageSnippet::*; match snip { CommandName(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) => writeln!(writer, " {}", s.paint(style.description)), Text(s) => writeln!(writer, " {}", s.paint(style.example_text)), Title(s) => writeln!(writer, " {}", s.paint(style.command_name)), Linebreak => writeln!(writer), } } tealdeer-1.8.0/src/types.rs000064400000000000000000000157011046102023000137240ustar 00000000000000//! Shared types used in tealdeer. 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 PlatformType { Linux, OsX, Windows, SunOs, Android, FreeBsd, NetBsd, OpenBsd, Common, } impl fmt::Display for PlatformType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { 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 { 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, Title(String), Description(String), ExampleText(String), ExampleCode(String), Other(String), } impl<'a> From<&'a str> for LineType { /// Convert a string slice to a `LineType`. Newlines and trailing whitespace are trimmed. fn from(line: &'a str) -> Self { let trimmed: &str = line.trim_end(); let mut chars = trimmed.chars(); match chars.next() { None => Self::Empty, Some('#') => Self::Title( trimmed .trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace()) .into(), ), Some('>') => Self::Description( trimmed .trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace()) .into(), ), Some(' ') => Self::ExampleCode(trimmed.trim_start_matches(char::is_whitespace).into()), Some(_) => Self::ExampleText(trimmed.into()), } } } impl LineType { /// Support for old format. /// TODO: Remove once old format has been phased out! pub fn from_v1(line: &str) -> Self { let trimmed = line.trim(); let mut chars = trimmed.chars(); match chars.next() { None => Self::Empty, Some('#') => Self::Title( trimmed .trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace()) .into(), ), Some('>') => Self::Description( trimmed .trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace()) .into(), ), Some('-') => Self::ExampleText( trimmed .trim_start_matches(|chr: char| chr == '-' || chr.is_whitespace()) .into(), ), Some('`') if chars.last() == Some('`') => Self::ExampleCode( trimmed .trim_matches(|chr: char| chr == '`' || chr.is_whitespace()) .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; #[test] fn test_linetype_from_str() { assert_eq!(LineType::from(""), LineType::Empty); assert_eq!(LineType::from(" \n \r"), LineType::Empty); assert_eq!( LineType::from("# Hello there"), LineType::Title("Hello there".into()) ); assert_eq!( LineType::from("> tis a description \n"), LineType::Description("tis a description".into()) ); assert_eq!( LineType::from("some command "), LineType::ExampleText("some command".into()) ); assert_eq!( LineType::from(" $ cargo run "), LineType::ExampleCode("$ cargo run".into()) ); } } tealdeer-1.8.0/src/utils.rs000064400000000000000000000013451046102023000137170ustar 00000000000000use 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}"); } } tealdeer-1.8.0/tests/cache/pages.en/common/git-checkout.md000064400000000000000000000016771046102023000215370ustar 00000000000000# git checkout > Checkout a branch or paths to the working tree. > More information: . - 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}}` tealdeer-1.8.0/tests/cache/pages.en/common/inkscape-v1.md000064400000000000000000000022231046102023000212560ustar 00000000000000# 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` tealdeer-1.8.0/tests/cache/pages.en/common/inkscape-v2.md000064400000000000000000000022321046102023000212570ustar 00000000000000inkscape ======== > 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 tealdeer-1.8.0/tests/cache/pages.en/common/which.md000064400000000000000000000004001046102023000202320ustar 00000000000000# 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}}` tealdeer-1.8.0/tests/cache/pages.ja/common/apt.md000064400000000000000000000022521046102023000177130ustar 00000000000000# apt > Debian系ディストリビューションで使われるパッケージ管理システムです。 > Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。 > 詳しくはこちら: - 利用可能なパーケージとバージョンのリストの更新(他の`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` tealdeer-1.8.0/tests/custom-pages/inkscape-v2.patch.md000064400000000000000000000000571046102023000207340ustar 00000000000000Custom inkscape entry My Inkscape example tealdeer-1.8.0/tests/lib.rs000064400000000000000000001165051046102023000137050ustar 00000000000000//! Integration tests. use std::{ fs::{self, create_dir_all, File}, io::{self, Write}, path::{Path, PathBuf}, process::Command, time::{Duration, SystemTime}, }; use assert_cmd::prelude::*; use predicates::{ boolean::PredicateBooleanExt, ord::eq, prelude::predicate::str::{contains, diff, is_empty, is_match}, }; use tempfile::{Builder as TempfileBuilder, TempDir}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; struct TestEnv { _test_dir: TempDir, pub default_features: bool, pub features: Vec, } impl TestEnv { fn new() -> Self { let test_dir: TempDir = TempfileBuilder::new() .prefix(".tldr.test") .tempdir() .unwrap(); let this = TestEnv { _test_dir: test_dir, default_features: true, features: vec![], }; create_dir_all(this.cache_dir()).unwrap(); create_dir_all(this.config_dir()).unwrap(); create_dir_all(this.custom_pages_dir()).unwrap(); this.init_config(); this } fn cache_dir(&self) -> PathBuf { self._test_dir.path().join(".cache") } fn config_dir(&self) -> PathBuf { self._test_dir.path().join(".config") } fn custom_pages_dir(&self) -> PathBuf { self._test_dir.path().join(".custom_pages") } fn append_to_config(&self, content: impl AsRef) { File::options() .create(true) .append(true) .open(self.config_dir().join("config.toml")) .expect("Failed to open config file") .write_all(content.as_ref().as_bytes()) .expect("Failed to append to config file."); } fn delete_config(&self) { fs::remove_file(self.config_dir().join("config.toml")).unwrap(); } fn init_config(&self) { self.append_to_config(format!( "directories.cache_dir = '{}'\n", self.cache_dir().to_str().unwrap(), )); } fn create_secondary_config(self) -> Self { self.append_to_secondary_config(format!( "directories.cache_dir = '{}'\n", self.cache_dir().to_str().unwrap(), )); self } fn append_to_secondary_config(&self, content: impl AsRef) { File::options() .create(true) .append(true) .open(self.config_dir().join("config-secondary.toml")) .expect("Failed to open config file") .write_all(content.as_ref().as_bytes()) .expect("Failed to append to config file."); } fn remove_initial_config(self) -> Self { let _ = fs::remove_file(self.config_dir().join("config.toml")); self } /// Add entry for that environment to the "common" pages. fn add_entry(&self, name: &str, contents: &str) { self.add_os_entry("common", name, contents); } /// Add entry for that environment to an OS-specific subfolder. fn add_os_entry(&self, os: &str, name: &str, contents: &str) { self.add_os_lang_entry(os, "en", name, contents); } /// Add entry for that environment to a language-specific subfolder. fn add_lang_entry(&self, lang: &str, name: &str, contents: &str) { self.add_os_lang_entry("common", lang, name, contents); } /// Add entry for that environment to an OS- and language specific subfolder. fn add_os_lang_entry(&self, os: &str, lang: &str, name: &str, contents: &str) { let dir = self .cache_dir() .join(TLDR_PAGES_DIR) .join(format!("pages.{lang}")) .join(os); create_dir_all(&dir).unwrap(); fs::write(dir.join(format!("{name}.md")), contents.as_bytes()).unwrap(); } /// Add custom patch entry to the custom_pages_dir fn add_page_entry(&self, name: &str, contents: &str) { let dir = &self.custom_pages_dir(); create_dir_all(dir).unwrap(); fs::write(dir.join(format!("{name}.page.md")), contents.as_bytes()).unwrap(); } /// Add custom patch entry to the custom_pages_dir fn add_patch_entry(&self, name: &str, contents: &str) { let dir = &self.custom_pages_dir(); create_dir_all(dir).unwrap(); fs::write(dir.join(format!("{name}.patch.md")), contents.as_bytes()).unwrap(); } /// Disable default features. fn no_default_features(mut self) -> Self { self.default_features = false; self } /// Add the specified feature. fn with_feature>(mut self, feature: S) -> Self { self.features.push(feature.into()); self } /// Return a new `Command` with env vars set. fn command(&self) -> Command { let mut build = escargot::CargoBuild::new() .bin("tldr") .arg("--color=never") .current_release() .current_target(); if !self.default_features { build = build.no_default_features(); } if !self.features.is_empty() { build = build.features(self.features.join(" ")) } let run = build.run().expect("Failed to build tealdeer for testing"); let mut cmd = run.command(); // Avoid inheriting those from the test process. We can't just use .env_clear() because // this breaks tests on Windows in GitHub Actions. let relevant_env_variables = [ "LANG", "LANGUAGE", "TEALDEER_CACHE_DIR", "EDITOR", "NO_COLOR", ]; for variable_name in relevant_env_variables { cmd.env_remove(variable_name); } cmd.env("TEALDEER_CONFIG_DIR", self.config_dir().to_str().unwrap()); cmd } fn install_default_cache(self) -> Self { copy_recursively( &PathBuf::from_iter([env!("CARGO_MANIFEST_DIR"), "tests", "cache"]), &self.cache_dir().join(TLDR_PAGES_DIR), ) .expect("Failed to copy the cache to the test environment"); self } fn install_default_custom_pages(self) -> Self { copy_recursively( &PathBuf::from_iter([env!("CARGO_MANIFEST_DIR"), "tests", "custom-pages"]), self.custom_pages_dir().as_path(), ) .expect("Failed to copy the custom pages to the test environment"); self.write_custom_pages_config() } fn write_custom_pages_config(self) -> Self { self.append_to_config(format!( "directories.custom_pages_dir = '{}'\n", self.custom_pages_dir().to_str().unwrap() )); self } } fn copy_recursively(source: &Path, destination: &Path) -> io::Result<()> { if source.is_dir() { fs::create_dir_all(destination)?; for entry in fs::read_dir(source)? { let entry = entry?; copy_recursively(&entry.path(), &destination.join(entry.file_name()))?; } } else { fs::copy(source, destination)?; } Ok(()) } #[test] #[should_panic] fn test_cannot_build_without_tls_feature() { let _ = TestEnv::new().no_default_features().command(); } #[test] fn test_load_the_correct_config() { let testenv = TestEnv::new() .install_default_cache() .create_secondary_config(); testenv.append_to_secondary_config(include_str!("style-config.toml")); let expected_default = include_str!("rendered/inkscape-default.expected"); let expected_with_config = include_str!("rendered/inkscape-with-config.expected"); testenv .command() .args(["--color", "always", "inkscape-v2"]) .assert() .success() .stdout(diff(expected_default)); testenv .command() .args([ "--color", "always", "--config-path", testenv .config_dir() .join("config-secondary.toml") .to_str() .unwrap(), "inkscape-v2", ]) .assert() .success() .stdout(diff(expected_with_config)); } #[test] fn test_fail_on_custom_config_path_is_directory() { let testenv = TestEnv::new(); let error = if cfg!(windows) { "Access is denied" } else { "Is a directory" }; testenv .command() .args([ "--config-path", testenv.config_dir().to_str().unwrap(), "sl", ]) .assert() .failure() .stderr(contains(error)); } #[test] fn test_missing_cache() { TestEnv::new() .command() .args(["sl"]) .assert() .failure() .stderr(contains("Page cache not found. Please run `tldr --update`")); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_update_cache_default_features() { let testenv = TestEnv::new(); testenv .command() .args(["sl"]) .assert() .failure() .stderr(contains("Page cache not found. Please run `tldr --update`")); testenv .command() .args(["--update"]) .assert() .success() .stderr(contains("Successfully updated cache.")); testenv.command().args(["sl"]).assert().success(); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_update_cache_rustls_webpki() { let testenv = TestEnv::new() .no_default_features() .with_feature("rustls-with-webpki-roots"); testenv .command() .args(["sl"]) .assert() .failure() .stderr(contains("Page cache not found. Please run `tldr --update`")); testenv .command() .args(["--update"]) .assert() .success() .stderr(contains("Successfully updated cache.")); testenv.command().args(["sl"]).assert().success(); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_update_cache_native_tls() { let testenv = TestEnv::new() .no_default_features() .with_feature("rustls-with-native-roots"); testenv .command() .args(["sl"]) .assert() .failure() .stderr(contains("Page cache not found. Please run `tldr --update`")); testenv .command() .args(["--update"]) .assert() .success() .stderr(contains("Successfully updated cache.")); testenv.command().args(["sl"]).assert().success(); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_quiet_cache() { let testenv = TestEnv::new(); testenv .command() .args(["--update", "--quiet"]) .assert() .success() .stdout(is_empty()); testenv .command() .args(["--clear-cache", "--quiet"]) .assert() .success() .stdout(is_empty()); } #[test] fn test_clear_only_pages_directory() { let testenv = TestEnv::new().install_default_cache(); testenv .command() .args(["--clear-cache"]) .assert() .success() .stderr(contains(format!( "Successfully cleared cache at `{}`.", testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), ))); assert!(testenv.cache_dir().is_dir()); assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); } #[test] fn test_always_delete_old_pages_directory() { let testenv = TestEnv::new().install_default_cache(); fs::rename( testenv.cache_dir().join(TLDR_PAGES_DIR), testenv.cache_dir().join(TLDR_OLD_PAGES_DIR), ) .unwrap(); testenv .command() .arg("--list") .assert() .failure() .stderr(contains("Cleared pages from old cache location.")) .stderr(contains("Page cache not found.")); assert!(testenv.cache_dir().is_dir()); assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); assert!(!testenv.cache_dir().join(TLDR_OLD_PAGES_DIR).exists()); } #[test] fn test_warn_invalid_tls_backend() { let testenv = TestEnv::new() .no_default_features() .with_feature("rustls-with-webpki-roots") .remove_initial_config(); testenv.append_to_config("updates.tls_backend = 'invalid-tls-backend'\n"); testenv .command() .args(["sl"]) .assert() .failure() .stderr(contains("unknown variant `invalid-tls-backend`, expected one of `native-tls`, `rustls-with-webpki-roots`, `rustls-with-native-roots`")); } #[test] fn test_quiet_failures() { let testenv = TestEnv::new().install_default_cache(); testenv .command() .args(["fakeprogram", "-q"]) .assert() .failure() .stdout(is_empty()); } #[test] fn test_quiet_old_cache() { let testenv = TestEnv::new().install_default_cache(); filetime::set_file_mtime( testenv.cache_dir().join(TLDR_PAGES_DIR), filetime::FileTime::from_unix_time(1, 0), ) .unwrap(); testenv .command() .args(["which"]) .assert() .success() .stderr(contains("The cache hasn't been updated for ")); testenv .command() .args(["which", "--quiet"]) .assert() .success() .stderr(contains("The cache hasn't been updated for ").not()); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_create_cache_directory_path() { let testenv = TestEnv::new().remove_initial_config(); let cache_dir = &testenv.cache_dir(); let internal_cache_dir = cache_dir.join("internal"); testenv.append_to_config(format!( "directories.cache_dir = '{}'\n", internal_cache_dir.to_str().unwrap() )); let mut command = testenv.command(); assert!(!internal_cache_dir.exists()); command .arg("--update") .assert() .success() .stderr(contains(format!( "Successfully created cache directory `{}`.", internal_cache_dir.join(TLDR_PAGES_DIR).to_str().unwrap() ))) .stderr(contains("Successfully updated cache.")); assert!(internal_cache_dir.is_dir()); } #[test] fn test_cache_location_not_a_directory() { let testenv = TestEnv::new(); let cache_dir = &testenv.cache_dir(); File::create(cache_dir.join(TLDR_PAGES_DIR)).unwrap(); testenv .command() .arg("--list") .assert() .failure() .stderr(contains(format!( "Cache directory `{}` exists, but is not a directory.", cache_dir.join(TLDR_PAGES_DIR).display(), ))); } #[cfg(unix)] #[test] fn test_cache_location_permission_denied() { use std::os::unix::fs::PermissionsExt; let testenv = TestEnv::new().install_default_cache(); testenv .command() .arg("--list") .assert() .success() .stderr(contains("Permission denied").not()); // Make cache directory unreadable let cache_dir = testenv.cache_dir(); let mut permissions = cache_dir.metadata().unwrap().permissions(); permissions.set_mode(0); fs::set_permissions(cache_dir, permissions).unwrap(); testenv .command() .arg("--list") .assert() .failure() .stderr(contains("Permission denied")); } #[test] fn test_cache_location_source() { let testenv = TestEnv::new().remove_initial_config(); let default_cache_dir = &testenv.cache_dir(); let tmp_cache_dir = TempfileBuilder::new() .prefix(".tldr.test.cache_dir") .tempdir() .unwrap(); // Source: Default (OS convention) let mut command = testenv.command(); command .arg("--show-paths") .assert() .success() .stdout(is_match("\nCache dir: [^(]* \\(OS convention\\)\n").unwrap()); // Source: Config variable let mut command = testenv.command(); testenv.append_to_config(format!( "directories.cache_dir = '{}'\n", tmp_cache_dir.path().to_str().unwrap(), )); command .arg("--show-paths") .assert() .success() .stdout(is_match("\nCache dir: [^(]* \\(config file\\)\n").unwrap()); // Source: Env var let mut command = testenv.command(); command.env("TEALDEER_CACHE_DIR", default_cache_dir.to_str().unwrap()); command .arg("--show-paths") .assert() .success() .stdout(is_match("\nCache dir: [^(]* \\(env variable\\)\n").unwrap()); } #[test] fn test_setup_seed_config() { let testenv = TestEnv::new(); testenv .command() .args(["--seed-config"]) .assert() .failure() .stderr(contains("A configuration file already exists")); assert!(testenv.config_dir().join("config.toml").is_file()); let testenv = testenv.remove_initial_config(); testenv .command() .args(["--seed-config"]) .assert() .success() .stderr(contains("Successfully created seed config file here")); assert!(testenv.config_dir().join("config.toml").is_file()); // Create parent directories as needed for the default config path. fs::remove_dir_all(testenv.config_dir()).unwrap(); testenv .command() .args(["--seed-config"]) .assert() .success() .stderr(contains("Successfully created seed config file here")); assert!(testenv.config_dir().join("config.toml").is_file()); // Write the default config to --config-path if specified by the user // at the same time. let custom_config_path = testenv.config_dir().join("config_custom.toml"); testenv .command() .args([ "--seed-config", "--config-path", custom_config_path.to_str().unwrap(), ]) .assert() .success() .stderr(contains("Successfully created seed config file here")); assert!(custom_config_path.is_file()); // DON'T create parent directories for a custom config path. fs::remove_dir_all(testenv.config_dir()).unwrap(); testenv .command() .args([ "--seed-config", "--config-path", custom_config_path.to_str().unwrap(), ]) .assert() .failure() .stderr(contains("Could not create config file")); assert!(!custom_config_path.is_file()); } #[test] fn test_show_paths() { let testenv = TestEnv::new(); // Show general commands testenv .command() .args(["--show-paths"]) .assert() .success() .stdout(contains(format!( "Config dir: {}", testenv.config_dir().to_str().unwrap(), ))) .stdout(contains(format!( "Config path: {}", testenv.config_dir().join("config.toml").to_str().unwrap(), ))) .stdout(contains(format!( "Cache dir: {}", testenv.cache_dir().to_str().unwrap(), ))) .stdout(contains(format!( "Pages dir: {}", testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), ))); let testenv = testenv.write_custom_pages_config(); // Now ensure that this path is contained in the output testenv .command() .args(["--show-paths"]) .assert() .success() .stdout(contains(format!( "Custom pages dir: {}", testenv.custom_pages_dir().to_str().unwrap(), ))); } #[test] fn test_os_specific_page() { let testenv = TestEnv::new(); testenv.add_os_entry("sunos", "truss", "contents"); testenv .command() .args(["--platform", "sunos", "truss"]) .assert() .success(); } #[test] fn test_config_platforms() { let testenv = TestEnv::new(); testenv.add_os_entry("sunos", "sunos-command", ""); let set_config_platforms = |platforms| { testenv.delete_config(); testenv.init_config(); testenv.append_to_config(format!("search.platforms = {platforms}")); }; // By default all platforms are searched testenv.command().arg("sunos-command").assert().success(); set_config_platforms("[]"); testenv.command().arg("sunos-command").assert().failure(); set_config_platforms("['linux']"); testenv.command().arg("sunos-command").assert().failure(); set_config_platforms("['sunos']"); testenv.command().arg("sunos-command").assert().success(); set_config_platforms("['linux', 'all']"); testenv.command().arg("sunos-command").assert().success(); set_config_platforms("['current', 'all']"); testenv.command().arg("sunos-command").assert().success(); } #[test] fn test_markdown_rendering() { let testenv = TestEnv::new().install_default_cache(); let expected = include_str!("cache/pages.en/common/which.md"); testenv .command() .args(["--raw", "which"]) .assert() .success() .stdout(diff(expected)); } fn _test_correct_rendering(page: &str, expected: &'static str, additional_args: &[&str]) { let testenv = TestEnv::new().install_default_cache(); testenv .command() .args(additional_args) .arg(page) .assert() .success() .stdout(diff(expected)); } /// An end-to-end integration test for direct file rendering (v1 syntax). #[test] fn test_correct_rendering_v1() { _test_correct_rendering( "inkscape-v1", include_str!("rendered/inkscape-default.expected"), &["--color", "always"], ); } /// An end-to-end integration test for direct file rendering (v2 syntax). #[test] fn test_correct_rendering_v2() { _test_correct_rendering( "inkscape-v2", include_str!("rendered/inkscape-default.expected"), &["--color", "always"], ); } #[test] /// An end-to-end integration test for direct file rendering with the `--color auto` option. This /// will not use styling since output is not stdout. fn test_rendering_color_auto() { _test_correct_rendering( "inkscape-v2", include_str!("rendered/inkscape-default-no-color.expected"), &["--color", "auto"], ); } #[test] /// An end-to-end integration test for direct file rendering with the `--color never` option. fn test_rendering_color_never() { _test_correct_rendering( "inkscape-v2", include_str!("rendered/inkscape-default-no-color.expected"), &["--color", "never"], ); } #[test] fn test_rendering_i18n() { _test_correct_rendering( "apt", include_str!("rendered/apt.ja.expected"), &["--color", "always", "--language", "ja"], ); } /// An end-to-end integration test for rendering with custom syntax config. #[test] fn test_correct_rendering_with_config() { let testenv = TestEnv::new().install_default_cache(); testenv.append_to_config(include_str!("style-config.toml")); let expected = include_str!("rendered/inkscape-with-config.expected"); testenv .command() .args(["--color", "always", "inkscape-v2"]) .assert() .success() .stdout(diff(expected)); } /// An end-to-end integration test for rendering with show_title config option enabled. #[test] fn test_show_title_config() { // Test that default behavior without show_title shows no title let testenv = TestEnv::new().install_default_cache(); let expected_no_title = include_str!("rendered/inkscape-default.expected"); testenv .command() .args(["--color", "always", "inkscape-v2"]) .assert() .success() .stdout(diff(expected_no_title)); // Configure to enable show_title testenv.append_to_config("display.show_title = true\n"); let expected_no_color = include_str!("rendered/inkscape-with-title-no-color.expected"); testenv .command() .args(["inkscape-v2"]) .assert() .success() .stdout(diff(expected_no_color)); let expected = include_str!("rendered/inkscape-with-title.expected"); testenv .command() .args(["--color", "always", "inkscape-v2"]) .assert() .success() .stdout(diff(expected)); } #[test] fn test_spaces_find_command() { let testenv = TestEnv::new().install_default_cache(); testenv .command() .args(["git", "checkout"]) .assert() .success(); } #[test] fn test_pager_flag_enable() { let testenv = TestEnv::new().install_default_cache(); testenv .command() .args(["--pager", "which"]) .assert() .success(); } #[test] fn test_multiple_platform_command_search() { let testenv = TestEnv::new(); testenv.add_os_entry("linux", "linux-only", "this command only exists for linux"); testenv.add_os_entry( "linux", "windows-and-linux", "# windows-and-linux \n\n > linux version", ); testenv.add_os_entry( "windows", "windows-and-linux", "# windows-and-linux \n\n > windows version", ); testenv .command() .args(["--platform", "windows", "--platform", "linux", "linux-only"]) .assert() .success(); // test order of platforms supplied if preserved testenv .command() .args([ "--platform", "windows", "--platform", "linux", "windows-and-linux", ]) .assert() .success() .stdout(contains("windows version")); testenv .command() .args([ "--platform", "linux", "--platform", "windows", "windows-and-linux", ]) .assert() .success() .stdout(contains("linux version")); } #[test] fn test_multiple_platform_command_search_not_found() { let testenv = TestEnv::new(); testenv.add_os_entry( "windows", "windows-only", "this command only exists for Windows", ); testenv .command() .args(["--platform", "macos", "--platform", "linux", "windows-only"]) .assert() .stderr(contains("Page `windows-only` not found in cache.")); } #[test] fn test_macos_is_alias_for_osx() { let testenv = TestEnv::new(); testenv.add_os_entry("osx", "maconly", "this command only exists on mac"); testenv .command() .args(["--platform", "macos", "maconly"]) .assert() .success(); testenv .command() .args(["--platform", "osx", "maconly"]) .assert() .success(); testenv .command() .args(["--platform", "macos", "--list"]) .assert() .stdout("maconly\n"); testenv .command() .args(["--platform", "osx", "--list"]) .assert() .stdout("maconly\n"); testenv.append_to_config("search.platforms = ['osx']\n"); testenv.command().arg("--list").assert().stdout("maconly\n"); testenv.delete_config(); testenv.init_config(); testenv.append_to_config("search.platforms = ['macos']\n"); testenv.command().arg("--list").assert().stdout("maconly\n"); } #[test] fn test_common_platform_is_used_as_fallback() { let testenv = TestEnv::new(); testenv.add_entry("in-common", "this command comes from common"); // No platform specified testenv.command().args(["in-common"]).assert().success(); // Platform specified testenv .command() .args(["--platform", "linux", "in-common"]) .assert() .success(); } #[test] fn test_search_language_precedence() { let testenv = TestEnv::new(); for lang in ["en", "de", "it", "fr", "pl", "nl"] { testenv.add_lang_entry(lang, lang, ""); } let run = |cases: &[(Vec<(&str, &str)>, Vec<&str>, &str)]| { for (extra_env, extra_args, expected) in cases { let mut cmd = testenv.command(); for (key, value) in extra_env { cmd.env(key, value); } cmd.args(extra_args); cmd.arg("--list"); cmd.assert().success().stdout(eq(*expected)); } }; let env_cases = &[ (vec![], vec![], "en\n"), (vec![("LANGUAGE", "de:it")], vec![], "en\n"), ( vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec![], "de\nen\nfr\nit\n", ), ( vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec!["--language", "pl"], "pl\n", ), ]; run(env_cases); // Environment is only used when config setting is not set testenv.append_to_config("search.languages = ['nl']\n"); let config_cases = &[ (vec![], vec![], "nl\n"), (vec![("LANGUAGE", "de:it")], vec![], "nl\n"), (vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec![], "nl\n"), ( vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec!["--language", "pl"], "pl\n", ), ]; run(config_cases); // The above update setting does not change anything testenv.append_to_config("updates.download_languages = ['cz']"); run(config_cases); testenv.delete_config(); testenv.init_config(); testenv.append_to_config("updates.download_languages = ['cz']"); run(env_cases); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_update_language_arg() { let testenv = TestEnv::new(); testenv .command() .env("LANG", "it") .arg("--update") .assert() .success() .stderr(contains("it")) .stderr(contains("en")); testenv .command() .env("LANG", "en") .args(["--language", "it"]) .arg("--update") .assert() .success() .stderr(contains("it")) .stderr(contains("en").not()); } #[test] fn test_list_flag_rendering() { let testenv = TestEnv::new().write_custom_pages_config(); testenv .command() .args(["--list"]) .assert() .failure() .stderr(contains("Page cache not found. Please run `tldr --update`")); testenv.add_entry("foo", ""); testenv .command() .args(["--list"]) .assert() .success() .stdout("foo\n"); testenv.add_entry("bar", ""); testenv.add_entry("baz", ""); testenv.add_entry("qux", ""); testenv.add_page_entry("faz", ""); testenv.add_page_entry("bar", ""); testenv.add_page_entry("fiz", ""); testenv.add_patch_entry("buz", ""); testenv .command() .args(["--list"]) .assert() .success() .stdout("bar\nbaz\nfaz\nfiz\nfoo\nqux\n"); } #[test] fn test_multi_platform_list_flag_rendering() { let testenv = TestEnv::new().write_custom_pages_config(); testenv.add_entry("common", ""); testenv .command() .args(["--list"]) .assert() .success() .stdout("common\n"); testenv .command() .args(["--platform", "linux", "--list"]) .assert() .success() .stdout("common\n"); testenv .command() .args(["--platform", "windows", "--list"]) .assert() .success() .stdout("common\n"); testenv.add_os_entry("linux", "rm", ""); testenv.add_os_entry("linux", "ls", ""); testenv.add_os_entry("windows", "del", ""); testenv.add_os_entry("windows", "dir", ""); testenv.add_os_entry("linux", "winux", ""); testenv.add_os_entry("windows", "winux", ""); // test `--list` for `--platform linux` by itself testenv .command() .args(["--platform", "linux", "--list"]) .assert() .success() .stdout("common\nls\nrm\nwinux\n"); // test `--list` for `--platform windows` by itself testenv .command() .args(["--platform", "windows", "--list"]) .assert() .success() .stdout("common\ndel\ndir\nwinux\n"); // test `--list` for `--platform linux --platform windows` testenv .command() .args(["--platform", "linux", "--platform", "windows", "--list"]) .assert() .success() .stdout("common\ndel\ndir\nls\nrm\nwinux\n"); // test `--list` for `--platform windows --platform linux` testenv .command() .args(["--platform", "linux", "--platform", "windows", "--list"]) .assert() .success() .stdout("common\ndel\ndir\nls\nrm\nwinux\n"); } #[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_autoupdate_cache() { let testenv = TestEnv::new(); // The first time, if automatic updates are disabled, the cache should not be found testenv .command() .args(["--list"]) .assert() .failure() .stderr(contains("Page cache not found. Please run `tldr --update`")); let cache_file_path = testenv.cache_dir().join(TLDR_PAGES_DIR); testenv .append_to_config("updates.auto_update = true\nupdates.auto_update_interval_hours = 24\n"); // Helper function that runs `tldr --list` and asserts that the cache is automatically updated // or not, depending on the value of `expected`. let check_cache_updated = |expected| { let assert = testenv.command().args(["--list"]).assert().success(); let pred = contains("Successfully updated cache"); if expected { assert.stderr(pred) } else { assert.stderr(pred.not()) }; }; // The cache is updated the first time we run `tldr --list` check_cache_updated(true); // The cache is not updated with a subsequent call check_cache_updated(false); // We update the modification and access times such that they are about 23 hours from now. // auto-update interval is 24 hours, the cache should not be updated let new_mtime = SystemTime::now() - Duration::from_secs(82_800); filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap(); check_cache_updated(false); // We update the modification and access times such that they are about 25 hours from now. // auto-update interval is 24 hours, the cache should be updated let new_mtime = SystemTime::now() - Duration::from_secs(90_000); filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap(); check_cache_updated(true); // The cache is not updated with a subsequent call check_cache_updated(false); } /// End-end test to ensure .page.md files overwrite pages in cache_dir #[test] fn test_custom_page_overwrites() { let testenv = TestEnv::new().write_custom_pages_config(); // Add file that should be ignored to the cache dir testenv.add_entry("inkscape-v2", ""); // Add .page.md file to custom_pages_dir testenv.add_page_entry( "inkscape-v2", include_str!("cache/pages.en/common/inkscape-v2.md"), ); // Load expected output let expected = include_str!("rendered/inkscape-default-no-color.expected"); testenv .command() .args(["inkscape-v2", "--color", "never"]) .assert() .success() .stdout(diff(expected)); } /// End-End test to ensure that .patch.md files are appended to pages in the cache_dir #[test] fn test_custom_patch_appends_to_common() { let testenv = TestEnv::new() .install_default_cache() .install_default_custom_pages(); // Load expected output let expected = include_str!("rendered/inkscape-patched-no-color.expected"); testenv .command() .args(["inkscape-v2", "--color", "never"]) .assert() .success() .stdout(diff(expected)); } /// End-End test to ensure that .patch.md files are not appended to .page.md files in the custom_pages_dir /// Maybe this interaction should change but I put this test here for the coverage #[test] fn test_custom_patch_does_not_append_to_custom() { let testenv = TestEnv::new() .install_default_cache() .install_default_custom_pages(); // In addition to the page in the cache, add the same page as a custom page. testenv.add_page_entry( "inkscape-v2", include_str!("cache/pages.en/common/inkscape-v2.md"), ); // Load expected output let expected = include_str!("rendered/inkscape-default-no-color.expected"); testenv .command() .args(["inkscape-v2", "--color", "never"]) .assert() .success() .stdout(diff(expected)); } #[test] #[cfg(target_os = "windows")] fn test_pager_warning() { let testenv = TestEnv::new().install_default_cache(); // Regular call should not show a "pager flag not available on windows" warning testenv .command() .args(["which"]) .assert() .success() .stderr(contains("pager flag not available on Windows").not()); // But it should be shown if the pager flag is true testenv .command() .args(["--pager", "which"]) .assert() .success() .stderr(contains("pager flag not available on Windows")); } /// Ensure that page lookup is case insensitive, so a page lookup for `eyed3` /// and `eyeD3` should return the same page. #[test] fn test_lowercased_page_lookup() { let testenv = TestEnv::new(); // Lookup `eyed3`, initially fails testenv.command().args(["eyed3"]).assert().failure(); // Add entry testenv.add_entry("eyed3", "contents"); // Lookup `eyed3` again testenv.command().args(["eyed3"]).assert().success(); // Lookup `eyeD3`, should succeed as well testenv.command().args(["eyeD3"]).assert().success(); } /// Regression test for #219: It should be possible to combine `--raw` and `-f`. #[test] fn test_raw_render_file() { let testenv = TestEnv::new().install_default_cache(); let path = testenv .cache_dir() .join(TLDR_PAGES_DIR) .join("pages.en/common/inkscape-v1.md"); let mut args = vec!["--color", "never", "-f", &path.to_str().unwrap()]; // Default render testenv .command() .args(&args) .assert() .success() .stdout(diff(include_str!( "rendered/inkscape-default-no-color.expected" ))); // Raw render args.push("--raw"); testenv .command() .args(&args) .assert() .success() .stdout(diff(include_str!("cache/pages.en/common/inkscape-v1.md"))); } fn touch_custom_page(testenv: &TestEnv) { let args = vec!["--edit-page", "foo"]; testenv .command() .args(&args) .env("EDITOR", "touch") .assert() .success(); assert!(testenv.custom_pages_dir().join("foo.page.md").exists()); } fn touch_custom_patch(testenv: &TestEnv) { let args = vec!["--edit-patch", "foo"]; testenv .command() .args(&args) .env("EDITOR", "touch") .assert() .success(); assert!(testenv.custom_pages_dir().join("foo.patch.md").exists()); } #[test] fn test_edit_page() { let testenv = TestEnv::new().write_custom_pages_config(); touch_custom_page(&testenv); } #[test] fn test_edit_patch() { let testenv = TestEnv::new().write_custom_pages_config(); touch_custom_patch(&testenv); } #[test] fn test_recreate_dir() { let testenv = TestEnv::new().write_custom_pages_config(); touch_custom_patch(&testenv); touch_custom_page(&testenv); } #[test] fn test_custom_pages_dir_is_not_dir() { let testenv = TestEnv::new().write_custom_pages_config(); let _ = std::fs::remove_dir_all(testenv.custom_pages_dir()); let _ = File::create(testenv.custom_pages_dir()).unwrap(); assert!(testenv.custom_pages_dir().is_file()); let args = vec!["--edit-patch", "foo"]; testenv .command() .args(&args) .env("EDITOR", "touch") .assert() .failure(); } tealdeer-1.8.0/tests/rendered/apt.ja.expected000064400000000000000000000030451046102023000172530ustar 00000000000000 Debian系ディストリビューションで使われるパッケージ管理システムです。 Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。 詳しくはこちら: 利用可能なパーケージとバージョンのリストの更新(他の`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 tealdeer-1.8.0/tests/rendered/inkscape-default-no-color.expected000064400000000000000000000021611046102023000230410ustar 00000000000000 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 tealdeer-1.8.0/tests/rendered/inkscape-default.expected000064400000000000000000000032251046102023000213150ustar 00000000000000 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 tealdeer-1.8.0/tests/rendered/inkscape-patched-no-color.expected000064400000000000000000000022451046102023000230300ustar 00000000000000 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 tealdeer-1.8.0/tests/rendered/inkscape-with-config.expected000064400000000000000000000026371046102023000221150ustar 00000000000000 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 tealdeer-1.8.0/tests/rendered/inkscape-with-title-no-color.expected000064400000000000000000000021751046102023000235140ustar 00000000000000 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 tealdeer-1.8.0/tests/rendered/inkscape-with-title.expected000064400000000000000000000032521046102023000217630ustar 00000000000000 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 tealdeer-1.8.0/tests/style-config.toml000064400000000000000000000004631046102023000160640ustar 00000000000000[style.highlight] foreground = "green" underline = false bold = false [style.command_name] bold = true [style.description] underline = false bold = false [style.example_text] foreground = "black" background = "blue" underline = false [style.example_variable] underline = true bold = false italic = true