diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 000000000..5d230ee5d --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/r3bl-open-core.iml b/.idea/r3bl-open-core.iml index dbd55ae8f..ad3ff7b3b 100644 --- a/.idea/r3bl-open-core.iml +++ b/.idea/r3bl-open-core.iml @@ -20,6 +20,8 @@ + + diff --git a/.vscode/settings.json b/.vscode/settings.json index 3bcb83893..a34e5e753 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "Blackbox", "BOOKM", "boop", + "callsite", "CBOR", "chrono", "cicd", diff --git a/CHANGELOG.md b/CHANGELOG.md index e9744723a..898a7cbc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,10 @@ - [v0.0.3 2024-09-12](#v003-2024-09-12) - [v0.0.2 2024-07-13](#v002-2024-07-13) - [v0.0.1 2024-07-12](#v001-2024-07-12) +- [r3bl_log](#r3bl_log) + - [v_next_release_log](#v_next_release_r3bl_log) +- [r3bl_script](#r3bl_script) + - [v_next_release_script](#v_next_release_r3bl_script) - [r3bl_terminal_async](#r3bl_terminal_async) - [v0.6.0 2024-10-21](#v060-2024-10-21) - [v0.5.7 2024-09-12](#v057-2024-09-12) @@ -778,11 +782,20 @@ Changed: replace them with doc comments that compile successfully. - Added: + - `UnicodeString` now implements `std::fmt::Display`, so it is no longer necessary to + use `UnicodeString.string` to get to the underlying string. This is just more + ergonomic. This is added in `convert.rs`. More work needs to be done to introduce + different types for holding "width / height" aka "column / row counts", and "column / + row index". Currently there is a `Size` struct, and `Position` struct, but most of the + APIs don't really use one or another, they just use `ChUnit` directly. - `lolcat_api` enhancements that now allow for an optional default style to be passed in to `ColorWheel::lolcat_into_string` and `ColorWheel::colorize_into_string`, that will be applied to the generated lolcat output. - `convert_to_ansi_color_styles` module that adds the ability to convert a `TuiStyle` into a `Vec` of `r3bl_ansi_term::Style`. + - `string_helpers.rs` has new utility functions to check whether a given string contains + any ANSI escape codes `contains_ansi_escape_sequence`. And to remove needless escaped + `\"` characters from a string `remove_escaped_quotes`. - A new declarative macro `create_global_singleton!` that takes a struct (which must implement `Default` trait) and allows it to be simply turned into a singleton. - You can still use the struct directly. Or just use the supplied generated associated @@ -986,6 +999,24 @@ links for this release: [crates.io](https://crates.io/crates/r3bl_test_fixtures) crates in this monorepo to use them. These fixtures are migrated from `r3bl_terminal_async` crate, where they were gestated, before being graduated for use by the entire monorepo. +## `r3bl_log` + +### v_next_release_r3bl_log + +This is the first release of this crate. It is a top level crate in the `r3bl-open-core` +that is meant to hold all the logging related functionality for all the other crates in +this monorepo. It uses `tracing` under the covers to provide structured logging. It also +provides a custom formatter that is a `tracing-subscriber` crate plugin. + +## `r3bl_script` + +### v_next_release_r3bl_script + +This is the first release of this crate. It is a top level crate in the `r3bl-open-core` +that is meant to hold all the scripting related functionality for all the other crates in +this monorepo. It provides a way to run scripts in a safe and secure way, that is meant to +be a replacement for writing scripts in `fish` or `bash` or `nushell` syntax. + ## `r3bl_terminal_async` ### v0.6.0 (2024-10-21) diff --git a/Cargo.lock b/Cargo.lock index efc877bec..ac47c21e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -264,9 +264,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -297,7 +297,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", - "terminal_size 0.4.0", + "terminal_size", ] [[package]] @@ -309,7 +309,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -700,7 +700,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -1061,7 +1061,7 @@ dependencies = [ "serde", "serde_json", "sled", - "thiserror", + "thiserror 1.0.64", "toml", ] @@ -1166,9 +1166,9 @@ dependencies = [ [[package]] name = "miette" -version = "7.2.0" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" +checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64" dependencies = [ "backtrace", "backtrace-ext", @@ -1178,21 +1178,21 @@ dependencies = [ "supports-color", "supports-hyperlinks", "supports-unicode", - "terminal_size 0.3.0", + "terminal_size", "textwrap", - "thiserror", + "thiserror 1.0.64", "unicode-width 0.1.14", ] [[package]] name = "miette-derive" -version = "7.2.0" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" +checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -1402,7 +1402,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -1537,7 +1537,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -1634,9 +1634,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1673,6 +1673,7 @@ dependencies = [ "r3bl_analytics_schema", "r3bl_ansi_color", "r3bl_core", + "r3bl_log", "r3bl_macro", "r3bl_tui", "r3bl_tuify", @@ -1682,7 +1683,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "thiserror", + "thiserror 1.0.64", "tokio", "tracing", "tracing-appender", @@ -1713,7 +1714,6 @@ dependencies = [ name = "r3bl_core" version = "0.10.0" dependencies = [ - "assert_cmd", "async-stream", "bincode", "chrono", @@ -1736,8 +1736,7 @@ dependencies = [ "strip-ansi", "strum", "strum_macros", - "tempfile", - "thiserror", + "thiserror 1.0.64", "time", "tokio", "tracing", @@ -1748,6 +1747,29 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "r3bl_log" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "chrono", + "crossterm", + "miette", + "pretty_assertions", + "r3bl_ansi_color", + "r3bl_core", + "r3bl_macro", + "r3bl_test_fixtures", + "serial_test", + "textwrap", + "thiserror 1.0.64", + "tokio", + "tracing", + "tracing-appender", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "r3bl_macro" version = "0.10.0" @@ -1757,7 +1779,31 @@ dependencies = [ "proc-macro2", "quote", "r3bl_core", - "syn 2.0.82", + "syn 2.0.90", +] + +[[package]] +name = "r3bl_script" +version = "0.1.0" +dependencies = [ + "chrono", + "crossterm", + "futures-util", + "miette", + "r3bl_ansi_color", + "r3bl_core", + "r3bl_macro", + "reqwest", + "serde_json", + "serial_test", + "strum", + "strum_macros", + "textwrap", + "thiserror 2.0.7", + "tokio", + "tracing", + "tracing-core", + "tracing-subscriber", ] [[package]] @@ -1772,12 +1818,13 @@ dependencies = [ "pretty_assertions", "r3bl_ansi_color", "r3bl_core", + "r3bl_log", "r3bl_test_fixtures", "r3bl_tui", "r3bl_tuify", "strum", "strum_macros", - "thiserror", + "thiserror 1.0.64", "tokio", "tracing", "tracing-appender", @@ -1801,7 +1848,7 @@ dependencies = [ "strip-ansi-escapes", "strum", "strum_macros", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-test", "tracing", @@ -1823,6 +1870,7 @@ dependencies = [ "pretty_assertions", "r3bl_ansi_color", "r3bl_core", + "r3bl_log", "r3bl_macro", "r3bl_terminal_async", "r3bl_test_fixtures", @@ -1835,7 +1883,7 @@ dependencies = [ "strum_macros", "syntect", "textwrap", - "thiserror", + "thiserror 1.0.64", "tokio", "tracing", "tracing-appender", @@ -1854,6 +1902,7 @@ dependencies = [ "pretty_assertions", "r3bl_ansi_color", "r3bl_core", + "r3bl_log", "reedline", "serde", "serde_json", @@ -1921,7 +1970,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -1939,7 +1988,7 @@ dependencies = [ "strip-ansi-escapes", "strum", "strum_macros", - "thiserror", + "thiserror 1.0.64", "unicode-segmentation", "unicode-width 0.1.14", ] @@ -1975,9 +2024,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64", "bytes", @@ -2052,9 +2101,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.15" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "rustls-pki-types", @@ -2186,14 +2235,14 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -2215,9 +2264,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ "futures", "log", @@ -2229,13 +2278,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -2441,7 +2490,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -2484,9 +2533,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.82" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2519,7 +2568,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.64", "walkdir", "yaml-rust", ] @@ -2558,16 +2607,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[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 = "terminal_size" version = "0.4.0" @@ -2602,7 +2641,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -2613,7 +2652,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", "test-case-core", ] @@ -2634,7 +2673,16 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.64", +] + +[[package]] +name = "thiserror" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +dependencies = [ + "thiserror-impl 2.0.7", ] [[package]] @@ -2645,7 +2693,18 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -2706,9 +2765,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -2731,7 +2790,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -2746,12 +2805,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] @@ -2809,9 +2867,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2825,27 +2883,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.64", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -2864,9 +2922,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term 0.46.0", "sharded-slab", @@ -2975,7 +3033,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] @@ -3072,7 +3130,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -3106,7 +3164,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3515,7 +3573,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.90", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 275291dee..c7fc6adf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ members = [ "test_fixtures", "tui", "tuify", + "log", + "script", ] # Make sure to keep these in sync with `run` nushell script `workspace_folders`. resolver = "2" diff --git a/cmdr/Cargo.toml b/cmdr/Cargo.toml index 6f1f949b8..d3ee1ef32 100644 --- a/cmdr/Cargo.toml +++ b/cmdr/Cargo.toml @@ -43,12 +43,13 @@ path = "src/lib.rs" [dependencies] # R3BL crates (from this mono repo). -r3bl_ansi_color = { path = "../ansi_color", version = "0.7.0" } # version is requried to publish to crates.io -r3bl_core = { path = "../core", version = "0.10.0" } # version is requried to publish to crates.io -r3bl_macro = { path = "../macro", version = "0.10.0" } # version is requried to publish to crates.io -r3bl_tui = { path = "../tui", version = "0.6.0" } # version is requried to publish to crates.io -r3bl_tuify = { path = "../tuify", version = "0.2.0" } # version is requried to publish to crates.io -r3bl_analytics_schema = { path = "../analytics_schema", version = "0.0.2" } # version is requried to publish to crates.io +r3bl_ansi_color = { path = "../ansi_color", version = "0.7.0" } # version is required to publish to crates.io +r3bl_core = { path = "../core", version = "0.10.0" } # version is required to publish to crates.io +r3bl_macro = { path = "../macro", version = "0.10.0" } # version is required to publish to crates.io +r3bl_tui = { path = "../tui", version = "0.6.0" } # version is required to publish to crates.io +r3bl_tuify = { path = "../tuify", version = "0.2.0" } # version is required to publish to crates.io +r3bl_analytics_schema = { path = "../analytics_schema", version = "0.0.2" } # version is required to publish to crates.io +r3bl_log = { path = "../log", version = "0.1.0" } # version is required to publish to crates.io # Reqwest (HTTP client). reqwest = { version = "0.12.8", features = ["json"] } diff --git a/cmdr/src/bin/edi.rs b/cmdr/src/bin/edi.rs index 386ee4645..f911ac224 100644 --- a/cmdr/src/bin/edi.rs +++ b/cmdr/src/bin/edi.rs @@ -22,12 +22,12 @@ use r3bl_ansi_color::{AnsiStyledText, Style}; use r3bl_cmdr::{edi::launcher, report_analytics, upgrade_check, AnalyticsAction}; use r3bl_core::{call_if_true, throws, - try_initialize_global_logging, ColorWheel, CommonResult, GradientGenerationPolicy, TextColorizationPolicy, UnicodeString}; +use r3bl_log::try_initialize_logging_global; use r3bl_tuify::{select_from_list, SelectionMode, StyleSheet, LIZARD_GREEN, SLATE_GRAY}; use crate::clap_config::CLIArg; @@ -42,7 +42,7 @@ async fn main() -> CommonResult<()> { // Start logging. let enable_logging = cli_arg.global_options.enable_logging; call_if_true!(enable_logging, { - try_initialize_global_logging(tracing_core::LevelFilter::DEBUG).ok(); + try_initialize_logging_global(tracing_core::LevelFilter::DEBUG).ok(); tracing::debug!("Start logging... cli_args {:?}", cli_arg); }); diff --git a/cmdr/src/bin/giti.rs b/cmdr/src/bin/giti.rs index 043423343..6ba1423b1 100644 --- a/cmdr/src/bin/giti.rs +++ b/cmdr/src/bin/giti.rs @@ -34,7 +34,8 @@ use r3bl_cmdr::{color_constants::DefaultColors::{FrozenBlue, GuardsRed, Moonligh report_analytics, upgrade_check, AnalyticsAction}; -use r3bl_core::{call_if_true, throws, try_initialize_global_logging, CommonResult}; +use r3bl_core::{call_if_true, throws, CommonResult}; +use r3bl_log::try_initialize_logging_global; use r3bl_tuify::{select_from_list_with_multi_line_header, SelectionMode, StyleSheet}; #[tokio::main] @@ -47,7 +48,7 @@ async fn main() -> CommonResult<()> { let enable_logging = cli_arg.global_options.enable_logging; call_if_true!(enable_logging, { - try_initialize_global_logging(tracing_core::LevelFilter::DEBUG).ok(); + try_initialize_logging_global(tracing_core::LevelFilter::DEBUG).ok(); tracing::debug!("Start logging... cli_args {:?}", cli_arg); }); diff --git a/core/Cargo.toml b/core/Cargo.toml index f53e794f4..a762a706d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -96,12 +96,3 @@ paste = "1.0.15" # for assert_eq! macro pretty_assertions = "1.4.1" serial_test = "3.1.1" - -# Testing - temp files and folders. -tempfile = "3.13.0" - -# Bin targets for testing stdout and stderr. -assert_cmd = "2.0.16" -[[bin]] -name = "tracing_test_bin" -path = "src/bin/tracing_test_bin.rs" diff --git a/core/src/common/mod.rs b/core/src/common/mod.rs index cd73afe8e..0f4677724 100644 --- a/core/src/common/mod.rs +++ b/core/src/common/mod.rs @@ -20,9 +20,13 @@ pub mod common_enums; pub mod common_math; pub mod common_result_and_error; pub mod miette_setup_global_report_handler; +pub mod ordered_map; +pub mod text_default_styles; // Re-export. pub use common_enums::*; pub use common_math::*; pub use common_result_and_error::*; pub use miette_setup_global_report_handler::*; +pub use ordered_map::*; +pub use text_default_styles::*; diff --git a/core/src/common/ordered_map.rs b/core/src/common/ordered_map.rs new file mode 100644 index 000000000..1c1b6cd9b --- /dev/null +++ b/core/src/common/ordered_map.rs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::HashMap; + +#[derive(Debug, Default)] +pub struct OrderedMap { + keys: Vec, + map: HashMap, +} + +impl OrderedMap { + pub fn new() -> Self { + OrderedMap { + keys: Vec::new(), + map: HashMap::new(), + } + } + + pub fn insert(&mut self, key: K, value: V) { + if !self.map.contains_key(&key) { + self.keys.push(key.clone()); + } + self.map.insert(key, value); + } + + pub fn get(&self, key: &K) -> Option<&V> { self.map.get(key) } + + pub fn iter(&self) -> impl Iterator { + self.keys + .iter() + .filter_map(move |key| self.map.get(key).map(|value| (key, value))) + } +} + +#[cfg(test)] +mod tests_ordered_map { + use super::*; + + #[test] + fn test_ordered_map_insert() { + let mut map = OrderedMap::new(); + map.insert("key2", "value2"); + map.insert("key1", "value1"); + map.insert("key3", "value3"); + + let mut iter = map.iter(); + assert_eq!(iter.next(), Some((&"key2", &"value2"))); + assert_eq!(iter.next(), Some((&"key1", &"value1"))); + assert_eq!(iter.next(), Some((&"key3", &"value3"))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_ordered_map_delete() { + let mut map = OrderedMap::new(); + map.insert("key1", "value1"); + map.insert("key2", "value2"); + map.insert("key3", "value3"); + + // Delete a key and check if it is removed. + map.map.remove("key2"); + let mut iter = map.iter(); + assert_eq!(iter.next(), Some((&"key1", &"value1"))); + assert_eq!(iter.next(), Some((&"key3", &"value3"))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_ordered_map_update() { + let mut map = OrderedMap::new(); + map.insert("key1", "value1"); + map.insert("key2", "value2"); + map.insert("key3", "value3"); + + // Update a value and check if it is updated. + map.insert("key2", "new_value2"); + let mut iter = map.iter(); + assert_eq!(iter.next(), Some((&"key1", &"value1"))); + assert_eq!(iter.next(), Some((&"key2", &"new_value2"))); + assert_eq!(iter.next(), Some((&"key3", &"value3"))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_ordered_map_get() { + let mut map = OrderedMap::new(); + map.insert("key1", "value1"); + map.insert("key2", "value2"); + map.insert("key3", "value3"); + + assert_eq!(map.get(&"key1"), Some(&"value1")); + assert_eq!(map.get(&"key2"), Some(&"value2")); + assert_eq!(map.get(&"key3"), Some(&"value3")); + assert_eq!(map.get(&"key4"), None); + } + + #[test] + fn test_ordered_map_iter() { + let mut map = OrderedMap::new(); + map.insert("key1", "value1"); + map.insert("key2", "value2"); + map.insert("key3", "value3"); + + let mut iter = map.iter(); + assert_eq!(iter.next(), Some((&"key1", &"value1"))); + assert_eq!(iter.next(), Some((&"key2", &"value2"))); + assert_eq!(iter.next(), Some((&"key3", &"value3"))); + assert_eq!(iter.next(), None); + } +} diff --git a/core/src/logging/color_text_default_styles.rs b/core/src/common/text_default_styles.rs similarity index 100% rename from core/src/logging/color_text_default_styles.rs rename to core/src/common/text_default_styles.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index 4676a211f..f16ac0824 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -124,21 +124,17 @@ // Connect to source file. pub mod common; pub mod decl_macros; -pub mod logging; pub mod misc; pub mod storage; pub mod term; pub mod terminal_io; -pub mod tracing_logging; pub mod tui_core; // Re-export. pub use common::*; pub use decl_macros::*; -pub use logging::*; pub use misc::*; pub use storage::*; pub use term::*; pub use terminal_io::*; -pub use tracing_logging::*; pub use tui_core::*; diff --git a/core/src/logging/logging_api.rs b/core/src/logging/logging_api.rs deleted file mode 100644 index cdd962dbb..000000000 --- a/core/src/logging/logging_api.rs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2022 R3BL LLC - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -//! This is just a shim (thin wrapper) around the [crate::tracing_logging] module. -//! -//! You can use the functions in this module or just use the [mod@crate::init_tracing] -//! functions directly, along with using [tracing::info!], [tracing::debug!], etc. macros. -//! -//! This file is here as a convenience for backward compatibility w/ the old logging -//! system. - -use crate::{ok, TracingConfig, WriterConfig}; - -const LOG_FILE_NAME: &str = "log.txt"; - -/// Logging is **DISABLED** by **default**. -/// -/// If you don't call this function w/ a value other than -/// [tracing_core::LevelFilter::OFF], then logging won't be enabled. It won't matter if -/// you call any of the other logging functions in this module, or directly use the -/// [tracing::info!], [tracing::debug!], etc. macros. -/// -/// This is a convenience method to setup Tokio [`tracing_subscriber`] with `stdout` as -/// the output destination. This method also ensures that the [`crate::SharedWriter`] is -/// used for concurrent writes to `stdout`. You can also use the [`TracingConfig`] struct -/// to customize the behavior of the tracing setup, by choosing whether to display output -/// to `stdout`, `stderr`, or a [`crate::SharedWriter`]. By default, both display and file -/// logging are enabled. You can also customize the log level, and the file path and -/// prefix for the log file. -pub fn try_initialize_global_logging( - level_filter: tracing_core::LevelFilter, -) -> miette::Result<()> { - // Early return if the level filter is off. - if matches!(level_filter, tracing_core::LevelFilter::OFF) { - return ok!(); - } - - // Try to initialize the tracing system w/ (rolling) file log output. - TracingConfig { - level_filter, - writer_config: WriterConfig::File(LOG_FILE_NAME.to_string()), - } - .install_global()?; - - ok!() -} diff --git a/core/src/logging/simple_file_logging_impl.rs b/core/src/logging/simple_file_logging_impl.rs deleted file mode 100644 index 11dd95e3f..000000000 --- a/core/src/logging/simple_file_logging_impl.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2024 R3BL LLC - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -use std::{fs::OpenOptions, io::Write, path::Path}; - -/// This is a simple function that logs a message to a file. This is meant to be used when -/// there are no other logging facilities available. -/// -/// # Arguments -/// * `file_path` - The path to the file to log to. If `None`, the default path is `debug.log`. -/// * `message` - The message to log. -pub fn file_log(file_path: Option<&Path>, message: &str) { - let file_path = file_path.unwrap_or(Path::new("debug.log")); - let message = if message.ends_with('\n') { - message.to_string() - } else { - format!("{}\n", message) - }; - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(file_path) - .unwrap(); - file.write_all(message.as_bytes()).unwrap(); -} diff --git a/core/src/misc/mod.rs b/core/src/misc/mod.rs index beb4899f8..3546c4f2d 100644 --- a/core/src/misc/mod.rs +++ b/core/src/misc/mod.rs @@ -18,7 +18,11 @@ // Attach sources. pub mod calc_str_len; pub mod friendly_random_id; +pub mod string_helpers; +pub mod temp_dir; // Re-export. pub use calc_str_len::*; pub use friendly_random_id::*; +pub use string_helpers::*; +pub use temp_dir::*; diff --git a/core/src/misc/string_helpers.rs b/core/src/misc/string_helpers.rs new file mode 100644 index 000000000..57c8dbb62 --- /dev/null +++ b/core/src/misc/string_helpers.rs @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::{ch, ChUnit, UnicodeString}; + +/// Tests whether the given text contains an ANSI escape sequence. +pub fn contains_ansi_escape_sequence(text: &str) -> bool { + text.chars().any(|it| it == '\x1b') +} + +#[test] +fn test_contains_ansi_escape_sequence() { + use r3bl_ansi_color::{AnsiStyledText, Color, Style}; + + use crate::assert_eq2; + + assert_eq2!( + contains_ansi_escape_sequence( + "\x1b[31mThis is red text.\x1b[0m And this is normal text." + ), + true + ); + + assert_eq2!(contains_ansi_escape_sequence("This is normal text."), false); + + assert_eq2!( + contains_ansi_escape_sequence( + &AnsiStyledText { + text: "Print a formatted (bold, italic, underline) string w/ ANSI color codes.", + style: &[ + Style::Bold, + Style::Italic, + Style::Underline, + Style::Foreground(Color::Rgb(50, 50, 50)), + Style::Background(Color::Rgb(100, 200, 1)), + ], + } + .to_string() + ), + true + ); +} + +/// Replace escaped quotes with unescaped quotes. The escaped quotes are generated +/// when [std::fmt::Debug] is used to format the output using [format!], eg: +/// ``` +/// use r3bl_core::remove_escaped_quotes; +/// +/// let s = format!("{:?}", "Hello\", world!"); +/// assert_eq!(s, "\"Hello\\\", world!\""); +/// let s = remove_escaped_quotes(&s); +/// assert_eq!(s, "Hello, world!"); +/// ``` +pub fn remove_escaped_quotes(s: &str) -> String { + s.replace("\\\"", "\"").replace("\"", "") +} + +pub mod string_helpers_constants { + pub const SUFFIX: &str = "..."; + pub const SPACER: &str = " "; +} + +/// Take into account the fact that there maybe emoji in the string. +pub fn truncate_from_right( + string: &str, + display_width: impl Into, + pad: bool, +) -> String { + use string_helpers_constants::{SPACER, SUFFIX}; + + let display_width = display_width.into(); + let string = UnicodeString::from(string); + let string_display_width = string.display_width; + + // Handle truncation. + if string_display_width > display_width { + let suffix = UnicodeString::from(SUFFIX); + let suffix_display_width = suffix.display_width; + + let trunc_string = + string.truncate_end_by_n_col(display_width - suffix_display_width - 1); + // let trunc_string = string.truncate_to_fit_size(crate::size!( + // col_count: display_width - suffix_display_width, + // row_count: 1 + // )); + + format!("{}{}", trunc_string, suffix.string) + } + // Handle padding. + else if pad { + let mut padded_string = string.to_string(); + let display_width_to_pad = ch!(@to_usize display_width - string_display_width); + let display_width_to_pad = display_width_to_pad as f64; + let spacer_display_width = + ch!(@to_usize UnicodeString::from(SPACER).display_width); + let spacer_display_width = spacer_display_width as f64; + let repeat_count = (display_width_to_pad / spacer_display_width).ceil() as usize; + padded_string.push_str(&SPACER.repeat(repeat_count)); + padded_string + } + // No post processing needed. + else { + string.to_string() + } +} + +pub fn truncate_from_left(text: &str, display_width: usize, pad: bool) -> String { + let display_width = ch!(display_width); + let text = UnicodeString::from(text); + let text_width = text.display_width; + + if text_width > display_width { + let suffix = UnicodeString::from(string_helpers_constants::SUFFIX); + let suffix_width = suffix.display_width; + + let truncated_text = + text.truncate_start_by_n_col(display_width - suffix_width - 1); + + format!("{}{}", suffix.string, truncated_text) + } else if pad { + let mut padded_text = text.to_string(); + let width_to_pad = ch!(@to_usize display_width - text_width); + let spacer_width = ch!(@to_usize UnicodeString::from(string_helpers_constants::SPACER).display_width); + let repeat_count = (width_to_pad as f64 / spacer_width as f64).ceil() as usize; + padded_text.insert_str(0, &string_helpers_constants::SPACER.repeat(repeat_count)); + padded_text + } else { + text.to_string() + } +} + +#[cfg(test)] +mod tests_truncate_or_pad { + use super::*; + + #[test] + fn test_truncate_or_pad_from_right() { + let long_string = "Hello, world!"; + let short_string = "Hi!"; + let width = 10; + + assert_eq!(truncate_from_right(long_string, width, true), "Hello, ..."); + assert_eq!(truncate_from_right(short_string, width, true), "Hi! "); + + assert_eq!(truncate_from_right(long_string, width, false), "Hello, ..."); + assert_eq!(truncate_from_right(short_string, width, false), "Hi!"); + } + + #[test] + fn test_truncate_or_pad_from_left() { + let long_string = "Hello, world!"; + let short_string = "Hi!"; + let width = 10; + + assert_eq!(truncate_from_left(long_string, width, true), "... world!"); + assert_eq!(truncate_from_left(short_string, width, true), " Hi!"); + + assert_eq!(truncate_from_left(long_string, width, false), "... world!"); + assert_eq!(truncate_from_left(short_string, width, false), "Hi!"); + } +} diff --git a/test_fixtures/src/temp_dir.rs b/core/src/misc/temp_dir.rs similarity index 79% rename from test_fixtures/src/temp_dir.rs rename to core/src/misc/temp_dir.rs index fd0418690..5c62eb1ab 100644 --- a/test_fixtures/src/temp_dir.rs +++ b/core/src/misc/temp_dir.rs @@ -20,12 +20,20 @@ use std::{fmt::{Display, Formatter}, path::Path}; use miette::IntoDiagnostic; -use r3bl_core::friendly_random_id; + +use crate::friendly_random_id; pub struct TempDir { inner: std::path::PathBuf, } +impl TempDir { + /// Join a path to the temporary directory. + pub fn join>(&self, path: P) -> std::path::PathBuf { + self.inner.join(path) + } +} + /// Create a temporary directory. The directory is automatically deleted when the /// [TempDir] struct is dropped. pub fn create_temp_dir() -> miette::Result { @@ -51,7 +59,7 @@ impl Drop for TempDir { /// # Example /// /// ```no_run -/// use r3bl_test_fixtures::create_temp_dir; +/// use r3bl_core::create_temp_dir; /// let root = create_temp_dir().unwrap(); /// let new_dir = root.join("test_set_file_executable"); /// ``` @@ -68,7 +76,7 @@ impl Deref for TempDir { /// # Example /// /// ```no_run -/// use r3bl_test_fixtures::create_temp_dir; +/// use r3bl_core::create_temp_dir; /// let root = create_temp_dir().unwrap(); /// println!("Temp dir: {}", root); /// ``` @@ -88,7 +96,7 @@ impl Display for TempDir { /// # Example /// /// ```no_run -/// use r3bl_test_fixtures::create_temp_dir; +/// use r3bl_core::create_temp_dir; /// let root = create_temp_dir().unwrap(); /// std::fs::create_dir_all(root.join("test_set_file_executable")).unwrap(); /// std::fs::remove_dir_all(root).unwrap(); @@ -98,7 +106,7 @@ impl AsRef for TempDir { } #[cfg(test)] -mod tests { +mod tests_temp_dir { use crossterm::style::Stylize as _; use super::*; @@ -114,6 +122,22 @@ mod tests { assert!(temp_dir.inner.exists()); } + #[test] + fn test_temp_dir_join() { + let temp_dir = create_temp_dir().unwrap(); + let expected_prefix = temp_dir.inner.display().to_string(); + + let new_sub_dir = temp_dir.join("test_set_file_executable"); + let expected_postfix = new_sub_dir.display().to_string(); + + let expected_full_path = new_sub_dir.display().to_string(); + + assert!(temp_dir.exists()); + assert!(!new_sub_dir.exists()); + assert!(expected_full_path.starts_with(&expected_prefix)); + assert!(expected_full_path.ends_with(&expected_postfix)); + } + #[test] fn test_temp_dir_drop() { let temp_dir = create_temp_dir().unwrap(); diff --git a/core/src/storage/kv.rs b/core/src/storage/kv.rs index 3aa8206bc..e5e46ed08 100644 --- a/core/src/storage/kv.rs +++ b/core/src/storage/kv.rs @@ -353,19 +353,16 @@ use kv_error::*; #[cfg(test)] mod kv_tests { - use std::{collections::HashMap, - path::{Path, PathBuf}}; + use std::{collections::HashMap, path::Path}; use serial_test::serial; - use tempfile::tempdir; use tracing::{instrument, Level}; use super::*; + use crate::create_temp_dir; fn check_folder_exists(path: &Path) -> bool { path.exists() && path.is_dir() } - fn join_path_with_str(path: &Path, str: &str) -> PathBuf { path.join(str) } - fn setup_tracing() { let _ = tracing_subscriber::fmt() .with_max_level(Level::INFO) @@ -378,21 +375,13 @@ mod kv_tests { .try_init(); } - fn get_path(dir: &tempfile::TempDir, folder_name: &str) -> PathBuf { - join_path_with_str(dir.path(), folder_name) - } - - fn create_temp_folder() -> tempfile::TempDir { - tempdir().expect("Failed to create temp dir") - } - #[instrument] fn perform_db_operations() -> miette::Result<()> { let bucket_name = "bucket".to_string(); - // Setup temp folder. - let dir = create_temp_folder(); - let path_buf = get_path(&dir, "db_folder"); + // Setup temp dir (this will be dropped when `dir` is out of scope). + let root_temp_dir = create_temp_dir()?; + let path_buf = root_temp_dir.join("db_folder"); setup_tracing(); @@ -470,9 +459,9 @@ mod kv_tests { fn perform_db_operations_error_conditions() -> miette::Result<()> { let bucket_name = "bucket".to_string(); - // Setup temp folder. - let dir = create_temp_folder(); - let path_buf = get_path(&dir, "db_folder"); + // Setup temp dir (this will be dropped when `dir` is out of scope). + let root_temp_dir = create_temp_dir()?; + let path_buf = root_temp_dir.join("db_folder"); setup_tracing(); diff --git a/core/src/tui_core/graphemes/access.rs b/core/src/tui_core/graphemes/access.rs index 41359cceb..abb572074 100644 --- a/core/src/tui_core/graphemes/access.rs +++ b/core/src/tui_core/graphemes/access.rs @@ -52,11 +52,22 @@ impl UnicodeString { display_width } + /// The `size` is a column index and row index. Not width or height. + /// - To convert width -> size / column index subtract 1. + /// - To convert size / column index to width add 1. + /// + /// Note the [Self::truncate_end_by_n_col] and [Self::truncate_start_by_n_col] + /// functions take a width. pub fn truncate_to_fit_size(&self, size: Size) -> &str { let display_cols: ChUnit = size.col_count; self.truncate_end_to_fit_width(display_cols) } + /// The `n_display_col` is a width, not a [Size]. + /// - To convert width -> size / column index subtract 1. + /// - To convert size / column index to width add 1. + /// + /// Note the [Self::truncate_to_fit_size] function takes a size / column index. pub fn truncate_end_by_n_col(&self, n_display_col: ChUnit) -> &str { let mut countdown_col_count = n_display_col; let mut string_end_byte_index = 0; diff --git a/core/src/tui_core/graphemes/convert.rs b/core/src/tui_core/graphemes/convert.rs index 490fa7042..dd9769c7c 100644 --- a/core/src/tui_core/graphemes/convert.rs +++ b/core/src/tui_core/graphemes/convert.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; use crate::UnicodeString; -// Convert to UnicodeString +/// Convert to UnicodeString. impl From<&str> for UnicodeString { fn from(s: &str) -> Self { UnicodeString::new(s) } } @@ -40,12 +40,12 @@ impl From<&String> for UnicodeString { fn from(s: &String) -> Self { UnicodeString::new(s) } } -// Convert to String +/// Convert to String. impl From for String { fn from(s: UnicodeString) -> Self { s.string } } -// UnicodeStringExt +/// UnicodeStringExt trait. pub trait UnicodeStringExt { fn unicode_string(&self) -> UnicodeString; } @@ -61,3 +61,10 @@ impl UnicodeStringExt for &str { impl UnicodeStringExt for String { fn unicode_string(&self) -> UnicodeString { UnicodeString::from(self) } } + +/// Implement Display trait. +impl std::fmt::Display for UnicodeString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.string) + } +} \ No newline at end of file diff --git a/log/Cargo.toml b/log/Cargo.toml new file mode 100644 index 000000000..2795321be --- /dev/null +++ b/log/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "r3bl_log" +version = "0.1.0" +edition = "2024" +resolver = "2" +description = "Tokio tracing plugins for formatted log output for R3BL TUI crates" +# At most 5 keywords w/ no spaces, each has max length of 20 char. +keywords = ["log", "tracing", "ANSI", "terminal", "formatted"] +categories = ["command-line-interface", "command-line-utilities"] +readme = "README.md" # This is not included in cargo docs. +# Email address(es) has to be verified at https://crates.io/me/ +authors = [ + "Nazmul Idris ", + "Nadia Idris ", +] +repository = "https://github.com/r3bl-org/r3bl-open-core/tree/main/log" +documentation = "https://docs.rs/r3bl_log" +homepage = "https://r3bl.com" +license = "Apache-2.0" + +[dependencies] +# Tokio / Tracing / Logging. +# https://tokio.rs/tokio/topics/tracing +# https://tokio.rs/tokio/topics/tracing-next-steps +tokio = { version = "1.40.0", features = ["full", "tracing"] } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +tracing-appender = "0.2.3" +tracing-core = "0.1.32" + +# CustomEventFormatter. +chrono = "0.4.39" +textwrap = { version = "0.16.1", features = ["unicode-linebreak"] } + +# Error handling. +thiserror = "1.0.64" +miette = { version = "7.2.0", features = ["fancy"] } +pretty_assertions = "1.4.1" + +# r3bl-open-core. +r3bl_ansi_color = { path = "../ansi_color", version = "0.7.0" } # Convert between ansi and rgb. +r3bl_core = { path = "../core", version = "0.10.0" } # Core functionality. +r3bl_macro = { path = "../macro", version = "0.10.0" } # Macros for r3bl-open-core. +r3bl_test_fixtures = { path = "../test_fixtures", version = "0.1.0" } # Test fixtures. + +# Terminal color output. +crossterm = "0.28.1" + +[dev-dependencies] + +# for assert_eq! macro +pretty_assertions = "1.4.1" +serial_test = "3.1.1" + +# Bin targets for testing stdout and stderr. +assert_cmd = "2.0.16" + +[[bin]] +name = "tracing_test_bin" +path = "src/bin/tracing_test_bin.rs" diff --git a/core/src/bin/tracing_test_bin.rs b/log/src/bin/tracing_test_bin.rs similarity index 69% rename from core/src/bin/tracing_test_bin.rs rename to log/src/bin/tracing_test_bin.rs index 12a0233a6..907d68360 100644 --- a/core/src/bin/tracing_test_bin.rs +++ b/log/src/bin/tracing_test_bin.rs @@ -15,16 +15,26 @@ * limitations under the License. */ -use r3bl_core::{DisplayPreference, TracingConfig, WriterConfig}; +use r3bl_log::{DisplayPreference, TracingConfig, WriterConfig}; use tracing_core::LevelFilter; -/// `assert_cmd` : +/// This test works with the binary under test, which is `tracing_stdout_test_bin`. That +/// binary takes 1 string argument: "stdout" or "stderr". It uses the `assert_cmd` crate +/// to verify that the [DisplayPreference::Stdout] and [DisplayPreference::Stderr] work as +/// expected. There is no easy way to actually test `stdout` and `stderr` without spawning +/// a new process, so this is the best way to test it. +/// +/// +/// This is the binary under test, which is tested by the `test_tracing_bin_stdio` test +/// module. /// -/// This is the binary under test, which is tested by the `test_tracing_stdout` test. /// It takes 1 argument: "stdout" or "stderr". Depending on the argument, it will /// display the logs to stdout or stderr. /// -/// See: `init_tracing.rs` and `test_tracing_bin_stdio()` test. +/// See: +/// 1. Test module: `test_tracing_bin_stdio` +/// 2. Binary under test: `tracing_test_bin.rs` <- you are here. +/// 3. `assert_cmd` : fn main() { // Get the argument passed to the binary. let arg = std::env::args().nth(1).unwrap_or_default(); diff --git a/core/src/logging/mod.rs b/log/src/lib.rs similarity index 72% rename from core/src/logging/mod.rs rename to log/src/lib.rs index 1ce0c7f0b..4afa5851f 100644 --- a/core/src/logging/mod.rs +++ b/log/src/lib.rs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 R3BL LLC + * Copyright (c) 2024 R3BL LLC * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,12 +15,8 @@ * limitations under the License. */ -// Attach. -pub mod color_text_default_styles; -pub mod logging_api; -pub mod simple_file_logging_impl; +// Attach sources. +pub mod log_support; // Re-export. -pub use color_text_default_styles::*; -pub use logging_api::*; -pub use simple_file_logging_impl::*; +pub use log_support::*; diff --git a/log/src/log_support/custom_event_formatter.rs b/log/src/log_support/custom_event_formatter.rs new file mode 100644 index 000000000..e597d72fc --- /dev/null +++ b/log/src/log_support/custom_event_formatter.rs @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::fmt; + +use chrono::Local; +use crossterm::style::Stylize; +use custom_event_formatter_constants::*; +use r3bl_ansi_color::{AnsiStyledText, Color, Style}; +use r3bl_core::{ColorWheel, + OrderedMap, + UnicodeString, + ch, + get_terminal_width, + remove_escaped_quotes, + string_helpers_constants, + truncate_from_right}; +use r3bl_macro::tui_style; +use textwrap::{Options, WordSeparator, wrap}; +use tracing::{Event, + Subscriber, + field::{Field, Visit}}; +use tracing_subscriber::{fmt::{FormatEvent, FormatFields}, + registry::LookupSpan}; + +pub struct CustomEventFormatter; + +// Colors: +pub mod custom_event_formatter_constants { + use super::*; + + pub const FIRST_LINE_PREFIX: &str = " ðœą "; + pub const SUBSEQUENT_LINE_PREFIX: &str = " "; + pub const LEVEL_SUFFIX: &str = ":"; + + pub const ERROR_SIGIL: &str = "E"; + pub const WARN_SIGIL: &str = "W"; + pub const INFO_SIGIL: &str = "I"; + pub const DEBUG_SIGIL: &str = "D"; + pub const TRACE_SIGIL: &str = "T"; + + pub const ENTRY_SEPARATOR_CHAR: &str = "â€ū"; + + pub const BODY_FG_COLOR: Color = Color::Rgb(175, 175, 175); + pub const BODY_FG_COLOR_BRIGHT: Color = Color::Rgb(200, 200, 200); + pub const HEADING_BG_COLOR: Color = Color::Rgb(70, 70, 90); + + pub const INFO_FG_COLOR: Color = Color::Rgb(100, 100, 200); + pub const ERROR_FG_COLOR: Color = Color::Rgb(255, 182, 193); + pub const WARN_FG_COLOR: Color = Color::Rgb(255, 140, 0); + pub const DEBUG_FG_COLOR: Color = Color::Rgb(255, 255, 0); + pub const TRACE_FG_COLOR: Color = Color::Rgb(186, 85, 211); +} + +impl FormatEvent for CustomEventFormatter +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + /// Format the event into 2 lines: + /// 1. Timestamp, span context, level, and message truncated to the available visible + /// width. + /// 2. Body that is text wrapped to the visible width. + /// + /// This function takes into account text that can contain emoji. + fn format_event( + &self, + ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, + mut writer: tracing_subscriber::fmt::format::Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + // Get spacer. + let spacer = string_helpers_constants::SPACER; + let spacer_display_width = UnicodeString::from(spacer).display_width; + + // Length accumulator (for line width calculations). + let mut line_width_used = ch!(0); + + // Custom timestamp. + let timestamp = Local::now(); + let timestamp_str = + format!("{ts}{sp}", ts = timestamp.format("%I:%M%P"), sp = spacer); + line_width_used += UnicodeString::from(×tamp_str).display_width; + let timestamp_str_fmt = AnsiStyledText { + text: ×tamp_str, + style: &[ + Style::Foreground(BODY_FG_COLOR_BRIGHT), + Style::Background(HEADING_BG_COLOR), + ], + }; + write!(writer, "\n{timestamp_str_fmt}")?; + + // Custom span context. + if let Some(scope) = ctx.lookup_current() { + let scope_str = format!("[{}] ", scope.name()); + line_width_used += UnicodeString::from(&scope_str).display_width; + let scope_str_fmt = AnsiStyledText { + text: &scope_str, + style: &[ + Style::Foreground(BODY_FG_COLOR_BRIGHT), + Style::Background(HEADING_BG_COLOR), + Style::Italic, + ], + }; + write!(writer, "{scope_str_fmt}")?; + } + + // Custom metadata formatting. For eg: + // + // metadata: Metadata { + // name: "event src/bin/gen-certs.rs:110", + // target: "gen_certs", + // level: Level( + // Debug, + // ), + // module_path: "gen_certs", + // location: src/bin/gen-certs.rs:110, + // fields: {msg, body}, + // callsite: Identifier(0x5a46fb928d40), + // kind: Kind(EVENT), + // } + let mut style_acc: Vec