mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-01 06:55:13 +00:00
Compare commits
5 commits
c779081381
...
f3151c3f84
Author | SHA1 | Date | |
---|---|---|---|
|
f3151c3f84 | ||
|
098ae380c3 | ||
|
1e448e56eb | ||
|
d54dd6429e | ||
|
9dee1784df |
468
Cargo.lock
generated
468
Cargo.lock
generated
|
@ -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",
|
||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -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" }
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:?}…");
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub mod max_width;
|
|
@ -1,5 +0,0 @@
|
|||
pub mod arg;
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod table;
|
||||
|
||||
pub use table::*;
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue