Compare commits

...

5 commits

Author SHA1 Message Date
Clément DOUIN f3151c3f84
rearrange try_to_sync_cache_builder func 2024-05-14 18:34:45 +02:00
Perma Alesheikh 098ae380c3
use comfy-table instead of builtin impl for table
This is to out-source the table making in terminal to the external
library.

I removed the in-house table implementation since it is not used any
more, and had been replaced by comfy-table, we use this instead.

I also have reimplemented table_max_width since new implementation
removed max width , with the new implemetation it will work again.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-05-14 18:23:34 +02:00
Perma Alesheikh 1e448e56eb
replace dialoguer with inquire
In order to reduce our dependencies, we are replacing the dependencies
that use console_rs with those that use crossterm.

This commit will completely replace dialoguer with inquire.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-05-14 18:20:54 +02:00
Clément DOUIN d54dd6429e
replace default log level warn by off 2024-05-14 18:19:53 +02:00
Clément DOUIN 9dee1784df
replace imap by imap-codec 2024-05-14 18:19:39 +02:00
31 changed files with 1138 additions and 1255 deletions

468
Cargo.lock generated
View file

@ -8,6 +8,15 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "abnf-core"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea"
dependencies = [
"nom",
]
[[package]]
name = "addr2line"
version = "0.21.0"
@ -160,7 +169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "367fd0ad87307588d087544707bc5fbf4805ded96c7db922b70d368fa1cb5702"
dependencies = [
"unicode-width",
"yansi 0.5.1",
"yansi",
]
[[package]]
@ -186,6 +195,15 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-ctrlc"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907279f6e91a51c8ec7cac24711e8308f21da7c10c7700ca2f7e125694ed2df1"
dependencies = [
"ctrlc",
]
[[package]]
name = "async-executor"
version = "1.11.0"
@ -345,6 +363,33 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "aws-lc-rs"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8487b59d62764df8231cb371c459314df895b41756df457a1fb1243d65c89195"
dependencies = [
"aws-lc-sys",
"mirai-annotations",
"paste",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c15eb61145320320eb919d9bab524617a7aa4216c78d342fae3a758bc33073e4"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
"fs_extra",
"libc",
"paste",
]
[[package]]
name = "backtrace"
version = "0.3.71"
@ -390,6 +435,29 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bindgen"
version = "0.69.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
dependencies = [
"bitflags 2.5.0",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.59",
"which",
]
[[package]]
name = "bitfield"
version = "0.14.0"
@ -474,6 +542,26 @@ dependencies = [
"cipher 0.4.4",
]
[[package]]
name = "bounded-static"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2325bd33fa7e3018e7e37f5b0591ba009124963b5a3f8b7cae6d0a8c1028ed4"
dependencies = [
"bounded-static-derive",
]
[[package]]
name = "bounded-static-derive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f10dd247355bf631d98d2753d87ae62c84c8dcb996ad9b24a4168e0aec29bd6b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "bstr"
version = "1.9.1"
@ -494,12 +582,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]]
name = "build-rs"
version = "0.1.2"
@ -574,6 +656,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfb-mode"
version = "0.8.2"
@ -599,6 +690,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chrono"
version = "0.4.38"
@ -645,6 +742,17 @@ dependencies = [
"inout",
]
[[package]]
name = "clang-sys"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.4"
@ -705,6 +813,15 @@ dependencies = [
"roff",
]
[[package]]
name = "cmake"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
[[package]]
name = "color-eyre"
version = "0.6.3"
@ -738,6 +855,18 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "comfy-table"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7"
dependencies = [
"crossterm 0.27.0",
"strum",
"strum_macros",
"unicode-width",
]
[[package]]
name = "concurrent-queue"
version = "2.4.0"
@ -868,6 +997,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.5.0",
"crossterm_winapi",
"libc",
"parking_lot 0.12.1",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
@ -909,6 +1051,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "ctrlc"
version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
dependencies = [
"nix 0.28.0",
"windows-sys 0.52.0",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.2"
@ -1090,18 +1242,6 @@ dependencies = [
"cipher 0.4.4",
]
[[package]]
name = "dialoguer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87"
dependencies = [
"console",
"shell-words",
"tempfile",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -1176,6 +1316,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "dunce"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
[[package]]
name = "dyn-clone"
version = "1.0.17"
@ -1250,10 +1396,10 @@ dependencies = [
[[package]]
name = "email-lib"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20eca48c1ae74c6c7ec6ef9a6c2af935e3cc601c50fce1385c24bec5dd5e20e9"
source = "git+https://git.sr.ht/~soywod/pimalaya#033ba2a2e193769e1272c9493aa1d6c975346eb5"
dependencies = [
"advisory-lock",
"async-ctrlc",
"async-trait",
"chrono",
"chumsky",
@ -1265,10 +1411,8 @@ dependencies = [
"hickory-resolver",
"hyper",
"hyper-rustls",
"imap",
"imap-proto",
"imap-client",
"keyring-lib",
"log",
"mail-builder",
"mail-parser",
"mail-send",
@ -1279,7 +1423,7 @@ dependencies = [
"notmuch",
"oauth-lib",
"once_cell",
"ouroboros 0.15.6",
"ouroboros",
"paste",
"pgp-lib",
"process-lib",
@ -1292,6 +1436,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-rustls 0.25.0",
"tracing",
"tree_magic_mini",
"urlencoding",
"utf7-imap",
@ -1563,6 +1708,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.30"
@ -1738,6 +1889,12 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "gpg-error"
version = "0.6.1"
@ -1904,8 +2061,8 @@ dependencies = [
"clap_complete",
"clap_mangen",
"color-eyre",
"comfy-table",
"console",
"dialoguer",
"dirs 4.0.0",
"email-lib",
"email_address",
@ -2108,29 +2265,60 @@ dependencies = [
]
[[package]]
name = "imap"
version = "3.0.0-alpha.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd82d66124b97aabeba7a4744b82bf244160e56611d8be9f89b74ed4ee481f1e"
name = "imap-client"
version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [
"base64 0.22.0",
"bufstream",
"chrono",
"imap-proto",
"lazy_static",
"nom",
"ouroboros 0.18.3",
"regex",
"rustls-connector",
"imap-flow",
"once_cell",
"rustls-native-certs 0.7.0",
"tasks",
"thiserror",
"tokio",
"tokio-rustls 0.26.0",
"tracing",
]
[[package]]
name = "imap-proto"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "305c25c6e69416059e3396c4a062b84dc7b0a782cd4c84d82bab268eb0421ec7"
name = "imap-codec"
version = "2.0.0"
source = "git+https://github.com/duesee/imap-codec.git#d6b265fd01123334db2d48100537eb140932589c"
dependencies = [
"abnf-core",
"base64 0.21.7",
"bounded-static",
"chrono",
"imap-types",
"log",
"nom",
"thiserror",
]
[[package]]
name = "imap-flow"
version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [
"bounded-static",
"bytes",
"imap-codec",
"imap-types",
"rustls 0.23.5",
"thiserror",
"tokio",
"tokio-rustls 0.26.0",
"tracing",
]
[[package]]
name = "imap-types"
version = "2.0.0"
source = "git+https://github.com/duesee/imap-codec.git#d6b265fd01123334db2d48100537eb140932589c"
dependencies = [
"base64 0.21.7",
"bounded-static",
"chrono",
"thiserror",
]
[[package]]
@ -2198,7 +2386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe95f33091b9b7b517a5849bce4dce1b550b430fc20d58059fcaa319ed895d8b"
dependencies = [
"bitflags 2.5.0",
"crossterm",
"crossterm 0.25.0",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
@ -2342,6 +2530,12 @@ dependencies = [
"spin 0.5.2",
]
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.153"
@ -2359,6 +2553,16 @@ dependencies = [
"winreg 0.52.0",
]
[[package]]
name = "libloading"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets 0.52.5",
]
[[package]]
name = "libm"
version = "0.2.8"
@ -2608,6 +2812,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mirai-annotations"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1"
[[package]]
name = "mml-lib"
version = "1.0.12"
@ -2657,6 +2867,18 @@ dependencies = [
"memoffset 0.7.1",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -2966,18 +3188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db"
dependencies = [
"aliasable",
"ouroboros_macro 0.15.6",
]
[[package]]
name = "ouroboros"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b7be5a8a3462b752f4be3ff2b2bf2f7f1d00834902e46be2a4d68b87b0573c"
dependencies = [
"aliasable",
"ouroboros_macro 0.18.3",
"static_assertions",
"ouroboros_macro",
]
[[package]]
@ -2993,20 +3204,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "ouroboros_macro"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b645dcde5f119c2c454a92d0dfa271a2a3b205da92e4292a68ead4bdbfde1f33"
dependencies = [
"heck 0.4.1",
"itertools",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.59",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -3329,6 +3526,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettyplease"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
dependencies = [
"proc-macro2",
"syn 2.0.59",
]
[[package]]
name = "primeorder"
version = "0.13.6"
@ -3381,19 +3588,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"version_check",
"yansi 1.0.1",
]
[[package]]
name = "process-lib"
version = "0.4.2"
@ -3711,6 +3905,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -3774,16 +3974,18 @@ dependencies = [
]
[[package]]
name = "rustls-connector"
version = "0.19.2"
name = "rustls"
version = "0.23.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5bd40675c79c896f46d0031bf64c448b35e583dd2bc949751ddd800351e453a"
checksum = "afabcee0551bd1aa3e18e5adbf2c0544722014b899adb31bd186ec638d3da97e"
dependencies = [
"aws-lc-rs",
"log",
"rustls 0.22.3",
"rustls-native-certs 0.7.0",
"once_cell",
"rustls-pki-types",
"rustls-webpki 0.102.2",
"subtle",
"zeroize",
]
[[package]]
@ -3852,11 +4054,18 @@ version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
"aws-lc-rs",
"ring 0.17.8",
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]]
name = "rustversion"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]]
name = "ryu"
version = "1.0.17"
@ -4107,12 +4316,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shellexpand"
version = "3.1.0"
@ -4135,6 +4338,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
@ -4278,6 +4487,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
[[package]]
name = "strum_macros"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.59",
]
[[package]]
name = "subtle"
version = "2.5.0"
@ -4346,12 +4574,33 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tag-generator"
version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [
"imap-types",
"rand",
]
[[package]]
name = "target-lexicon"
version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tasks"
version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [
"imap-flow",
"imap-types",
"tag-generator",
"thiserror",
"tracing",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.1.3"
@ -4518,6 +4767,17 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.5",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.10"
@ -4929,6 +5189,18 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.32",
]
[[package]]
name = "widestring"
version = "1.1.0"
@ -5214,12 +5486,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "z-base-32"
version = "0.1.4"
@ -5250,7 +5516,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"nix",
"nix 0.26.4",
"once_cell",
"ordered-stream",
"rand",

View file

@ -52,10 +52,10 @@ clap = { version = "4.4", features = ["derive", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6.3"
comfy-table = "7.1.1"
console = "0.15.2"
dialoguer = "0.10.2"
dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive"] }
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
email_address = "0.2.4"
erased-serde = "0.3"
indicatif = "0.17"
@ -83,3 +83,11 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-width = "0.1"
url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io]
# WIP: transition from `imap` to `imap-codec`
email-lib = { git = "https://git.sr.ht/~soywod/pimalaya" }
imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
imap-codec = { git = "https://github.com/duesee/imap-codec.git" }
imap-types = { git = "https://github.com/duesee/imap-codec.git" }

View file

@ -2,12 +2,7 @@ use clap::Parser;
use color_eyre::Result;
use tracing::info;
use crate::{
account::Accounts,
config::TomlConfig,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
};
use crate::{account::Accounts, config::TomlConfig, printer::Printer};
/// List all accounts.
///
@ -15,8 +10,13 @@ use crate::{
/// file.
#[derive(Debug, Parser)]
pub struct AccountListCommand {
#[command(flatten)]
pub table: TableMaxWidthFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
impl AccountListCommand {
@ -25,12 +25,7 @@ impl AccountListCommand {
let accounts: Accounts = config.accounts.iter().into();
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: &Default::default(),
max_width: self.table.max_width,
},
)
printer.print_table(accounts, self.table_max_width)?;
Ok(())
}
}

View file

@ -4,13 +4,11 @@ pub mod config;
pub(crate) mod wizard;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
use serde::Serialize;
use std::{collections::hash_map::Iter, fmt, ops::Deref};
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::table::{Cell, Row, Table},
};
use crate::printer::{PrintTable, WriteColor};
use self::config::TomlAccountConfig;
@ -41,20 +39,22 @@ impl fmt::Display for Account {
}
}
impl Table for Account {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
.cell(Cell::new("BACKENDS").bold().underline().white())
.cell(Cell::new("DEFAULT").bold().underline().white())
impl From<Account> for Row {
fn from(account: Account) -> Self {
let mut r = Row::new();
r.add_cell(Cell::new(account.name).fg(Color::Green));
r.add_cell(Cell::new(account.backend).fg(Color::Blue));
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
r
}
fn row(&self) -> Row {
let default = if self.default { "yes" } else { "" };
Row::new()
.cell(Cell::new(&self.name).shrinkable().green())
.cell(Cell::new(&self.backend).blue())
.cell(Cell::new(default).white())
}
impl From<&Account> for Row {
fn from(account: &Account) -> Self {
let mut r = Row::new();
r.add_cell(Cell::new(&account.name).fg(Color::Green));
r.add_cell(Cell::new(&account.backend).fg(Color::Blue));
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
r
}
}
@ -70,10 +70,46 @@ impl Deref for Accounts {
}
}
impl From<Accounts> for Table {
fn from(accounts: Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.into_iter().map(Row::from));
table
}
}
impl From<&Accounts> for Table {
fn from(accounts: &Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.iter().map(Row::from));
table
}
}
impl PrintTable for Accounts {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
Table::print(writer, self, opts)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}

View file

@ -2,13 +2,10 @@
use crate::account::config::SyncConfig;
use color_eyre::{eyre::OptionExt, Result};
#[cfg(feature = "account-sync")]
use dialoguer::Confirm;
use email_address::EmailAddress;
use inquire::validator::{ErrorMessage, Validation};
use std::{path::PathBuf, str::FromStr};
#[cfg(feature = "account-sync")]
use crate::wizard_prompt;
#[cfg(feature = "account-discovery")]
use crate::wizard_warn;
use crate::{
@ -144,13 +141,11 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
#[cfg(feature = "account-sync")]
{
let should_configure_sync = Confirm::new()
.with_prompt(wizard_prompt!(
"Do you need offline access for your account?"
))
.default(false)
.interact_opt()?
.unwrap_or_default();
let should_configure_sync =
inquire::Confirm::new("Do you need offline access for your account?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
if should_configure_sync {
config.sync = Some(SyncConfig {

View file

@ -1,9 +1,9 @@
pub mod config;
pub(crate) mod wizard;
use color_eyre::Result;
use async_trait::async_trait;
use std::{ops::Deref, sync::Arc};
use color_eyre::Result;
use std::{fmt::Display, ops::Deref, sync::Arc};
#[cfg(feature = "imap")]
use email::imap::{ImapContextBuilder, ImapContextSync};
@ -70,30 +70,32 @@ pub enum BackendKind {
Sendmail,
}
impl ToString for BackendKind {
fn to_string(&self) -> String {
let kind = match self {
Self::None => "None",
impl Display for BackendKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::None => "None",
#[cfg(feature = "imap")]
Self::Imap => "IMAP",
#[cfg(all(feature = "imap", feature = "account-sync"))]
Self::ImapCache => "IMAP cache",
#[cfg(feature = "imap")]
Self::Imap => "IMAP",
#[cfg(all(feature = "imap", feature = "account-sync"))]
Self::ImapCache => "IMAP cache",
#[cfg(feature = "maildir")]
Self::Maildir => "Maildir",
#[cfg(feature = "maildir")]
Self::Maildir => "Maildir",
#[cfg(feature = "notmuch")]
Self::Notmuch => "Notmuch",
#[cfg(feature = "notmuch")]
Self::Notmuch => "Notmuch",
#[cfg(feature = "smtp")]
Self::Smtp => "SMTP",
#[cfg(feature = "smtp")]
Self::Smtp => "SMTP",
#[cfg(feature = "sendmail")]
Self::Sendmail => "Sendmail",
};
kind.to_string()
#[cfg(feature = "sendmail")]
Self::Sendmail => "Sendmail",
}
)
}
}

View file

@ -1,7 +1,7 @@
use color_eyre::Result;
use dialoguer::Select;
#[cfg(feature = "account-discovery")]
use email::account::discover::config::AutoConfig;
use inquire::Select;
#[cfg(feature = "imap")]
use crate::imap;
@ -13,7 +13,6 @@ use crate::notmuch;
use crate::sendmail;
#[cfg(feature = "smtp")]
use crate::smtp;
use crate::ui::THEME;
use super::{config::BackendConfig, BackendKind};
@ -38,12 +37,9 @@ pub(crate) async fn configure(
email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Default email backend")
.items(DEFAULT_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));
let kind = Select::new("Default email backend", DEFAULT_BACKEND_KINDS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let config = match kind {
#[cfg(feature = "imap")]
@ -71,12 +67,12 @@ pub(crate) async fn configure_sender(
email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Backend for sending messages")
.items(SEND_MESSAGE_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone));
let kind = Select::new(
"Backend for sending messages",
SEND_MESSAGE_BACKEND_KINDS.to_vec(),
)
.with_starting_cursor(0)
.prompt_skippable()?;
let config = match kind {
#[cfg(feature = "smtp")]

View file

@ -18,7 +18,7 @@ use tracing::debug;
#[cfg(feature = "account-sync")]
use crate::backend::BackendKind;
use crate::{account::config::TomlAccountConfig, wizard_prompt, wizard_warn};
use crate::{account::config::TomlAccountConfig, wizard_warn};
/// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
@ -46,8 +46,8 @@ impl TomlConfig {
1 => {
let path = &paths[0];
let ref content = fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?;
let content = &(fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?);
toml::from_str(content).context(format!("cannot parse config file at {path:?}"))
}
@ -85,17 +85,13 @@ impl TomlConfig {
///
/// NOTE: the wizard can only be used with interactive shells.
async fn from_wizard(path: &PathBuf) -> Result<Self> {
use dialoguer::Confirm;
use std::process;
wizard_warn!("Cannot find existing configuration at {path:?}.");
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create one with the wizard?"
))
.default(true)
.interact_opt()?
let confirm = inquire::Confirm::new("Would you like to create one with the wizard? ")
.with_default(true)
.prompt_skippable()?
.unwrap_or_default();
if !confirm {

View file

@ -1,10 +1,10 @@
use color_eyre::Result;
use dialoguer::{Confirm, Input, Select};
use inquire::{Confirm, Select, Text};
use shellexpand_utils::expand;
use std::{fs, path::PathBuf, process};
use std::{fs, path::Path, process};
use toml_edit::{DocumentMut, Item};
use crate::{account, ui::THEME};
use crate::account;
use super::TomlConfig;
@ -31,7 +31,7 @@ macro_rules! wizard_log {
};
}
pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
pub(crate) async fn configure(path: &Path) -> Result<TomlConfig> {
wizard_log!("Configuring your first account:");
let mut config = TomlConfig::default();
@ -39,12 +39,9 @@ pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
while let Some((name, account_config)) = account::wizard::configure().await? {
config.accounts.insert(name, account_config);
if !Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to configure another account?"
))
.default(false)
.interact_opt()?
if !Confirm::new("Would you like to configure another account?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default()
{
break;
@ -68,14 +65,13 @@ pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
println!("{} accounts have been configured.", accounts.len());
Select::with_theme(&*THEME)
.with_prompt(wizard_prompt!(
"Which account would you like to set as your default?"
))
.items(&accounts)
.default(0)
.interact_opt()?
.and_then(|idx| config.accounts.get_mut(accounts[idx]))
Select::new(
"Which account would you like to set as your default?",
accounts,
)
.with_starting_cursor(0)
.prompt_skippable()?
.and_then(|input| config.accounts.get_mut(input))
}
};
@ -85,12 +81,9 @@ pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
process::exit(0)
}
let path = Input::with_theme(&*THEME)
.with_prompt(wizard_prompt!(
"Where would you like to save your configuration?"
))
.default(path.to_string_lossy().to_string())
.interact()?;
let path = Text::new("Where would you like to save your configuration?")
.with_default(&path.to_string_lossy())
.prompt()?;
let path = expand::path(path);
println!("Writing the configuration to {path:?}");

View file

@ -11,12 +11,8 @@ use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
};
/// List all envelopes.
@ -41,9 +37,6 @@ pub struct ListEnvelopesCommand {
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[command(flatten)]
pub table: TableMaxWidthFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
@ -51,6 +44,14 @@ pub struct ListEnvelopesCommand {
#[command(flatten)]
pub account: AccountNameFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
/// The list envelopes filter and sort query.
///
/// The query can be a filter query, a sort query or both
@ -128,11 +129,11 @@ impl Default for ListEnvelopesCommand {
folder: Default::default(),
page: 1,
page_size: Default::default(),
table: Default::default(),
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
}
}
}
@ -197,13 +198,7 @@ impl ListEnvelopesCommand {
let envelopes = backend.list_envelopes(folder, opts).await?;
printer.print_table(
Box::new(envelopes),
PrintTableOpts {
format: &account_config.get_message_read_format(),
max_width: self.table.max_width,
},
)?;
printer.print_table(envelopes, self.table_max_width)?;
Ok(())
}

View file

@ -4,6 +4,7 @@ pub mod config;
pub mod flag;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
use email::account::config::AccountConfig;
use serde::Serialize;
use std::ops;
@ -11,8 +12,7 @@ use std::ops;
use crate::{
cache::IdMapper,
flag::{Flag, Flags},
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
printer::{PrintTable, WriteColor},
};
#[derive(Clone, Debug, Default, Serialize)]
@ -30,49 +30,125 @@ pub struct Envelope {
pub to: Mailbox,
pub date: String,
}
impl From<Envelope> for Row {
fn from(envelope: Envelope) -> Self {
let mut all_attributes = vec![];
impl Table for Envelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("FROM").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
let unseen = !envelope.flags.contains(&Flag::Seen);
if unseen {
all_attributes.push(Attribute::Bold)
}
fn row(&self) -> Row {
let id = self.id.to_string();
let unseen = !self.flags.contains(&Flag::Seen);
let flags = {
let mut flags = String::new();
flags.push_str(if !unseen { " " } else { "" });
flags.push_str(if self.flags.contains(&Flag::Answered) {
""
flags.push(if !unseen { ' ' } else { '✷' });
flags.push(if envelope.flags.contains(&Flag::Answered) {
'↵'
} else {
" "
' '
});
flags.push_str(if self.flags.contains(&Flag::Flagged) {
""
flags.push(if envelope.flags.contains(&Flag::Flagged) {
'⚑'
} else {
" "
' '
});
flags
};
let subject = &self.subject;
let sender = if let Some(name) = &self.from.name {
name
} else {
&self.from.addr
};
let date = &self.date;
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
let mut row = Row::new();
row.add_cell(
Cell::new(envelope.id)
.add_attributes(all_attributes.clone())
.fg(Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(Color::White),
)
.add_cell(
Cell::new(envelope.subject)
.add_attributes(all_attributes.clone())
.fg(Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = envelope.from.name {
name
} else {
envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(Color::Blue),
)
.add_cell(
Cell::new(envelope.date)
.add_attributes(all_attributes)
.fg(Color::Yellow),
);
row
}
}
impl From<&Envelope> for Row {
fn from(envelope: &Envelope) -> Self {
let mut all_attributes = vec![];
let unseen = !envelope.flags.contains(&Flag::Seen);
if unseen {
all_attributes.push(Attribute::Bold)
}
let flags = {
let mut flags = String::new();
flags.push(if !unseen { ' ' } else { '✷' });
flags.push(if envelope.flags.contains(&Flag::Answered) {
'↵'
} else {
' '
});
flags.push(if envelope.flags.contains(&Flag::Flagged) {
'⚑'
} else {
' '
});
flags
};
let mut row = Row::new();
row.add_cell(
Cell::new(&envelope.id)
.add_attributes(all_attributes.clone())
.fg(Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(Color::White),
)
.add_cell(
Cell::new(&envelope.subject)
.add_attributes(all_attributes.clone())
.fg(Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = &envelope.from.name {
name
} else {
&envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(Color::Blue),
)
.add_cell(
Cell::new(&envelope.date)
.add_attributes(all_attributes)
.fg(Color::Yellow),
);
row
}
}
@ -80,6 +156,44 @@ impl Table for Envelope {
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<Envelope>);
impl From<Envelopes> for Table {
fn from(envelopes: Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.into_iter().map(Row::from));
table
}
}
impl From<&Envelopes> for Table {
fn from(envelopes: &Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.iter().map(Row::from));
table
}
}
impl Envelopes {
pub fn from_backend(
config: &AccountConfig,
@ -119,9 +233,13 @@ impl ops::Deref for Envelopes {
}
impl PrintTable for Envelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
Table::print(writer, self, opts)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}

View file

@ -1,7 +1,7 @@
use clap::Parser;
use color_eyre::Result;
use dialoguer::Confirm;
use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder};
use inquire::Confirm;
use std::process;
use tracing::info;
@ -35,12 +35,9 @@ impl FolderDeleteCommand {
let folder = &self.folder.name;
let confirm_msg = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted.");
let confirm = Confirm::new()
.with_prompt(confirm_msg)
.default(false)
.report(false)
.interact_opt()?;
let confirm = Confirm::new(&format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted."))
.with_default(false).prompt_skippable()?;
if let Some(false) | None = confirm {
process::exit(0);
};

View file

@ -6,12 +6,8 @@ use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
folder::Folders,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, folder::Folders,
printer::Printer,
};
/// List all folders.
@ -19,15 +15,20 @@ use crate::{
/// This command allows you to list all exsting folders.
#[derive(Debug, Parser)]
pub struct FolderListCommand {
#[command(flatten)]
pub table: TableMaxWidthFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
impl FolderListCommand {
@ -52,12 +53,7 @@ impl FolderListCommand {
let folders: Folders = backend.list_folders().await?.into();
printer.print_table(
Box::new(folders),
PrintTableOpts {
format: &account_config.get_message_read_format(),
max_width: self.table.max_width,
},
)
printer.print_table(folders, self.table_max_width)?;
Ok(())
}
}

View file

@ -1,6 +1,5 @@
use clap::Parser;
use color_eyre::Result;
use dialoguer::Confirm;
use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
use std::process;
use tracing::info;
@ -35,12 +34,10 @@ impl FolderPurgeCommand {
let folder = &self.folder.name;
let confirm_msg = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted.");
let confirm = Confirm::new()
.with_prompt(confirm_msg)
.default(false)
.report(false)
.interact_opt()?;
let confirm = inquire::Confirm::new(&format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted."))
.with_default(false)
.prompt_skippable()?;
if let Some(false) | None = confirm {
process::exit(0);
};

View file

@ -3,13 +3,11 @@ pub mod command;
pub mod config;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table};
use serde::Serialize;
use std::ops;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
use crate::printer::{PrintTable, WriteColor};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
@ -25,24 +23,58 @@ impl From<&email::folder::Folder> for Folder {
}
}
}
impl From<&Folder> for Row {
fn from(folder: &Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(&folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(&folder.desc).fg(comfy_table::Color::Green));
impl Table for Folder {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").bold().underline().white())
.cell(Cell::new("DESC").bold().underline().white())
row
}
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.name).blue())
.cell(Cell::new(&self.desc).green())
impl From<Folder> for Row {
fn from(folder: Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(folder.desc).fg(comfy_table::Color::Green));
row
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>);
impl From<Folders> for Table {
fn from(folders: Folders) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse),
]))
.add_rows(folders.0.into_iter().map(Row::from));
table
}
}
impl From<&Folders> for Table {
fn from(folders: &Folders) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse),
]))
.add_rows(folders.0.iter().map(Row::from));
table
}
}
impl ops::Deref for Folders {
type Target = Vec<Folder>;
@ -58,9 +90,13 @@ impl From<email::folder::Folders> for Folders {
}
impl PrintTable for Folders {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
Table::print(writer, self, opts)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}

View file

@ -1,5 +1,4 @@
use color_eyre::Result;
use dialoguer::{Confirm, Input, Password, Select};
#[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::{
@ -9,14 +8,11 @@ use email::{
},
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
};
use inquire::validator::{ErrorMessage, StringValidator, Validation};
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{
backend::config::BackendConfig,
ui::{prompt, THEME},
wizard_log, wizard_prompt,
};
use crate::{backend::config::BackendConfig, ui::prompt, wizard_log};
const ENCRYPTIONS: &[ImapEncryptionKind] = &[
ImapEncryptionKind::Tls,
@ -33,12 +29,35 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
#[derive(Clone, Copy)]
struct U16Validator;
impl StringValidator for U16Validator {
fn validate(
&self,
input: &str,
) -> std::prelude::v1::Result<Validation, inquire::CustomUserError> {
if input.parse::<u16>().is_ok() {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid(ErrorMessage::Custom(format!(
"you should enter a number between {} and {}",
u16::MIN,
u16::MAX
))))
}
}
}
#[cfg(feature = "account-discovery")]
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text};
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
@ -54,10 +73,9 @@ pub(crate) async fn configure(
let default_host =
autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1));
let host = Input::with_theme(&*THEME)
.with_prompt("IMAP hostname")
.default(default_host)
.interact()?;
let host = Text::new("IMAP hostname")
.with_default(&default_host)
.prompt()?;
let autoconfig_encryption = autoconfig_server
.and_then(|imap| {
@ -75,11 +93,9 @@ pub(crate) async fn configure(
ImapEncryptionKind::None => 2,
};
let encryption_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP encryption")
.items(ENCRYPTIONS)
.default(default_encryption_idx)
.interact_opt()?;
let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(default_encryption_idx)
.prompt_skippable()?;
let autoconfig_port = autoconfig_server
.and_then(|s| s.port())
@ -91,23 +107,26 @@ pub(crate) async fn configure(
});
let (encryption, default_port) = match encryption_idx {
Some(idx) if idx == default_encryption_idx => {
Some(enc_kind)
if &enc_kind
== ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre(
"something impossible happened while selecting the encryption of imap.",
)? =>
{
(Some(autoconfig_encryption), autoconfig_port)
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
(Some(ImapEncryptionKind::Tls), 993)
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
(Some(ImapEncryptionKind::StartTls), 143)
}
Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993),
Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143),
_ => (Some(ImapEncryptionKind::None), 143),
};
let port = Input::with_theme(&*THEME)
.with_prompt("IMAP port")
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
.default(default_port.to_string())
.interact()
let port = Text::new("IMAP port")
.with_validators(&[
Box::new(MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let autoconfig_login = autoconfig_server.map(|imap| match imap.username() {
@ -118,10 +137,9 @@ pub(crate) async fn configure(
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Input::with_theme(&*THEME)
.with_prompt("IMAP login")
.default(default_login)
.interact()?;
let login = Text::new("IMAP login")
.with_default(&default_login)
.prompt()?;
let default_oauth2_enabled = autoconfig_server
.and_then(|imap| {
@ -132,10 +150,9 @@ pub(crate) async fn configure(
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
let oauth2_enabled = Confirm::new()
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
.default(default_oauth2_enabled)
.interact_opt()?
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
let auth = if oauth2_enabled {
@ -143,25 +160,19 @@ pub(crate) async fn configure(
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 client id")
.interact()?;
config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 client secret")
.interact()?;
let client_secret: String = Password::new("IMAP OAuth 2.0 client secret").prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
@ -172,38 +183,28 @@ pub(crate) async fn configure(
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 authorization URL")
.default(default_auth_url)
.interact()?;
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 token URL")
.default(default_token_url)
.interact()?;
config.token_url = Text::new("IMAP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::with_theme(&*THEME)
.with_prompt(prompt)
.items(scopes)
.default(0)
.interact_opt()?
.and_then(|idx| scopes.get(idx))
.map(|scope| scope.to_string()),
None => Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()),
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => {
Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty())
}
})
};
@ -212,12 +213,9 @@ pub(crate) async fn configure(
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more IMAP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
@ -236,12 +234,9 @@ pub(crate) async fn configure(
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to enable PKCE verification?"
))
.default(true)
.interact_opt()?
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -289,26 +284,23 @@ pub(crate) async fn configure(
ImapAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => {
Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-imap-passwd"))
.interact()?,
Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(CMD) => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-imap-passwd"))
.prompt()?,
),
_ => Default::default(),
};
@ -330,47 +322,44 @@ pub(crate) async fn configure(
#[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
use inquire::{
validator::MinLengthValidator, Confirm, Password, PasswordDisplayMode, Select, Text,
};
let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1);
let host = Input::with_theme(&*THEME)
.with_prompt("IMAP hostname")
.default(default_host)
.interact()?;
let host = Text::new("IMAP hostname")
.with_default(&default_host)
.prompt()?;
let encryption_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP encryption")
.items(ENCRYPTIONS)
.default(0)
.interact_opt()?;
let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let (encryption, default_port) = match encryption_idx {
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
(Some(ImapEncryptionKind::Tls), 993)
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
(Some(ImapEncryptionKind::StartTls), 143)
}
Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993),
Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143),
_ => (Some(ImapEncryptionKind::None), 143),
};
let port = Input::with_theme(&*THEME)
.with_prompt("IMAP port")
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
.default(default_port.to_string())
.interact()
let port = Text::new("IMAP port")
.with_validators(&[
Box::new(MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let default_login = email.to_owned();
let login = Input::with_theme(&*THEME)
.with_prompt("IMAP login")
.default(default_login)
.interact()?;
let login = Text::new("IMAP login")
.with_default(&default_login)
.prompt()?;
let oauth2_enabled = Confirm::new()
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
.default(false)
.interact_opt()?
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
let auth = if oauth2_enabled {
@ -378,25 +367,21 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 client id")
.interact()?;
config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 client secret")
.interact()?;
let client_secret: String = Password::new("IMAP OAuth 2.0 client secret")
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
@ -404,23 +389,12 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
.set_only_keyring(&client_secret)
.await?;
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 authorization URL")
.interact()?;
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL").prompt()?;
config.token_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 token URL")
.interact()?;
config.token_url = Text::new("IMAP OAuth 2.0 token URL").prompt()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()))
Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()))
};
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
@ -428,12 +402,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more IMAP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
@ -452,12 +423,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to enable PKCE verification?"
))
.default(true)
.interact_opt()?
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -505,26 +473,23 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
ImapAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => {
Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-imap-passwd"))
.interact()?,
Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(CMD) => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-imap-passwd"))
.prompt()?,
),
_ => Default::default(),
};

View file

@ -1,23 +1,25 @@
use color_eyre::Result;
use dialoguer::Input;
use dirs::home_dir;
use email::maildir::config::MaildirConfig;
use inquire::Text;
use crate::{backend::config::BackendConfig, ui::THEME};
use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = MaildirConfig::default();
let mut input = Input::with_theme(&*THEME);
let mut input = Text::new("Maildir directory");
if let Some(home) = home_dir() {
input.default(home.join("Mail").display().to_string());
let Some(home) = home_dir() else {
config.root_dir = input.prompt()?.into();
return Ok(BackendConfig::Maildir(config));
};
config.root_dir = input
.with_prompt("Maildir directory")
.interact_text()?
.into();
let def = home.join("Mail").display().to_string();
input = input.with_default(&def);
config.root_dir = input.prompt()?.into();
Ok(BackendConfig::Maildir(config))
}

View file

@ -1,24 +1,23 @@
use color_eyre::Result;
use dialoguer::Input;
use email::notmuch::config::NotmuchConfig;
use inquire::Text;
use crate::{backend::config::BackendConfig, ui::THEME};
use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = NotmuchConfig::default();
let default_database_path = NotmuchConfig::get_default_database_path()
.unwrap_or_default()
.to_string_lossy()
.to_string();
config.database_path = Some(
Input::with_theme(&*THEME)
.with_prompt("Notmuch database path")
.default(default_database_path)
.interact_text()?
.into(),
);
let config = NotmuchConfig {
database_path: Some(
Text::new("Notmuch database path")
.with_default(
&NotmuchConfig::get_default_database_path()
.unwrap_or_default()
.to_string_lossy(),
)
.prompt()?
.into(),
),
..Default::default()
};
Ok(BackendConfig::Notmuch(config))
}

View file

@ -1,8 +1,13 @@
pub mod print;
pub mod print_table;
#[allow(clippy::module_inception)]
pub mod printer;
use std::io;
pub use print::*;
pub use print_table::*;
pub use printer::*;
use termcolor::StandardStream;
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}

View file

@ -1,17 +0,0 @@
use color_eyre::Result;
use email::email::config::EmailTextPlainFormat;
use std::io;
use termcolor::{self, StandardStream};
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}
pub trait PrintTable {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
}
pub struct PrintTableOpts<'a> {
pub format: &'a EmailTextPlainFormat,
pub max_width: Option<usize>,
}

View file

@ -1,12 +1,15 @@
use clap::ArgMatches;
use color_eyre::{eyre::Context, Report, Result};
use std::fmt::{self, Debug};
use std::fmt::Debug;
use termcolor::StandardStream;
use crate::{
output::{args, ColorFmt, OutputFmt},
printer::{Print, PrintTable, PrintTableOpts, WriteColor},
printer::{Print, WriteColor},
};
pub trait PrintTable {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()>;
}
pub trait Printer {
// TODO: rename end
@ -14,12 +17,12 @@ pub trait Printer {
// TODO: rename log
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
// TODO: rename table
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
fn print_table<T: Debug + PrintTable>(
&mut self,
// TODO: remove Box
data: Box<T>,
opts: PrintTableOpts,
data: T,
table_max_width: Option<u16>,
) -> Result<()>;
fn is_json(&self) -> bool;
}
@ -59,25 +62,17 @@ impl Printer for StdoutPrinter {
}
}
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print_table(self.writer.as_mut(), opts),
OutputFmt::Json => {
let json = &mut serde_json::Serializer::new(self.writer.as_mut());
let ser = &mut <dyn erased_serde::Serializer>::erase(json);
data.erased_serialize(ser).unwrap();
Ok(())
}
}
}
fn is_json(&self) -> bool {
self.fmt == OutputFmt::Json
}
fn print_table<T: Debug + PrintTable>(
&mut self,
data: T,
table_max_width: Option<u16>,
) -> Result<()> {
data.print_table(self.writer.as_mut(), table_max_width)
}
}
impl From<OutputFmt> for StdoutPrinter {

View file

@ -1,15 +1,14 @@
use color_eyre::Result;
use dialoguer::Input;
use email::sendmail::config::SendmailConfig;
use inquire::Text;
use crate::{backend::config::BackendConfig, ui::THEME};
use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> {
let config = SendmailConfig {
cmd: Input::with_theme(&*THEME)
.with_prompt("Sendmail-compatible shell command to send emails")
.default(String::from("/usr/bin/msmtp"))
.interact()?
cmd: Text::new("Sendmail-compatible shell command to send emails")
.with_default("/usr/bin/msmtp")
.prompt()?
.into(),
};

View file

@ -1,5 +1,4 @@
use color_eyre::Result;
use dialoguer::{Confirm, Input, Password, Select};
#[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::{
@ -9,14 +8,11 @@ use email::{
},
smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind},
};
use inquire::validator::{ErrorMessage, StringValidator, Validation};
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{
backend::config::BackendConfig,
ui::{prompt, THEME},
wizard_log, wizard_prompt,
};
use crate::{backend::config::BackendConfig, ui::prompt, wizard_log};
const ENCRYPTIONS: &[SmtpEncryptionKind] = &[
SmtpEncryptionKind::Tls,
@ -33,12 +29,35 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
#[derive(Clone, Copy)]
struct U16Validator;
impl StringValidator for U16Validator {
fn validate(
&self,
input: &str,
) -> std::prelude::v1::Result<Validation, inquire::CustomUserError> {
if input.parse::<u16>().is_ok() {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid(ErrorMessage::Custom(format!(
"you should enter a number between {} and {}",
u16::MIN,
u16::MAX
))))
}
}
}
#[cfg(feature = "account-discovery")]
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator, Confirm, Password, Select, Text};
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
@ -54,10 +73,9 @@ pub(crate) async fn configure(
let default_host =
autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1));
let host = Input::with_theme(&*THEME)
.with_prompt("SMTP hostname")
.default(default_host)
.interact()?;
let host = Text::new("SMTP hostname")
.with_default(&default_host)
.prompt()?;
let autoconfig_encryption = autoconfig_server
.and_then(|smtp| {
@ -75,11 +93,9 @@ pub(crate) async fn configure(
SmtpEncryptionKind::None => 2,
};
let encryption_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP encryption")
.items(ENCRYPTIONS)
.default(default_encryption_idx)
.interact_opt()?;
let encryption_kind = Select::new("SMTP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(default_encryption_idx)
.prompt_skippable()?;
let autoconfig_port = autoconfig_server
.and_then(|s| s.port())
@ -90,24 +106,27 @@ pub(crate) async fn configure(
SmtpEncryptionKind::None => 25,
});
let (encryption, default_port) = match encryption_idx {
Some(idx) if idx == default_encryption_idx => {
let (encryption, default_port) = match encryption_kind {
Some(idx)
if &idx
== ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre(
"something impossible happened during finding default match for encryption.",
)? =>
{
(Some(autoconfig_encryption), autoconfig_port)
}
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => {
(Some(SmtpEncryptionKind::Tls), 465)
}
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => {
(Some(SmtpEncryptionKind::StartTls), 587)
}
Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465),
Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587),
_ => (Some(SmtpEncryptionKind::None), 25),
};
let port = Input::with_theme(&*THEME)
.with_prompt("SMTP port")
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
.default(default_port.to_string())
.interact()
let port = Text::new("SMTP port")
.with_validators(&[
Box::new(validator::MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let autoconfig_login = autoconfig_server.map(|smtp| match smtp.username() {
@ -118,10 +137,9 @@ pub(crate) async fn configure(
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Input::with_theme(&*THEME)
.with_prompt("SMTP login")
.default(default_login)
.interact()?;
let login = Text::new("SMTP login")
.with_default(&default_login)
.prompt()?;
let default_oauth2_enabled = autoconfig_server
.and_then(|smtp| {
@ -132,10 +150,9 @@ pub(crate) async fn configure(
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
let oauth2_enabled = Confirm::new()
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
.default(default_oauth2_enabled)
.interact_opt()?
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
let auth = if oauth2_enabled {
@ -143,25 +160,21 @@ pub(crate) async fn configure(
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 client id")
.interact()?;
config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 client secret")
.interact()?;
let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config
@ -172,38 +185,26 @@ pub(crate) async fn configure(
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 authorization URL")
.default(default_auth_url)
.interact()?;
config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 token URL")
.default(default_token_url)
.interact()?;
config.token_url = Text::new("SMTP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::with_theme(&*THEME)
.with_prompt(prompt)
.items(scopes)
.default(0)
.interact_opt()?
.and_then(|idx| scopes.get(idx))
.map(|scope| scope.to_string()),
None => Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()),
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
})
};
@ -212,12 +213,9 @@ pub(crate) async fn configure(
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more SMTP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
@ -236,12 +234,9 @@ pub(crate) async fn configure(
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to enable PKCE verification?"
))
.default(true)
.interact_opt()?
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -289,26 +284,23 @@ pub(crate) async fn configure(
SmtpAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => {
Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret
.set_only_keyring(prompt::passwd("SMTP password")?)
.await?;
secret
}
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-smtp-passwd"))
.interact()?,
Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(sec) if sec == CMD => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-smtp-passwd"))
.prompt()?,
),
_ => Default::default(),
};
@ -329,47 +321,42 @@ pub(crate) async fn configure(
#[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text};
let default_host = format!("smtp.{}", email.rsplit_once('@').unwrap().1);
let host = Input::with_theme(&*THEME)
.with_prompt("SMTP hostname")
.default(default_host)
.interact()?;
let host = Text::new("SMTP hostname")
.with_default(&default_host)
.prompt()?;
let encryption_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP encryption")
.items(ENCRYPTIONS)
.default(0)
.interact_opt()?;
let encryption_idx = Select::new("SMTP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let (encryption, default_port) = match encryption_idx {
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => {
(Some(SmtpEncryptionKind::Tls), 465)
}
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => {
(Some(SmtpEncryptionKind::StartTls), 587)
}
Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465),
Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587),
_ => (Some(SmtpEncryptionKind::None), 25),
};
let port = Input::with_theme(&*THEME)
.with_prompt("SMTP port")
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
.default(default_port.to_string())
.interact()
let port = Text::new("SMTP port")
.with_validators(&[
Box::new(MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let default_login = email.to_owned();
let login = Input::with_theme(&*THEME)
.with_prompt("SMTP login")
.default(default_login)
.interact()?;
let login = Text::new("SMTP login")
.with_default(&default_login)
.prompt()?;
let oauth2_enabled = Confirm::new()
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
.default(false)
.interact_opt()?
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
let auth = if oauth2_enabled {
@ -377,25 +364,21 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 client id")
.interact()?;
config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 client secret")
.interact()?;
let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config
@ -403,23 +386,12 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
.set_only_keyring(&client_secret)
.await?;
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 authorization URL")
.interact()?;
config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL").prompt()?;
config.token_url = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 token URL")
.interact()?;
config.token_url = Text::new("SMTP OAuth 2.0 token URL").prompt()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()))
Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()))
};
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
@ -427,12 +399,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more SMTP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
@ -451,12 +420,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to enable PKCE verification?"
))
.default(true)
.interact_opt()?
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -504,26 +470,23 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
SmtpAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => {
Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret
.set_only_keyring(prompt::passwd("SMTP password")?)
.await?;
secret
}
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-smtp-passwd"))
.interact()?,
Some(RAW) => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(CMD) => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-smtp-passwd"))
.prompt()?,
),
_ => Default::default(),
};

View file

@ -4,10 +4,10 @@ use tracing_error::ErrorLayer;
use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter};
pub fn install() -> Result<LevelFilter> {
let fmt_layer = fmt::layer().with_target(false);
let fmt_layer = fmt::layer();
let (filter_layer, current_filter) = match EnvFilter::try_from_default_env() {
Err(_) => (EnvFilter::try_new("warn").unwrap(), LevelFilter::OFF),
Err(_) => (EnvFilter::try_new("off").unwrap(), LevelFilter::OFF),
Ok(layer) => {
let level = layer.max_level_hint().unwrap_or(LevelFilter::OFF);
(layer, level)

View file

@ -1,7 +1,7 @@
use color_eyre::Result;
use dialoguer::Select;
use std::fmt::Display;
use super::THEME;
use color_eyre::Result;
use inquire::Select;
#[derive(Clone, Debug)]
pub enum PreEditChoice {
@ -10,13 +10,17 @@ pub enum PreEditChoice {
Quit,
}
impl ToString for PreEditChoice {
fn to_string(&self) -> String {
match self {
Self::Edit => "Edit it".into(),
Self::Discard => "Discard it".into(),
Self::Quit => "Quit".into(),
}
impl Display for PreEditChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Edit => "Edit it",
Self::Discard => "Discard it",
Self::Quit => "Quit",
}
)
}
}
@ -27,13 +31,15 @@ pub fn pre_edit() -> Result<PreEditChoice> {
PreEditChoice::Quit,
];
let choice_idx = Select::with_theme(&*THEME)
.with_prompt("A draft was found, what would you like to do with it?")
.items(&choices)
.default(0)
.interact()?;
let user_choice = Select::new(
"A draft was found, what would you like to do with it?",
choices.to_vec(),
)
.with_starting_cursor(0)
.with_vim_mode(true)
.prompt()?;
Ok(choices[choice_idx].clone())
Ok(user_choice)
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -45,15 +51,19 @@ pub enum PostEditChoice {
Discard,
}
impl ToString for PostEditChoice {
fn to_string(&self) -> String {
match self {
Self::Send => "Send it".into(),
Self::Edit => "Edit it again".into(),
Self::LocalDraft => "Save it as local draft".into(),
Self::RemoteDraft => "Save it as remote draft".into(),
Self::Discard => "Discard it".into(),
}
impl Display for PostEditChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Send => "Send it",
Self::Edit => "Edit it again",
Self::LocalDraft => "Save it as local draft",
Self::RemoteDraft => "Save it as remote draft",
Self::Discard => "Discard it",
}
)
}
}
@ -66,11 +76,13 @@ pub fn post_edit() -> Result<PostEditChoice> {
PostEditChoice::Discard,
];
let choice_idx = Select::with_theme(&*THEME)
.with_prompt("What would you like to do with this message?")
.items(&choices)
.default(0)
.interact()?;
let user_choice = inquire::Select::new(
"What would you like to do with this message?",
choices.to_vec(),
)
.with_starting_cursor(0)
.with_vim_mode(true)
.prompt()?;
Ok(choices[choice_idx].clone())
Ok(user_choice)
}

View file

@ -1,11 +1,3 @@
pub mod choice;
pub mod editor;
pub(crate) mod prompt;
pub mod table;
use dialoguer::theme::ColorfulTheme;
use once_cell::sync::Lazy;
pub use self::table::*;
pub(crate) static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);

View file

@ -1,21 +1,28 @@
use dialoguer::Password;
use std::io;
use super::THEME;
pub(crate) fn passwd(prompt: &str) -> io::Result<String> {
Password::with_theme(&*THEME)
.with_prompt(prompt)
.with_confirmation(
"Confirm password",
"Passwords do not match, please try again.",
)
.interact()
inquire::Password::new(prompt)
.with_custom_confirmation_message("Confirm password")
.with_custom_confirmation_error_message("Passwords do not match, please try again.")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Interrupted,
format!("failed to get password: {e}"),
)
})
}
pub(crate) fn secret(prompt: &str) -> io::Result<String> {
Password::with_theme(&*THEME)
.with_prompt(prompt)
.report(false)
.interact()
inquire::Password::new(prompt)
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.without_confirmation()
.prompt()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Interrupted,
format!("failed to get secret: {e}"),
)
})
}

