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..6f2bf66dd
--- /dev/null
+++ b/core/src/misc/string_helpers.rs
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+
+// 00: [x] test if a string has ansi escape sequences
+
+use crate::{ch, 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: usize, pad: bool) -> String {
+ use string_helpers_constants::{SPACER, SUFFIX};
+
+ let display_width = ch!(display_width);
+ let string = UnicodeString::from(string);
+ let string_display_width = string.display_width;
+
+ if string_display_width > display_width
+ // Handle truncation.
+ {
+ 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..de56c9826
--- /dev/null
+++ b/log/src/log_support/custom_event_formatter.rs
@@ -0,0 +1,254 @@
+/*
+ * 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::{DEBUG_SIGIL,
+ ERROR_SIGIL,
+ FIRST_LINE_PREFIX,
+ INFO_SIGIL,
+ LEVEL_SUFFIX,
+ SUBSEQUENT_LINE_PREFIX,
+ TRACE_SIGIL,
+ WARN_SIGIL};
+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;
+
+pub mod custom_event_formatter_constants {
+ 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";
+}
+
+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 =
+ ch!(@to_usize UnicodeString::from(spacer).display_width);
+
+ // Length accumulator (for line width calculations).
+ let mut line_width_used = 0;
+
+ // Custom timestamp.
+ let timestamp = Local::now();
+ let timestamp_str =
+ format!("{ts}{sp}", ts = timestamp.format("%I:%M%P"), sp = spacer);
+ line_width_used += timestamp_str.len();
+ let timestamp_str_fmt = timestamp_str.grey().on_dark_grey().underlined();
+ 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 += scope_str.len();
+ let scope_str_fmt = scope_str.grey().on_dark_grey().italic().underlined();
+ 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 (level_str, level_str_fmt) = match *event.metadata().level() {
+ tracing::Level::ERROR => {
+ let text = format!("{ERROR_SIGIL}{LEVEL_SUFFIX}{spacer}");
+ let text_fmt = text.clone().red();
+ (text, text_fmt)
+ }
+ tracing::Level::WARN => {
+ let text = format!("{WARN_SIGIL}{LEVEL_SUFFIX}{spacer}");
+ let text_fmt = text.clone().magenta();
+ (text, text_fmt)
+ }
+ tracing::Level::INFO => {
+ let text = format!("{INFO_SIGIL}{LEVEL_SUFFIX}{spacer}");
+ let text_fmt = text.clone().blue();
+ (text, text_fmt)
+ }
+ tracing::Level::DEBUG => {
+ let text = format!("{DEBUG_SIGIL}{LEVEL_SUFFIX}{spacer}");
+ let text_fmt = text.clone().yellow();
+ (text, text_fmt)
+ }
+ tracing::Level::TRACE => {
+ let text = format!("{TRACE_SIGIL}{LEVEL_SUFFIX}{spacer}");
+ let text_fmt = text.clone().grey();
+ (text, text_fmt)
+ }
+ };
+ let level_str_fmt = level_str_fmt.on_dark_grey().bold().underlined();
+ let level_str_display_width =
+ ch!(@to_usize UnicodeString::from(&level_str).display_width);
+ line_width_used += spacer_display_width;
+ line_width_used += level_str_display_width;
+ write!(writer, "{level_str_fmt}")?;
+
+ // Custom field formatting. For eg:
+ //
+ // fields: ValueSet {
+ // msg: "pwd at end",
+ // body: "Ok(\"/home/nazmul/github/rust-scratch/tls\")",
+ // callsite: Identifier(0x5a46fb928d40),
+ // }
+ //
+ // Instead of:
+ // ctx.field_format().format_fields(writer.by_ref(), event)?;
+ let mut ordered_map = OrderedMap::::default();
+ event.record(&mut VisitEventAndPopulateOrderedMapWithFields {
+ inner: &mut ordered_map,
+ });
+
+ let max_display_width = get_terminal_width();
+
+ let text_wrap_options = Options::new(max_display_width)
+ .initial_indent(FIRST_LINE_PREFIX)
+ .subsequent_indent(SUBSEQUENT_LINE_PREFIX)
+ .word_separator(WordSeparator::UnicodeBreakProperties);
+
+ for (msg, body) in ordered_map.iter() {
+ // Prepare the msg and body.
+ let msg = remove_escaped_quotes(msg);
+ let body = remove_escaped_quotes(body);
+
+ // Write msg line.
+ line_width_used += 1;
+ let line_1_width = max_display_width - line_width_used;
+ let msg = format!(" {}\n", truncate_from_right(&msg, line_1_width, false));
+ let msg_fmt = ColorWheel::lolcat_into_string(
+ &msg,
+ Some(tui_style!(
+ attrib: [bold, italic, underline]
+ )),
+ );
+ write!(writer, "{msg_fmt}")?;
+
+ // Write body line(s).
+ let body = wrap(&body, &text_wrap_options);
+ for body_line in body.iter() {
+ let body_line = truncate_from_right(body_line, max_display_width, true);
+ let body_line_fmt = body_line.to_string().dark_grey();
+ writeln!(writer, "{body_line_fmt}")?;
+ }
+ }
+
+ // Write the terminating line separator.
+ let line_separator = "‾".repeat(max_display_width);
+ let line_separator_fmt = line_separator.dark_green();
+ writeln!(writer, "{line_separator_fmt}")
+ }
+}
+
+pub struct VisitEventAndPopulateOrderedMapWithFields<'a> {
+ inner: &'a mut OrderedMap,
+}
+
+impl Visit for VisitEventAndPopulateOrderedMapWithFields<'_> {
+ fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
+ let field_name = field.name();
+ let field_value = format!("{:?}", value);
+ self.inner.insert(field_name.to_string(), field_value);
+ }
+}
+
+#[cfg(test)]
+mod tests_tracing_custom_event_formatter {
+ use std::sync::Mutex;
+
+ use chrono::Local;
+ use r3bl_test_fixtures::StdoutMock;
+ use tracing::{info, subscriber::set_global_default};
+ use tracing_subscriber::fmt::SubscriberBuilder;
+
+ use super::*;
+
+ #[test]
+ fn test_custom_formatter() {
+ let mock_stdout = StdoutMock::new();
+ let mock_stdout_clone = mock_stdout.clone();
+ let subscriber = SubscriberBuilder::default()
+ .event_format(CustomEventFormatter)
+ .with_writer(Mutex::new(mock_stdout))
+ .finish();
+
+ set_global_default(subscriber).expect("Failed to set subscriber");
+
+ info!(message = "This is a test log entry");
+
+ let time = Local::now().format("%I:%M%P").to_string();
+ let it = mock_stdout_clone.get_copy_of_buffer_as_string();
+ let it_no_ansi = mock_stdout_clone.get_copy_of_buffer_as_string_strip_ansi();
+
+ // println!("{}", it);
+ // println!("{}", it_no_ansi);
+
+ assert!(it_no_ansi.contains("message")); // lolcat colorized each char, so strip the colors.
+ assert!(it.matches("░").count() >= 2);
+ assert!(it.matches("‾").count() >= 1);
+ assert!(it.contains("This is a test log entry"));
+ assert!(it.contains("I:"));
+ assert!(it.contains(&time));
+ assert!(it.matches('\n').count() >= 4); // There are many new lines.
+ }
+}
diff --git a/core/src/tracing_logging/mod.rs b/log/src/log_support/mod.rs
similarity index 83%
rename from core/src/tracing_logging/mod.rs
rename to log/src/log_support/mod.rs
index 91d5390f1..2c2f533e6 100644
--- a/core/src/tracing_logging/mod.rs
+++ b/log/src/log_support/mod.rs
@@ -16,11 +16,15 @@
*/
// Attach sources.
-pub mod init_tracing;
+pub mod custom_event_formatter;
+pub mod public_api;
pub mod rolling_file_appender_impl;
pub mod tracing_config;
+pub mod tracing_init;
// Re-export.
-pub use init_tracing::*;
+pub use custom_event_formatter::*;
+pub use public_api::*;
pub use rolling_file_appender_impl::*;
pub use tracing_config::*;
+pub use tracing_init::*;
diff --git a/log/src/log_support/public_api.rs b/log/src/log_support/public_api.rs
new file mode 100644
index 000000000..df13d7ef1
--- /dev/null
+++ b/log/src/log_support/public_api.rs
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+use std::{fs::OpenOptions, io::Write, path::Path};
+
+use r3bl_core::ok;
+use tracing::dispatcher;
+
+use crate::{TracingConfig, WriterConfig};
+
+const LOG_FILE_NAME: &str = "log.txt";
+
+/// Global default subscriber, which once set, can't be unset or changed.
+/// - This is great for apps.
+/// - Docs for [Global default tracing
+/// subscriber](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html)
+///
+/// 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 [`r3bl_core::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 [`r3bl_core::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.
+///
+/// You can use the functions in this module or just use the [mod@crate::log_support]
+/// functions directly, along with using [tracing::info!], [tracing::debug!], etc. macros.
+///
+/// If you don't want to use sophisticated logging, you can use the [file_log] function to
+/// log messages to a file.
+pub fn try_initialize_logging_global(
+ 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!()
+}
+
+/// Thread local subscriber, which is thread local, and you can assign different ones
+/// to different threads.
+/// - This is great for tests.
+/// - Docs for [Thread local tracing
+/// subscriber](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_default.html)
+///
+/// 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.
+///
+/// Unlike [try_initialize_logging_global], this function initializes the logging system
+/// per thread. This is useful when you want to have different log levels for different
+/// threads, eg in different tests.
+///
+/// If you don't want to use sophisticated logging, you can use the [file_log] function to
+/// log messages to a file.
+pub fn try_initialize_logging_thread_local(
+ level_filter: tracing_core::LevelFilter,
+) -> miette::Result