diff options
author | Cyborus <cyborus@noreply.codeberg.org> | 2024-06-08 22:43:36 +0200 |
---|---|---|
committer | Cyborus <cyborus@noreply.codeberg.org> | 2024-06-08 22:43:36 +0200 |
commit | d10b5172bef9a293f4f038587c07503c81b3d932 (patch) | |
tree | afc21423caacf414b71c7dd408364c11276a8e46 | |
parent | Merge pull request 'guess pr number from commit' (#74) from guess-pr into main (diff) | |
parent | fix first line of blockquote being grey (diff) | |
download | forgejo-cli-d10b5172bef9a293f4f038587c07503c81b3d932.tar.xz forgejo-cli-d10b5172bef9a293f4f038587c07503c81b3d932.zip |
Merge pull request 'print markdown text nicely' (#75) from pretty-text into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-cli/pulls/75
-rw-r--r-- | Cargo.lock | 406 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/issues.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 463 | ||||
-rw-r--r-- | src/prs.rs | 9 | ||||
-rw-r--r-- | src/release.rs | 12 | ||||
-rw-r--r-- | src/repo.rs | 7 |
7 files changed, 879 insertions, 24 deletions
@@ -18,6 +18,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[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.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -127,6 +136,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -196,6 +229,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -223,6 +257,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] +name = "comrak" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a972c8ec1be8065f7b597b5f7f5b3be535db780280644aebdcd1966decf58dc" +dependencies = [ + "clap", + "derive_builder", + "entities", + "memchr", + "once_cell", + "regex", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + +[[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -248,6 +302,40 @@ dependencies = [ ] [[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -258,6 +346,41 @@ dependencies = [ ] [[package]] +name = "darling" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -268,6 +391,43 @@ dependencies = [ ] [[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + +[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -317,6 +477,12 @@ dependencies = [ ] [[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -343,6 +509,16 @@ dependencies = [ ] [[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -355,6 +531,8 @@ dependencies = [ "auth-git2", "base64ct", "clap", + "comrak", + "crossterm", "directories", "eyre", "forgejo-api", @@ -375,6 +553,16 @@ dependencies = [ ] [[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -744,6 +932,12 @@ dependencies = [ ] [[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -881,6 +1075,18 @@ dependencies = [ ] [[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -940,6 +1146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -994,6 +1201,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] name = "open" version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1108,6 +1337,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64", + "indexmap", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1129,6 +1372,15 @@ dependencies = [ ] [[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1188,6 +1440,35 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] name = "reqwest" version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1263,6 +1544,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[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.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1368,6 +1658,33 @@ dependencies = [ ] [[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1386,6 +1703,16 @@ dependencies = [ ] [[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1431,6 +1758,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] name = "system-configuration" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1474,6 +1824,16 @@ dependencies = [ ] [[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] name = "thiserror" version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1625,6 +1985,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1661,6 +2027,12 @@ dependencies = [ ] [[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1700,6 +2072,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[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 = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1807,6 +2189,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1962,6 +2353,21 @@ dependencies = [ ] [[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -9,6 +9,8 @@ edition = "2021" auth-git2 = "0.5.3" base64ct = { version = "1.6.0", features = ["std"] } clap = { version = "4.3.11", features = ["derive"] } +comrak = "0.24.1" +crossterm = "0.27.0" directories = "5.0.1" eyre = "0.6.8" forgejo-api = "0.3.0" diff --git a/src/issues.rs b/src/issues.rs index b7b3a6e..233d603 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -253,7 +253,7 @@ pub async fn view_issue(repo: &RepoName, api: &Forgejo, id: u64) -> eyre::Result println!("By {}", username); if let Some(body) = &issue.body { println!(); - println!("{}", body); + println!("{}", crate::markdown(body)); } Ok(()) } @@ -355,7 +355,7 @@ fn print_comment(comment: &Comment) -> eyre::Result<()> { .as_ref() .ok_or_else(|| eyre::eyre!("user does not have login"))?; println!("{} said:", username); - println!("{}", body); + println!("{}", crate::markdown(&body)); let assets = comment .assets .as_ref() diff --git a/src/main.rs b/src/main.rs index 1cc81e5..6bd8a45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,11 +167,12 @@ enum Style { } struct SpecialRender { - colors: bool, + fancy: bool, dash: char, bullet: char, body_prefix: char, + horiz_rule: char, red: &'static str, bright_red: &'static str, @@ -189,11 +190,21 @@ struct SpecialRender { dark_grey: &'static str, light_grey: &'static str, white: &'static str, + no_fg: &'static str, reset: &'static str, + dark_grey_bg: &'static str, + no_bg: &'static str, + hide_cursor: &'static str, show_cursor: &'static str, clear_line: &'static str, + + italic: &'static str, + bold: &'static str, + strike: &'static str, + no_italic_bold: &'static str, + no_strike: &'static str, } impl SpecialRender { @@ -208,11 +219,12 @@ impl SpecialRender { fn fancy() -> Self { Self { - colors: true, + fancy: true, dash: '—', bullet: '•', body_prefix: '▌', + horiz_rule: '─', red: "\x1b[31m", bright_red: "\x1b[91m", @@ -230,21 +242,32 @@ impl SpecialRender { dark_grey: "\x1b[90m", light_grey: "\x1b[37m", white: "\x1b[97m", + no_fg: "\x1b[39m", reset: "\x1b[0m", + dark_grey_bg: "\x1b[100m", + no_bg: "\x1b[49", + hide_cursor: "\x1b[?25l", show_cursor: "\x1b[?25h", clear_line: "\x1b[2K", + + italic: "\x1b[3m", + bold: "\x1b[1m", + strike: "\x1b[9m", + no_italic_bold: "\x1b[23m", + no_strike: "\x1b[29m", } } fn minimal() -> Self { Self { - colors: false, + fancy: false, dash: '-', bullet: '-', body_prefix: '>', + horiz_rule: '-', red: "", bright_red: "", @@ -262,11 +285,445 @@ impl SpecialRender { dark_grey: "", light_grey: "", white: "", + no_fg: "", reset: "", + dark_grey_bg: "", + no_bg: "", + hide_cursor: "", show_cursor: "", clear_line: "", + + italic: "", + bold: "", + strike: "~~", + no_italic_bold: "", + no_strike: "~~", + } + } +} + +fn markdown(text: &str) -> String { + let SpecialRender { + fancy, + + bullet, + horiz_rule, + bright_blue, + dark_grey_bg, + body_prefix, + .. + } = *special_render(); + + if !fancy { + let mut out = String::new(); + for line in text.lines() { + use std::fmt::Write; + let _ = writeln!(&mut out, "{body_prefix} {line}"); + } + return out; + } + + let arena = comrak::Arena::new(); + let mut options = comrak::Options::default(); + options.extension.strikethrough = true; + let root = comrak::parse_document(&arena, text, &options); + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Side { + Start, + End, + } + + let mut explore_stack = Vec::new(); + let mut render_queue = Vec::new(); + + explore_stack.extend(root.reverse_children().map(|x| (x, Side::Start))); + while let Some((node, side)) = explore_stack.pop() { + if side == Side::Start { + explore_stack.push((node, Side::End)); + explore_stack.extend(node.reverse_children().map(|x| (x, Side::Start))); + } + render_queue.push((node, side)); + } + + let mut list_numbers = Vec::new(); + + let (terminal_width, _) = crossterm::terminal::size().unwrap_or((80, 24)); + let max_line_len = (terminal_width as usize - 2).min(80); + + let mut links = Vec::new(); + + let mut ansi_printer = AnsiPrinter::new(max_line_len); + ansi_printer.pause_style(); + ansi_printer.prefix(); + ansi_printer.resume_style(); + for (item, side) in render_queue { + use comrak::nodes::NodeValue; + use Side::*; + match (&item.data.borrow().value, side) { + (NodeValue::Paragraph, Start) => (), + (NodeValue::Paragraph, End) => { + ansi_printer.newline(); + ansi_printer.newline(); + } + (NodeValue::Text(s), Start) => ansi_printer.text(s), + (NodeValue::Link(_), Start) => { + ansi_printer.start_fg(bright_blue); + } + (NodeValue::Link(link), End) => { + use std::fmt::Write; + ansi_printer.stop_fg(); + links.push(link.url.clone()); + let _ = write!(&mut ansi_printer, "({})", links.len()); + } + (NodeValue::Image(_), Start) => { + ansi_printer.start_fg(bright_blue); + } + (NodeValue::Image(link), End) => { + use std::fmt::Write; + ansi_printer.stop_fg(); + links.push(link.url.clone()); + let _ = write!(&mut ansi_printer, "({})", links.len()); + } + (NodeValue::Code(code), Start) => { + ansi_printer.pause_style(); + ansi_printer.start_bg(dark_grey_bg); + ansi_printer.text(&code.literal); + ansi_printer.resume_style(); + } + (NodeValue::CodeBlock(code), Start) => { + if ansi_printer.cur_line_len != 0 { + ansi_printer.newline(); + } + ansi_printer.pause_style(); + ansi_printer.start_bg(dark_grey_bg); + ansi_printer.text(&code.literal); + ansi_printer.newline(); + ansi_printer.resume_style(); + ansi_printer.newline(); + } + (NodeValue::BlockQuote, Start) => { + ansi_printer.blockquote_depth += 1; + ansi_printer.pause_style(); + ansi_printer.prefix(); + ansi_printer.resume_style(); + } + (NodeValue::BlockQuote, End) => { + ansi_printer.blockquote_depth -= 1; + ansi_printer.newline(); + } + (NodeValue::HtmlInline(html), Start) => { + ansi_printer.pause_style(); + ansi_printer.text(html); + ansi_printer.resume_style(); + } + (NodeValue::HtmlBlock(html), Start) => { + if ansi_printer.cur_line_len != 0 { + ansi_printer.newline(); + } + ansi_printer.pause_style(); + ansi_printer.text(&html.literal); + ansi_printer.newline(); + ansi_printer.resume_style(); + } + + (NodeValue::Heading(heading), Start) => { + ansi_printer.reset(); + ansi_printer.start_bold(); + ansi_printer + .out + .extend(std::iter::repeat('#').take(heading.level as usize)); + ansi_printer.out.push(' '); + ansi_printer.cur_line_len += heading.level as usize + 1; + } + (NodeValue::Heading(_), End) => { + ansi_printer.reset(); + ansi_printer.newline(); + } + + (NodeValue::List(list), Start) => { + if list.list_type == comrak::nodes::ListType::Ordered { + list_numbers.push(0); + } + } + (NodeValue::List(list), End) => { + if list.list_type == comrak::nodes::ListType::Ordered { + list_numbers.pop(); + } + } + (NodeValue::Item(list), Start) => { + if list.list_type == comrak::nodes::ListType::Ordered { + use std::fmt::Write; + let number: usize = if let Some(number) = list_numbers.last_mut() { + *number += 1; + *number + } else { + 0 + }; + let _ = write!(&mut ansi_printer, "{number}. "); + } else { + ansi_printer.out.push(bullet); + ansi_printer.out.push(' '); + ansi_printer.cur_line_len += 2; + } + } + + (NodeValue::LineBreak, Start) => ansi_printer.newline(), + (NodeValue::SoftBreak, Start) => ansi_printer.newline(), + (NodeValue::ThematicBreak, Start) => { + if ansi_printer.cur_line_len != 0 { + ansi_printer.newline(); + } + ansi_printer + .out + .extend(std::iter::repeat(horiz_rule).take(max_line_len)); + ansi_printer.newline(); + ansi_printer.newline(); + } + + (NodeValue::Emph, Start) => ansi_printer.start_italic(), + (NodeValue::Emph, End) => ansi_printer.stop_italic(), + (NodeValue::Strong, Start) => ansi_printer.start_bold(), + (NodeValue::Strong, End) => ansi_printer.stop_bold(), + (NodeValue::Strikethrough, Start) => ansi_printer.start_strike(), + (NodeValue::Strikethrough, End) => ansi_printer.stop_strike(), + + (NodeValue::Escaped, Start) => (), + (_, End) => (), + (_, Start) => ansi_printer.text("?TODO?"), + } + } + if !links.is_empty() { + ansi_printer.out.push('\n'); + for (i, url) in links.into_iter().enumerate() { + use std::fmt::Write; + let _ = writeln!(&mut ansi_printer.out, "({}. {url} )", i + 1); + } + } + + ansi_printer.out +} + +#[derive(Default)] +struct RenderStyling { + bold: bool, + italic: bool, + strike: bool, + + fg: Option<&'static str>, + bg: Option<&'static str>, +} + +struct AnsiPrinter { + special_render: &'static SpecialRender, + + out: String, + + cur_line_len: usize, + max_line_len: usize, + + blockquote_depth: usize, + + style_frames: Vec<RenderStyling>, +} + +impl AnsiPrinter { + fn new(max_line_len: usize) -> Self { + Self { + special_render: special_render(), + + out: String::new(), + + cur_line_len: 0, + max_line_len, + + blockquote_depth: 0, + + style_frames: vec![RenderStyling::default()], } } + + fn text(&mut self, text: &str) { + let mut iter = text.lines().peekable(); + while let Some(mut line) = iter.next() { + loop { + let this_len = line.chars().count(); + if self.cur_line_len + this_len > self.max_line_len { + let mut split_at = self.max_line_len - self.cur_line_len; + loop { + if line.is_char_boundary(split_at) { + break; + } + split_at -= 1; + } + let split_at = line + .split_at(split_at) + .0 + .char_indices() + .rev() + .find(|(_, c)| c.is_whitespace()) + .map(|(i, _)| i) + .unwrap_or(split_at); + let (head, tail) = line.split_at(split_at); + self.out.push_str(head); + self.cur_line_len += split_at; + self.newline(); + line = tail.trim_start(); + } else { + self.out.push_str(line); + self.cur_line_len += this_len; + break; + } + } + if iter.peek().is_some() { + self.newline(); + } + } + } + + fn current_fg(&self) -> Option<&'static str> { + self.current_style().fg + } + + fn start_fg(&mut self, color: &'static str) { + self.current_style_mut().fg = Some(color); + self.out.push_str(color); + } + + fn stop_fg(&mut self) { + self.current_style_mut().fg = None; + self.out.push_str(self.special_render.no_fg); + } + + fn current_bg(&self) -> Option<&'static str> { + self.current_style().bg + } + + fn start_bg(&mut self, color: &'static str) { + self.current_style_mut().bg = Some(color); + self.out.push_str(color); + } + + fn stop_bg(&mut self) { + self.current_style_mut().bg = None; + self.out.push_str(self.special_render.no_bg); + } + + fn is_bold(&self) -> bool { + self.current_style().bold + } + + fn start_bold(&mut self) { + self.current_style_mut().bold = true; + self.out.push_str(self.special_render.bold); + } + + fn stop_bold(&mut self) { + self.current_style_mut().bold = false; + self.out.push_str(self.special_render.reset); + if self.is_italic() { + self.out.push_str(self.special_render.italic); + } + if self.is_strike() { + self.out.push_str(self.special_render.strike); + } + } + + fn is_italic(&self) -> bool { + self.current_style().italic + } + + fn start_italic(&mut self) { + self.current_style_mut().italic = true; + self.out.push_str(self.special_render.italic); + } + + fn stop_italic(&mut self) { + self.current_style_mut().italic = false; + self.out.push_str(self.special_render.no_italic_bold); + if self.is_bold() { + self.out.push_str(self.special_render.bold); + } + } + + fn is_strike(&self) -> bool { + self.current_style().strike + } + + fn start_strike(&mut self) { + self.current_style_mut().strike = true; + self.out.push_str(self.special_render.strike); + } + + fn stop_strike(&mut self) { + self.current_style_mut().strike = false; + self.out.push_str(self.special_render.no_strike); + } + + fn reset(&mut self) { + *self.current_style_mut() = RenderStyling::default(); + self.out.push_str(self.special_render.reset); + } + + fn pause_style(&mut self) { + self.out.push_str(self.special_render.reset); + self.style_frames.push(RenderStyling::default()); + } + + fn resume_style(&mut self) { + self.out.push_str(self.special_render.reset); + self.style_frames.pop(); + if let Some(bg) = self.current_bg() { + self.out.push_str(bg); + } + if self.is_bold() { + self.out.push_str(self.special_render.bold); + } + if self.is_italic() { + self.out.push_str(self.special_render.italic); + } + if self.is_strike() { + self.out.push_str(self.special_render.strike); + } + } + + fn newline(&mut self) { + if self.current_bg().is_some() { + self.out + .extend(std::iter::repeat(' ').take(self.max_line_len - self.cur_line_len)); + } + self.pause_style(); + self.out.push('\n'); + self.prefix(); + for _ in 0..self.blockquote_depth { + self.prefix(); + } + self.resume_style(); + self.cur_line_len = self.blockquote_depth * 2; + } + + fn prefix(&mut self) { + self.out.push_str(self.special_render.dark_grey); + self.out.push(self.special_render.body_prefix); + self.out.push(' '); + } + + fn current_style(&self) -> &RenderStyling { + self.style_frames.last().expect("Ran out of style frames") + } + + fn current_style_mut(&mut self) -> &mut RenderStyling { + self.style_frames + .last_mut() + .expect("Ran out of style frames") + } +} + +impl std::fmt::Write for AnsiPrinter { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.text(s); + Ok(()) + } } @@ -393,7 +393,6 @@ impl PrCommand { pub async fn view_pr(repo: &RepoName, api: &Forgejo, id: Option<u64>) -> eyre::Result<()> { let crate::SpecialRender { dash, - body_prefix, bright_red, bright_green, @@ -489,9 +488,7 @@ pub async fn view_pr(repo: &RepoName, api: &Forgejo, id: Option<u64>) -> eyre::R if let Some(body) = &pr.body { if !body.trim().is_empty() { println!(); - for line in body.lines() { - println!("{dark_grey}{body_prefix}{reset} {line}"); - } + println!("{}", crate::markdown(body)); } } println!(); @@ -507,13 +504,13 @@ async fn view_pr_labels(repo: &RepoName, api: &Forgejo, pr: Option<u64>) -> eyre let pr = try_get_pr(repo, api, pr).await?; let labels = pr.labels.as_deref().unwrap_or_default(); let SpecialRender { - colors, + fancy, black, white, reset, .. } = *crate::special_render(); - if colors { + if fancy { let mut total_width = 0; for label in labels { let name = label.name.as_deref().unwrap_or("???").trim(); diff --git a/src/release.rs b/src/release.rs index b5a7566..1745a5e 100644 --- a/src/release.rs +++ b/src/release.rs @@ -375,22 +375,14 @@ async fn view_release( &time::format_description::well_known::Rfc2822, )?; println!(); - let SpecialRender { - bullet, - body_prefix, - dark_grey, - reset, - .. - } = crate::special_render(); + let SpecialRender { bullet, .. } = crate::special_render(); let body = release .body .as_ref() .ok_or_else(|| eyre::eyre!("release does not have body"))?; if !body.is_empty() { println!(); - for line in body.lines() { - println!("{dark_grey}{body_prefix}{reset} {line}"); - } + println!("{}", crate::markdown(&body)); println!(); } let assets = release diff --git a/src/repo.rs b/src/repo.rs index 7520af8..33befec 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -371,6 +371,7 @@ impl RepoCommand { } } let desc = repo.description.as_deref().unwrap_or_default(); + // Don't use body::markdown, this is plain text. if !desc.is_empty() { if desc.lines().count() > 1 { println!(); @@ -475,7 +476,7 @@ impl RepoCommand { let path = path.unwrap_or_else(|| PathBuf::from(format!("./{repo_name}"))); let SpecialRender { - colors, // actually using it to indicate fanciness FIXME + fancy, // actually using it to indicate fanciness FIXME hide_cursor, show_cursor, clear_line, @@ -489,7 +490,7 @@ impl RepoCommand { let mut callbacks = git2::RemoteCallbacks::new(); callbacks.credentials(auth.credentials(&git_config)); - if colors { + if fancy { print!("{hide_cursor}"); print!(" Preparing..."); let _ = std::io::stdout().flush(); @@ -529,7 +530,7 @@ impl RepoCommand { let local_repo = git2::build::RepoBuilder::new() .fetch_options(options) .clone(clone_url.as_str(), &path)?; - if colors { + if fancy { print!("{clear_line}{show_cursor}\r"); } println!("Cloned {} into {}", repo_full_name, path.display()); |