View file

@ -1,13 +0,0 @@
use clap::Parser;
/// The table max width argument parser.
#[derive(Debug, Default, Parser)]
pub struct TableMaxWidthFlag {
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
pub max_width: Option<usize>,
}

View file

@ -1 +0,0 @@
pub mod max_width;

View file

@ -1,5 +0,0 @@
pub mod arg;
#[allow(clippy::module_inception)]
pub mod table;
pub use table::*;

View file

@ -1,446 +0,0 @@
//! Toolbox for building responsive tables.
//! A table is composed of rows, a row is composed of cells.
//! The toolbox uses the [builder design pattern].
//!
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use color_eyre::{eyre::Context, Result};
use email::email::config::EmailTextPlainFormat;
use termcolor::{Color, ColorSpec};
use terminal_size::terminal_size;
use tracing::trace;
use unicode_width::UnicodeWidthStr;
use crate::printer::{Print, PrintTableOpts, WriteColor};
/// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate.
/// TODO: make this customizable.
pub const DEFAULT_TERM_WIDTH: usize = 80;
/// Defines the minimum size of a shrunk cell.
/// TODO: make this customizable.
pub const MAX_SHRINK_WIDTH: usize = 5;
/// Represents a cell in a table.
#[derive(Debug, Default)]
pub struct Cell {
/// Represents the style of the cell.
style: ColorSpec,
/// Represents the content of the cell.
value: String,
/// (Dis)allowes the cell to shrink when the table exceeds the container width.
shrinkable: bool,
}
impl Cell {
pub fn new<T: AsRef<str>>(value: T) -> Self {
Self {
// Removes carriage returns, new line feeds, tabulations
// and [variation selectors].
//
// [variation selectors]: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
value: String::from(value.as_ref()).replace(
|c| ['\r', '\n', '\t', '\u{fe0e}', '\u{fe0f}'].contains(&c),
"",
),
..Self::default()
}
}
/// Returns the unicode width of the cell's value.
pub fn unicode_width(&self) -> usize {
UnicodeWidthStr::width(self.value.as_str())
}
/// Makes the cell shrinkable. If the table exceeds the terminal width, this cell will be the
/// one to shrink in order to prevent the table to overflow.
pub fn shrinkable(mut self) -> Self {
self.shrinkable = true;
self
}
/// Returns the shrinkable state of a cell.
pub fn is_shrinkable(&self) -> bool {
self.shrinkable
}
/// Applies the bold style to the cell.
pub fn bold(mut self) -> Self {
self.style.set_bold(true);
self
}
/// Applies the bold style to the cell conditionally.
pub fn bold_if(self, predicate: bool) -> Self {
if predicate {
self.bold()
} else {
self
}
}
/// Applies the underline style to the cell.
pub fn underline(mut self) -> Self {
self.style.set_underline(true);
self
}
/// Applies the red color to the cell.
pub fn red(mut self) -> Self {
self.style.set_fg(Some(Color::Red));
self
}
/// Applies the green color to the cell.
pub fn green(mut self) -> Self {
self.style.set_fg(Some(Color::Green));
self
}
/// Applies the yellow color to the cell.
pub fn yellow(mut self) -> Self {
self.style.set_fg(Some(Color::Yellow));
self
}
/// Applies the blue color to the cell.
pub fn blue(mut self) -> Self {
self.style.set_fg(Some(Color::Blue));
self
}
/// Applies the white color to the cell.
pub fn white(mut self) -> Self {
self.style.set_fg(Some(Color::White));
self
}
/// Applies the custom ansi color to the cell.
pub fn ansi_256(mut self, code: u8) -> Self {
self.style.set_fg(Some(Color::Ansi256(code)));
self
}
}
/// Makes the cell printable.
impl Print for Cell {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
// Applies colors to the cell
writer
.set_color(&self.style)
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
// Writes the colorized cell to stdout
write!(writer, "{}", self.value)
.context(format!(r#"cannot print cell "{}""#, self.value))?;
Ok(writer.reset()?)
}
}
/// Represents a row in a table.
#[derive(Debug, Default)]
pub struct Row(
/// Represents a list of cells.
pub Vec<Cell>,
);
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn cell(mut self, cell: Cell) -> Self {
self.0.push(cell);
self
}
}
/// Represents a table abstraction.
pub trait Table
where
Self: Sized,
{
/// Defines the header row.
fn head() -> Row;
/// Defines the row template.
fn row(&self) -> Row;
/// Writes the table to the writer.
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
let is_format_flowed = matches!(opts.format, EmailTextPlainFormat::Flowed);
let max_width = match opts.format {
EmailTextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width),
EmailTextPlainFormat::Flowed => 0,
EmailTextPlainFormat::Auto => opts
.max_width
.or_else(|| terminal_size().map(|(w, _)| w.0 as usize))
.unwrap_or(DEFAULT_TERM_WIDTH),
};
let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
table.extend(
items
.iter()
.map(|item| {
let row = item.row();
row.0.iter().enumerate().for_each(|(i, cell)| {
cell_widths[i] = cell_widths[i].max(cell.unicode_width());
});
row
})
.collect::<Vec<_>>(),
);
trace!("cell widths: {:?}", cell_widths);
let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
trace!("table width: {}", table_width);
for row in table.iter_mut() {
let mut glue = Cell::default();
for (i, cell) in row.0.iter_mut().enumerate() {
glue.print(writer)?;
let table_is_overflowing = table_width > max_width;
if table_is_overflowing && !is_format_flowed && cell.is_shrinkable() {
trace!("table is overflowing and cell is shrinkable");
let shrink_width = table_width - max_width;
trace!("shrink width: {}", shrink_width);
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
cell_widths[i] - shrink_width
} else {
MAX_SHRINK_WIDTH
};
trace!("cell width: {}", cell_width);
trace!("cell unicode width: {}", cell.unicode_width());
let cell_is_overflowing = cell.unicode_width() > cell_width;
if cell_is_overflowing {
trace!("cell is overflowing");
let mut value = String::new();
let mut chars_width = 0;
for c in cell.value.chars() {
let char_width = UnicodeWidthStr::width(c.to_string().as_str());
if chars_width + char_width >= cell_width {
break;
}
chars_width += char_width;
value.push(c);
}
value.push_str("");
trace!("chars width: {}", chars_width);
trace!("shrunk value: {}", value);
let spaces_count = cell_width - chars_width - 1;
trace!("number of spaces added to shrunk value: {}", spaces_count);
value.push_str(&" ".repeat(spaces_count));
cell.value = value;
} else {
trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
} else {
trace!("table is not overflowing or cell is not shrinkable");
trace!("cell width: {}", cell_widths[i]);
trace!("cell unicode width: {}", cell.unicode_width());
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
cell.print(writer)?;
glue = Cell::new("").ansi_256(8);
}
writeln!(writer)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use email::email::config::EmailTextPlainFormat;
use std::io;
use super::*;
#[derive(Debug, Default)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
struct Item {
id: u16,
name: String,
desc: String,
}
impl<'a> Item {
pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
Self {
id,
name: String::from(name),
desc: String::from(desc),
}
}
}
impl Table for Item {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID"))
.cell(Cell::new("NAME").shrinkable())
.cell(Cell::new("DESC"))
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(self.id.to_string()))
.cell(Cell::new(self.name.as_str()).shrinkable())
.cell(Cell::new(self.desc.as_str()))
}
}
macro_rules! write_items {
($writer:expr, $($item:expr),*) => {
Table::print($writer, &[$($item,)*], PrintTableOpts { format: &EmailTextPlainFormat::Auto, max_width: Some(20) }).unwrap();
};
}
#[test]
fn row_smaller_than_head() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2, "b", "bb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2 │b │bb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn row_bigger_than_head() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writer.content);
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │cccccc │cc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn basic_shrink() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "", "desc"),
Item::new(2, "short", "desc"),
Item::new(3, "loooooong", "desc"),
Item::new(4, "shriiiiink", "desc"),
Item::new(5, "shriiiiiiiiiink", "desc"),
Item::new(6, "😍😍😍😍", "desc"),
Item::new(7, "😍😍😍😍😍", "desc"),
Item::new(8, "!😍😍😍😍😍", "desc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │ │desc \n",
"2 │short │desc \n",
"3 │loooooong │desc \n",
"4 │shriiiii… │desc \n",
"5 │shriiiii… │desc \n",
"6 │😍😍😍😍 │desc \n",
"7 │😍😍😍😍… │desc \n",
"8 │!😍😍😍… │desc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn max_shrink_width() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1111, "shriiiiiiiink", "desc very looong"),
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
];
let expected = concat![
"ID │NAME │DESC \n",
"1111 │shri… │desc very looong \n",
"2222 │shri… │desc very loooooooooong \n",
];
assert_eq!(expected, writer.content);
}
}