Compare commits
125 Commits
v0.8.5-rc.
...
master
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 4bdfb3a31b | |
Manos Pitsidianakis | 671d35e21e | |
Manos Pitsidianakis | a4ebe3b7d4 | |
Manos Pitsidianakis | 57e3e643a1 | |
Manos Pitsidianakis | a8c7582fa3 | |
Manos Pitsidianakis | a9c3b151f1 | |
Manos Pitsidianakis | 1abce964c7 | |
Manos Pitsidianakis | 735b44f286 | |
Manos Pitsidianakis | 50ff16c44f | |
Manos Pitsidianakis | 9ca34a6864 | |
Manos Pitsidianakis | 8fff740176 | |
Manos Pitsidianakis | 8eaf03554f | |
Manos Pitsidianakis | 8ec6f22090 | |
Manos Pitsidianakis | b5ddc397df | |
Manos Pitsidianakis | 46e40856ba | |
Manos Pitsidianakis | 35408b1689 | |
Manos Pitsidianakis | 5d915baa81 | |
Manos Pitsidianakis | 684fae3ed8 | |
Manos Pitsidianakis | ab04189887 | |
Manos Pitsidianakis | 36b7c00b97 | |
Manos Pitsidianakis | 3a5306e9dd | |
Manos Pitsidianakis | 89c7972e12 | |
Manos Pitsidianakis | 8f3dee9b22 | |
Manos Pitsidianakis | 660022ce23 | |
Manos Pitsidianakis | 29cc1bce5b | |
Manos Pitsidianakis | bc1b65316d | |
Manos Pitsidianakis | 11a0586d56 | |
Manos Pitsidianakis | f70496f14c | |
Manos Pitsidianakis | 8a16cf6db4 | |
Manos Pitsidianakis | 11f3077b06 | |
Manos Pitsidianakis | dedee908d1 | |
Manos Pitsidianakis | 255e93764a | |
Manos Pitsidianakis | c5e9e67604 | |
Manos Pitsidianakis | ae96038fbf | |
Manos Pitsidianakis | 07072e2e3f | |
Manos Pitsidianakis | aa5737a004 | |
Manos Pitsidianakis | 48cb9ee204 | |
Guillaume Ranquet | c53a32de4c | |
Manos Pitsidianakis | a69c674c07 | |
Manos Pitsidianakis | 6a66afe93e | |
Manos Pitsidianakis | 974502c6ff | |
Manos Pitsidianakis | 3e9144657b | |
Manos Pitsidianakis | 35a9f33aab | |
Manos Pitsidianakis | 475609fe92 | |
Manos Pitsidianakis | 38bca8f8bc | |
Manos Pitsidianakis | ec01a4412a | |
Manos Pitsidianakis | 4e941a9e8b | |
Manos Pitsidianakis | 742f038f74 | |
Manos Pitsidianakis | 484712b0c3 | |
Manos Pitsidianakis | 264782d228 | |
Manos Pitsidianakis | 41e965b8a3 | |
Manos Pitsidianakis | f31b5c4000 | |
Manos Pitsidianakis | 8014af2563 | |
Manos Pitsidianakis | 4ce616aeca | |
Manos Pitsidianakis | a3aaec382a | |
Manos Pitsidianakis | b8b24282a0 | |
Manos Pitsidianakis | e481880321 | |
Geert Stappers | a88b8c5ea0 | |
Manos Pitsidianakis | b820bd6d9c | |
Manos Pitsidianakis | 3b93fa8e7c | |
Manos Pitsidianakis | 634bd1917a | |
Manos Pitsidianakis | b5fd3f57a7 | |
Manos Pitsidianakis | 1fcb1d59b8 | |
Manos Pitsidianakis | e2cdebe89c | |
Manos Pitsidianakis | 3884c0da1f | |
Manos Pitsidianakis | 26928e3ae9 | |
Manos Pitsidianakis | 070930e671 | |
Manos Pitsidianakis | c7aee72525 | |
Manos Pitsidianakis | 30a3205e4f | |
Manos Pitsidianakis | 9af284b8db | |
Manos Pitsidianakis | 62aee4644b | |
Manos Pitsidianakis | 5af2e1ee66 | |
Manos Pitsidianakis | 4e7b665672 | |
Manos Pitsidianakis | fd64fe0bf8 | |
Manos Pitsidianakis | 51e3f163d4 | |
Manos Pitsidianakis | 417b24cd84 | |
Manos Pitsidianakis | 873a67d0fb | |
Manos Pitsidianakis | c332c2f5ff | |
Manos Pitsidianakis | 1048ce6824 | |
Manos Pitsidianakis | 70fc2b455c | |
Manos Pitsidianakis | 8de8addd11 | |
Manos Pitsidianakis | 1fe3619208 | |
Manos Pitsidianakis | 0b468d88ad | |
Manos Pitsidianakis | 1eca34b398 | |
Manos Pitsidianakis | 5afc078587 | |
Guillaume Ranquet | a37d5fc1d1 | |
Manos Pitsidianakis | 60f26f9dae | |
Ethra | e80ea9c9de | |
Manos Pitsidianakis | 64e60cb0ee | |
Manos Pitsidianakis | 81d1c0536b | |
Manos Pitsidianakis | cd448924ed | |
Manos Pitsidianakis | 61a0c3c27f | |
Manos Pitsidianakis | 7952006870 | |
Manos Pitsidianakis | ddab3179c2 | |
Manos Pitsidianakis | 7861fb0402 | |
Manos Pitsidianakis | 148f0433d9 | |
Manos Pitsidianakis | 8185f2cf7d | |
Manos Pitsidianakis | 0270db0123 | |
Manos Pitsidianakis | 8ddd673dd8 | |
Manos Pitsidianakis | e3351d2755 | |
Manos Pitsidianakis | 31401fa35c | |
Manos Pitsidianakis | 33408146a1 | |
Manos Pitsidianakis | 8a95febb78 | |
Manos Pitsidianakis | 73d5b24e98 | |
Manos Pitsidianakis | 0da97dd8c1 | |
Manos Pitsidianakis | 933bf157ae | |
Manos Pitsidianakis | f685726eac | |
Manos Pitsidianakis | ab1b946fd9 | |
Manos Pitsidianakis | ce4ba06ce9 | |
Manos Pitsidianakis | bebb473d1b | |
Manos Pitsidianakis | f0866a3965 | |
Manos Pitsidianakis | f63774fa6d | |
Manos Pitsidianakis | 808aa4942d | |
Manos Pitsidianakis | 08518e1ca8 | |
Manos Pitsidianakis | 34a2d52e7e | |
Manos Pitsidianakis | 4026e25428 | |
Manos Pitsidianakis | ca7d7bb95d | |
Manos Pitsidianakis | ebe1b3da7e | |
Manos Pitsidianakis | 506ae9f594 | |
Manos Pitsidianakis | b6f769b2f4 | |
Manos Pitsidianakis | 3691cd2962 | |
Manos Pitsidianakis | 97eb636375 | |
Manos Pitsidianakis | b3079715f6 | |
Manos Pitsidianakis | 86bbf1ea57 | |
Manos Pitsidianakis | 1b0bdd0a9a |
|
@ -5,7 +5,7 @@ env:
|
|||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
|
@ -56,7 +56,8 @@ jobs:
|
|||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
||||
source "${HOME}/.cargo/env"
|
||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
||||
rustup toolchain install --profile minimal --component clippy,rustfmt ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
rustup toolchain install --profile minimal --component clippy,rustfmt --target ${{ matrix.target }} -- "${{ matrix.rust }}"
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
|
|
|
@ -5,7 +5,7 @@ env:
|
|||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ env:
|
|||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
|
@ -90,11 +90,14 @@ jobs:
|
|||
if: success() || failure()
|
||||
run: cargo test --all --no-fail-fast --all-features --no-run --locked
|
||||
- name: cargo test
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
|
||||
#cargo test --all --no-fail-fast --all-features -- --nocapture --quiet
|
||||
- name: rustdoc build
|
||||
if: success() || failure()
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
make build-rustdoc
|
||||
- name: rustdoc tests
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make test-docs
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
# Build `meli`
|
||||
|
||||
For a quick start, build and install locally:
|
||||
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
Available subcommands for `make` are listed with `make help`.
|
||||
The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
`meli` requires rust version 1.70.0 or later and rust's package manager, Cargo.
|
||||
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||
|
||||
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
|
||||
Run `make install` to install the binary and man pages.
|
||||
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
|
||||
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
|
||||
## Build features
|
||||
|
||||
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
|
||||
|
||||
- `gpgme` enables GPG support via `libgpgme` (on by default)
|
||||
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
|
||||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
|
||||
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
|
||||
- `regexp` provides experimental support for theming some e-mail fields based
|
||||
on regular expressions.
|
||||
It uses the `pcre2` library.
|
||||
Since it's actual use in the code is very limited, it is not recommended to use this (off by default).
|
||||
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
|
||||
|
||||
Though not a feature, the presence of the environment variable `UNICODE_REGENERATE_TABLES` in compile-time of the `melib` crate will force the regeneration of unicode tables.
|
||||
Otherwise the tables are included with the source code, and there's no real reason to regenerate them unless you intend to modify the code or update to a new Unicode version.
|
||||
|
||||
## Build Debian package (*deb*)
|
||||
|
||||
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
|
||||
|
||||
A `*.deb` package can be built with `make deb-dist`
|
||||
|
||||
## Using notmuch
|
||||
|
||||
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
|
||||
In Debian-like systems, install the `libnotmuch5` packages.
|
||||
`meli` detects the library's presence on runtime.
|
||||
If it is not detected, you can use the `library_file_path` setting on your notmuch account to specify the absolute path of the library.
|
||||
|
||||
## Using GPG
|
||||
|
||||
To use the optional gpg feature, you must have `libgpgme` installed in your system.
|
||||
In Debian-like systems, install the `libgpgme11` package.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
## Development
|
||||
|
||||
Development builds can be built and/or run with
|
||||
|
||||
```
|
||||
cargo build
|
||||
cargo run
|
||||
```
|
||||
|
||||
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
|
||||
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
|
||||
|
||||
To trace network and protocol communications you can enable the following features:
|
||||
|
||||
- `imap-trace`
|
||||
- `jmap-trace`
|
||||
- `nntp-trace`
|
||||
- `smtp-trace`
|
1079
CHANGELOG.md
1079
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,23 @@
|
|||
# Development
|
||||
|
||||
Code style follows the `rustfmt.toml` file.
|
||||
|
||||
## Trace logs
|
||||
|
||||
Enable trace logs to `stderr` with:
|
||||
|
||||
```sh
|
||||
export MELI_DEBUG_STDERR=yes
|
||||
```
|
||||
|
||||
This means you will have to to redirect `stderr` to a file like `meli 2> trace.log`.
|
||||
|
||||
Tracing is opt-in by build features:
|
||||
|
||||
```sh
|
||||
cargo build --features=debug-tracing,imap-trace,smtp-trace
|
||||
```
|
||||
|
||||
## use `.git-blame-ignore-revs` file _optional_
|
||||
|
||||
Use this file to ignore formatting commits from `git-blame`.
|
||||
|
@ -9,8 +27,24 @@ It needs to be set up per project because `git-blame` will fail if it's missing.
|
|||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
```
|
||||
|
||||
## Formatting with `rustfmt`
|
||||
|
||||
```sh
|
||||
make fmt
|
||||
```
|
||||
|
||||
## Linting with `clippy`
|
||||
|
||||
```sh
|
||||
make lint
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
|
||||
How to run specific tests:
|
||||
|
||||
```sh
|
||||
|
@ -20,14 +54,14 @@ cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
|
|||
## Profiling
|
||||
|
||||
```sh
|
||||
perf record -g target/debug/bin
|
||||
perf record -g target/debug/meli
|
||||
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
|
||||
```
|
||||
|
||||
## Running fuzz targets
|
||||
|
||||
Note: `cargo-fuzz` requires the nightly toolchain.
|
||||
|
||||
```sh
|
||||
cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict
|
||||
```
|
||||
<!-- -->
|
||||
<!-- ## Running fuzz targets -->
|
||||
<!-- -->
|
||||
<!-- Note: `cargo-fuzz` requires the nightly toolchain. -->
|
||||
<!-- -->
|
||||
<!-- ```sh -->
|
||||
<!-- cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict -->
|
||||
<!-- ``` -->
|
||||
|
|
38
Makefile
38
Makefile
|
@ -24,6 +24,7 @@ TAGREF_BIN ?= tagref
|
|||
CARGO_ARGS ?=
|
||||
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility
|
||||
CARGO_SORT_BIN = cargo-sort
|
||||
CARGO_HACK_BIN = cargo-hack
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
# Options
|
||||
|
@ -119,6 +120,10 @@ test: test-docs
|
|||
test-docs:
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --doc
|
||||
|
||||
.PHONY: test-feature-permutations
|
||||
test-feature-permutations:
|
||||
$(CARGO_HACK_BIN) hack --feature-powerset
|
||||
|
||||
.PHONY: check-deps
|
||||
check-deps:
|
||||
@(if ! echo ${MIN_RUSTC}\\n`${CARGO_BIN} --version | grep ^cargo | cut -d ' ' -f 2` | sort -CV; then echo "rust version >= ${RED}${MIN_RUSTC}${ANSI_RESET} required, found: `which ${CARGO_BIN}` `${CARGO_BIN} --version | cut -d ' ' -f 2`" \
|
||||
|
@ -178,10 +183,11 @@ install-bin: meli
|
|||
.NOTPARALLEL: yes
|
||||
install: meli install-bin install-doc
|
||||
@(if [ -z $${NO_MAN+x} ]; then \
|
||||
echo "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
|
||||
$(PRINTF) "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
|
||||
$(PRINTF) "\n or the tutorial in meli(7) (\`man 7 meli\`).\n" ;\
|
||||
fi)
|
||||
@echo " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli.delivery${ANSI_RESET}"
|
||||
@echo " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker."
|
||||
@$(PRINTF) " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli-email.org${ANSI_RESET}\n"
|
||||
@$(PRINTF) " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker.\n"
|
||||
|
||||
.PHONY: dist
|
||||
dist:
|
||||
|
@ -192,7 +198,7 @@ dist:
|
|||
deb-dist:
|
||||
@author=$(grep -m1 authors meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
@dpkg-buildpackage -b -rfakeroot -us -uc --build-by="${author}" --release-by="${author}"
|
||||
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_amd64.deb
|
||||
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_`dpkg --print-architecture`.deb
|
||||
|
||||
.PHONY: build-rustdoc
|
||||
build-rustdoc:
|
||||
|
@ -216,3 +222,27 @@ test-makefile:
|
|||
@export DATEVAL=$$(printf "%s" ${DATE} | wc -c | tr -d "[:blank:]" 2>&1); ([ "$${DATEVAL}" = "10" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${RED}ERROR${ANSI_RESET}\n'date -I' does not produce a YYYY-MM-DD output on this platform.\n" 1>&2
|
||||
@$(PRINTF) "Checking that the git commit SHA can be detected. "
|
||||
@([ ! -z "$(GIT_COMMIT)" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${YELLOW}WARN${ANSI_RESET}\nGIT_COMMIT env var is empty.\n" 1>&2
|
||||
|
||||
# Checking if mdoc changes produce new lint warnings from mandoc(1) compared to HEAD version:
|
||||
#
|
||||
# example invocation: `mandoc_lint meli.1`
|
||||
#
|
||||
# with diff(1)
|
||||
# ============
|
||||
#function mandoc_lint () {
|
||||
#diff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
|
||||
#}
|
||||
#
|
||||
# with sdiff(1) (side by side)
|
||||
# ============================
|
||||
#
|
||||
#function mandoc_lint () {
|
||||
#sdiff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
|
||||
#}
|
||||
#
|
||||
# with delta(1)
|
||||
# =============
|
||||
#
|
||||
#function mandoc_lint () {
|
||||
#delta --side-by-side <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
|
||||
#}
|
||||
|
|
152
README.md
152
README.md
|
@ -1,27 +1,46 @@
|
|||
# meli [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
|
||||
# meli ![Established, created in 2017](https://img.shields.io/badge/Est.-2017-blue) ![Minimum Supported Rust Version](https://img.shields.io/badge/MSRV-1.70.0-blue) [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
|
||||
|
||||
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
|
||||
**BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
|
||||
|
||||
* [mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC
|
||||
* Main repository: <https://git.meli.delivery/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
* Official mirrors:
|
||||
- <https://codeberg.org/epilys/meli>
|
||||
- <https://github.com/meli/meli>
|
||||
Try an [old online interactive web demo](https://meli-email.org/wasm2.html "online interactive web demo") powered by WebAssembly!
|
||||
|
||||
* `#meli` on OFTC IRC | [mailing lists](https://lists.meli-email.org/)
|
||||
* Repository:
|
||||
- Main <https://git.meli-email.org/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli-email.org/meli/meli/issues "meli gitea issue tracker")
|
||||
- Official mirror <https://codeberg.org/meli/meli>
|
||||
- Official mirror <https://github.com/meli/meli>
|
||||
|
||||
**Table of contents**:
|
||||
|
||||
- [Install](#install)
|
||||
- [Build](#build)
|
||||
- [Quick start](#quick-start)
|
||||
- [Supported E-mail backends](#supported-e-mail-backends)
|
||||
- [E-mail submission backends](#e-mail-submission-backends)
|
||||
- [Non-exhaustive list of features](#non-exhaustive-list-of-features)
|
||||
- [HTML Rendering](#html-rendering)
|
||||
- [Documentation](#documentation)
|
||||
|
||||
## Install
|
||||
|
||||
- Try an [old online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
|
||||
- Pre-built binaries for [pkgsrc](https://pkgsrc.se/mail/meli) and [openbsd ports](https://openports.pl/path/mail/meli).
|
||||
- `cargo install meli` or `cargo install --git https://git.meli.delivery/meli/meli.git meli`
|
||||
- [Download and install pre-built debian package, static linux binary](https://github.com/meli/meli/releases/ "github releases for meli"), or
|
||||
- Install with [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'").
|
||||
- [pkgsrc](https://pkgsrc.se/mail/meli)
|
||||
- [openbsd ports](https://openports.pl/path/mail/meli)
|
||||
- `cargo install meli` or `cargo install --git https://git.meli-email.org/meli/meli.git meli`
|
||||
- [Pre-built debian package, static binaries](https://github.com/meli/meli/releases/ "github releases for meli")
|
||||
- [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'")
|
||||
|
||||
## Build
|
||||
|
||||
Run `cargo build --release --bin meli` or `make`.
|
||||
|
||||
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
|
||||
|
||||
## Quick start
|
||||
|
||||
<table>
|
||||
<tr><td>
|
||||
|
||||
```shell
|
||||
```sh
|
||||
# Create configuration file in ${XDG_CONFIG_HOME}/meli/config.toml:
|
||||
$ meli create-config
|
||||
# Edit configuration in ${EDITOR} or ${VISUAL}:
|
||||
|
@ -36,9 +55,12 @@ $ meli
|
|||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation"). `meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
|
||||
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
|
||||
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
|
||||
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`.
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
|
||||
|
||||
|
@ -100,92 +122,32 @@ Main view | Compact main view | Compose with embed terminal editor
|
|||
- GPG signing, encryption, signing + encryption
|
||||
- GPG signature verification
|
||||
|
||||
## Documentation
|
||||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
|
||||
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
|
||||
Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
|
||||
|
||||
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config cargo run
|
||||
```
|
||||
|
||||
## Build
|
||||
For a quick start, build and install locally:
|
||||
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
Available subcommands for `make` are listed with `make help`.
|
||||
The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
`meli` requires rust version 1.68.2 or later and rust's package manager, Cargo.
|
||||
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||
|
||||
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
|
||||
Run `make install` to install the binary and man pages.
|
||||
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
|
||||
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
|
||||
### Build features
|
||||
|
||||
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
|
||||
|
||||
- `gpgme` enables GPG support via `libgpgme` (on by default)
|
||||
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
|
||||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
|
||||
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
|
||||
- `regexp` provides experimental support for theming some e-mail fields based
|
||||
on regular expressions.
|
||||
It uses the `pcre2` library.
|
||||
Since it's actual use in the code is very limited, it is not recommended to use this (off by default).
|
||||
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
|
||||
|
||||
### Build Debian package (*deb*)
|
||||
|
||||
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
|
||||
|
||||
A `*.deb` package can be built with `make deb-dist`
|
||||
|
||||
### Using notmuch
|
||||
|
||||
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
|
||||
In Debian-like systems, install the `libnotmuch5` packages.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
### Using GPG
|
||||
|
||||
To use the optional gpg feature, you must have `libgpgme` installed in your system.
|
||||
In Debian-like systems, install the `libgpgme11` package.
|
||||
`meli` detects the library's presence on runtime.
|
||||
|
||||
### HTML Rendering
|
||||
## HTML Rendering
|
||||
|
||||
HTML rendering is achieved using [w3m](https://github.com/tats/w3m) by default.
|
||||
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./meli/docs/meli.conf.5)).
|
||||
|
||||
# Development
|
||||
|
||||
Development builds can be built and/or run with
|
||||
## Documentation
|
||||
|
||||
```
|
||||
cargo build
|
||||
cargo run
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
|
||||
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
|
||||
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
|
||||
|
||||
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, or use the `[-c, --config]` argument:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config meli
|
||||
```
|
||||
|
||||
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
|
||||
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
|
||||
or
|
||||
|
||||
Code style follows the `rustfmt.toml` file.
|
||||
```sh
|
||||
meli -c ./test_config
|
||||
```
|
||||
|
|
|
@ -22,10 +22,10 @@ body = """
|
|||
{% if not version %}
|
||||
## [Unreleased]
|
||||
{% else %}
|
||||
## [{{ version }}](https://git.meliemail.org/meli/meli/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
## [{{ version }}](https://git.meli-email.org/meli/meli/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif %}
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=8, end="") }}]({{ "https://git.meliemail.org/meli/meli/commit/" ~ commit.id }}) {% if commit.scope %}*({{commit.scope | lower }})* {% endif %}{{ commit.message | split(pat="\n")| first | upper_first }}{% endmacro -%}
|
||||
- [{{ commit.id | truncate(length=8, end="") }}]({{ "https://git.meli-email.org/meli/meli/commit/" ~ commit.id }}) {% if commit.scope %}*({{commit.scope | lower }})* {% endif %}{{ commit.message | split(pat="\n")| first | upper_first }}{% endmacro -%}
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
|
@ -58,7 +58,7 @@ filter_unconventional = false
|
|||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meliemail.org/meli/meli/issues/${2}))" },
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meli-email.org/meli/meli/issues/${2}))" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"@context": ["https://doi.org/10.5063/schema/codemeta-2.0", "http://schema.org/"],
|
||||
"@type": "SoftwareSourceCode",
|
||||
"applicationCategory": "E-mail client",
|
||||
"author": [
|
||||
{
|
||||
"@id": "https://pitsidianak.is/",
|
||||
"@type": "Person",
|
||||
"name": "epilys",
|
||||
"email": "manos@pitsidianak.is",
|
||||
"familyName": "Pitsidianakis",
|
||||
"givenName": "Manos",
|
||||
"url": "https://pitsidianak.is/"
|
||||
}
|
||||
],
|
||||
"codeRepository": "https://git.meli-email.org/meli/meli.git",
|
||||
"dateCreated": "2016-04-25",
|
||||
"dateModified": "2023-12-11",
|
||||
"datePublished": "2017-07-23",
|
||||
"description": "BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).",
|
||||
"downloadUrl": "https://git.meli-email.org/meli/meli/archive/v0.8.5.tar.gz",
|
||||
"identifier": "https://meli-email.org/",
|
||||
"isPartOf": "https://meli-email.org/",
|
||||
"keywords": [
|
||||
"e-mail",
|
||||
"email",
|
||||
"mail",
|
||||
"terminal user interface",
|
||||
"client",
|
||||
"mua",
|
||||
"mail user agent",
|
||||
"smtp",
|
||||
"imap",
|
||||
"jmap",
|
||||
"mbox",
|
||||
"maildir",
|
||||
"nntp"
|
||||
],
|
||||
"license": [
|
||||
"https://spdx.org/licenses/EUPL-1.2",
|
||||
"https://spdx.org/licenses/GPL-3.0-or-later"
|
||||
],
|
||||
"name": "meli",
|
||||
"operatingSystem": [
|
||||
"Linux",
|
||||
"macOS",
|
||||
"OpenBSD",
|
||||
"NetBSD"
|
||||
],
|
||||
"programmingLanguage": "Rust",
|
||||
"relatedLink": [
|
||||
"https://codeberg.org/meli/meli",
|
||||
"https://github.com/meli/meli",
|
||||
"https://lists.meli-email.org/"
|
||||
],
|
||||
"version": "0.8.5",
|
||||
"contIntegration": "https://git.meli-email.org/meli/meli/actions",
|
||||
"developmentStatus": "active",
|
||||
"issueTracker": "https://git.meli-email.org/meli/meli/issues",
|
||||
"readme": "https://git.meli-email.org/meli/meli/raw/commit/dedee908d1e0b42773bade8e0604e94b14810e2d/README.md",
|
||||
"buildInstructions": "https://git.meli-email.org/meli/meli/raw/commit/dedee908d1e0b42773bade8e0604e94b14810e2d/BUILD.md"
|
||||
}
|
|
@ -1,13 +1,236 @@
|
|||
meli (0.8.5-1) bookworm; urgency=low
|
||||
|
||||
Contributors in alphabetical order:
|
||||
|
||||
- Andrei Zisu
|
||||
- Ethra
|
||||
- Geert Stappers
|
||||
- Guillaume Ranquet
|
||||
- Manos Pitsidianakis
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- 0e3a0c4b Add safe UI widget area drawing API
|
||||
- 0114e695 Add next_search_result and previous_search_result shortcuts
|
||||
- 0b468d88 Improve Error messages
|
||||
- 5af2e1ee Add subcommand to print config file location
|
||||
- 62aee464 Add subcommand to print log file location
|
||||
- e2cdebe8 Add option to highlight self in mailing list threads
|
||||
- cd448924 Add clear-selection command
|
||||
- 3a5306e9 View manpages in pager inside meli
|
||||
- a37d5fc1 Implement a key to command mapping
|
||||
- ce4ba06c Add a flag set/unset command
|
||||
- 148f0433 Implement flag set/unset action in UI
|
||||
- 417b24cd Print invalid command on error
|
||||
- 4e941a9e Add default_mailbox setting
|
||||
- 974502c6 Impl Hash for Card
|
||||
- ba7a97e9 Add x axis scroll support
|
||||
- ccf6f9a2 Remember previous set index_style preferences
|
||||
|
||||
Bug Fixes
|
||||
=========
|
||||
|
||||
- bcec745c Fix command and status bar drawing
|
||||
- 62b8465f Fix ThreadView for new TUI API
|
||||
- 28fa66cc Fix ThreadedListing for new TUI API
|
||||
- 2c6f180d Fix macos compilation
|
||||
- 24971d19 Fix compilation with 1.70.0 cargo
|
||||
- 34a2d52e Fix rustdoc::redundant_explicit_links
|
||||
- f63774fa Fix new clippy lints (1.75)
|
||||
- 33408146 Fix feature permutation mis-compilations found with cargo-hack
|
||||
- e3351d27 Fix set unseen updating all mboxes
|
||||
- 8185f2cf Add deny clippy lints and fix them
|
||||
- 7861fb04 Fix typos found with typos tool
|
||||
- 64e60cb0 Fix select modifier regression
|
||||
- 60f26f9d Fix some old pre-intradoc rustdoc links
|
||||
- 1fe36192 Make conf validation recognize AccountSettings extra keys
|
||||
- c332c2f5 Fix new clippy lints (mostly clippy::blocks_in_conditions)
|
||||
- 070930e6 Fix auto index build when missing
|
||||
- 26928e3a Fix compilation for macos
|
||||
- 3884c0da Small typographic fixups
|
||||
- b820bd6d Remove unused imap_trace! and fix comp
|
||||
- a88b8c5e Debian/changelog warning fix
|
||||
- 4ce616ae Fix lints.yaml rustup install step
|
||||
- 264782d2 Various unimportant minor style/doc fixups
|
||||
- 475609fe Make {prev,next}_entry shortcut behavior consistent
|
||||
- a69c674c Fix new 1.77 clippy lints
|
||||
- 48cb9ee2 Fix compilation for macos
|
||||
- 8a16cf6d Fix wrong column index crash
|
||||
- bc1b6531 Fix constant redrawing
|
||||
- 29cc1bce Remove obsolete file melib/src/text/tables.rs.gz
|
||||
- ab041898 Fix new warnings for 1.78.0
|
||||
- 46e40856 Fix UIConfirmationDialog highlight printing
|
||||
- 3b93fa8e Don't draw messages above embedded terminal
|
||||
- 684fae3e Copy old content to new buf when resizing
|
||||
- 5d915baa Use Screen::resize instead of CellBuffer::resize
|
||||
- 6a66afe9 Make add contact dialog scrollable on overflow
|
||||
- aa5737a0 Prevent drawing pager on embedded mode
|
||||
- 07072e2e Prevent panic if envelope is deleted
|
||||
- 8ddd673d Update all mailboxes
|
||||
- 3691cd29 Send EnvelopeUpdate event after self.collection.update_flags()
|
||||
- 1fcb1d59 Remove rerun when build.rs changes
|
||||
- 933bf157 Ack \ as an atom
|
||||
- a1cbb198 Return Results instead of panicking
|
||||
- b5ddc397 Remove unwrap() from get_events() loop
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
- 61a0c3c2 Do not clear selection after action
|
||||
- 9af284b8 Don't hide unread count for mailboxes that are partly truncated
|
||||
- 35408b16 Run pager filter asynchronously
|
||||
- e80ea9c9 Changed default manpage install path
|
||||
- 742f038f Move sent_mailbox to settings
|
||||
- 86bbf1ea Refresh NotmuchMailbox counts when setting flags
|
||||
- f0866a39 Make config error more user-friendly
|
||||
- 11f3077b Add more possible values for manpage names
|
||||
- 1eca34b3 Set lowest priority to shortcut command UIEvents
|
||||
- 484712b0 Check for unrecoverable errors in is_online
|
||||
- 8ec6f220 Use ShellExpandTrait::expand in more user-provided paths
|
||||
|
||||
Refactoring
|
||||
===========
|
||||
|
||||
- 0500e451 Add missing EnvelopeRemove event handler
|
||||
- ab14f819 Make write_string_to_grid a CellBuffer method
|
||||
- e0adcdfe Move rest of methods under CellBuffer
|
||||
- 0a74c7d0 Overhaul refactor
|
||||
- 3b4acc15 Add tests
|
||||
- 7eedd860 Remove address_list! macro
|
||||
- f3e85738 Move build.rs scripts to build directory
|
||||
- 77325486 Remove on-push hooks for actions w/ run on-pr
|
||||
- 08518e1c Remove obsolete position.rs module
|
||||
- ddab3179 Move tests to tests module
|
||||
- 79520068 Remove doctests, add tests module
|
||||
- 4e7b6656 Sqlite caching refactor
|
||||
- b5fd3f57 Make self.view an Option
|
||||
- a3aaec38 Remove unused imports
|
||||
- 11a0586d Remove num_cpus dependency
|
||||
- 8f3dee9b Extract mod manpages to standalone file
|
||||
- 89c7972e Add suggestions to BadValue variant
|
||||
- 35a9f33a Extract common FlagString logic
|
||||
- 1b0bdd0a Split queries and mailbox into submodules
|
||||
- 506ae9f5 Add ErrorKind::LinkedLibrary variant
|
||||
- ebe1b3da Wrap *mut struct fields in NonNull<_>
|
||||
- ca7d7bb9 Use message freeze/thaw for flag changes
|
||||
- 4026e254 Add some doc comments
|
||||
- 808aa494 Rename text_processing to text for the whole brevity thing
|
||||
- bebb473d Derive extra traits for enums
|
||||
- ab1b946f Don't print details if it's an empty string.
|
||||
- f685726e Add backtrace field to ParsingError
|
||||
- 73d5b24e Merge integration tests in one crate
|
||||
- 31401fa3 Add LazyCountSet::contains method
|
||||
- 0270db01 From<&[u8]> -> From<B: AsRef<[u9]>>
|
||||
- 873a67d0 Replace erroneous use of set_err_kind with set_kind
|
||||
- 51e3f163 Use Url instead of String in deserializing
|
||||
- 8014af25 Reduce debug prints
|
||||
- f31b5c40 Don't print raw bytes as escaped unicode
|
||||
- 41e965b8 Split mbox/job stuff in submodules
|
||||
- ec01a441 Turn some sync connections to unsync
|
||||
- 3e914465 Store children process metadata
|
||||
- c53a32de Re-enables horizontal thread view
|
||||
- 36b7c00b Put doc text type names and co. in backtics
|
||||
- 634bd191 Convert log prints to traces
|
||||
- 1048ce68 Add hostname() utility function
|
||||
- 7645ff1b Rename write_string{to_grid,}
|
||||
- c2ae19d1 Return Option from current_pos
|
||||
- b61fc3ab Add HelpView struct for shortcuts widget
|
||||
- 3495ffd6 Change UIEvent::Notification structure
|
||||
- 23c15261 Abstract envelope view filters away
|
||||
- 031d0f7d Add area.is_empty() checks in cell iterators
|
||||
- e37997d6 Store Link URL value in Link type
|
||||
- b6f769b2 Add field names to row_attr! bool values
|
||||
- 0da97dd8 Check row_updates in is_dirty()
|
||||
- 6506fffb Rewrite email flag modifications
|
||||
- 23507932 Update cache on set_flags
|
||||
- 470cae6b Update thread cache on email flag modifications
|
||||
- 84f3641e Re-add on-screen message display
|
||||
- 54d21f25 Re-add contact list and editor support
|
||||
- 458258e1 Re-enable compact listing style
|
||||
- 1c1be7d6 Add display_name(), display_slice(), display_name_slice() methods
|
||||
- 5dd71ef1 Upgrade JobsView component to new TUI API
|
||||
- b5cc2a09 Upgrade MailboxManager component to new TUI API
|
||||
- ed8a5de2 Re-enable EditAttachments component
|
||||
- 77a8d9e2 Make ModSequence publicly accessible
|
||||
- 64898a05 Make UIDStore constructor pub
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
- e4818803 Various manpage touchups and URL updates
|
||||
- 38bca8f8 Mention use_oauth2=true for gmail oauth2
|
||||
- 660022ce Add mailaddr.7 manpage
|
||||
- c5e9e676 Add historical-manpages dir
|
||||
- 5afc0785 Update README.md, DEVELOPMENT.md and create BUILD.md
|
||||
- d018f07a Retouch manual pages
|
||||
- 3adba40e Add macos manpage mirror url
|
||||
|
||||
Packaging
|
||||
=========
|
||||
|
||||
- cd2ba80f Update metadata
|
||||
- 5f8d7c80 Update deb-dist target command with author metadata
|
||||
- 59c99fdc Update debian package metadata
|
||||
- 97eb6363 Add dpkg --print-architecture to deb filename
|
||||
- 7412c238 Bump meli version to 0.8.5-rc.3
|
||||
- 500fe7f7 Update CHANGELOG.md
|
||||
- 5ff4e8ae Run builds.yaml when any manifest file changes
|
||||
- 0a617410 Split test.yaml to test.yaml and lints.yaml
|
||||
- 3ba1603a Add manifest file only lints workflow
|
||||
- 1617212c Add scripts/check_debian_changelog.sh lint
|
||||
- c41f35fd Use actions/checkout@v3
|
||||
- 876616d4 Use actions/upload-artifact@v3
|
||||
- 2419f4bd Add debian package build workflow
|
||||
- 10c3b0ea Bump version to 0.8.5-rc.1
|
||||
- d16afc7d Bump version to 0.8.5-rc.2
|
||||
- da251455 Bump meli version to 0.8.5-rc.2
|
||||
|
||||
Miscellaneous Tasks
|
||||
===================
|
||||
|
||||
- c4344529 Add .git-blame-ignore-revs file
|
||||
- f70496f1 Add codemeta.json
|
||||
- b3079715 Disable flakey test_smtp()
|
||||
- 8a95febb Set debuginfo=0 in test/lint builds
|
||||
- 81d1c053 Add mandoc_lint.sh
|
||||
- 8de8addd Add cfg for musl builds
|
||||
- 70fc2b45 Update nix dependency to 0.27
|
||||
- fd64fe0b Update codeberg.org URL
|
||||
- 30a3205e Add clippy::doc_markdown
|
||||
- c7aee725 Add clippy::doc_markdown
|
||||
- b8b24282 Update all instances of old domains with meli-email.org
|
||||
- ae96038f Make unicode-segmentation a hard dependency
|
||||
- 255e9376 Update linkify dep from 0.8.1 to 0.10.0
|
||||
- dedee908 Update notify dep from 4.0.17 to 6.1.1
|
||||
- c1c41c91 Update README.md and add Codeberg mirror
|
||||
- 71f3ffe7 Update Makefile
|
||||
- 63a63253 Use type alias for c_char
|
||||
- c751b2e8 Re-enable conversations listing style
|
||||
- 3a709794 Update minimum rust version from 1.65.0 to 1.68.2
|
||||
- f900dbea Use cargo-derivefmt to sort derives alphabetically
|
||||
- e19f3e57 Cargo-sort all Cargo.toml files
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 05 May 2024 18:46:42 +0300
|
||||
|
||||
meli (0.8.5-rc.3-1) bookworm; urgency=low
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 10 Dec 2023 15:22:18 +0000
|
||||
* Update to 0.8.5-rc.3
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 10 Dec 2023 15:22:18 +0000
|
||||
|
||||
meli (0.8.5-rc.2-1) bookworm; urgency=low
|
||||
|
||||
* Update to 0.8.5-rc.2
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 4 Dec 2023 19:34:00 +0200
|
||||
|
||||
meli (0.8.4-1) bookworm; urgency=low
|
||||
|
||||
* Update to 0.8.4
|
||||
|
||||
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 27 Nov 2023 19:34:00 +0200
|
||||
|
||||
meli (0.7.2-1) bullseye; urgency=low
|
||||
Added
|
||||
|
||||
|
@ -35,7 +258,7 @@ meli (0.7.1-1) bullseye; urgency=low
|
|||
- melib/nntp: implement refresh
|
||||
- melib/nntp: update total/new counters on new articles
|
||||
- melib/nntp: implement NNTP posting
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on extra unused conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
Fixed
|
||||
|
|
|
@ -5,9 +5,9 @@ Maintainer: Manos Pitsidianakis <manos@pitsidianak.is>
|
|||
Build-Depends: debhelper-compat (=13), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
|
||||
Standards-Version: 4.1.4
|
||||
Rules-Requires-Root: no
|
||||
Vcs-Git: https://git.meliemail.org/meli/meli.git
|
||||
Vcs-Browser: https://git.meliemail.org/meli/meli
|
||||
Homepage: https://meliemail.org
|
||||
Vcs-Git: https://git.meli-email.org/meli/meli.git
|
||||
Vcs-Browser: https://git.meli-email.org/meli/meli
|
||||
Homepage: https://meli-email.org
|
||||
|
||||
Package: meli
|
||||
Architecture: any
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: meli
|
||||
Source: https://git.meliemail.org/meli/meli
|
||||
Source: https://git.meli-email.org/meli/meli
|
||||
#
|
||||
# Please double check copyright with the licensecheck(1) command.
|
||||
|
||||
|
|
|
@ -2,6 +2,6 @@ WARNING: This package is not distributed by debian, it was generated from the so
|
|||
|
||||
Please do not report bugs to debian, but to the appropriate issue tracker for meli:
|
||||
|
||||
- https://git.meliemail.org/meli/meli/issues
|
||||
- Send e-mail to the mailing list, "meli general" <meli-general@meliemail.org>
|
||||
https://lists.meliemail.org/list/meli-general/
|
||||
- https://git.meli-email.org/meli/meli/issues
|
||||
- Send e-mail to the mailing list, "meli general" <meli-general@meli-email.org>
|
||||
https://lists.meli-email.org/list/meli-general/
|
||||
|
|
|
@ -14,10 +14,7 @@ path = "fuzz_targets/envelope_parse.rs"
|
|||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.3"
|
||||
|
||||
[dependencies.melib]
|
||||
path = "../melib"
|
||||
features = ["unicode-algorithms"]
|
||||
melib = { path = "../melib" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.8.5-rc.3"
|
||||
version = "0.8.5"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.68.2"
|
||||
rust-version = "1.70.0"
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
description = "terminal e-mail client"
|
||||
homepage = "https://meli.delivery"
|
||||
repository = "https://git.meli.delivery/meli/meli.git"
|
||||
homepage = "https://meli-email.org"
|
||||
repository = "https://git.meli-email.org/meli/meli.git"
|
||||
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
|
||||
categories = ["command-line-utilities", "email"]
|
||||
default-run = "meli"
|
||||
|
@ -30,11 +30,9 @@ futures = "0.3.5"
|
|||
indexmap = { version = "^1.6", features = ["serde-1"] }
|
||||
libc = { version = "0.2.125", default-features = false, features = ["extra_traits"] }
|
||||
libz-sys = { version = "1.1", features = ["static"], optional = true }
|
||||
linkify = { version = "^0.8", default-features = false }
|
||||
melib = { path = "../melib", version = "0.8.5-rc.3" }
|
||||
nix = { version = "^0.24", default-features = false }
|
||||
notify = { version = "4.0.1", default-features = false } # >:c
|
||||
num_cpus = "1.12.0"
|
||||
linkify = { version = "^0.10", default-features = false }
|
||||
melib = { path = "../melib", version = "0.8.5", features = [] }
|
||||
nix = { version = "0.27", default-features = false, features = ["signal", "poll", "term", "ioctl", "process"] }
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = "1.0"
|
||||
|
@ -44,7 +42,7 @@ smallvec = { version = "^1.5.0", features = ["serde"] }
|
|||
structopt = { version = "0.3.14", default-features = false }
|
||||
svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
toml = { version = "0.5.6", default-features = false, features = ["preserve_order"] }
|
||||
toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] }
|
||||
xdg = "2.1.0"
|
||||
|
||||
[dependencies.pcre2]
|
||||
|
@ -54,7 +52,7 @@ version = "0.2.3"
|
|||
optional = true
|
||||
|
||||
[features]
|
||||
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "text-processing", "static"]
|
||||
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
|
||||
notmuch = ["melib/notmuch"]
|
||||
jmap = ["melib/jmap"]
|
||||
sqlite3 = ["melib/sqlite3"]
|
||||
|
@ -64,7 +62,6 @@ regexp = ["dep:pcre2"]
|
|||
dbus-notifications = ["dep:notify-rust"]
|
||||
cli-docs = ["dep:flate2"]
|
||||
svgscreenshot = ["dep:svg_crate"]
|
||||
text-processing = ["melib/unicode-algorithms"]
|
||||
gpgme = ["melib/gpgme"]
|
||||
# Static / vendoring features.
|
||||
tls-static = ["melib/tls-static"]
|
||||
|
|
|
@ -25,7 +25,6 @@ extern crate syn;
|
|||
include!("config_macros.rs");
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
|
||||
override_derive(&[
|
||||
("src/conf/pager.rs", "PagerSettings"),
|
||||
|
|
|
@ -56,7 +56,7 @@ pub(crate) fn override_derive(filenames: &[(&str, &str)]) {
|
|||
|
||||
#![allow(clippy::derivable_impls)]
|
||||
|
||||
//! This module is automatically generated by config_macros.rs.
|
||||
//! This module is automatically generated by `config_macros.rs`.
|
||||
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
@ -192,7 +192,7 @@ use melib::HeaderName;
|
|||
#(#attrs_tokens)*
|
||||
impl Default for #override_ident {
|
||||
fn default() -> Self {
|
||||
#override_ident {
|
||||
Self {
|
||||
#(#field_idents: None),*
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,354 @@
|
|||
'\" t
|
||||
.\"<!-- Copyright 1998 - 2007 Double Precision, Inc. See COPYING for -->
|
||||
.\"<!-- distribution information. -->
|
||||
.\" Title: maildir
|
||||
.\" Author: Sam Varshavchik
|
||||
.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/>
|
||||
.\" Date: 07/24/2017
|
||||
.\" Manual: Double Precision, Inc.
|
||||
.\" Source: Courier Mail Server
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "MAILDIR" "5" "07/24/2017" "Courier Mail Server" "Double Precision, Inc\&."
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * Define some portability stuff
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.\" http://bugs.debian.org/507673
|
||||
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
|
||||
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * set default formatting
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" disable hyphenation
|
||||
.nh
|
||||
.\" disable justification (adjust text to left margin only)
|
||||
.ad l
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * MAIN CONTENT STARTS HERE *
|
||||
.\" -----------------------------------------------------------------
|
||||
.SH "NAME"
|
||||
maildir \- E\-mail directory
|
||||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
$HOME/Maildir
|
||||
.SH "DESCRIPTION"
|
||||
.PP
|
||||
A
|
||||
\(lqMaildir\(rq
|
||||
is a structured directory that holds E\-mail messages\&. Maildirs were first implemented by the
|
||||
Qmail
|
||||
mail server\&. Qmail\*(Aqs maildirs were a simple data structure, nothing more than a single collection of E\-mail messages\&. The
|
||||
Courier
|
||||
mail server builds upon
|
||||
Qmail\*(Aqs maildirs to provide extended functionality, such as folders and quotas\&. This document describes the
|
||||
Courier
|
||||
mail server\*(Aqs extended maildirs, without explicitly identifying The
|
||||
Courier
|
||||
mail server\-specific extensions\&. See
|
||||
\fBmaildir\fR(5)
|
||||
in Qmail\*(Aqs documentation for the original definition of maildirs\&.
|
||||
.PP
|
||||
Traditionally, E\-mail folders were saved as plain text files, called
|
||||
\(lqmboxes\(rq\&. Mboxes have known limitations\&. Only one application can use an mbox at the same time\&. Locking is required in order to allow simultaneous concurrent access by different applications\&. Locking is often problematic, and not very reliable in network\-based filesystem requirements\&. Some network\-based filesystems don\*(Aqt offer any reliable locking mechanism at all\&. Furthermore, even bulletproof locking won\*(Aqt prevent occasional mbox corruption\&. A process can be killed or terminated in the middle of updating an mbox\&. This will likely result in corruption, and a loss of most messages in the mbox\&.
|
||||
.PP
|
||||
Maildirs allow multiple concurrent access by different applications\&. Maildirs do not require locking\&. Multiple applications can update a maildir at the same time, without stepping on each other\*(Aqs feet\&.
|
||||
.SS "Maildir contents"
|
||||
.PP
|
||||
A
|
||||
\(lqmaildir\(rq
|
||||
is a directory that\*(Aqs created by
|
||||
\m[blue]\fB\fBmaildirmake\fR(1)\fR\m[]\&\s-2\u[1]\d\s+2\&. Naturally, maildirs should not have any group or world permissions, unless you want other people to read your mail\&. A maildir contains three subdirectories:
|
||||
tmp,
|
||||
new, and
|
||||
cur\&. These three subdirectories comprise the primary folder, where new mail is delivered by the system\&.
|
||||
.PP
|
||||
Folders are additional subdirectories in the maildir whose names begin with a period: such as
|
||||
\&.Drafts
|
||||
or
|
||||
\&.Sent\&. Each folder itself contains the same three subdirectories,
|
||||
tmp,
|
||||
new, and
|
||||
cur, and an additional zero\-length file named
|
||||
maildirfolder, whose purpose is to inform any mail delivery agent that it\*(Aqs really delivering to a folder, and that the mail delivery agent should look in the parent directory for any maildir\-related information\&.
|
||||
.PP
|
||||
Folders are not physically nested\&. A folder subdirectory, such as
|
||||
\&.Sent
|
||||
does not itself contain any subfolders\&. The main maildir contains a single, flat list of subfolders\&. These folders are logically nested, and periods serve to separate folder hierarchies\&. For example,
|
||||
\&.Sent\&.2002
|
||||
is considered to be a subfolder called
|
||||
\(lq2002\(rq
|
||||
which is a subfolder of
|
||||
\(lqSent\(rq\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBFolder name encoding\fR
|
||||
.RS 4
|
||||
.PP
|
||||
Folder names can contain any Unicode character, except for control characters\&. US\-ASCII characters, U+0x0020 \- U+0x007F, except for the period, forward\-slash, and ampersand characters (U+0x002E, U+0x002F, and U+0x0026) represent themselves\&. The ampersand is represent by the two character sequence
|
||||
\(lq&\-\(rq\&. The period, forward slash, and non US\-ASCII Unicode characters are represented using the UTF\-7 character set, and encoded with a modified form of base64\-encoding\&.
|
||||
.PP
|
||||
The
|
||||
\(lq&\(rq
|
||||
character starts the modified base64\-encoded sequence; the sequence is terminated by the
|
||||
\(lq\-\(rq
|
||||
character\&. The sequence of 16\-bit Unicode characters is written in big\-endian order, and encoded using the base64\-encoding method described in section 5\&.2 of
|
||||
\m[blue]\fBRFC 1521\fR\m[]\&\s-2\u[2]\d\s+2, with the following modifications:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
The
|
||||
\(lq=\(rq
|
||||
padding character is omitted\&. When decoding, an incomplete 16\-bit character is discarded\&.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
The comma character,
|
||||
\(lq,\(rq
|
||||
is used in place of the
|
||||
\(lq/\(rq
|
||||
character in the base64 alphabet\&.
|
||||
.RE
|
||||
.PP
|
||||
For example, the word
|
||||
\(lqResume\(rq
|
||||
with both "e"s being the e\-acute character, U+0x00e9, is encoded as
|
||||
\(lqR&AOk\-sum&AOk\-\(rq
|
||||
(so a folder of that name would be a maildir subdirectory called
|
||||
\(lq\&.R&AOk\-sum&AOk\-\(rq)\&.
|
||||
.RE
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBOther maildir contents\fR
|
||||
.RS 4
|
||||
.PP
|
||||
Software that uses maildirs may also create additional files besides the
|
||||
tmp,
|
||||
new, and
|
||||
cur
|
||||
subdirectories \-\- in the main maildir or a subfolder \-\- for its own purposes\&.
|
||||
.RE
|
||||
.SS "Messages"
|
||||
.PP
|
||||
E\-mail messages are stored in separate, individual files, one E\-mail message per file\&. The
|
||||
tmp
|
||||
subdirectory temporarily stores E\-mail messages that are in the process of being delivered to this maildir\&.
|
||||
tmp
|
||||
may also store other kinds of temporary files, as long as they are created in the same way that message files are created in
|
||||
tmp\&. The
|
||||
new
|
||||
subdirectory stores messages that have been delivered to this maildir, but have not yet been seen by any mail application\&. The
|
||||
cur
|
||||
subdirectory stores messages that have already been seen by mail applications\&.
|
||||
.SS "Adding new mail to maildirs"
|
||||
.PP
|
||||
The following process delivers a new message to the maildir:
|
||||
.PP
|
||||
A new unique filename is created using one of two possible forms:
|
||||
\(lqtime\&.MusecPpid\&.host\(rq, or
|
||||
\(lqtime\&.MusecPpid_unique\&.host\(rq\&.
|
||||
\(lqtime\(rq
|
||||
and
|
||||
\(lqusec\(rq
|
||||
is the current system time, obtained from
|
||||
\fBgettimeofday\fR(2)\&.
|
||||
\(lqpid\(rq
|
||||
is the process number of the process that is delivering this message to the maildir\&.
|
||||
\(lqhost\(rq
|
||||
is the name of the machine where the mail is being delivered\&. In the event that the same process creates multiple messages, a suffix unique to each message is appended to the process id; preferrably an underscore, followed by an increasing counter\&. This applies whether messages created by a process are all added to the same, or different, maildirs\&. This protocol allows multiple processes running on multiple machines on the same network to simultaneously create new messages without stomping on each other\&.
|
||||
.PP
|
||||
The filename created in the previous step is checked for existence by executing the
|
||||
\fBstat\fR(2)
|
||||
system call\&. If
|
||||
\fBstat\fR(2)
|
||||
results in ANYTHING OTHER than the system error
|
||||
ENOENT, the process must sleep for two seconds, then go back and create another unique filename\&. This is an extra step to insure that each new message has a completely unique filename\&.
|
||||
.PP
|
||||
Other applications that wish to use
|
||||
tmp
|
||||
for temporary storage should observe the same protocol (but see READING MAIL FROM MAILDIRS below, because old files in
|
||||
tmp
|
||||
will be eventually deleted)\&.
|
||||
.PP
|
||||
If the
|
||||
\fBstat\fR(2)
|
||||
system call returned
|
||||
ENOENT, the process may proceed to create the file in the
|
||||
tmp
|
||||
subdirectory, and save the entire message in the new file\&. The message saved MUST NOT have the
|
||||
\(lqFrom_\(rq
|
||||
header that is used to mboxes\&. The message also MUST NOT have any
|
||||
\(lqFrom_\(rq
|
||||
lines in the contents of the message prefixed by the
|
||||
\(lq>\(rq
|
||||
character\&.
|
||||
.PP
|
||||
When saving the message, the number of bytes returned by the
|
||||
\fBwrite\fR(2)
|
||||
system call must be checked, in order to make sure that the complete message has been written out\&.
|
||||
.PP
|
||||
After the message is saved, the file descriptor is
|
||||
\fBfstat\fR(2)\-ed\&. The file\*(Aqs device number, inode number, and the its byte size, are saved\&. The file is closed and is then immediately moved/renamed into the
|
||||
new
|
||||
subdirectory\&. The name of the file in
|
||||
new
|
||||
should be
|
||||
\(lqtime\&.MusecPpidVdevIino\&.host,S=\fIcnt\fR\(rq, or
|
||||
\(lqtime\&.MusecPpidVdevIino_unique\&.host,S=\fIcnt\fR\(rq\&.
|
||||
\(lqdev\(rq
|
||||
is the message\*(Aqs device number,
|
||||
\(lqino\(rq
|
||||
is the message\*(Aqs inode number (from the previous
|
||||
\fBfstat\fR(2)
|
||||
call); and
|
||||
\(lqcnt\(rq
|
||||
is the message\*(Aqs size, in bytes\&.
|
||||
.PP
|
||||
The
|
||||
\(lq,S=\fIcnt\fR\(rq
|
||||
part optimizes the
|
||||
\m[blue]\fBCourier\fR\m[]\&\s-2\u[3]\d\s+2
|
||||
mail server\*(Aqs maildir quota enhancement; it allows the size of all the mail stored in the maildir to be added up without issuing the
|
||||
\fBstat\fR(2)
|
||||
system call for each individual message (this can be quite a performance drain with certain network filesystems)\&.
|
||||
.SS "READING MAIL FROM MAILDIRS"
|
||||
.PP
|
||||
Applications that read mail from maildirs should do it in the following order:
|
||||
.PP
|
||||
When opening a maildir or a maildir folder, read the
|
||||
tmp
|
||||
subdirectory and delete any files in there that are at least 36 hours old\&.
|
||||
.PP
|
||||
Look for new messages in the
|
||||
new
|
||||
subdirectory\&. Rename
|
||||
\fInew/filename\fR, as
|
||||
\fIcur/filename:2,info\fR\&. Here,
|
||||
\fIinfo\fR
|
||||
represents the state of the message, and it consists of zero or more boolean flags chosen from the following:
|
||||
\(lqD\(rq
|
||||
\- this is a \*(Aqdraft\*(Aq message,
|
||||
\(lqR\(rq
|
||||
\- this message has been replied to,
|
||||
\(lqS\(rq
|
||||
\- this message has been viewed (seen),
|
||||
\(lqT\(rq
|
||||
\- this message has been marked to be deleted (trashed), but is not yet removed (messages are removed from maildirs simply by deleting their file),
|
||||
\(lqF\(rq
|
||||
\- this message has been marked by the user, for some purpose\&. These flags must be stored in alphabetical order\&. New messages contain only the
|
||||
:2,
|
||||
suffix, with no flags, indicating that the messages were not seen, replied, marked, or deleted\&.
|
||||
.PP
|
||||
Maildirs may have maximum size quotas defined, but these quotas are purely voluntary\&. If you need to implement mandatory quotas, you should use any quota facilities provided by the underlying filesystem that is used to store the maildirs\&. The maildir quota enhancement is designed to be used in certain situations where filesystem\-based quotas cannot be used for some reason\&. The implementation is designed to avoid the use of any locking\&. As such, at certain times the calculated quota may be imprecise, and certain anomalous situations may result in the maildir actually going over the stated quota\&. One such situation would be when applications create messages without updating the quota estimate for the maildir\&. Eventually it will be precisely recalculated, but wherever possible new messages should be created in compliance with the voluntary quota protocol\&.
|
||||
.PP
|
||||
The voluntary quota protocol involves some additional procedures that must be followed when creating or deleting messages within a given maildir or its subfolders\&. The
|
||||
\m[blue]\fB\fBdeliverquota\fR(8)\fR\m[]\&\s-2\u[4]\d\s+2
|
||||
command is a tiny application that delivers a single message to a maildir using the voluntary quota protocol, and hopefully it can be used as a measure of last resort\&. Alternatively, applications can use the
|
||||
libmaildir\&.a
|
||||
library to handle all the low\-level dirty details for them\&. The voluntary quota enhancement is described in the
|
||||
\m[blue]\fB\fBmaildirquota\fR(7)\fR\m[]\&\s-2\u[5]\d\s+2
|
||||
man page\&.
|
||||
.SS "Maildir Quotas"
|
||||
.PP
|
||||
This is a voluntary mechanism for enforcing "loose" quotas on the maximum sizes of maildirs\&. This mechanism is enforced in software, and not by the operating system\&. Therefore it is only effective as long as the maildirs themselves are not directly accessible by their users, since this mechanism is trivially disabled\&.
|
||||
.PP
|
||||
If possible, operating system\-enforced quotas are preferrable\&. Where operating system quota enforcement is not available, or not possible, this voluntary quota enforcement mechanism might be an acceptable compromise\&. Since it\*(Aqs enforced in software, all software that modifies or accesses the maildirs is required to voluntary obey and enforce a quota\&. The voluntary quota implementation is flexible enough to allow non quota\-aware applications to also access the maildirs, without any drastic consequences\&. There will be some non\-drastic consequences, though\&. Of course, non quota\-aware applications will not enforce any defined quotas\&. Furthermore, this voluntary maildir quota mechanism works by estimating the current size of the maildir, with periodic exact recalculation\&. Obviously non quota\-aware maildir applications will not update the maildir size estimation, so the estimate will be thrown off for some period of time, until the next recalculation\&.
|
||||
.PP
|
||||
This voluntary quota mechanism is designed to be a reasonable compromise between effectiveness, and performance\&. The entire purpose of using maildir\-based mail storage is to avoid any kind of locking, and to permit parallel access to mail by multiple applications\&. In order to compute the exact size of a maildir, the maildir must be locked somehow to prevent any modifications while its contents are added up\&. Obviously something like that defeats the original purpose of using maildirs, therefore the voluntary quota mechanism does not use locking, and that\*(Aqs why the current recorded maildir size is always considered to be an estimate\&. Regular size recalculations will compensate for any occasional race conditions that result in the estimate to be thrown off\&.
|
||||
.PP
|
||||
A quota for an existing maildir is installed by running maildirmake with the
|
||||
\-q
|
||||
option, and naming an existing maildir\&. The
|
||||
\-q
|
||||
option takes a parameter,
|
||||
\fIquota\fR, which is a comma\-separated list of quota specifications\&. A quota specification consists of a number followed by either \*(AqS\*(Aq, indicating the maximum message size in bytes, or \*(AqC\*(Aq, maximum number of messages\&. For example:
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
\fBmaildirmake \-q 5000000S,1000C \&./Maildir\fR
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.PP
|
||||
This sets the quota to 5,000,000 bytes or 1000 messages, whichever comes first\&.
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
\fBmaildirmake \-q 1000000S \&./Maildir\fR
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.PP
|
||||
This sets the quota to 1,000,000 bytes, without limiting the number of messages\&.
|
||||
.PP
|
||||
A quota of an existing maildir can be changed by rerunning the
|
||||
\fBmaildirmake\fR
|
||||
command with a new
|
||||
\-q
|
||||
option\&. To delete a quota entirely, delete the
|
||||
\fIMaildir\fR/maildirsize
|
||||
file\&.
|
||||
.SH "SEE ALSO"
|
||||
.PP
|
||||
\m[blue]\fB\fBmaildirmake\fR(1)\fR\m[]\&\s-2\u[1]\d\s+2\&.
|
||||
.SH "AUTHOR"
|
||||
.PP
|
||||
\fBSam Varshavchik\fR
|
||||
.RS 4
|
||||
Author
|
||||
.RE
|
||||
.SH "NOTES"
|
||||
.IP " 1." 4
|
||||
\fBmaildirmake\fR(1)
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org/maildirmake.html
|
||||
.RE
|
||||
.IP " 2." 4
|
||||
RFC 1521
|
||||
.RS 4
|
||||
\%http://www.rfc-editor.org/rfc/rfc1521.txt
|
||||
.RE
|
||||
.IP " 3." 4
|
||||
Courier
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org
|
||||
.RE
|
||||
.IP " 4." 4
|
||||
\fBdeliverquota\fR(8)
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org/deliverquota.html
|
||||
.RE
|
||||
.IP " 5." 4
|
||||
\fBmaildirquota\fR(7)
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org/maildirquota.html
|
||||
.RE
|
|
@ -0,0 +1,187 @@
|
|||
'\" t
|
||||
.\" -*-nroff-*-
|
||||
.\"
|
||||
.\" Copyright (C) 2000 Thomas Roessler <roessler@does-not-exist.org>
|
||||
.\"
|
||||
.\" This document is in the public domain and may be distributed and
|
||||
.\" changed arbitrarily.
|
||||
.\"
|
||||
.TH mbox 5 "February 19th, 2002" Unix "User Manuals"
|
||||
.\"
|
||||
.SH NAME
|
||||
mbox \- Format for mail message storage.
|
||||
.\"
|
||||
.SH DESCRIPTION
|
||||
This document describes the format traditionally used by Unix hosts
|
||||
to store mail messages locally.
|
||||
.B mbox
|
||||
files typically reside in the system's mail spool, under various
|
||||
names in users' Mail directories, and under the name
|
||||
.B mbox
|
||||
in users' home directories.
|
||||
.PP
|
||||
An
|
||||
.B mbox
|
||||
is a text file containing an arbitrary number of e-mail messages.
|
||||
Each message consists of a postmark, followed by an e-mail message
|
||||
formatted according to \fBRFC822\fP, \fBRFC2822\fP. The file format
|
||||
is line-oriented. Lines are separated by line feed characters (ASCII 10).
|
||||
.PP
|
||||
A postmark line consists of the four characters "From", followed by
|
||||
a space character, followed by the message's envelope sender
|
||||
address, followed by whitespace, and followed by a time stamp. This
|
||||
line is often called From_ line.
|
||||
.PP
|
||||
The sender address is expected to be
|
||||
.B addr-spec
|
||||
as defined in \fBRFC2822\fP 3.4.1. The date is expected to be
|
||||
.B date-time
|
||||
as output by
|
||||
.BR asctime (3).
|
||||
For compatibility reasons with legacy software, two-digit years
|
||||
greater than or equal to 70 should be interpreted as the years
|
||||
1970+, while two-digit years less than 70 should be interpreted as
|
||||
the years 2000-2069. Software reading files in this format should
|
||||
also be prepared to accept non-numeric timezone information such as
|
||||
"CET DST" for Central European Time, daylight saving time.
|
||||
.PP
|
||||
Example:
|
||||
.IP "" 1
|
||||
>From example@example.com Fri Jun 23 02:56:55 2000
|
||||
.PP
|
||||
In order to avoid misinterpretation of lines in message bodies
|
||||
which begin with the four characters "From", followed by a space
|
||||
character, the mail delivery agent must quote any occurrence
|
||||
of "From " at the start of a body line.
|
||||
.sp
|
||||
There are two different quoting schemes, the first (\fBMBOXO\fP) only
|
||||
quotes plain "From " lines in the body by prepending a '>' to the
|
||||
line; the second (\fBMBOXRD\fP) also quotes already quoted "From "
|
||||
lines by prepending a '>' (i.e. ">From ", ">>From ", ...). The later
|
||||
has the advantage that lines like
|
||||
.IP "" 1
|
||||
>From the command line you can use the '\-p' option
|
||||
.PP
|
||||
aren't dequoted wrongly as a \fBMBOXRD\fP-MDA would turn the line
|
||||
into
|
||||
.IP "" 1
|
||||
>>From the command line you can use the '\-p' option
|
||||
.PP
|
||||
before storing it. Besides \fBMBOXO\fP and \fBMBOXRD\fP there is also
|
||||
\fBMBOXCL\fP which is \fBMBOXO\fP with a "Content-Length:"\-field with the
|
||||
number of bytes in the message body; some MUAs (like
|
||||
.BR mutt (1))
|
||||
do automatically transform \fBMBOXO\fP mailboxes into \fBMBOXCL\fP ones when
|
||||
ever they write them back as \fBMBOXCL\fP can be read by any \fBMBOXO\fP-MUA
|
||||
without any problems.
|
||||
.PP
|
||||
If the modification-time (usually determined via
|
||||
.BR stat (2))
|
||||
of a nonempty
|
||||
.B mbox
|
||||
file is greater than the access-time the file has new mail. Many MUAs
|
||||
place a Status: header in each message to indicate which messages have
|
||||
already been read.
|
||||
.\"
|
||||
.SH LOCKING
|
||||
Since
|
||||
.B mbox
|
||||
files are frequently accessed by multiple programs in parallel,
|
||||
.B mbox
|
||||
files should generally not be accessed without locking.
|
||||
.PP
|
||||
Three different locking mechanisms (and combinations thereof) are in
|
||||
general use:
|
||||
.IP "\(bu"
|
||||
.BR fcntl (2)
|
||||
locking is mostly used on recent, POSIX-compliant systems. Use of
|
||||
this locking method is, in particular, advisable if
|
||||
.B mbox
|
||||
files are accessed through the Network File System (NFS), since it
|
||||
seems the only way to reliably invalidate NFS clients' caches.
|
||||
.IP "\(bu"
|
||||
.BR flock (2)
|
||||
locking is mostly used on BSD-based systems.
|
||||
.IP "\(bu"
|
||||
Dotlocking is used on all kinds of systems. In order to lock an
|
||||
.B mbox
|
||||
file named \fIfolder\fR, an application first creates a temporary file
|
||||
with a unique name in the directory in which the
|
||||
\fIfolder\fR resides. The application then tries to use the
|
||||
.BR link (2)
|
||||
system call to create a hard link named \fIfolder.lock\fR
|
||||
to the temporary file. The success of the
|
||||
.BR link (2)
|
||||
system call should be additionally verified using
|
||||
.BR stat (2)
|
||||
calls. If the link has succeeded, the mail folder is considered
|
||||
dotlocked. The temporary file can then safely be unlinked.
|
||||
.IP ""
|
||||
In order to release the lock, an application just unlinks the
|
||||
\fIfolder.lock\fR file.
|
||||
.PP
|
||||
If multiple methods are combined, implementors should make sure to
|
||||
use the non-blocking variants of the
|
||||
.BR fcntl (2)
|
||||
and
|
||||
.BR flock (2)
|
||||
system calls in order to avoid deadlocks.
|
||||
.PP
|
||||
If multiple methods are combined, an
|
||||
.B mbox
|
||||
file must not be considered to have been successfully locked before
|
||||
all individual locks were obtained. When one of the individual
|
||||
locking methods fails, an application should release all locks it
|
||||
acquired successfully, and restart the entire locking procedure from
|
||||
the beginning, after a suitable delay.
|
||||
.PP
|
||||
The locking mechanism used on a particular system is a matter of
|
||||
local policy, and should be consistently used by all applications
|
||||
installed on the system which access
|
||||
.B mbox
|
||||
files. Failure to do so may result in loss of e-mail data, and in
|
||||
corrupted
|
||||
.B mbox
|
||||
files.
|
||||
.\"
|
||||
.SH FILES
|
||||
.IR /var/spool/mail/$LOGNAME
|
||||
.RS
|
||||
\fB$LOGNAME\fP's incoming mail folder.
|
||||
.RE
|
||||
.PP
|
||||
.IR $HOME/mbox
|
||||
.RS
|
||||
user's archived mail messages, in his \fB$HOME\fP directory.
|
||||
.RE
|
||||
.PP
|
||||
.IR $HOME/Mail/
|
||||
.RS
|
||||
A directory in user's \fB$HOME\fP directory which is commonly used to hold
|
||||
.B mbox
|
||||
format folders.
|
||||
.RE
|
||||
.PP
|
||||
.\"
|
||||
.SH "SEE ALSO"
|
||||
.BR mutt (1),
|
||||
.BR fcntl (2),
|
||||
.BR flock (2),
|
||||
.BR link (2),
|
||||
.BR stat (2),
|
||||
.BR asctime (3),
|
||||
.BR maildir (5),
|
||||
.BR mmdf (5),
|
||||
.BR RFC822 ,
|
||||
.BR RFC976 ,
|
||||
.BR RFC2822
|
||||
.\"
|
||||
.SH AUTHOR
|
||||
Thomas Roessler <roessler@does-not-exist.org>, Urs Janssen <urs@tin.org>
|
||||
.\"
|
||||
.SH HISTORY
|
||||
The
|
||||
.B mbox
|
||||
format occurred in Version 6 AT&T Unix.
|
||||
.br
|
||||
A variant of this format was documented in \fBRFC976\fP.
|
|
@ -0,0 +1,235 @@
|
|||
.TH mbox 5
|
||||
.SH "NAME"
|
||||
mbox \- file containing mail messages
|
||||
.SH "INTRODUCTION"
|
||||
The most common format for storage of mail messages is
|
||||
.I mbox
|
||||
format.
|
||||
An
|
||||
.I mbox
|
||||
is a single file containing zero or more mail messages.
|
||||
.SH "MESSAGE FORMAT"
|
||||
A message encoded in
|
||||
.I mbox
|
||||
format begins with a
|
||||
.B From_
|
||||
line, continues with a series of
|
||||
.B \fRnon-\fBFrom_
|
||||
lines,
|
||||
and ends with a blank line.
|
||||
A
|
||||
.B From_
|
||||
line means any line that begins with the characters
|
||||
F, r, o, m, space:
|
||||
|
||||
.EX
|
||||
From god@heaven.af.mil Sat Jan 3 01:05:34 1996
|
||||
.br
|
||||
Return-Path: <god@heaven.af.mil>
|
||||
.br
|
||||
Delivered-To: djb@silverton.berkeley.edu
|
||||
.br
|
||||
Date: 3 Jan 1996 01:05:34 -0000
|
||||
.br
|
||||
From: God <god@heaven.af.mil>
|
||||
.br
|
||||
To: djb@silverton.berkeley.edu (D. J. Bernstein)
|
||||
.br
|
||||
|
||||
.br
|
||||
How's that mail system project coming along?
|
||||
.br
|
||||
|
||||
.EE
|
||||
|
||||
The final line is a completely blank line (no spaces or tabs).
|
||||
Notice that blank lines may also appear elsewhere in the message.
|
||||
|
||||
The
|
||||
.B From_
|
||||
line always looks like
|
||||
.B From
|
||||
.I envsender
|
||||
.I date
|
||||
.IR moreinfo .
|
||||
.I envsender
|
||||
is one word, without spaces or tabs;
|
||||
it is usually the envelope sender of the message.
|
||||
.I date
|
||||
is the delivery date of the message.
|
||||
It always contains exactly 24 characters in
|
||||
.B asctime
|
||||
format.
|
||||
.I moreinfo
|
||||
is optional; it may contain arbitrary information.
|
||||
|
||||
Between the
|
||||
.B From_
|
||||
line and the blank line is a message in RFC 822 format,
|
||||
as described in
|
||||
.BR qmail-header(5) ,
|
||||
subject to
|
||||
.B >From quoting
|
||||
as described below.
|
||||
.SH "HOW A MESSAGE IS DELIVERED"
|
||||
Here is how a program appends a message to an
|
||||
.I mbox
|
||||
file.
|
||||
|
||||
It first creates a
|
||||
.B From_
|
||||
line given the message's envelope sender and the current date.
|
||||
If the envelope sender is empty (i.e., if this is a bounce message),
|
||||
the program uses
|
||||
.B MAILER-DAEMON
|
||||
instead.
|
||||
If the envelope sender contains spaces, tabs, or newlines,
|
||||
the program replaces them with hyphens.
|
||||
|
||||
The program then copies the message, applying
|
||||
.B >From quoting
|
||||
to each line.
|
||||
.B >From quoting
|
||||
ensures that the resulting lines are not
|
||||
.B From_
|
||||
lines:
|
||||
the program prepends a
|
||||
.B >
|
||||
to any
|
||||
.B From_
|
||||
line,
|
||||
.B >From_
|
||||
line,
|
||||
.B >>From_
|
||||
line,
|
||||
.B >>>From_
|
||||
line,
|
||||
etc.
|
||||
|
||||
Finally the program appends a blank line to the message.
|
||||
If the last line of the message was a partial line,
|
||||
it writes two newlines;
|
||||
otherwise it writes one.
|
||||
.SH "HOW A MESSAGE IS READ"
|
||||
A reader scans through an
|
||||
.I mbox
|
||||
file looking for
|
||||
.B From_
|
||||
lines.
|
||||
Any
|
||||
.B From_
|
||||
line marks the beginning of a message.
|
||||
The reader should not attempt to take advantage of the fact that every
|
||||
.B From_
|
||||
line (past the beginning of the file)
|
||||
is preceded by a blank line.
|
||||
|
||||
Once the reader finds a message,
|
||||
it extracts a (possibly corrupted) envelope sender
|
||||
and delivery date out of the
|
||||
.B From_
|
||||
line.
|
||||
It then reads until the next
|
||||
.B From_
|
||||
line or end of file, whichever comes first.
|
||||
It strips off the final blank line
|
||||
and
|
||||
deletes the
|
||||
quoting of
|
||||
.B >From_
|
||||
lines and
|
||||
.B >>From_
|
||||
lines and so on.
|
||||
The result is an RFC 822 message.
|
||||
.SH "COMMON MBOX VARIANTS"
|
||||
There are many variants of
|
||||
.I mbox
|
||||
format.
|
||||
The variant described above is
|
||||
.I mboxrd
|
||||
format, popularized by Rahul Dhesi in June 1995.
|
||||
|
||||
The original
|
||||
.I mboxo
|
||||
format quotes only
|
||||
.B From_
|
||||
lines, not
|
||||
.B >From_
|
||||
lines.
|
||||
As a result it is impossible to tell whether
|
||||
|
||||
.EX
|
||||
From: djb@silverton.berkeley.edu (D. J. Bernstein)
|
||||
.br
|
||||
To: god@heaven.af.mil
|
||||
.br
|
||||
|
||||
.br
|
||||
>From now through August I'll be doing beta testing.
|
||||
.br
|
||||
Thanks for your interest.
|
||||
.EE
|
||||
|
||||
was quoted in the original message.
|
||||
An
|
||||
.I mboxrd
|
||||
reader will always strip off the quoting.
|
||||
|
||||
.I mboxcl
|
||||
format is like
|
||||
.I mboxo
|
||||
format, but includes a Content-Length field with the
|
||||
number of bytes in the message.
|
||||
.I mboxcl2
|
||||
format is like
|
||||
.I mboxcl
|
||||
but has no
|
||||
.B >From
|
||||
quoting.
|
||||
These formats are used by SVR4 mailers.
|
||||
.I mboxcl2
|
||||
cannot be read safely by
|
||||
.I mboxrd
|
||||
readers.
|
||||
.SH "UNSPECIFIED DETAILS"
|
||||
There are many locking mechanisms for
|
||||
.I mbox
|
||||
files.
|
||||
.B qmail-local
|
||||
always uses
|
||||
.B flock
|
||||
on systems that have it, otherwise
|
||||
.BR lockf .
|
||||
|
||||
The delivery date in a
|
||||
.B From_
|
||||
line does not specify a time zone.
|
||||
.B qmail-local
|
||||
always creates the delivery date in GMT
|
||||
so that
|
||||
.I mbox
|
||||
files can be safely transported from one time zone to another.
|
||||
|
||||
If the mtime on a nonempty
|
||||
.I mbox
|
||||
file is greater than the atime,
|
||||
the file has new mail.
|
||||
If the mtime is smaller than the atime,
|
||||
the new mail has been read.
|
||||
If the atime equals the mtime,
|
||||
there is no way to tell whether the file has new mail,
|
||||
since
|
||||
.B qmail-local
|
||||
takes much less than a second to run.
|
||||
One solution is for a mail reader to artificially set the
|
||||
atime to the mtime plus 1.
|
||||
Then the file has new mail if and only if the atime is
|
||||
less than or equal to the mtime.
|
||||
|
||||
Some mail readers place
|
||||
.B Status
|
||||
fields in each message to indicate which messages have been read.
|
||||
.SH "SEE ALSO"
|
||||
maildir(5),
|
||||
qmail-header(5),
|
||||
qmail-local(8)
|
|
@ -0,0 +1,239 @@
|
|||
.TH maildir 5
|
||||
.SH "NAME"
|
||||
maildir \- directory for incoming mail messages
|
||||
.SH "INTRODUCTION"
|
||||
.I maildir
|
||||
is a structure for
|
||||
directories of incoming mail messages.
|
||||
It solves the reliability problems that plague
|
||||
.I mbox
|
||||
files and
|
||||
.I mh
|
||||
folders.
|
||||
.SH "RELIABILITY ISSUES"
|
||||
A machine may crash while it is delivering a message.
|
||||
For both
|
||||
.I mbox
|
||||
files and
|
||||
.I mh
|
||||
folders this means that the message will be silently truncated.
|
||||
Even worse: for
|
||||
.I mbox
|
||||
format, if the message is truncated in the middle of a line,
|
||||
it will be silently joined to the next message.
|
||||
The mail transport agent will try again later to deliver the message,
|
||||
but it is unacceptable that a corrupted message should show up at all.
|
||||
In
|
||||
.IR maildir ,
|
||||
every message is guaranteed complete upon delivery.
|
||||
|
||||
A machine may have two programs simultaneously delivering mail
|
||||
to the same user.
|
||||
The
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
formats require the programs to update a single central file.
|
||||
If the programs do not use some locking mechanism,
|
||||
the central file will be corrupted.
|
||||
There are several
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
locking mechanisms,
|
||||
none of which work portably and reliably.
|
||||
In contrast, in
|
||||
.IR maildir ,
|
||||
no locks are ever necessary.
|
||||
Different delivery processes never touch the same file.
|
||||
|
||||
A user may try to delete messages from his mailbox at the same
|
||||
moment that the machine delivers a new message.
|
||||
For
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
formats, the user's mail-reading program must know
|
||||
what locking mechanism the mail-delivery programs use.
|
||||
In contrast, in
|
||||
.IR maildir ,
|
||||
any delivered message
|
||||
can be safely updated or deleted by a mail-reading program.
|
||||
|
||||
Many sites use Sun's
|
||||
.B Network F\fPa\fBil\fPur\fBe System
|
||||
(NFS),
|
||||
presumably because the operating system vendor does not offer
|
||||
anything else.
|
||||
NFS exacerbates all of the above problems.
|
||||
Some NFS implementations don't provide
|
||||
.B any
|
||||
reliable locking mechanism.
|
||||
With
|
||||
.I mbox
|
||||
and
|
||||
.I mh
|
||||
formats,
|
||||
if two machines deliver mail to the same user,
|
||||
or if a user reads mail anywhere except the delivery machine,
|
||||
the user's mail is at risk.
|
||||
.I maildir
|
||||
works without trouble over NFS.
|
||||
.SH "THE MAILDIR STRUCTURE"
|
||||
A directory in
|
||||
.I maildir
|
||||
format has three subdirectories,
|
||||
all on the same filesystem:
|
||||
.BR tmp ,
|
||||
.BR new ,
|
||||
and
|
||||
.BR cur .
|
||||
|
||||
Each file in
|
||||
.B new
|
||||
is a newly delivered mail message.
|
||||
The modification time of the file is the delivery date of the message.
|
||||
The message is delivered
|
||||
.I without
|
||||
an extra UUCP-style
|
||||
.B From_
|
||||
line,
|
||||
.I without
|
||||
any
|
||||
.B >From
|
||||
quoting,
|
||||
and
|
||||
.I without
|
||||
an extra blank line at the end.
|
||||
The message is normally in RFC 822 format,
|
||||
starting with a
|
||||
.B Return-Path
|
||||
line and a
|
||||
.B Delivered-To
|
||||
line,
|
||||
but it could contain arbitrary binary data.
|
||||
It might not even end with a newline.
|
||||
|
||||
Files in
|
||||
.B cur
|
||||
are just like files in
|
||||
.BR new .
|
||||
The big difference is that files in
|
||||
.B cur
|
||||
are no longer new mail:
|
||||
they have been seen by the user's mail-reading program.
|
||||
.SH "HOW A MESSAGE IS DELIVERED"
|
||||
The
|
||||
.B tmp
|
||||
directory is used to ensure reliable delivery,
|
||||
as discussed here.
|
||||
|
||||
A program delivers a mail message in six steps.
|
||||
First, it
|
||||
.B chdir()\fPs
|
||||
to the
|
||||
.I maildir
|
||||
directory.
|
||||
Second, it
|
||||
.B stat()s
|
||||
the name
|
||||
.BR tmp/\fItime.pid.host ,
|
||||
where
|
||||
.I time
|
||||
is the number of seconds since the beginning of 1970 GMT,
|
||||
.I pid
|
||||
is the program's process ID,
|
||||
and
|
||||
.I host
|
||||
is the host name.
|
||||
Third, if
|
||||
.B stat()
|
||||
returned anything other than ENOENT,
|
||||
the program sleeps for two seconds, updates
|
||||
.IR time ,
|
||||
and tries the
|
||||
.B stat()
|
||||
again, a limited number of times.
|
||||
Fourth, the program
|
||||
creates
|
||||
.BR tmp/\fItime.pid.host .
|
||||
Fifth, the program
|
||||
.I NFS-writes
|
||||
the message to the file.
|
||||
Sixth, the program
|
||||
.BR link() s
|
||||
the file to
|
||||
.BR new/\fItime.pid.host .
|
||||
At that instant the message has been successfully delivered.
|
||||
|
||||
The delivery program is required to start a 24-hour timer before
|
||||
creating
|
||||
.BR tmp/\fItime.pid.host ,
|
||||
and to abort the delivery
|
||||
if the timer expires.
|
||||
Upon error, timeout, or normal completion,
|
||||
the delivery program may attempt to
|
||||
.B unlink()
|
||||
.BR tmp/\fItime.pid.host .
|
||||
|
||||
.I NFS-writing
|
||||
means
|
||||
(1) as usual, checking the number of bytes returned from each
|
||||
.B write()
|
||||
call;
|
||||
(2) calling
|
||||
.B fsync()
|
||||
and checking its return value;
|
||||
(3) calling
|
||||
.B close()
|
||||
and checking its return value.
|
||||
(Standard NFS implementations handle
|
||||
.B fsync()
|
||||
incorrectly
|
||||
but make up for it by abusing
|
||||
.BR close() .)
|
||||
.SH "HOW A MESSAGE IS READ"
|
||||
A mail reader operates as follows.
|
||||
|
||||
It looks through the
|
||||
.B new
|
||||
directory for new messages.
|
||||
Say there is a new message,
|
||||
.BR new/\fIunique .
|
||||
The reader may freely display the contents of
|
||||
.BR new/\fIunique ,
|
||||
delete
|
||||
.BR new/\fIunique ,
|
||||
or rename
|
||||
.B new/\fIunique
|
||||
as
|
||||
.BR cur/\fIunique:info .
|
||||
See
|
||||
.B http://pobox.com/~djb/proto/maildir.html
|
||||
for the meaning of
|
||||
.IR info .
|
||||
|
||||
The reader is also expected to look through the
|
||||
.B tmp
|
||||
directory and to clean up any old files found there.
|
||||
A file in
|
||||
.B tmp
|
||||
may be safely removed if it
|
||||
has not been accessed in 36 hours.
|
||||
|
||||
It is a good idea for readers to skip all filenames in
|
||||
.B new
|
||||
and
|
||||
.B cur
|
||||
starting with a dot.
|
||||
Other than this, readers should not attempt to parse filenames.
|
||||
.SH "ENVIRONMENT VARIABLES"
|
||||
Mail readers supporting
|
||||
.I maildir
|
||||
use the
|
||||
.B MAILDIR
|
||||
environment variable
|
||||
as the name of the user's primary mail directory.
|
||||
.SH "SEE ALSO"
|
||||
mbox(5),
|
||||
qmail-local(8)
|
|
@ -17,7 +17,8 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.Dd November 11, 2022
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI-THEMES 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -31,15 +32,15 @@ comes with two themes,
|
|||
.Ic dark
|
||||
(default) and
|
||||
.Ic light .
|
||||
.sp
|
||||
.Pp
|
||||
Custom themes are defined as lists of key-values in the configuration files:
|
||||
.Bl -bullet -compact
|
||||
.Bl -item -compact -offset 2
|
||||
.It
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It
|
||||
.Pa $XDG_CONFIG_HOME/meli/themes/*.toml
|
||||
.El
|
||||
.sp
|
||||
.Pp
|
||||
The application theme is defined in the configuration as follows:
|
||||
.Bd -literal
|
||||
[terminal]
|
||||
|
@ -56,9 +57,9 @@ keys are settings for the
|
|||
.Ic compact
|
||||
mail listing style.
|
||||
A setting contains three fields: fg for foreground color, bg for background color, and attrs for text attribute.
|
||||
.sp
|
||||
.Pp
|
||||
.Dl \&"widget.key.label\&" = { fg = \&"Default\&", bg = \&"Default\&", attrs = \&"Default\&" }
|
||||
.sp
|
||||
.Pp
|
||||
Each field contains a value, which may be either a color/attribute, a link (key name) or a valid alias.
|
||||
An alias is a string starting with the \&"\&$\&" character and must be declared in advance in the
|
||||
.Ic color_aliases
|
||||
|
@ -69,10 +70,14 @@ An alias' value can be any valid value, including links and other aliases, as lo
|
|||
In the case of a link the setting's real value depends on the value of the referred key.
|
||||
This allows for defaults within a group of associated values.
|
||||
Cyclic references in a theme results in an error:
|
||||
.sp
|
||||
.Pp
|
||||
.Dl spooky theme contains a cycle: fg: mail.listing.compact.even -> mail.listing.compact.highlighted -> mail.listing.compact.odd -> mail.listing.compact.even
|
||||
.Pp
|
||||
Two themes are included by default, `light` and `dark`.
|
||||
Two themes are included by default,
|
||||
.Ql light
|
||||
and
|
||||
.Ql dark Ns
|
||||
\&.
|
||||
.Sh EXAMPLES
|
||||
Specific settings from already defined themes can be overwritten:
|
||||
.Bd -literal
|
||||
|
@ -100,18 +105,18 @@ Custom themes can be included in your configuration files or be saved independen
|
|||
.Pa $XDG_CONFIG_HOME/meli/themes/
|
||||
directory as TOML files.
|
||||
To start creating a theme right away, you can begin by editing the default theme keys and values:
|
||||
.sp
|
||||
.Pp
|
||||
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
|
||||
.sp
|
||||
.Pp
|
||||
.Pa new_theme.toml
|
||||
will now include all keys and values of the "dark" theme.
|
||||
.sp
|
||||
.Pp
|
||||
.Dl meli print-loaded-themes
|
||||
.sp
|
||||
.Pp
|
||||
will print all loaded themes with the links resolved.
|
||||
.Sh VALID ATTRIBUTE VALUES
|
||||
Case-sensitive.
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
"Default"
|
||||
.It
|
||||
|
@ -133,7 +138,7 @@ Any combo of the above separated by a bitwise XOR "\&|" eg "Dim | Italics"
|
|||
.El
|
||||
.Sh VALID COLOR VALUES
|
||||
Color values are of type String with the following valid contents:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
"Default" is the terminal default. (Case-sensitive)
|
||||
.It
|
||||
|
@ -146,8 +151,10 @@ Three character shorthand is also valid, e.g. #09c → #0099cc (Case-insensitive
|
|||
name but with some modifications (for a full table see COLOR NAMES addendum) (Case-sensitive)
|
||||
.El
|
||||
.Sh NO COLOR
|
||||
To completely disable ANSI colors, there are two options:
|
||||
.Bl -bullet -compact
|
||||
To completely disable
|
||||
.Tn ANSI
|
||||
colors, there are two options:
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Set the
|
||||
.Ic use_color
|
||||
|
@ -157,17 +164,22 @@ option (section
|
|||
.It
|
||||
The
|
||||
.Ev NO_COLOR
|
||||
environmental variable, when present (regardless of its value), prevents the addition of ANSI color.
|
||||
environmental variable, when present (regardless of its value), prevents the addition of
|
||||
.Tn ANSI
|
||||
color.
|
||||
When the configuration value
|
||||
.Ic use_color
|
||||
is explicitly set to true by the user,
|
||||
.Ev NO_COLOR
|
||||
is ignored.
|
||||
.El
|
||||
.sp
|
||||
In this mode, cursor locations (i.e., currently selected entries/items) will use the "reverse video" ANSI attribute to invert the terminal's default foreground/background colors.
|
||||
.Pp
|
||||
In this mode, cursor locations (i.e., currently selected entries/items) will use the
|
||||
.Ql reverse video
|
||||
.Tn ANSI
|
||||
attribute to invert the terminal's default foreground/background colors.
|
||||
.Sh VALID KEYS
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
theme_default
|
||||
.It
|
||||
|
@ -312,7 +324,7 @@ pager.highlight_search_current
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Aqua:14:_:Black:0
|
||||
Aquamarine1:122:_:Maroon:1
|
||||
Aquamarine2:86:_:Green:2
|
||||
|
@ -348,7 +360,7 @@ DarkMagenta1:91:_:SpringGreen6:29
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
DarkOliveGreen1:192:_:Turquoise4:30
|
||||
DarkOliveGreen2:155:_:DeepSkyBlue3:31
|
||||
DarkOliveGreen3:191:_:DeepSkyBlue4:32
|
||||
|
@ -384,7 +396,7 @@ DeepPink4:125:_:Grey37:59
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
DeepPink6:162:_:MediumPurple6:60
|
||||
DeepPink7:89:_:SlateBlue2:61
|
||||
DeepPink8:53:_:SlateBlue3:62
|
||||
|
@ -420,7 +432,7 @@ Grey19:236:_:DeepPink7:89
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Grey23:237:_:DarkMagenta:90
|
||||
Grey27:238:_:DarkMagenta1:91
|
||||
Grey3:232:_:DarkViolet1:92
|
||||
|
@ -456,7 +468,7 @@ HotPink2:169:_:LightGreen:119
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
HotPink3:132:_:LightGreen1:120
|
||||
HotPink4:168:_:PaleGreen1:121
|
||||
IndianRed:131:_:Aquamarine1:122
|
||||
|
@ -492,7 +504,7 @@ LightSlateGrey:103:_:DarkOliveGreen6:149
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
LightSteelBlue:147:_:DarkSeaGreen6:150
|
||||
LightSteelBlue1:189:_:DarkSeaGreen3:151
|
||||
LightSteelBlue3:146:_:LightCyan3:152
|
||||
|
@ -528,7 +540,7 @@ NavajoWhite3:144:_:LightGoldenrod3:179
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Navy:4:_:Tan:180
|
||||
NavyBlue:17:_:MistyRose3:181
|
||||
Olive:3:_:Thistle3:182
|
||||
|
@ -564,7 +576,7 @@ Purple5:55:_:Salmon1:209
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Red:9:_:LightCoral:210
|
||||
Red1:196:_:PaleVioletRed1:211
|
||||
Red2:124:_:Orchid2:212
|
||||
|
@ -600,7 +612,7 @@ Tan:180:_:Grey30:239
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name ↓:byte:_:name:byte ↓
|
||||
name \(da:byte:_:name:byte \(da
|
||||
Teal:6:_:Grey35:240
|
||||
Thistle1:225:_:Grey39:241
|
||||
Thistle3:182:_:Grey42:242
|
||||
|
@ -621,16 +633,34 @@ Yellow6:148:_:Grey93:255
|
|||
.Sh SEE ALSO
|
||||
.Xr meli 1 ,
|
||||
.Xr meli.conf 5
|
||||
.Sh CONFORMING TO
|
||||
TOML Standard v.0.5.0
|
||||
.Lk https://toml.io/en/v0.5.0
|
||||
.sp
|
||||
.Lk https://no-color.org/
|
||||
.Sh STANDARDS
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://toml.io/en/v0.5.0 "TOML Standard v.0.5.0"
|
||||
.It
|
||||
.Lk https://no\-color.org/ "NO_COLOR: disabling ANSI color output by default"
|
||||
.El
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2019
|
||||
.An Manos Pitsidianakis Aq manos@pitsidianak.is
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.sp
|
||||
.Lk https://meli.delivery
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
456
meli/docs/meli.1
456
meli/docs/meli.1
|
@ -17,6 +17,10 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.de HorizontalRule
|
||||
.\"\l'\n(.l\(ru1.25'
|
||||
.sp
|
||||
..
|
||||
.de Shortcut
|
||||
.Sm
|
||||
.Aq \\$1
|
||||
|
@ -40,12 +44,13 @@
|
|||
.Ed
|
||||
.sp
|
||||
..
|
||||
.Dd November 11, 2022
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd terminal e-mail client
|
||||
.Nd terminal e\-mail client
|
||||
.Em μέλι
|
||||
is the Greek word for honey
|
||||
.Sh SYNOPSIS
|
||||
|
@ -65,36 +70,44 @@ Create configuration file in
|
|||
.Pa path
|
||||
if given, or at
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It Cm edit-config
|
||||
Edit configuration files with
|
||||
.Ev EDITOR
|
||||
or
|
||||
.Ev VISUAL Ns
|
||||
\&.
|
||||
.It Cm test-config Op Ar path
|
||||
Test a configuration file for syntax issues or missing options.
|
||||
.It Cm man Op Ar page
|
||||
Print documentation page and exit (Piping to a pager is recommended).
|
||||
.It Cm install-man Op Ar path
|
||||
Install manual pages to the first location provided by
|
||||
.Ar MANPATH
|
||||
.Ev MANPATH
|
||||
or
|
||||
.Xr manpath 1 ,
|
||||
unless you specify the directory as an argument.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm edit-config
|
||||
Edit configuration files with
|
||||
.Ev EDITOR
|
||||
or
|
||||
.Ev VISUAL Ns
|
||||
\&.
|
||||
.It Cm help
|
||||
Prints help information or the help of the given subcommand(s).
|
||||
.It Cm print-app-directories
|
||||
Print all directories that
|
||||
.Ns Nm
|
||||
creates and uses.
|
||||
.It Cm print-config-path
|
||||
Print location of configuration file that will be loaded on normal app startup.
|
||||
.It Cm print-default-theme
|
||||
Print default theme keys and values in TOML syntax, to be used as a blueprint.
|
||||
.It Cm print-loaded-themes
|
||||
Print all loaded themes in TOML syntax.
|
||||
.It Cm print-used-paths
|
||||
Print all paths that are created and used.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm print-log-path
|
||||
Print log file location.
|
||||
.It Cm view
|
||||
View mail from input file.
|
||||
.El
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
is a terminal mail client aiming for extensive and user-friendly configurability.
|
||||
.Bd -literal
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -128,11 +141,28 @@ At any time, you may press
|
|||
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
|
||||
.Pp
|
||||
The main visual navigation tool, the left-side sidebar may be toggled with
|
||||
.ShortcutPeriod ` listing toggle_menu_visibility
|
||||
.ShortcutPeriod \(ga listing toggle_menu_visibility
|
||||
\&.
|
||||
.Pp
|
||||
Each mailbox may be viewed in 4 modes:
|
||||
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Tg index-style-plain
|
||||
.Em Plain
|
||||
views each mail individually,
|
||||
.It
|
||||
.Tg index-style-threaded
|
||||
.Em Threaded
|
||||
shows their thread relationship visually,
|
||||
.It
|
||||
.Tg index-style-conversations
|
||||
.Em Conversations
|
||||
collapses each thread of e\-mails into a single entry,
|
||||
.It
|
||||
.Tg index-style-compact
|
||||
.Em Compact
|
||||
shows one row per thread.
|
||||
.El
|
||||
.Pp
|
||||
If you're using a light color palette in your terminal, you should set
|
||||
.Em theme = "light"
|
||||
|
@ -148,6 +178,10 @@ See
|
|||
for a more detailed tutorial on using
|
||||
.Nm Ns
|
||||
\&.
|
||||
.Sh SHORTCUTS
|
||||
See
|
||||
.Xr meli.conf 5 SHORTCUTS
|
||||
for shortcuts and their default values.
|
||||
.Sh VIEWING MAIL
|
||||
Open attachments by typing their index in the attachments list and then
|
||||
.ShortcutPeriod a envelope_view open_attachment
|
||||
|
@ -173,7 +207,7 @@ If the path provided is a directory, the attachment is saved with its filename s
|
|||
If the 0th index is provided, the entire message is saved.
|
||||
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
|
||||
.Sh SEARCH
|
||||
Each e-mail storage backend has a default search method assigned.
|
||||
Each e\-mail storage backend has a default search method assigned.
|
||||
.Em IMAP
|
||||
uses the SEARCH command,
|
||||
.Em notmuch
|
||||
|
@ -222,9 +256,8 @@ alias:
|
|||
.Pc
|
||||
String keywords with spaces must be quoted.
|
||||
Quotes should always be escaped.
|
||||
.sp
|
||||
.Sy Important Notice about IMAP/JMAP
|
||||
.sp
|
||||
.Ss Important Notice about IMAP/JMAP
|
||||
.HorizontalRule
|
||||
To prevent downloading all your messages from your IMAP/JMAP server, don't set
|
||||
.Em search_backend
|
||||
to
|
||||
|
@ -233,9 +266,10 @@ to
|
|||
.Nm
|
||||
will relay your queries to the IMAP server.
|
||||
Expect a delay between query and response.
|
||||
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
|
||||
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable delay.
|
||||
.Ss QUERY ABNF SYNTAX
|
||||
.Bl -bullet
|
||||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
|
||||
.It
|
||||
|
@ -265,10 +299,23 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
|
|||
.It
|
||||
.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
|
||||
.El
|
||||
.Sh FLAGS
|
||||
.Nm
|
||||
supports the basic maildir flags: passed, replied, seen, trashed, draft and flagged.
|
||||
Flags can be searched with the
|
||||
.Ns Ql flags:
|
||||
prefix in a search query, and can be modified by
|
||||
.Command flag set FLAG
|
||||
and
|
||||
.Command flag unset FLAG
|
||||
.Sh TAGS
|
||||
.Nm
|
||||
supports tagging in notmuch and IMAP/JMAP backends.
|
||||
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
|
||||
Tags can be searched with the
|
||||
.Ns Ql tags:
|
||||
or
|
||||
.Ns Ql flags:
|
||||
prefix in a search query, and can be modified by
|
||||
.Command tag add TAG
|
||||
and
|
||||
.Command tag remove TAG
|
||||
|
@ -289,7 +336,8 @@ To reply to a mail, press
|
|||
\&.
|
||||
Both these actions open the mail composer view in a new tab.
|
||||
.Ss Editing text
|
||||
.Bl -bullet -compact
|
||||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Edit the header fields by selecting with the arrow keys and pressing
|
||||
.Shortcut Enter general focus_in_text_field
|
||||
|
@ -332,12 +380,14 @@ and to resume editing press the
|
|||
command again.
|
||||
.El
|
||||
.Ss Attachments
|
||||
.HorizontalRule
|
||||
Attachments may be handled with the
|
||||
.Cm add-attachment Ns
|
||||
,
|
||||
.Cm remove-attachment
|
||||
commands (see below).
|
||||
.Ss Sending
|
||||
.HorizontalRule
|
||||
Finally, pressing
|
||||
.Shortcut s composing send_mail
|
||||
will send your message according to your settings
|
||||
|
@ -355,6 +405,7 @@ On complete failure to save your draft or sent message it will be saved in your
|
|||
.Em tmp
|
||||
directory instead and you will be notified of its location.
|
||||
.Ss Drafts
|
||||
.HorizontalRule
|
||||
To save your draft without sending it, issue
|
||||
.Em COMMAND
|
||||
.Cm close
|
||||
|
@ -366,8 +417,7 @@ To open a draft for further editing, select your draft in the mail listing and p
|
|||
.Sh CONTACTS
|
||||
.Nm
|
||||
supports three kinds of contact backends:
|
||||
.sp
|
||||
.Bl -enum -compact -offset indent
|
||||
.Bl -enum -compact
|
||||
.It
|
||||
an internal format that gets saved under
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
|
||||
|
@ -389,7 +439,7 @@ compatible alias file in the option
|
|||
.sp
|
||||
See
|
||||
.Xr meli.conf 5 ACCOUNTS
|
||||
for the complete account configuration values.
|
||||
for the complete account contact configuration values.
|
||||
.Sh MODES
|
||||
.Bl -tag -compact -width 8n
|
||||
.It NORMAL
|
||||
|
@ -409,8 +459,9 @@ captures all input as text input, and is exited with
|
|||
.Cm Esc
|
||||
key.
|
||||
.El
|
||||
.Ss COMMAND Mode
|
||||
.Sh COMMAND
|
||||
.Ss Mail listing commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm set Ar plain | threaded | compact | conversations
|
||||
set the way mailboxes are displayed
|
||||
|
@ -445,6 +496,8 @@ Escape exits search results.
|
|||
select threads matching
|
||||
.Ar STRING
|
||||
query.
|
||||
.It Cm clear-selection
|
||||
Clear current selection.
|
||||
.It Cm set seen, set unseen
|
||||
Set seen status of message.
|
||||
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
|
||||
|
@ -452,25 +505,26 @@ Import mail from file into given mailbox.
|
|||
.It Cm copyto, moveto Ar MAILBOX_PATH
|
||||
Copy or move to other mailbox.
|
||||
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
|
||||
Copy or move to another account's mailbox.
|
||||
Copy or move to another account's mailbox.
|
||||
.It Cm delete
|
||||
Delete selected threads.
|
||||
.It Cm export-mbox Ar FILEPATH
|
||||
Export selected threads to mboxcl2 file.
|
||||
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm create\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
create mailbox with given path.
|
||||
Be careful with backends and separator sensitivity (eg IMAP)
|
||||
.It Cm subscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm subscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
subscribe to mailbox with given path
|
||||
.It Cm unsubscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm unsubscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
unsubscribe to mailbox with given path
|
||||
.It Cm rename-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
|
||||
.It Cm rename\-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
|
||||
rename mailbox
|
||||
.It Cm delete-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
.It Cm delete\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
deletes mailbox in the mail backend.
|
||||
This action is unreversible.
|
||||
This action is irreversible.
|
||||
.El
|
||||
.Ss Mail view commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm pipe Ar EXECUTABLE Ar ARGS
|
||||
pipe pager contents to binary
|
||||
|
@ -484,7 +538,8 @@ unsubscribe automatically from list of viewed envelope
|
|||
open list archive with
|
||||
.Cm xdg-open
|
||||
.El
|
||||
.Ss composing mail commands
|
||||
.Ss Composing mail commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm mailto Ar MAILTO_ADDRESS
|
||||
Opens a composer tab with initial values parsed from the
|
||||
|
@ -519,7 +574,8 @@ for PGP configuration.
|
|||
.It Cm save-draft
|
||||
saves a copy of the draft in the Draft folder
|
||||
.El
|
||||
.Ss generic commands
|
||||
.Ss Generic commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm open-in-tab
|
||||
opens envelope view in new tab
|
||||
|
@ -543,10 +599,6 @@ Useful if you want to reload some settings without restarting
|
|||
.Nm Ns
|
||||
\&.
|
||||
.El
|
||||
.Sh SHORTCUTS
|
||||
See
|
||||
.Xr meli.conf 5 SHORTCUTS
|
||||
for shortcuts and their default values.
|
||||
.Sh EXIT STATUS
|
||||
.Nm
|
||||
exits with 0 on a successful run.
|
||||
|
@ -564,7 +616,9 @@ Specifies the editor to use
|
|||
.It Ev MELI_CONFIG
|
||||
Override the configuration file
|
||||
.It Ev NO_COLOR
|
||||
When present (regardless of its value), prevents the addition of ANSI color.
|
||||
When defined (regardless of its value), prevents the addition of
|
||||
.Tn ANSI
|
||||
color.
|
||||
The configuration value
|
||||
.Ic use_color
|
||||
overrides this.
|
||||
|
@ -621,75 +675,265 @@ Mailcap entries are searched for in the following files, in this order:
|
|||
.It
|
||||
.Pa /usr/local/etc/mailcap
|
||||
.El
|
||||
.Sh STANDARDS
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Rs
|
||||
.%B XDG Base Directory Specification
|
||||
.%O Version 0.8
|
||||
.%A Waldo Bastian
|
||||
.%A Allison Karlitskaya
|
||||
.%A Lennart Poettering
|
||||
.%A Johannes Löthberg
|
||||
.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
.%D May 08, 2021
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B maildir
|
||||
.%A Daniel J. Bernstein
|
||||
.%U https://cr.yp.to/proto/maildir.html
|
||||
.%D 1995
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC1524 A User Agent Configuration Mechanism For Multimedia Mail Format Information
|
||||
.%O mailcap file
|
||||
.%I Legacy
|
||||
.%D September 01, 1993
|
||||
.%A Dr. Nathaniel S. Borenstein
|
||||
.%U https://datatracker.ietf.org/doc/rfc1524/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2047 MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
|
||||
.%I IETF
|
||||
.%D November 01, 1996
|
||||
.%A Keith Moore
|
||||
.%U https://datatracker.ietf.org/doc/rfc2047/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2183 Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
|
||||
.%I Legacy
|
||||
.%D August 01, 1997
|
||||
.%A Rens Troost
|
||||
.%A Steve Dorner
|
||||
.%A Keith Moore
|
||||
.%U https://datatracker.ietf.org/doc/rfc2183/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2369 The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
|
||||
.%I Legacy
|
||||
.%D July 01, 1998
|
||||
.%A Joshua D. Baer
|
||||
.%A Grant Neufeld
|
||||
.%U https://datatracker.ietf.org/doc/rfc2369/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC2426 vCard MIME Directory Profile
|
||||
.%O vCard Version 3
|
||||
.%I IETF
|
||||
.%D September 01, 1998
|
||||
.%A Frank Dawson
|
||||
.%A Tim Howes
|
||||
.%U https://datatracker.ietf.org/doc/rfc2426/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3156 MIME Security with OpenPGP
|
||||
.%I IETF
|
||||
.%D August 01, 2001
|
||||
.%A Thomas Roessler
|
||||
.%A Michael Elkins
|
||||
.%A Raph Levien
|
||||
.%A Dave Del Torto
|
||||
.%U https://datatracker.ietf.org/doc/rfc3156/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3461 Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
|
||||
.%I IETF
|
||||
.%D January 23, 2003
|
||||
.%A Keith Moore
|
||||
.%U https://datatracker.ietf.org/doc/rfc3461/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
|
||||
.%I IETF
|
||||
.%D March 18, 2003
|
||||
.%A Mark Crispin
|
||||
.%U https://datatracker.ietf.org/doc/rfc3501/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3676 The Text/Plain Format and DelSp Parameters
|
||||
.%I IETF
|
||||
.%D February 19, 2004
|
||||
.%A Randall Gellens
|
||||
.%U https://datatracker.ietf.org/doc/rfc3676/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3691 Internet Message Access Protocol (IMAP) UNSELECT command
|
||||
.%I IETF
|
||||
.%D February 20, 2004
|
||||
.%A Alexey Melnikov
|
||||
.%U https://datatracker.ietf.org/doc/rfc3691/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC3977 Network News Transfer Protocol (NNTP)
|
||||
.%I IETF
|
||||
.%D October 26, 2006
|
||||
.%A Clive Feather
|
||||
.%U https://datatracker.ietf.org/doc/rfc3977/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
|
||||
.%I IETF
|
||||
.%D June 16, 2006
|
||||
.%A Alexey Melnikov
|
||||
.%U https://datatracker.ietf.org/doc/rfc4549/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4616 The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.%I IETF
|
||||
.%D August 31, 2006
|
||||
.%A Kurt Zeilenga
|
||||
.%U https://datatracker.ietf.org/doc/rfc4616/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC4954 SMTP Service Extension for Authentication
|
||||
.%I IETF
|
||||
.%D July 23, 2007
|
||||
.%A Rob Siemborski
|
||||
.%A Alexey Melnikov
|
||||
.%U https://datatracker.ietf.org/doc/rfc4954/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC5321 Simple Mail Transfer Protocol
|
||||
.%I IETF
|
||||
.%D October 01, 2008
|
||||
.%A Dr. John C. Klensin
|
||||
.%U https://datatracker.ietf.org/doc/rfc5321/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC5322 Internet Message Format
|
||||
.%I IETF
|
||||
.%D October 01, 2008
|
||||
.%A Pete Resnick
|
||||
.%U https://datatracker.ietf.org/doc/rfc5322/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6048 Network News Transfer Protocol (NNTP) Additions to LIST Command
|
||||
.%I IETF
|
||||
.%D November 22, 2010
|
||||
.%A Julien ÉLIE
|
||||
.%U https://datatracker.ietf.org/doc/rfc6048/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6152 SMTP Service Extension for 8-bit MIME Transport
|
||||
.%I IETF
|
||||
.%D March 07, 2011
|
||||
.%A Dave Crocker
|
||||
.%A Dr. John C. Klensin
|
||||
.%A Dr. Marshall T. Rose
|
||||
.%A Ned Freed
|
||||
.%U https://datatracker.ietf.org/doc/rfc6152/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6350 vCard Format Specification
|
||||
.%O vCard Version 4
|
||||
.%I IETF
|
||||
.%D August 31, 2011
|
||||
.%A Simon Perreault
|
||||
.%U https://datatracker.ietf.org/doc/rfc6350/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6532 Internationalized Email Headers
|
||||
.%I IETF
|
||||
.%D February 17, 2012
|
||||
.%A Abel Yang
|
||||
.%A Shawn Steele
|
||||
.%A Ned Freed
|
||||
.%U https://datatracker.ietf.org/doc/rfc6532/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC6868 Parameter Value Encoding in iCalendar and vCard
|
||||
.%I IETF
|
||||
.%D February 14, 2013
|
||||
.%A Cyrus Daboo
|
||||
.%U https://datatracker.ietf.org/doc/rfc6868/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC7162 IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
|
||||
.%I IETF
|
||||
.%D May 23, 2014
|
||||
.%A Alexey Melnikov
|
||||
.%A Dave Cridland
|
||||
.%U https://datatracker.ietf.org/doc/rfc7162/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC8620 The JSON Meta Application Protocol (JMAP)
|
||||
.%I IETF
|
||||
.%D July 18, 2019
|
||||
.%A Neil Jenkins
|
||||
.%A Chris Newman
|
||||
.%U https://datatracker.ietf.org/doc/rfc8620/
|
||||
.Re
|
||||
.It
|
||||
.Rs
|
||||
.%B RFC8621 The JSON Meta Application Protocol (JMAP) for Mail
|
||||
.%I IETF
|
||||
.%D August 08, 2019
|
||||
.%A Neil Jenkins
|
||||
.%A Chris Newman
|
||||
.%U https://datatracker.ietf.org/doc/rfc8621/
|
||||
.Re
|
||||
.El
|
||||
.Sh SEE ALSO
|
||||
.Xr meli.conf 5 ,
|
||||
.Xr meli-themes 5 ,
|
||||
.Xr meli 7 ,
|
||||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh CONFORMING TO
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
XDG Standard
|
||||
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
|
||||
\&.
|
||||
.It
|
||||
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
|
||||
.It
|
||||
RFC 5322: Internet Message Format
|
||||
.It
|
||||
RFC 6532: Internationalized Email Headers
|
||||
.It
|
||||
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
|
||||
.It
|
||||
RFC 3676: The Text/Plain Format and DelSp Parameters
|
||||
.It
|
||||
RFC 3156: MIME Security with OpenPGP
|
||||
.It
|
||||
RFC 2183: Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
|
||||
.It
|
||||
RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
|
||||
.It
|
||||
.Li maildir
|
||||
.Lk https://cr.yp.to/proto/maildir.html Ns
|
||||
\&.
|
||||
.It
|
||||
RFC 5321: Simple Mail Transfer Protocol
|
||||
.It
|
||||
RFC 3461: Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
|
||||
.It
|
||||
RFC 4954: SMTP Service Extension for Authentication
|
||||
.It
|
||||
RFC 6152: SMTP Service Extension for 8-bit MIME Transport
|
||||
.It
|
||||
RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.It
|
||||
RFC 3501: INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
|
||||
.It
|
||||
RFC 3691: Internet Message Access Protocol (IMAP) UNSELECT command
|
||||
.It
|
||||
RFC 4549: Synch Ops for Disconnected IMAP4 Clients
|
||||
.It
|
||||
RFC 7162: IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
|
||||
.It
|
||||
RFC 8620: The JSON Meta Application Protocol (JMAP)
|
||||
.It
|
||||
RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail
|
||||
.It
|
||||
RFC 3977: Network News Transfer Protocol (NNTP)
|
||||
.It
|
||||
RFC 6048: Network News Transfer Protocol (NNTP) Additions to LIST Command
|
||||
.It
|
||||
vCard Version 3, RFC 2426: vCard MIME Directory Profile
|
||||
.It
|
||||
vCard Version 4, RFC 6350: vCard Format Specification
|
||||
.It
|
||||
RFC 6868 Parameter Value Encoding in iCalendar and vCard
|
||||
.El
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Aq manos@pitsidianak.is
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
126
meli/docs/meli.7
126
meli/docs/meli.7
|
@ -40,22 +40,23 @@
|
|||
.Pc Ns
|
||||
..
|
||||
.de Command
|
||||
.Bd -offset 1n -ragged
|
||||
.Bd -ragged -offset 1n
|
||||
.Cm \\$*
|
||||
.Ed
|
||||
..
|
||||
.Dd November 11, 2022
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI 7
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd Tutorial for the meli terminal e-mail client
|
||||
.Nd Tutorial for the meli terminal e\-mail client
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op ...
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
is a terminal mail client aiming for extensive and user\-friendly configurability.
|
||||
.Bd -literal -offset center
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -158,9 +159,9 @@ key.
|
|||
.It EMBED
|
||||
This is the mode of the embed terminal emulator.
|
||||
To exit an embedded application, issue
|
||||
.Aq Ctrl-C
|
||||
.Aq Ctrl\-C
|
||||
to kill it or
|
||||
.Aq Ctrl-Z
|
||||
.Aq Ctrl\-Z
|
||||
to stop the program and follow the instructions on
|
||||
.Nm
|
||||
to exit.
|
||||
|
@ -229,7 +230,7 @@ This is the view you will spend more time with in
|
|||
\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut ` listing toggle_menu_visibility
|
||||
.Shortcut \(ga listing toggle_menu_visibility
|
||||
to toggle the sidebars visibility.
|
||||
.Pp
|
||||
Press
|
||||
|
@ -237,16 +238,16 @@ Press
|
|||
to switch focus on the sidebar menu.
|
||||
Press
|
||||
.Shortcut Right listing focus_left
|
||||
to switch focus on the e-mail list.
|
||||
to switch focus on the e\-mail list.
|
||||
.Pp
|
||||
On the e-mail list, press
|
||||
On the e\-mail list, press
|
||||
.Shortcut k listing scroll_up
|
||||
to scroll up, and
|
||||
.Shortcut j listing scroll_down
|
||||
to scroll down.
|
||||
Press
|
||||
.Shortcut Enter listing open_entry
|
||||
to open an e-mail entry and
|
||||
to open an e\-mail entry and
|
||||
.Shortcut i listing exit_entry
|
||||
to exit it.
|
||||
.Bd -ragged
|
||||
|
@ -294,9 +295,9 @@ See
|
|||
for details.
|
||||
.Pp
|
||||
You can increase the sidebar's width with
|
||||
.Shortcut Ctrl-p listing increase_sidebar
|
||||
.Shortcut Ctrl\-p listing increase_sidebar
|
||||
and decrease with
|
||||
.ShortcutPeriod Ctrl-o listing decrease_sidebar
|
||||
.ShortcutPeriod Ctrl\-o listing decrease_sidebar
|
||||
\&.
|
||||
.Bd -ragged
|
||||
.Sy The status bar.
|
||||
|
@ -310,7 +311,7 @@ and decrease with
|
|||
The status bar shows which mode you are, and the status message of the current view.
|
||||
In the pictured example, it shows the status of a mailbox called
|
||||
.Dq Inbox
|
||||
with lots of e-mails.
|
||||
with lots of e\-mails.
|
||||
.Bd -ragged
|
||||
.Sy The number modifier buffer.
|
||||
.Ed
|
||||
|
@ -330,7 +331,7 @@ entries.
|
|||
Another use of the number buffer is opening URLs inside the pager.
|
||||
See
|
||||
.Sx PAGER
|
||||
for an explanation of interacting with URLs in e-mails.
|
||||
for an explanation of interacting with URLs in e\-mails.
|
||||
.Pp
|
||||
Pressing numbers in
|
||||
.Sy NORMAL
|
||||
|
@ -343,16 +344,16 @@ There are four different list styles:
|
|||
.Bl -hyphen -compact
|
||||
.It
|
||||
.Qq plain
|
||||
which shows one line per e-mail.
|
||||
which shows one line per e\-mail.
|
||||
.It
|
||||
.Qq threaded
|
||||
which shows a threaded view with drawn tree structure.
|
||||
.It
|
||||
.Qq compact
|
||||
which shows one line per thread which can include multiple e-mails.
|
||||
which shows one line per thread which can include multiple e\-mails.
|
||||
.It
|
||||
.Qq conversations
|
||||
which shows more than one line per thread which can include multiple e-mails with more details about the thread.
|
||||
which shows more than one line per thread which can include multiple e\-mails with more details about the thread.
|
||||
.El
|
||||
.Bd -ragged
|
||||
.Sy Plain view\&.
|
||||
|
@ -421,13 +422,13 @@ Simple set operations can be performed on a selection with these shortcut modifi
|
|||
.Bl -hyphen -compact
|
||||
.It
|
||||
Union modifier:
|
||||
.Shortcut Ctrl-u listing union_modifier
|
||||
.Shortcut Ctrl\-u listing union_modifier
|
||||
.It
|
||||
Difference modifier:
|
||||
.Shortcut Ctrl-d listing diff_modifier
|
||||
.Shortcut Ctrl\-d listing diff_modifier
|
||||
.It
|
||||
Intersection modifier:
|
||||
.Shortcut Ctrl-i listing intersection_modifier
|
||||
.Shortcut Ctrl\-i listing intersection_modifier
|
||||
.El
|
||||
.Pp
|
||||
To set an entry as
|
||||
|
@ -445,7 +446,11 @@ which also has its complement
|
|||
.sp
|
||||
action.
|
||||
.Pp
|
||||
For e-mail backends that support tags
|
||||
For e\-mail backends that support flags you can use the following commands on entries and selections to modify them:
|
||||
.Command flag set FLAG
|
||||
.Command flag unset FLAG
|
||||
.Pp
|
||||
For e\-mail backends that support tags
|
||||
.Po
|
||||
like
|
||||
.Qq IMAP
|
||||
|
@ -463,10 +468,13 @@ you can use the following commands on entries and selections to modify them:
|
|||
and
|
||||
.Ic ignore_tags
|
||||
for how to set tag colors and tag visibility)
|
||||
You can clear the selection with the
|
||||
.Aq Esc
|
||||
key.
|
||||
.Sh PAGER
|
||||
You can open an e-mail entry by pressing
|
||||
You can open an e\-mail entry by pressing
|
||||
.ShortcutPeriod Enter listing open_entry
|
||||
\&. This brings up the e-mail view with the e-mail content inside a pager.
|
||||
\&. This brings up the e\-mail view with the e\-mail content inside a pager.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│Date: Sat, 21 May 2022 16:16:11 +0300 ▀│
|
||||
|
@ -494,14 +502,14 @@ You can open an e-mail entry by pressing
|
|||
└────────────────────────────────────────────────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Em The\ pager\ displaying\ an\ e-mail\&.
|
||||
.Em The\ pager\ displaying\ an\ e\-mail\&.
|
||||
.Ed
|
||||
.Pp
|
||||
The pager is simple to use.
|
||||
Scroll with the following:
|
||||
.Bl -hang -width 27n
|
||||
.It Go to next pager page
|
||||
.Shortcut PageDown pager page_down
|
||||
.Shortcut PageDown pager page_down
|
||||
.It Go to previous pager page
|
||||
.Shortcut PageUp pager page_up
|
||||
.It Scroll down pager.
|
||||
|
@ -516,7 +524,7 @@ which will act as a multiplier.
|
|||
.Pp
|
||||
The pager can enter a special
|
||||
.Em url
|
||||
mode which will prefix all detected hyperlinks and e-mail addresses with a number inside square brackets
|
||||
mode which will prefix all detected hyperlinks and e\-mail addresses with a number inside square brackets
|
||||
.ShortcutPeriod u pager toggle_url_mode
|
||||
\&.
|
||||
Writing down a chosen number as a number modifier
|
||||
|
@ -547,13 +555,13 @@ for more details
|
|||
.Pc Ns
|
||||
\&.
|
||||
.Sh MAIL VIEW
|
||||
Other things you can do when viewing e-mail:
|
||||
.Bl -bullet -compact
|
||||
Other things you can do when viewing e\-mail:
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Most importantly, you can exit the mail view with:
|
||||
.Shortcut i listing exit_entry
|
||||
.It
|
||||
Add addresses from the e-mail headers to contacts:
|
||||
Add addresses from the e\-mail headers to contacts:
|
||||
.Shortcut c envelope_view add_addresses_to_contacts
|
||||
.It
|
||||
Open an attachment by entering its index as a number modifier and pressing:
|
||||
|
@ -569,39 +577,39 @@ Reply to envelope:
|
|||
.Shortcut R envelope_view reply
|
||||
.It
|
||||
Reply to author:
|
||||
.Shortcut Ctrl-r envelope_view reply_to_author
|
||||
.Shortcut Ctrl\-r envelope_view reply_to_author
|
||||
.It
|
||||
Reply to all/Reply to list/Follow up:
|
||||
.Shortcut Ctrl-g envelope_view reply_to_all
|
||||
.Shortcut Ctrl\-g envelope_view reply_to_all
|
||||
.It
|
||||
Forward email:
|
||||
.Shortcut Ctrl-f envelope_view forward
|
||||
Forward e\-mail:
|
||||
.Shortcut Ctrl\-f envelope_view forward
|
||||
.It
|
||||
Expand extra headers: (References and others)
|
||||
.Shortcut h envelope_view toggle_expand_headerk
|
||||
.It
|
||||
View envelope source in a pager: (toggles between raw and decoded source)
|
||||
.Shortcut M-r envelope_view view_raw_source
|
||||
.Shortcut M\-r envelope_view view_raw_source
|
||||
.It
|
||||
Return to envelope_view if viewing raw source or attachment:
|
||||
.Shortcut r envelope_view return_to_normal_view
|
||||
.El
|
||||
.Sh COMPOSING
|
||||
To compose an e-mail, you can either start with an empty draft by pressing
|
||||
To compose an e\-mail, you can either start with an empty draft by pressing
|
||||
.Shortcut m listing new_mail
|
||||
which opens a composer view in a new tab.
|
||||
To reply to a specific e-mail, when in envelope view you can select the specific action you want to take:
|
||||
To reply to a specific e\-mail, when in envelope view you can select the specific action you want to take:
|
||||
.sp
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
Reply to envelope.
|
||||
.Shortcut R envelope_view reply
|
||||
.It
|
||||
Reply to author.
|
||||
.Shortcut Ctrl-r envelope_view reply_to_author
|
||||
.Shortcut Ctrl\-r envelope_view reply_to_author
|
||||
.It
|
||||
Reply to all.
|
||||
.Shortcut Ctrl-g envelope_view reply_to_all
|
||||
.Shortcut Ctrl\-g envelope_view reply_to_all
|
||||
.El
|
||||
.sp
|
||||
To launch your editor, press
|
||||
|
@ -688,25 +696,25 @@ the\ actual\ embedding\ is\ seamless\&.
|
|||
.Ed
|
||||
.Ss composing mail commands
|
||||
.Bl -tag -width 36n
|
||||
.It Cm add-attachment Ar PATH
|
||||
.It Cm add\-attachment Ar PATH
|
||||
in composer, add
|
||||
.Ar PATH
|
||||
as an attachment
|
||||
.It Cm add-attachment < Ar CMD Ar ARGS
|
||||
.It Cm add\-attachment < Ar CMD Ar ARGS
|
||||
in composer, pipe
|
||||
.Ar CMD Ar ARGS
|
||||
output into an attachment
|
||||
.It Cm add-attachment-file-picker
|
||||
.It Cm add\-attachment\-file\-picker
|
||||
Launch command defined in the configuration value
|
||||
.Ic file_picker_command
|
||||
in
|
||||
.Xr meli.conf 5 TERMINAL
|
||||
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
|
||||
.It Cm add\-attachment\-file\-picker < Ar CMD Ar ARGS
|
||||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
.It Cm remove\-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
toggle between signing and not signing this message.
|
||||
|
@ -714,7 +722,7 @@ If the gpg invocation fails then the mail won't be sent.
|
|||
See
|
||||
.Xr meli.conf 5 PGP
|
||||
for PGP configuration.
|
||||
.It Cm save-draft
|
||||
.It Cm save\-draft
|
||||
saves a copy of the draft in the Draft folder
|
||||
.El
|
||||
.\" [ref:TODO]: add contacts section
|
||||
|
@ -731,12 +739,26 @@ for documentation on how to theme
|
|||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Aq manos@pitsidianak.is
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
.Lk https://github.com/meli/meli
|
||||
.Lk https://crates.io/crates/meli
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
|
@ -17,7 +17,33 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.Dd November 11, 2022
|
||||
.de HorizontalRule
|
||||
.\"\l'\n(.l\(ru1.25'
|
||||
.sp
|
||||
..
|
||||
.de LiteralStringValue
|
||||
.Sm
|
||||
.Po Qo
|
||||
.Em Li \\$1
|
||||
.Qc Pc
|
||||
.Sm
|
||||
..
|
||||
.de LiteralStringValueRenders
|
||||
.LiteralStringValue \\$1
|
||||
.shift 1
|
||||
.Bo
|
||||
.Sm
|
||||
Rendered as:
|
||||
.Li r##
|
||||
.Qo
|
||||
\\$1
|
||||
.Qc
|
||||
.Li ##
|
||||
.Bc
|
||||
.Sm
|
||||
..
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dt MELI.CONF 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -44,7 +70,7 @@ is written in
|
|||
.Tn TOML
|
||||
which has a few things to consider (quoting the specification):
|
||||
.sp
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Tn TOML
|
||||
is case sensitive.
|
||||
|
@ -94,7 +120,7 @@ include macro:
|
|||
.\"
|
||||
.Sh SECTIONS
|
||||
The top level sections of the configuration are:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
accounts
|
||||
.It
|
||||
|
@ -178,19 +204,24 @@ theme = "light"
|
|||
.\"
|
||||
.sp
|
||||
Available options are listed below.
|
||||
Default values are shown in parentheses.
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Sh ACCOUNTS
|
||||
.Ss Account configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic root_mailbox Ar String
|
||||
The backend-specific path of the root_mailbox, usually
|
||||
.Sy INBOX Ns
|
||||
\&.
|
||||
.It Ic default_mailbox Ar String
|
||||
.Pq Em optional
|
||||
The mailbox that is the default to open or view for this account.
|
||||
Must be a valid mailbox path.
|
||||
If not specified, the default will be the root mailbox.
|
||||
.It Ic format Ar String Op maildir mbox imap notmuch jmap
|
||||
The format of the mail backend.
|
||||
.It Ic subscribed_mailboxes Ar [String,]
|
||||
|
@ -215,14 +246,6 @@ When replying to an e-mail addressed to one of these identities, the
|
|||
:
|
||||
header will be adjusted to its value instead of the default identity.
|
||||
.El
|
||||
.TS
|
||||
allbox tab(:);
|
||||
lb l.
|
||||
conversations:shows one entry per thread
|
||||
compact:shows one row per thread
|
||||
threaded:shows threads as a tree structure
|
||||
plain:shows one row per mail, regardless of threading
|
||||
.TE
|
||||
.Bl -tag -width 36n
|
||||
.It Ic display_name Ar String
|
||||
.Pq Em optional
|
||||
|
@ -278,6 +301,7 @@ Its format is described below in
|
|||
\&.
|
||||
.El
|
||||
.Ss notmuch only
|
||||
.HorizontalRule
|
||||
notmuch is supported by loading the dynamic library
|
||||
.Sy libnotmuch Ns
|
||||
\&.
|
||||
|
@ -346,6 +370,7 @@ format = "notmuch"
|
|||
.\"
|
||||
.\"
|
||||
.Ss IMAP only
|
||||
.HorizontalRule
|
||||
.Tn IMAP
|
||||
specific options are:
|
||||
.Bl -tag -width 36n
|
||||
|
@ -439,17 +464,20 @@ seconds means there is no timeout.
|
|||
.Pq Em 16 \" default value
|
||||
.El
|
||||
.Ss Gmail
|
||||
.HorizontalRule
|
||||
.Tn Gmail
|
||||
has non-standard
|
||||
.Tn IMAP
|
||||
behaviors that need to be worked around.
|
||||
.Ss Gmail - sending mail
|
||||
.HorizontalRule
|
||||
Option
|
||||
.Ic store_sent_mail
|
||||
should be disabled since
|
||||
.Tn Gmail
|
||||
auto-saves sent mail by its own.
|
||||
.Ss Gmail OAUTH2
|
||||
.HorizontalRule
|
||||
To use
|
||||
.Tn OAUTH2 Ns
|
||||
, you must go through a process to register your own private
|
||||
|
@ -458,7 +486,12 @@ with
|
|||
.Tn Google
|
||||
that can use
|
||||
.Tn OAUTH2
|
||||
tokens.
|
||||
tokens,
|
||||
and set the option
|
||||
.Ic use_oauth2
|
||||
as
|
||||
.Ql true
|
||||
in the account configuration section.
|
||||
For convenience in the
|
||||
.Sy meli
|
||||
repository under the
|
||||
|
@ -472,7 +505,7 @@ to generate and request the appropriate data to perform
|
|||
authentication.
|
||||
.sp
|
||||
Steps:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
In
|
||||
.Tn Google API Ns
|
||||
|
@ -505,8 +538,27 @@ should evaluate this command which if successful must only return a
|
|||
.Tn base64 Ns
|
||||
-encoded token ready to be passed to
|
||||
.Tn IMAP.
|
||||
.Pp
|
||||
Your account section should look like this:
|
||||
.Bd -literal
|
||||
[accounts."gmail"]
|
||||
root_mailbox = '[Gmail]'
|
||||
format = "imap"
|
||||
server_hostname='imap.gmail.com'
|
||||
server_username="username@gmail.com"
|
||||
use_oauth2 = true
|
||||
server_password_command = "TOKEN=$(py...th2_string --quiet --access_token=$TOKEN"
|
||||
server_port="993"
|
||||
listing.index_style = "Conversations"
|
||||
identity = "username@gmail.com"
|
||||
display_name = "Name Name"
|
||||
subscribed_mailboxes = ["*" ]
|
||||
composing.store_sent_mail = false
|
||||
composing.send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "xoauth2", token_command = "...", require_auth = true }, security = { type = "STARTTLS" } }
|
||||
.Ed
|
||||
.El
|
||||
.Ss JMAP only
|
||||
.HorizontalRule
|
||||
.Tn JMAP
|
||||
specific options
|
||||
.Bl -tag -width 36n
|
||||
|
@ -527,6 +579,7 @@ certificates.
|
|||
.Pq Em false \" default value
|
||||
.El
|
||||
.Ss mbox only
|
||||
.HorizontalRule
|
||||
.Tn mbox
|
||||
specific options:
|
||||
.Bl -tag -width 36n
|
||||
|
@ -542,7 +595,7 @@ If the preferred format fails, the message is retried with mboxrd and then if
|
|||
it fails again there is a recover attempt, which discards the invalid message.
|
||||
.sp
|
||||
Valid values
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Ar auto
|
||||
.It
|
||||
|
@ -575,6 +628,7 @@ mailboxes."Python mailing list" = { path = "~/.mail/python.mbox", subscribe = tr
|
|||
.\"
|
||||
.\"
|
||||
.Ss NNTP
|
||||
.HorizontalRule
|
||||
.Tn NNTP
|
||||
specific options
|
||||
.Bl -tag -width 36n
|
||||
|
@ -585,7 +639,7 @@ example:
|
|||
Server username
|
||||
.It Ic server_password Ar String
|
||||
Server password
|
||||
.It Ic require_auth Ar bool
|
||||
.It Ic require_auth Ar boolean
|
||||
.Pq Em optional
|
||||
require authentication in every case
|
||||
.Pq Em true \" default value
|
||||
|
@ -646,6 +700,7 @@ composing.send_mail = "server_submission"
|
|||
.\"
|
||||
.\"
|
||||
.Ss MAILBOXES
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Ic alias Ar String
|
||||
.Pq Em optional
|
||||
|
@ -672,7 +727,7 @@ Silently insert updates for this mailbox, if any.
|
|||
.Pq Em optional
|
||||
special usage of this mailbox.
|
||||
Valid values are:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Ar Normal
|
||||
.Pq Em default
|
||||
|
@ -748,7 +803,7 @@ mailboxes.
|
|||
.\"
|
||||
.\"
|
||||
.Sh COMPOSING
|
||||
Composing specific options.
|
||||
.Ss Composing specific configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic send_mail Ar String|SmtpServerConf
|
||||
|
@ -825,7 +880,7 @@ This setting is meant to be disabled for non-standard behaviour in
|
|||
.Pq Em optional
|
||||
The attribution line appears above the quoted reply text.
|
||||
The format specifiers for the replied address are:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Li %+f
|
||||
— the sender's name and email address.
|
||||
|
@ -859,8 +914,10 @@ Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip.
|
|||
.It Ic reply_prefix Ar String
|
||||
.Pq Em optional
|
||||
The prefix to use in reply subjects.
|
||||
The default prefix is "Re:".
|
||||
.Pq Em `Re:` \" default value
|
||||
The default prefix is
|
||||
.Ns Ql Re: Ns
|
||||
\&.
|
||||
.Pq Ql Re: \" default value
|
||||
.Pp
|
||||
RFC 2822, "Internet Message Format" has this to say on the matter:
|
||||
.\"
|
||||
|
@ -902,7 +959,7 @@ compose-hooks run before submitting an e-mail.
|
|||
They perform draft validation and/or transformations.
|
||||
If a hook encounters an error or warning, it will show up as a notification.
|
||||
The currently available hooks are:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Ic past-date-warn
|
||||
— Warn if
|
||||
|
@ -939,9 +996,10 @@ or draft body mention attachments but they are missing.
|
|||
.\"
|
||||
.\"
|
||||
.Sh SHORTCUTS
|
||||
Default values are shown in parentheses.
|
||||
.Ss Values corresponding to keyboard keys, keycodes
|
||||
Shortcuts can take the following values:
|
||||
.Bl -bullet -compact
|
||||
.sp
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Em Backspace
|
||||
.It
|
||||
|
@ -979,9 +1037,13 @@ Shortcuts can take the following values:
|
|||
.It
|
||||
.Em char
|
||||
.El
|
||||
.Em char
|
||||
is a single character string.
|
||||
.sp
|
||||
Where
|
||||
.Em char
|
||||
is a single character string, maximum 4 bytes long, like the corresponding type
|
||||
in Rust.
|
||||
.Pp
|
||||
In the next subsection, you will find lists for each shortcut category.
|
||||
The headings before each list indicate the map key of the shortcut list.
|
||||
For example for the first list titled
|
||||
.Em general
|
||||
|
@ -1013,6 +1075,28 @@ exit_entry = 'i'
|
|||
.\"
|
||||
.sp
|
||||
.Pp
|
||||
.Em commands
|
||||
.sp
|
||||
In addition, each shortcuts section supports a TOML array of commands to
|
||||
associate a key to an array of
|
||||
.Em COMMAND
|
||||
mode commands.
|
||||
.sp
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Bd -literal
|
||||
[shortcuts.listing]
|
||||
commands = [ { command = [ "tag remove trash", "flag unset trash" ], shortcut = "D" },
|
||||
{ command = [ "tag add trash", "flag set trash" ], shortcut = "d" } ]
|
||||
.Ed
|
||||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.Ss Shortcut configuration settings
|
||||
.HorizontalRule
|
||||
Default values are shown in parentheses.
|
||||
.sp
|
||||
.Em general
|
||||
.Bl -tag -width 36n
|
||||
.It Ic toggle_help
|
||||
|
@ -1153,7 +1237,7 @@ current sorting.
|
|||
.Pq Em C-p \" default value
|
||||
.It Ic toggle_menu_visibility
|
||||
Toggle visibility of side menu in mail list.
|
||||
.Pq Em ` \" default value
|
||||
.Pq Em \(ga \" default value
|
||||
.It Ic focus_left
|
||||
Switch focus on the left.
|
||||
.Pq Em Left \" default value
|
||||
|
@ -1212,7 +1296,7 @@ Go to previous account.
|
|||
.Pq Em L \" default value
|
||||
.It Ic toggle_menu_visibility
|
||||
Toggle visibility of side menu in mail list.
|
||||
.Pq Em ` \" default value
|
||||
.Pq Em \(ga \" default value
|
||||
.El
|
||||
.sp
|
||||
.sp
|
||||
|
@ -1333,6 +1417,7 @@ Toggle between horizontal and vertical layout.
|
|||
.\"
|
||||
.\"
|
||||
.Sh NOTIFICATIONS
|
||||
.Ss Notification configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic enable Ar boolean
|
||||
|
@ -1367,6 +1452,7 @@ Play sound file in notifications if possible.
|
|||
.\"
|
||||
.\"
|
||||
.Sh PAGER
|
||||
.Ss Pager (viewing text) configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic sticky_headers Ar boolean
|
||||
|
@ -1385,11 +1471,11 @@ A command to open html files.
|
|||
.Pq Em optional
|
||||
A command to pipe mail output through for viewing in pager.
|
||||
.Pq Em none \" default value
|
||||
.It Ic format_flowed Ar bool
|
||||
.It Ic format_flowed Ar boolean
|
||||
.Pq Em optional
|
||||
Respect format=flowed
|
||||
.Pq Em true \" default value
|
||||
.It Ic split_long_lines Ar bool
|
||||
.It Ic split_long_lines Ar boolean
|
||||
.Pq Em optional
|
||||
Split long lines that would overflow on the x axis.
|
||||
.Pq Em true \" default value
|
||||
|
@ -1451,6 +1537,7 @@ INBOX = {}
|
|||
.\"
|
||||
.\"
|
||||
.Sh LISTING
|
||||
.Ss Listing (lists of e-mail entries in a mailbox) configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic show_menu_scrollbar Ar boolean
|
||||
|
@ -1465,7 +1552,9 @@ Datetime formatting passed verbatim to
|
|||
.Pq Em \&%Y-\&%m-\&%d \&%T \" default value
|
||||
.It Ic recent_dates Ar Boolean
|
||||
.Pq Em optional
|
||||
Show recent dates as `X {minutes,hours,days} ago`, up to 7 days.
|
||||
Show recent dates as
|
||||
.Ns Ql X {minutes,hours,days} ago
|
||||
.Ns , up to 7 days.
|
||||
.Pq Em true \" default value
|
||||
.It Ic filter Ar Query
|
||||
.Pq Em optional
|
||||
|
@ -1488,6 +1577,14 @@ filter = "not flags:seen" # show only unseen messages
|
|||
.\"
|
||||
.It Ic index_style Ar String
|
||||
Sets the way mailboxes are displayed.
|
||||
.TS
|
||||
allbox tab(:);
|
||||
lb l.
|
||||
conversations:shows one entry per thread
|
||||
compact:shows one row per thread
|
||||
threaded:shows threads as a tree structure
|
||||
plain:shows one row per mail, regardless of threading
|
||||
.TE
|
||||
.It Ic sidebar_mailbox_tree_has_sibling Ar String
|
||||
.Pq Em optional
|
||||
Sets the string to print in the mailbox tree for a level where its root has a
|
||||
|
@ -1518,33 +1615,43 @@ Flag to show if thread entry contains unseen mail.
|
|||
.Pq Em "●" \" default value
|
||||
.It Ic thread_snoozed_flag Ar Option<String>
|
||||
Flag to show if thread has been snoozed.
|
||||
.Pq Em "💤" \" default value
|
||||
.LiteralStringValueRenders 💤\e\uu{FE0E} 💤︎ \" default value
|
||||
.It Ic selected_flag Ar Option<String>
|
||||
Flag to show if thread entry has been selected.
|
||||
.Pq Em "☑️" \" default value
|
||||
.LiteralStringValueRenders ☑️ \e\uu{2007} ☑️
|
||||
.It Ic attachment_flag Ar Option<String>
|
||||
Flag to show if thread entry contains attachments.
|
||||
.Pq Em "📎" \" default value
|
||||
.It Ic thread_subject_pack Ar bool
|
||||
.LiteralStringValueRenders 📎\e\uu{FE0E} 📎︎ \" default value
|
||||
.It Ic highlight_self_flag Ar Option<String>
|
||||
Flag to show if any thread entry contains your address as a receiver.
|
||||
Useful to make mailing list threads that CC you stand out.
|
||||
.Pq Em "✸" \" default value
|
||||
.It Ic highlight_self Ar boolean
|
||||
Show
|
||||
.Ic highlight_self_flag
|
||||
or not.
|
||||
.Pq Em false \" default value
|
||||
.It Ic thread_subject_pack Ar boolean
|
||||
Should threads with differentiating Subjects show a list of those subjects on
|
||||
the entry title?
|
||||
.Pq Em "true" \" default value
|
||||
.It Ic threaded_repeat_identical_from_values Ar bool
|
||||
.Pq Em true \" default value
|
||||
.It Ic threaded_repeat_identical_from_values Ar boolean
|
||||
In threaded listing style, repeat identical From column values within a thread.
|
||||
Not repeating adds empty space in the From column which might result in less
|
||||
visual clutter.
|
||||
.Pq Em "false" \" default value
|
||||
.It Ic relative_menu_indices Ar bool
|
||||
.Pq Em false \" default value
|
||||
.It Ic relative_menu_indices Ar boolean
|
||||
Show relative indices in menu mailboxes to quickly help with jumping to them.
|
||||
.Pq Em true \" default value
|
||||
.It Ic relative_list_indices Ar bool
|
||||
.It Ic relative_list_indices Ar boolean
|
||||
Show relative indices in listings to quickly help with jumping to them.
|
||||
.Pq Em true \" default value
|
||||
.It Ic hide_sidebar_on_launch Ar bool
|
||||
.It Ic hide_sidebar_on_launch Ar boolean
|
||||
Start app with sidebar hidden.
|
||||
.Pq Em false \" default value
|
||||
.El
|
||||
.Ss Examples of sidebar mailbox tree customization
|
||||
.HorizontalRule
|
||||
The default values
|
||||
.sp
|
||||
.\"
|
||||
|
@ -1652,6 +1759,7 @@ no_sibling_leaf = " \\_"
|
|||
.\"
|
||||
.\"
|
||||
.Sh TAGS
|
||||
.Ss Tags (e-mail metadata in backends that support them) configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic colours Ar hash table String[Color]
|
||||
|
@ -1675,7 +1783,7 @@ colors = { signed="#Ff6600", replied="DeepSkyBlue4", draft="#f00", replied="8" }
|
|||
\&...
|
||||
[accounts.dummy.mailboxes]
|
||||
# per mailbox override:
|
||||
"INBOX" = { tags.ignore_tags=["inbox", ] }
|
||||
"INBOX" = { tags.ignore_tags=["inbox", ] }
|
||||
.Ed
|
||||
.\"
|
||||
.\"
|
||||
|
@ -1740,7 +1848,7 @@ Use comma to separate values.
|
|||
.Pq Em Local,WKD \" default value
|
||||
.Pp
|
||||
Possible mechanisms:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Em cert
|
||||
.It
|
||||
|
@ -1765,6 +1873,29 @@ Possible mechanisms:
|
|||
.\"
|
||||
.\"
|
||||
.Sh TERMINAL
|
||||
.Ss Note about emojis and other multi-width characters in string values
|
||||
Some useful unicode combining marks
|
||||
.Po
|
||||
invisible characters that modify the presentation of visible characters before
|
||||
them
|
||||
.Pc
|
||||
are:
|
||||
.sp
|
||||
.Bl -tag -width 15n
|
||||
.It Ns
|
||||
.Li \e\uu{FE0E}
|
||||
Emoji variation sequence select 15: renders an emoji as text style (monochrome)
|
||||
.It Ns
|
||||
.Li \e\uu{FE0F}
|
||||
Emoji variation sequence select 16: renders an emoji in color
|
||||
.It Ns
|
||||
.Li \e\uu{2007}
|
||||
Figure space, a space character with the width of a digit in a monospace
|
||||
typeface
|
||||
.El
|
||||
.sp
|
||||
.Ss Terminal configuration settings
|
||||
.HorizontalRule
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic theme Ar String
|
||||
|
@ -1777,7 +1908,9 @@ If true, box drawing will be done with ASCII characters.
|
|||
.Pq Em false \" default value
|
||||
.It Ic use_color Ar boolean
|
||||
.Pq Em optional
|
||||
If false, no ANSI colors are used.
|
||||
If false, no
|
||||
.Tn ANSI
|
||||
colors are used.
|
||||
.Pq Em true \" default value
|
||||
.It Ic window_title Ar String
|
||||
.Pq Em optional
|
||||
|
@ -1817,7 +1950,7 @@ theme = "themeB"
|
|||
.\"
|
||||
.\"
|
||||
.\"
|
||||
.It Ic use_mouse Ar bool
|
||||
.It Ic use_mouse Ar boolean
|
||||
Use mouse events.
|
||||
This will disable text selection, but you will be able to resize some widgets.
|
||||
This setting can be toggled with
|
||||
|
@ -1909,6 +2042,7 @@ progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
|
|||
.\"
|
||||
.\"
|
||||
.Sh LOG
|
||||
.Ss Logging configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic log_file Ar String
|
||||
|
@ -1922,7 +2056,7 @@ All levels less or equal to the
|
|||
.Ic maximum_level
|
||||
will be appended to the log file.
|
||||
Available levels are, in partial order:
|
||||
.Bl -bullet -compact
|
||||
.Bl -dash -compact
|
||||
.It
|
||||
.Em OFF
|
||||
.It
|
||||
|
@ -1949,6 +2083,7 @@ to
|
|||
.\"
|
||||
.\"
|
||||
.Sh SMTP Connections
|
||||
.Ss SMTP configuration settings
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic hostname Ar String
|
||||
|
@ -1994,7 +2129,7 @@ For type
|
|||
.Bl -tag -width 36n
|
||||
.It Ic username Ar String
|
||||
.It Ic password Ar SmtpPassword
|
||||
.It Ic require_auth Ar bool
|
||||
.It Ic require_auth Ar boolean
|
||||
.Pq Em optional
|
||||
require authentication in every case.
|
||||
.Pq Em true \" default value
|
||||
|
@ -2008,7 +2143,7 @@ For type
|
|||
Command to evaluate that returns an
|
||||
.Tn XOAUTH2
|
||||
token.
|
||||
.It Ic require_auth Ar bool
|
||||
.It Ic require_auth Ar boolean
|
||||
.Pq Em optional
|
||||
require authentication in every case.
|
||||
.Pq Em true \" default value
|
||||
|
@ -2077,7 +2212,7 @@ Default security type is
|
|||
\&.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic type Ar "none" | "auto" | "starttls" | "tls"
|
||||
.It Ic danger_accept_invalid_certs Ar bool
|
||||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
Accept invalid
|
||||
.Tn SSL
|
||||
/
|
||||
|
@ -2087,13 +2222,13 @@ certificates
|
|||
.El
|
||||
.Ss SmtpExtensions
|
||||
.Bl -tag -width 36n
|
||||
.It Ic pipelining Ar bool
|
||||
.It Ic pipelining Ar boolean
|
||||
RFC2920
|
||||
.Pq Em true \" default value
|
||||
.It Ic chunking Ar bool
|
||||
.It Ic chunking Ar boolean
|
||||
RFC3030
|
||||
.Pq Em true \" default value
|
||||
.It Ic prdr Ar bool
|
||||
.It Ic prdr Ar boolean
|
||||
draft-hall-prdr-00
|
||||
.Pq Em true \" default value
|
||||
.It Ic dsn_notify Ar String
|
||||
|
@ -2108,16 +2243,35 @@ RFC3461
|
|||
.Sh SEE ALSO
|
||||
.Xr meli 1 ,
|
||||
.Xr meli-themes 5
|
||||
.Sh CONFORMING TO
|
||||
.Sh STANDARDS
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Tn TOML
|
||||
Standard
|
||||
.Li v.0.5.0
|
||||
.Lk https://toml.io/en/v0.5.0
|
||||
.El
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2019
|
||||
.An Manos Pitsidianakis Aq manos@pitsidianak.is
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
.Po
|
||||
See
|
||||
.Pa COPYING
|
||||
for full copyright and warranty notices.
|
||||
.Pc
|
||||
.Ss Links
|
||||
.Bl -item -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
.El
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
//! Account management from user configuration.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap, HashSet, VecDeque},
|
||||
convert::TryFrom,
|
||||
fs,
|
||||
|
@ -36,17 +35,14 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{
|
||||
future::FutureExt,
|
||||
stream::{Stream, StreamExt},
|
||||
};
|
||||
use futures::{future::FutureExt, stream::StreamExt};
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::*,
|
||||
email::*,
|
||||
error::{Error, ErrorKind, Result},
|
||||
log,
|
||||
text_processing::GlobMatch,
|
||||
text::GlobMatch,
|
||||
thread::Threads,
|
||||
AddressBook, Collection, LogLevel, SortField, SortOrder,
|
||||
};
|
||||
|
@ -57,11 +53,19 @@ use crate::command::actions::AccountAction;
|
|||
use crate::{
|
||||
conf::{AccountConf, FileMailboxConf},
|
||||
jobs::{JobId, JoinHandle},
|
||||
types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification},
|
||||
types::{
|
||||
ForkType,
|
||||
UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification},
|
||||
},
|
||||
MainLoopHandler, StatusEvent, ThreadEvent,
|
||||
};
|
||||
|
||||
mod backend_ops;
|
||||
mod jobs;
|
||||
mod mailbox;
|
||||
|
||||
pub use jobs::*;
|
||||
pub use mailbox::*;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! try_recv_timeout {
|
||||
|
@ -79,6 +83,7 @@ macro_rules! try_recv_timeout {
|
|||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! is_variant {
|
||||
($n:ident, $($var:tt)+) => {
|
||||
#[inline]
|
||||
|
@ -88,86 +93,6 @@ macro_rules! is_variant {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum MailboxStatus {
|
||||
Available,
|
||||
Failed(Error),
|
||||
/// first argument is done work, and second is total work
|
||||
Parsing(usize, usize),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl MailboxStatus {
|
||||
is_variant! { is_available, Available }
|
||||
is_variant! { is_parsing, Parsing(_, _) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MailboxEntry {
|
||||
pub status: MailboxStatus,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub ref_mailbox: Mailbox,
|
||||
pub conf: FileMailboxConf,
|
||||
}
|
||||
|
||||
impl MailboxEntry {
|
||||
pub fn new(
|
||||
status: MailboxStatus,
|
||||
name: String,
|
||||
ref_mailbox: Mailbox,
|
||||
conf: FileMailboxConf,
|
||||
) -> Self {
|
||||
let mut ret = Self {
|
||||
status,
|
||||
name,
|
||||
path: ref_mailbox.path().into(),
|
||||
ref_mailbox,
|
||||
conf,
|
||||
};
|
||||
match ret.conf.mailbox_conf.extra.get("encoding") {
|
||||
None => {}
|
||||
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
|
||||
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
|
||||
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
|
||||
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
|
||||
}
|
||||
Some(other) => {
|
||||
log::warn!(
|
||||
"mailbox `{}`: unrecognized mailbox name charset: {}",
|
||||
&ret.name,
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
match self.status {
|
||||
MailboxStatus::Available => format!(
|
||||
"{} [{} messages]",
|
||||
self.name(),
|
||||
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
|
||||
),
|
||||
MailboxStatus::Failed(ref e) => e.to_string(),
|
||||
MailboxStatus::None => "Retrieving mailbox.".to_string(),
|
||||
MailboxStatus::Parsing(done, total) => {
|
||||
format!("Parsing messages. [{}/{}]", done, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
|
||||
name
|
||||
} else {
|
||||
self.ref_mailbox.name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum IsOnline {
|
||||
#[default]
|
||||
|
@ -194,6 +119,19 @@ impl IsOnline {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_recoverable(err: &Error) -> bool {
|
||||
!(err.kind.is_authentication()
|
||||
|| err.kind.is_configuration()
|
||||
|| err.kind.is_bug()
|
||||
|| err.kind.is_external()
|
||||
|| (err.kind.is_network() && !err.kind.is_network_down())
|
||||
|| err.kind.is_not_implemented()
|
||||
|| err.kind.is_not_supported()
|
||||
|| err.kind.is_protocol_error()
|
||||
|| err.kind.is_protocol_not_supported()
|
||||
|| err.kind.is_value_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -204,7 +142,6 @@ pub struct Account {
|
|||
pub mailbox_entries: IndexMap<MailboxHash, MailboxEntry>,
|
||||
pub mailboxes_order: Vec<MailboxHash>,
|
||||
pub tree: Vec<MailboxNode>,
|
||||
pub sent_mailbox: Option<MailboxHash>,
|
||||
pub collection: Collection,
|
||||
pub address_book: AddressBook,
|
||||
pub settings: AccountConf,
|
||||
|
@ -217,199 +154,6 @@ pub struct Account {
|
|||
pub backend_capabilities: MailBackendCapabilities,
|
||||
}
|
||||
|
||||
pub enum JobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handle: JoinHandle<(
|
||||
Option<Result<Vec<Envelope>>>,
|
||||
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
|
||||
)>,
|
||||
},
|
||||
Generic {
|
||||
name: Cow<'static, str>,
|
||||
log_level: LogLevel,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
on_finish: Option<crate::types::CallbackFn>,
|
||||
},
|
||||
IsOnline {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Refresh {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetFlags {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SaveMessage {
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SendMessage,
|
||||
SendMessageBackground {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
DeleteMessages {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
//RenameMailbox,
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Watch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Drop for JobRequest {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
JobRequest::Generic { handle, .. } |
|
||||
JobRequest::IsOnline { handle, .. } |
|
||||
JobRequest::Refresh { handle, .. } |
|
||||
JobRequest::SetFlags { handle, .. } |
|
||||
JobRequest::SaveMessage { handle, .. } |
|
||||
//JobRequest::RenameMailbox,
|
||||
JobRequest::SetMailboxPermissions { handle, .. } |
|
||||
JobRequest::SetMailboxSubscription { handle, .. } |
|
||||
JobRequest::Watch { handle, .. } |
|
||||
JobRequest::SendMessageBackground { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
JobRequest::DeleteMessages { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
JobRequest::CreateMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
JobRequest::DeleteMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
JobRequest::Fetch { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
JobRequest::Mailboxes { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
JobRequest::SendMessage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
JobRequest::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
|
||||
JobRequest::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
JobRequest::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
JobRequest::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
|
||||
JobRequest::Refresh { .. } => write!(f, "JobRequest::Refresh"),
|
||||
JobRequest::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
..
|
||||
} => f
|
||||
.debug_struct(stringify!(JobRequest::SetFlags))
|
||||
.field("env_hashes", &env_hashes)
|
||||
.field("mailbox_hash", &mailbox_hash)
|
||||
.field("flags", &flags)
|
||||
.finish(),
|
||||
JobRequest::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
|
||||
JobRequest::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
|
||||
JobRequest::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
JobRequest::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
//JobRequest::RenameMailbox,
|
||||
JobRequest::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
JobRequest::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
JobRequest::Watch { .. } => write!(f, "JobRequest::Watch"),
|
||||
JobRequest::SendMessage => write!(f, "JobRequest::SendMessage"),
|
||||
JobRequest::SendMessageBackground { .. } => {
|
||||
write!(f, "JobRequest::SendMessageBackground")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
JobRequest::Generic { name, .. } => write!(f, "{}", name),
|
||||
JobRequest::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
JobRequest::Fetch { .. } => write!(f, "Mailbox fetch"),
|
||||
JobRequest::IsOnline { .. } => write!(f, "Online status check"),
|
||||
JobRequest::Refresh { .. } => write!(f, "Refresh mailbox"),
|
||||
JobRequest::SetFlags {
|
||||
env_hashes, flags, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Set flags for {} message{}: {:?}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" },
|
||||
flags
|
||||
),
|
||||
JobRequest::SaveMessage { .. } => write!(f, "Save message"),
|
||||
JobRequest::DeleteMessages { env_hashes, .. } => write!(
|
||||
f,
|
||||
"Delete {} message{}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
JobRequest::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
JobRequest::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
//JobRequest::RenameMailbox,
|
||||
JobRequest::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
JobRequest::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
JobRequest::Watch { .. } => write!(f, "Background watch"),
|
||||
JobRequest::SendMessageBackground { .. } | JobRequest::SendMessage => {
|
||||
write!(f, "Sending message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRequest {
|
||||
is_variant! { is_watch, Watch { .. } }
|
||||
is_variant! { is_online, IsOnline { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, JobRequest::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Account {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(data_dir) = xdg::BaseDirectories::with_profile("meli", &self.name) {
|
||||
|
@ -461,15 +205,6 @@ impl Drop for Account {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MailboxNode {
|
||||
pub hash: MailboxHash,
|
||||
pub depth: usize,
|
||||
pub indentation: u32,
|
||||
pub has_sibling: bool,
|
||||
pub children: Vec<MailboxNode>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(
|
||||
hash: AccountHash,
|
||||
|
@ -552,7 +287,7 @@ impl Account {
|
|||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
if settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
|
||||
let db_path = match crate::sqlite3::db_path() {
|
||||
let db_path = match crate::sqlite3::AccountCache::db_path(&name) {
|
||||
Err(err) => {
|
||||
main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
|
@ -562,7 +297,7 @@ impl Account {
|
|||
)));
|
||||
None
|
||||
}
|
||||
Ok(path) => Some(path),
|
||||
Ok(path) => path,
|
||||
};
|
||||
if let Some(db_path) = db_path {
|
||||
if !db_path.exists() {
|
||||
|
@ -578,7 +313,7 @@ impl Account {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(Account {
|
||||
Ok(Self {
|
||||
hash,
|
||||
name,
|
||||
is_online: if !backend.capabilities().is_remote {
|
||||
|
@ -590,7 +325,6 @@ impl Account {
|
|||
mailboxes_order: Default::default(),
|
||||
tree: Default::default(),
|
||||
address_book,
|
||||
sent_mailbox: Default::default(),
|
||||
collection: backend.collection(),
|
||||
settings,
|
||||
main_loop_handler,
|
||||
|
@ -608,8 +342,6 @@ impl Account {
|
|||
IndexMap::with_capacity_and_hasher(ref_mailboxes.len(), Default::default());
|
||||
let mut mailboxes_order: Vec<MailboxHash> = Vec::with_capacity(ref_mailboxes.len());
|
||||
|
||||
let mut sent_mailbox = None;
|
||||
|
||||
/* Keep track of which mailbox config values we encounter in the actual
|
||||
* mailboxes returned by the backend. For each of the actual
|
||||
* mailboxes, delete the key from the hash set. If any are left, they
|
||||
|
@ -621,9 +353,19 @@ impl Account {
|
|||
.keys()
|
||||
.cloned()
|
||||
.collect::<HashSet<String>>();
|
||||
let mut default_mailbox = self
|
||||
.settings
|
||||
.conf
|
||||
.default_mailbox
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<HashSet<String>>();
|
||||
for f in ref_mailboxes.values_mut() {
|
||||
if let Some(conf) = self.settings.mailbox_confs.get_mut(f.path()) {
|
||||
mailbox_conf_hash_set.remove(f.path());
|
||||
if default_mailbox.remove(f.path()) {
|
||||
self.settings.default_mailbox = Some(f.hash());
|
||||
}
|
||||
conf.mailbox_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal {
|
||||
Some(f.special_usage())
|
||||
} else {
|
||||
|
@ -635,11 +377,11 @@ impl Account {
|
|||
};
|
||||
match conf.mailbox_conf.usage {
|
||||
Some(SpecialUsageMailbox::Sent) => {
|
||||
sent_mailbox = Some(f.hash());
|
||||
self.settings.sent_mailbox = Some(f.hash());
|
||||
}
|
||||
None => {
|
||||
if f.special_usage() == SpecialUsageMailbox::Sent {
|
||||
sent_mailbox = Some(f.hash());
|
||||
self.settings.sent_mailbox = Some(f.hash());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -665,7 +407,7 @@ impl Account {
|
|||
tmp
|
||||
};
|
||||
if new.mailbox_conf.usage == Some(SpecialUsageMailbox::Sent) {
|
||||
sent_mailbox = Some(f.hash());
|
||||
self.settings.sent_mailbox = Some(f.hash());
|
||||
}
|
||||
|
||||
mailbox_entries.insert(
|
||||
|
@ -718,6 +460,23 @@ impl Account {
|
|||
)));
|
||||
}
|
||||
|
||||
match self.settings.conf.default_mailbox {
|
||||
Some(ref v) if !default_mailbox.is_empty() => {
|
||||
let err = Error::new(format!(
|
||||
"Account `{}` has default mailbox set as `{}` but it doesn't exist.",
|
||||
&self.name, v
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration);
|
||||
self.is_online.set_err(err.clone());
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
|
||||
self.hash, None,
|
||||
)));
|
||||
return Err(err);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut tree: Vec<MailboxNode> = Vec::new();
|
||||
for (h, f) in ref_mailboxes.iter() {
|
||||
if !f.is_subscribed() {
|
||||
|
@ -766,7 +525,6 @@ impl Account {
|
|||
self.mailboxes_order = mailboxes_order;
|
||||
self.mailbox_entries = mailbox_entries;
|
||||
self.tree = tree;
|
||||
self.sent_mailbox = sent_mailbox;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -872,9 +630,9 @@ impl Account {
|
|||
};
|
||||
#[cfg(feature = "sqlite3")]
|
||||
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
|
||||
let handle = self.main_loop_handler.job_executor.spawn_blocking(
|
||||
let handle = self.main_loop_handler.job_executor.spawn_specialized(
|
||||
"sqlite3::insert".into(),
|
||||
crate::sqlite3::insert(
|
||||
crate::sqlite3::AccountCache::insert(
|
||||
(*envelope).clone(),
|
||||
self.backend.clone(),
|
||||
self.name.clone(),
|
||||
|
@ -951,15 +709,18 @@ impl Account {
|
|||
}
|
||||
#[cfg(feature = "sqlite3")]
|
||||
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
|
||||
if let Err(err) = crate::sqlite3::remove(env_hash) {
|
||||
let envelopes = self.collection.envelopes.read().unwrap();
|
||||
log::error!(
|
||||
"Failed to remove envelope {} [{}] in cache: {}",
|
||||
&envelopes[&env_hash].message_id_display(),
|
||||
env_hash,
|
||||
err
|
||||
);
|
||||
}
|
||||
let fut = crate::sqlite3::AccountCache::remove(self.name.clone(), env_hash);
|
||||
let handle = self
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("remove envelope from cache".into(), fut);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Refresh {
|
||||
mailbox_hash,
|
||||
handle,
|
||||
},
|
||||
);
|
||||
}
|
||||
let thread_hash = self.collection.get_env(env_hash).thread();
|
||||
if !self
|
||||
|
@ -1009,6 +770,7 @@ impl Account {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, mailbox_hash: MailboxHash) -> Result<()> {
|
||||
if let Some(ref refresh_command) = self.settings.conf().refresh_command {
|
||||
let child = std::process::Command::new("sh")
|
||||
|
@ -1022,9 +784,11 @@ impl Account {
|
|||
StatusEvent::DisplayMessage(format!("Running command {}", refresh_command)),
|
||||
)));
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Fork(
|
||||
crate::ForkType::Generic(child),
|
||||
)));
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Fork(ForkType::Generic {
|
||||
id: refresh_command.to_string().into(),
|
||||
command: Some(refresh_command.clone().into()),
|
||||
child,
|
||||
})));
|
||||
return Ok(());
|
||||
}
|
||||
let refresh_job = self.backend.write().unwrap().refresh(mailbox_hash);
|
||||
|
@ -1050,7 +814,9 @@ impl Account {
|
|||
}
|
||||
|
||||
pub fn watch(&mut self) {
|
||||
if self.settings.account().manual_refresh {
|
||||
if self.settings.account().manual_refresh
|
||||
|| matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1107,9 +873,10 @@ impl Account {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn mailboxes_order(&self) -> &Vec<MailboxHash> {
|
||||
pub fn mailboxes_order(&self) -> &[MailboxHash] {
|
||||
&self.mailboxes_order
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
@ -1568,17 +1335,7 @@ impl Account {
|
|||
ref mut retries,
|
||||
} => {
|
||||
let ret = Err(value.clone());
|
||||
if value.kind.is_authentication()
|
||||
|| value.kind.is_bug()
|
||||
|| value.kind.is_configuration()
|
||||
|| value.kind.is_external()
|
||||
|| (value.kind.is_network() && !value.kind.is_network_down())
|
||||
|| value.kind.is_not_implemented()
|
||||
|| value.kind.is_not_supported()
|
||||
|| value.kind.is_protocol_error()
|
||||
|| value.kind.is_protocol_not_supported()
|
||||
|| value.kind.is_value_error()
|
||||
{
|
||||
if !IsOnline::is_recoverable(value) {
|
||||
return ret;
|
||||
}
|
||||
let wait = if value.kind.is_timeout()
|
||||
|
@ -1590,7 +1347,7 @@ impl Account {
|
|||
*retries *= 2;
|
||||
}
|
||||
Some(Duration::from_millis(
|
||||
oldval * (4 * melib::utils::random::random_u8() as u64),
|
||||
oldval * (4 * u64::from(melib::utils::random::random_u8())),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
@ -1643,7 +1400,9 @@ impl Account {
|
|||
let query = melib::search::Query::try_from(search_term)?;
|
||||
match self.settings.conf.search_backend {
|
||||
#[cfg(feature = "sqlite3")]
|
||||
crate::conf::SearchBackend::Sqlite3 => crate::sqlite3::search(&query, _sort),
|
||||
crate::conf::SearchBackend::Sqlite3 => Ok(Box::pin(
|
||||
crate::sqlite3::AccountCache::search(self.name.clone(), query, _sort),
|
||||
)),
|
||||
crate::conf::SearchBackend::Auto | crate::conf::SearchBackend::None => {
|
||||
if self.backend_capabilities.supports_search {
|
||||
self.backend
|
||||
|
@ -1667,6 +1426,12 @@ impl Account {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn default_mailbox(&self) -> Option<MailboxHash> {
|
||||
self.settings
|
||||
.default_mailbox
|
||||
.or_else(|| Some(*self.mailboxes_order.first()?))
|
||||
}
|
||||
|
||||
pub fn mailbox_by_path(&self, path: &str) -> Result<MailboxHash> {
|
||||
if let Some((mailbox_hash, _)) = self
|
||||
.mailbox_entries
|
||||
|
@ -1691,21 +1456,22 @@ impl Account {
|
|||
JobRequest::Mailboxes { ref mut handle } => {
|
||||
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
|
||||
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
|
||||
if err.kind.is_authentication() {
|
||||
if !IsOnline::is_recoverable(&err) {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("{}: authentication error", &self.name).into(),
|
||||
),
|
||||
source: None,
|
||||
title: Some(self.name.clone().into()),
|
||||
source: Some(err.clone()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(crate::types::NotificationType::Error(err.kind)),
|
||||
},
|
||||
));
|
||||
self.is_online.set_err(err);
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(self.hash, None),
|
||||
UIEvent::AccountStatusChange(
|
||||
self.hash,
|
||||
Some(err.to_string().into()),
|
||||
),
|
||||
));
|
||||
self.is_online.set_err(err);
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
|
@ -1808,10 +1574,11 @@ impl Account {
|
|||
.into_iter()
|
||||
.map(|e| (e.hash(), e))
|
||||
.collect::<HashMap<EnvelopeHash, Envelope>>();
|
||||
if let Some(updated_mailboxes) =
|
||||
self.collection
|
||||
.merge(envelopes, mailbox_hash, self.sent_mailbox)
|
||||
{
|
||||
if let Some(updated_mailboxes) = self.collection.merge(
|
||||
envelopes,
|
||||
mailbox_hash,
|
||||
self.settings.sent_mailbox,
|
||||
) {
|
||||
for f in updated_mailboxes {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::MailboxUpdate((self.hash, f)),
|
||||
|
@ -1825,13 +1592,17 @@ impl Account {
|
|||
}
|
||||
}
|
||||
JobRequest::IsOnline { ref mut handle, .. } => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Ok(Some(is_online)) = handle.chan.try_recv() {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(self.hash, None),
|
||||
));
|
||||
match is_online {
|
||||
Ok(()) => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !value.kind.is_authentication())
|
||||
if matches!(self.is_online, IsOnline::Err { .. })
|
||||
|| matches!(self.is_online, IsOnline::Uninit)
|
||||
{
|
||||
self.watch();
|
||||
|
@ -1840,7 +1611,7 @@ impl Account {
|
|||
return true;
|
||||
}
|
||||
Err(value) => {
|
||||
self.is_online = IsOnline::Err { value, retries: 1 };
|
||||
self.is_online.set_err(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1859,12 +1630,15 @@ impl Account {
|
|||
};
|
||||
}
|
||||
JobRequest::Refresh { ref mut handle, .. } => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* canceled */ }
|
||||
Ok(None) => {}
|
||||
Ok(Some(Ok(()))) => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !value.kind.is_authentication())
|
||||
{
|
||||
if matches!(self.is_online, IsOnline::Err { .. }) {
|
||||
self.watch();
|
||||
}
|
||||
self.is_online = IsOnline::True;
|
||||
|
@ -1939,12 +1713,11 @@ impl Account {
|
|||
drop(env_lck);
|
||||
self.update_cached_env(env, None);
|
||||
}
|
||||
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::EnvelopeUpdate(env_hash)));
|
||||
}
|
||||
for env_hash in env_hashes.iter() {
|
||||
self.collection.update_flags(env_hash, *mailbox_hash);
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::EnvelopeUpdate(env_hash)));
|
||||
}
|
||||
}
|
||||
Err(_) | Ok(None) => {}
|
||||
|
@ -2151,8 +1924,8 @@ impl Account {
|
|||
{
|
||||
self.tree.remove(pos);
|
||||
}
|
||||
if self.sent_mailbox == Some(mailbox_hash) {
|
||||
self.sent_mailbox = None;
|
||||
if self.settings.sent_mailbox == Some(mailbox_hash) {
|
||||
self.settings.sent_mailbox = None;
|
||||
}
|
||||
self.collection
|
||||
.threads
|
||||
|
@ -2404,226 +2177,3 @@ impl IndexMut<&MailboxHash> for Account {
|
|||
self.mailbox_entries.get_mut(index).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mailboxes_order(
|
||||
tree: &mut Vec<MailboxNode>,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mailboxes_order: &mut Vec<MailboxHash>,
|
||||
) {
|
||||
tree.clear();
|
||||
mailboxes_order.clear();
|
||||
for (h, f) in mailbox_entries.iter() {
|
||||
if f.ref_mailbox.parent().is_none() {
|
||||
fn rec(
|
||||
h: MailboxHash,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
depth: usize,
|
||||
) -> MailboxNode {
|
||||
let mut node = MailboxNode {
|
||||
hash: h,
|
||||
children: Vec::new(),
|
||||
depth,
|
||||
indentation: 0,
|
||||
has_sibling: false,
|
||||
};
|
||||
for &c in mailbox_entries[&h].ref_mailbox.children() {
|
||||
if mailbox_entries.contains_key(&c) {
|
||||
node.children.push(rec(c, mailbox_entries, depth + 1));
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
tree.push(rec(*h, mailbox_entries, 0));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mailbox_eq_key {
|
||||
($mailbox:expr) => {{
|
||||
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
|
||||
(0, sort_order, $mailbox.ref_mailbox.path())
|
||||
} else {
|
||||
(1, 0, $mailbox.ref_mailbox.path())
|
||||
}
|
||||
}};
|
||||
}
|
||||
tree.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
|
||||
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
|
||||
for n in tree.iter_mut() {
|
||||
mailboxes_order.push(n.hash);
|
||||
n.children.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
stack.extend(n.children.iter().rev().map(Some));
|
||||
while let Some(Some(next)) = stack.pop() {
|
||||
mailboxes_order.push(next.hash);
|
||||
stack.extend(next.children.iter().rev().map(Some));
|
||||
}
|
||||
}
|
||||
drop(stack);
|
||||
for node in tree.iter_mut() {
|
||||
fn rec(
|
||||
node: &mut MailboxNode,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mut indentation: u32,
|
||||
has_sibling: bool,
|
||||
) {
|
||||
node.indentation = indentation;
|
||||
node.has_sibling = has_sibling;
|
||||
let mut iter = (0..node.children.len())
|
||||
.filter(|i| {
|
||||
mailbox_entries[&node.children[*i].hash]
|
||||
.ref_mailbox
|
||||
.is_subscribed()
|
||||
})
|
||||
.collect::<SmallVec<[_; 8]>>()
|
||||
.into_iter()
|
||||
.peekable();
|
||||
if has_sibling {
|
||||
indentation <<= 1;
|
||||
indentation |= 1;
|
||||
} else {
|
||||
indentation <<= 1;
|
||||
}
|
||||
while let Some(i) = iter.next() {
|
||||
let c = &mut node.children[i];
|
||||
rec(c, mailbox_entries, indentation, iter.peek().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
rec(node, mailbox_entries, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mailbox_utf7() {
|
||||
#[derive(Debug)]
|
||||
struct TestMailbox(String);
|
||||
|
||||
impl melib::BackendMailbox for TestMailbox {
|
||||
fn hash(&self) -> MailboxHash {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<MailboxHash> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn permissions(&self) -> MailboxPermissions {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_subscribed(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
for (n, d) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
|
||||
] {
|
||||
let ref_mbox = TestMailbox(n.to_string());
|
||||
let mut conf: melib::MailboxConf = Default::default();
|
||||
conf.extra.insert("encoding".to_string(), "utf7".into());
|
||||
|
||||
let entry = MailboxEntry::new(
|
||||
MailboxStatus::None,
|
||||
n.to_string(),
|
||||
Box::new(ref_mbox),
|
||||
FileMailboxConf {
|
||||
mailbox_conf: conf,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(&entry.path, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,28 +60,31 @@ impl Account {
|
|||
pub(super) fn update_cached_env(&mut self, env: Envelope, old_hash: Option<EnvelopeHash>) {
|
||||
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
|
||||
let msg_id = env.message_id_display().to_string();
|
||||
match crate::sqlite3::remove(old_hash.unwrap_or(env.hash()))
|
||||
.map(|_| crate::sqlite3::insert(env, self.backend.clone(), self.name.clone()))
|
||||
{
|
||||
Ok(job) => {
|
||||
let handle = self
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_blocking("sqlite3::remove".into(), job);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
|
||||
handle,
|
||||
log_level: LogLevel::TRACE,
|
||||
on_finish: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to update envelope {} in cache: {}", msg_id, err);
|
||||
}
|
||||
}
|
||||
let name = self.name.clone();
|
||||
let backend = self.backend.clone();
|
||||
let fut = async move {
|
||||
crate::sqlite3::AccountCache::remove(
|
||||
name.clone(),
|
||||
old_hash.unwrap_or_else(|| env.hash()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::sqlite3::AccountCache::insert(env, backend, name).await?;
|
||||
Ok(())
|
||||
};
|
||||
let handle = self
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("sqlite3::remove".into(), fut);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
|
||||
handle,
|
||||
log_level: LogLevel::TRACE,
|
||||
on_finish: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
//
|
||||
// meli - accounts module.
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, pin::Pin};
|
||||
|
||||
use futures::stream::Stream;
|
||||
use melib::{backends::*, email::*, error::Result, LogLevel};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{is_variant, jobs::JoinHandle};
|
||||
|
||||
pub enum JobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handle: JoinHandle<(
|
||||
Option<Result<Vec<Envelope>>>,
|
||||
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
|
||||
)>,
|
||||
},
|
||||
Generic {
|
||||
name: Cow<'static, str>,
|
||||
log_level: LogLevel,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
on_finish: Option<crate::types::CallbackFn>,
|
||||
},
|
||||
IsOnline {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Refresh {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetFlags {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SaveMessage {
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SendMessage,
|
||||
SendMessageBackground {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
DeleteMessages {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
//RenameMailbox,
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Watch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Drop for JobRequest {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Self::Generic { handle, .. } |
|
||||
Self::IsOnline { handle, .. } |
|
||||
Self::Refresh { handle, .. } |
|
||||
Self::SetFlags { handle, .. } |
|
||||
Self::SaveMessage { handle, .. } |
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { handle, .. } |
|
||||
Self::SetMailboxSubscription { handle, .. } |
|
||||
Self::Watch { handle, .. } |
|
||||
Self::SendMessageBackground { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMessages { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::CreateMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Fetch { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Mailboxes { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::SendMessage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
|
||||
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
Self::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
Self::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
|
||||
Self::Refresh { .. } => write!(f, "JobRequest::Refresh"),
|
||||
Self::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
..
|
||||
} => f
|
||||
.debug_struct(stringify!(JobRequest::SetFlags))
|
||||
.field("env_hashes", &env_hashes)
|
||||
.field("mailbox_hash", &mailbox_hash)
|
||||
.field("flags", &flags)
|
||||
.finish(),
|
||||
Self::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
|
||||
Self::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
|
||||
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
Self::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
Self::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
Self::Watch { .. } => write!(f, "JobRequest::Watch"),
|
||||
Self::SendMessage => write!(f, "JobRequest::SendMessage"),
|
||||
Self::SendMessageBackground { .. } => {
|
||||
write!(f, "JobRequest::SendMessageBackground")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "{}", name),
|
||||
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
Self::Fetch { .. } => write!(f, "Mailbox fetch"),
|
||||
Self::IsOnline { .. } => write!(f, "Online status check"),
|
||||
Self::Refresh { .. } => write!(f, "Refresh mailbox"),
|
||||
Self::SetFlags {
|
||||
env_hashes, flags, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Set flags for {} message{}: {:?}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" },
|
||||
flags
|
||||
),
|
||||
Self::SaveMessage { .. } => write!(f, "Save message"),
|
||||
Self::DeleteMessages { env_hashes, .. } => write!(
|
||||
f,
|
||||
"Delete {} message{}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
Self::Watch { .. } => write!(f, "Background watch"),
|
||||
Self::SendMessageBackground { .. } | Self::SendMessage => {
|
||||
write!(f, "Sending message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRequest {
|
||||
is_variant! { is_watch, Watch { .. } }
|
||||
is_variant! { is_online, IsOnline { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, Self::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
//
|
||||
// meli - accounts module.
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::{Mailbox, MailboxHash},
|
||||
error::Error,
|
||||
log,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{conf::FileMailboxConf, is_variant};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum MailboxStatus {
|
||||
Available,
|
||||
Failed(Error),
|
||||
/// first argument is done work, and second is total work
|
||||
Parsing(usize, usize),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl MailboxStatus {
|
||||
is_variant! { is_available, Available }
|
||||
is_variant! { is_parsing, Parsing(_, _) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MailboxEntry {
|
||||
pub status: MailboxStatus,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub ref_mailbox: Mailbox,
|
||||
pub conf: FileMailboxConf,
|
||||
}
|
||||
|
||||
impl MailboxEntry {
|
||||
pub fn new(
|
||||
status: MailboxStatus,
|
||||
name: String,
|
||||
ref_mailbox: Mailbox,
|
||||
conf: FileMailboxConf,
|
||||
) -> Self {
|
||||
let mut ret = Self {
|
||||
status,
|
||||
name,
|
||||
path: ref_mailbox.path().into(),
|
||||
ref_mailbox,
|
||||
conf,
|
||||
};
|
||||
match ret.conf.mailbox_conf.extra.get("encoding") {
|
||||
None => {}
|
||||
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
|
||||
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
|
||||
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
|
||||
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
|
||||
}
|
||||
Some(other) => {
|
||||
log::warn!(
|
||||
"mailbox `{}`: unrecognized mailbox name charset: {}",
|
||||
&ret.name,
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
match self.status {
|
||||
MailboxStatus::Available => format!(
|
||||
"{} [{} messages]",
|
||||
self.name(),
|
||||
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
|
||||
),
|
||||
MailboxStatus::Failed(ref e) => e.to_string(),
|
||||
MailboxStatus::None => "Retrieving mailbox.".to_string(),
|
||||
MailboxStatus::Parsing(done, total) => {
|
||||
format!("Parsing messages. [{}/{}]", done, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
|
||||
name
|
||||
} else {
|
||||
self.ref_mailbox.name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MailboxNode {
|
||||
pub hash: MailboxHash,
|
||||
pub depth: usize,
|
||||
pub indentation: u32,
|
||||
pub has_sibling: bool,
|
||||
pub children: Vec<MailboxNode>,
|
||||
}
|
||||
|
||||
pub fn build_mailboxes_order(
|
||||
tree: &mut Vec<MailboxNode>,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mailboxes_order: &mut Vec<MailboxHash>,
|
||||
) {
|
||||
tree.clear();
|
||||
mailboxes_order.clear();
|
||||
for (h, f) in mailbox_entries.iter() {
|
||||
if f.ref_mailbox.parent().is_none() {
|
||||
fn rec(
|
||||
h: MailboxHash,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
depth: usize,
|
||||
) -> MailboxNode {
|
||||
let mut node = MailboxNode {
|
||||
hash: h,
|
||||
children: Vec::new(),
|
||||
depth,
|
||||
indentation: 0,
|
||||
has_sibling: false,
|
||||
};
|
||||
for &c in mailbox_entries[&h].ref_mailbox.children() {
|
||||
if mailbox_entries.contains_key(&c) {
|
||||
node.children.push(rec(c, mailbox_entries, depth + 1));
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
tree.push(rec(*h, mailbox_entries, 0));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mailbox_eq_key {
|
||||
($mailbox:expr) => {{
|
||||
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
|
||||
(0, sort_order, $mailbox.ref_mailbox.path())
|
||||
} else {
|
||||
(1, 0, $mailbox.ref_mailbox.path())
|
||||
}
|
||||
}};
|
||||
}
|
||||
tree.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
|
||||
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
|
||||
for n in tree.iter_mut() {
|
||||
mailboxes_order.push(n.hash);
|
||||
n.children.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
stack.extend(n.children.iter().rev().map(Some));
|
||||
while let Some(Some(next)) = stack.pop() {
|
||||
mailboxes_order.push(next.hash);
|
||||
stack.extend(next.children.iter().rev().map(Some));
|
||||
}
|
||||
}
|
||||
drop(stack);
|
||||
for node in tree.iter_mut() {
|
||||
fn rec(
|
||||
node: &mut MailboxNode,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mut indentation: u32,
|
||||
has_sibling: bool,
|
||||
) {
|
||||
node.indentation = indentation;
|
||||
node.has_sibling = has_sibling;
|
||||
let mut iter = (0..node.children.len())
|
||||
.filter(|i| {
|
||||
mailbox_entries[&node.children[*i].hash]
|
||||
.ref_mailbox
|
||||
.is_subscribed()
|
||||
})
|
||||
.collect::<SmallVec<[_; 8]>>()
|
||||
.into_iter()
|
||||
.peekable();
|
||||
indentation <<= 1;
|
||||
if has_sibling {
|
||||
indentation |= 1;
|
||||
}
|
||||
while let Some(i) = iter.next() {
|
||||
let c = &mut node.children[i];
|
||||
rec(c, mailbox_entries, indentation, iter.peek().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
rec(node, mailbox_entries, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use melib::{
|
||||
backends::{Mailbox, MailboxHash},
|
||||
error::Result,
|
||||
MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
|
||||
use crate::accounts::{FileMailboxConf, MailboxEntry, MailboxStatus};
|
||||
|
||||
#[test]
|
||||
fn test_mailbox_utf7() {
|
||||
#[derive(Debug)]
|
||||
struct TestMailbox(String);
|
||||
|
||||
impl melib::BackendMailbox for TestMailbox {
|
||||
fn hash(&self) -> MailboxHash {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<MailboxHash> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn permissions(&self) -> MailboxPermissions {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_subscribed(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
for (n, d) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
|
||||
] {
|
||||
let ref_mbox = TestMailbox(n.to_string());
|
||||
let mut conf: melib::MailboxConf = Default::default();
|
||||
conf.extra.insert("encoding".to_string(), "utf7".into());
|
||||
|
||||
let entry = MailboxEntry::new(
|
||||
MailboxStatus::None,
|
||||
n.to_string(),
|
||||
Box::new(ref_mbox),
|
||||
FileMailboxConf {
|
||||
mailbox_conf: conf,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(&entry.path, d);
|
||||
}
|
||||
}
|
||||
}
|
146
meli/src/args.rs
146
meli/src/args.rs
|
@ -22,6 +22,8 @@
|
|||
//! Command line arguments.
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "cli-docs")]
|
||||
use crate::manpages;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
|
||||
|
@ -40,13 +42,16 @@ pub enum SubCommand {
|
|||
PrintDefaultTheme,
|
||||
/// print loaded themes in full to stdout and exit.
|
||||
PrintLoadedThemes,
|
||||
/// print all paths that meli creates/uses.
|
||||
PrintUsedPaths,
|
||||
/// print all directories that meli creates/uses.
|
||||
PrintAppDirectories,
|
||||
/// print location of configuration file that will be loaded on normal app
|
||||
/// startup.
|
||||
PrintConfigPath,
|
||||
/// edit configuration files with `$EDITOR`/`$VISUAL`.
|
||||
EditConfig,
|
||||
/// create a sample configuration file with available configuration options.
|
||||
/// If PATH is not specified, meli will try to create it in
|
||||
/// $XDG_CONFIG_HOME/meli/config.toml
|
||||
/// If `PATH` is not specified, meli will try to create it in
|
||||
/// `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
#[structopt(display_order = 1)]
|
||||
CreateConfig {
|
||||
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
|
||||
|
@ -64,15 +69,17 @@ pub enum SubCommand {
|
|||
Man(ManOpt),
|
||||
|
||||
#[structopt(display_order = 4)]
|
||||
/// Install manual pages to the first location provided by $MANPATH /
|
||||
/// manpath(1), unless you specify the directory as an argument.
|
||||
/// Install manual pages to the first location provided by `$MANPATH` /
|
||||
/// `manpath(1)`, unless you specify the directory as an argument.
|
||||
InstallMan {
|
||||
#[structopt(value_name = "DESTINATION_PATH", parse(from_os_str))]
|
||||
destination_path: Option<PathBuf>,
|
||||
},
|
||||
#[structopt(display_order = 5)]
|
||||
/// print compile time feature flags of this binary
|
||||
/// Print compile time feature flags of this binary
|
||||
CompiledWith,
|
||||
/// Print log file location.
|
||||
PrintLogPath,
|
||||
/// View mail from input file.
|
||||
View {
|
||||
#[structopt(value_name = "INPUT", parse(from_os_str))]
|
||||
|
@ -82,126 +89,15 @@ pub enum SubCommand {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ManOpt {
|
||||
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = manpages::parse_manpage))]
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(default_value = "meli", possible_values=manpages::POSSIBLE_VALUES, value_name="PAGE", parse(try_from_str = manpages::parse_manpage)))]
|
||||
/// Name of manual page.
|
||||
pub page: manpages::ManPages,
|
||||
/// If true, output text in stdout instead of spawning $PAGER.
|
||||
#[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")]
|
||||
/// If true, output text in stdout instead of spawning `$PAGER`.
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(
|
||||
feature = "cli-docs",
|
||||
structopt(long = "no-raw", alias = "no-raw", value_name = "bool")
|
||||
)]
|
||||
pub no_raw: Option<Option<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub mod manpages {
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use melib::log;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub fn parse_manpage(src: &str) -> Result<ManPages> {
|
||||
match src {
|
||||
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
|
||||
"meli.7" | "guide" => Ok(ManPages::Guide),
|
||||
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
|
||||
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => {
|
||||
Ok(ManPages::Themes)
|
||||
}
|
||||
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Choose manpage
|
||||
pub enum ManPages {
|
||||
/// meli(1)
|
||||
Main = 0,
|
||||
/// meli.conf(5)
|
||||
Conf = 1,
|
||||
/// meli-themes(5)
|
||||
Themes = 2,
|
||||
/// meli(7)
|
||||
Guide = 3,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ManPages {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Main => "meli.1",
|
||||
Self::Conf => "meli.conf.5",
|
||||
Self::Themes => "meli-themes.5",
|
||||
Self::Guide => "meli.7",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManPages {
|
||||
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
|
||||
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
|
||||
tries.push(p.into());
|
||||
p.exists()
|
||||
&& p.is_dir()
|
||||
&& fs::metadata(p)
|
||||
.ok()
|
||||
.map(|m| !m.permissions().readonly())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
let mut tries = vec![];
|
||||
let Some(mut path) = destination
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
.or_else(|| {
|
||||
if let Some(paths) = env::var_os("MANPATH") {
|
||||
if let Some(path) =
|
||||
env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
env::home_dir()
|
||||
.map(|p| p.join("local").join("share"))
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
})
|
||||
else {
|
||||
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
|
||||
};
|
||||
|
||||
for (p, dir) in [
|
||||
(ManPages::Main, "man1"),
|
||||
(ManPages::Conf, "man5"),
|
||||
(ManPages::Themes, "man5"),
|
||||
(ManPages::Guide, "man7"),
|
||||
] {
|
||||
let text = crate::subcommands::man(p, true)?;
|
||||
path.push(dir);
|
||||
std::fs::create_dir_all(&path).map_err(|err| {
|
||||
Error::new(format!("Could not create {} directory.", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
path.push(&p.to_string());
|
||||
|
||||
fs::write(&path, text.as_bytes()).map_err(|err| {
|
||||
Error::new(format!("Could not write to {}", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
log::trace!("Installed {} to {}", p, path.display());
|
||||
path.pop();
|
||||
path.pop();
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,14 +55,15 @@ pub use crate::actions::{
|
|||
AccountAction::{self, *},
|
||||
Action::{self, *},
|
||||
ComposeAction::{self, *},
|
||||
FlagAction,
|
||||
ListingAction::{self, *},
|
||||
MailingListAction::{self, *},
|
||||
TabAction::{self, *},
|
||||
TagAction::{self, *},
|
||||
TagAction,
|
||||
ViewAction::{self, *},
|
||||
};
|
||||
|
||||
/// Helper macro to convert an array of tokens into a TokenStream
|
||||
/// Helper macro to convert an array of tokens into a `TokenStream`
|
||||
macro_rules! to_stream {
|
||||
($token: expr) => {
|
||||
TokenStream {
|
||||
|
@ -142,6 +143,11 @@ impl TokenStream {
|
|||
tokens.append(&mut m);
|
||||
}
|
||||
}
|
||||
AlternativeStrings(v) => {
|
||||
for t in v.iter() {
|
||||
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, t));
|
||||
}
|
||||
}
|
||||
Seq(_s) => {}
|
||||
RestOfStringValue => {
|
||||
sugg.insert(String::new());
|
||||
|
@ -195,6 +201,20 @@ impl TokenStream {
|
|||
*s = "";
|
||||
}
|
||||
}
|
||||
AlternativeStrings(v) => {
|
||||
for lit in v.iter() {
|
||||
if lit.starts_with(*s) && lit.len() != s.len() {
|
||||
sugg.insert(lit[s.len()..].to_string());
|
||||
tokens.push((s, *t.inner()));
|
||||
return tokens;
|
||||
} else if s.starts_with(lit) {
|
||||
tokens.push((&s[..lit.len()], *t.inner()));
|
||||
*s = &s[lit.len()..];
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
}
|
||||
Seq(_s) => {
|
||||
return vec![];
|
||||
}
|
||||
|
@ -250,6 +270,7 @@ pub enum Token {
|
|||
Literal(&'static str),
|
||||
Filepath,
|
||||
Alternatives(&'static [TokenStream]),
|
||||
AlternativeStrings(&'static [&'static str]),
|
||||
Seq(&'static [TokenAdicity]),
|
||||
AccountName,
|
||||
MailboxPath,
|
||||
|
@ -339,6 +360,11 @@ define_commands!([
|
|||
tokens: &[One(Literal("search")), One(RestOfStringValue)],
|
||||
parser: parser::search
|
||||
},
|
||||
{ tags: ["clear-selection"],
|
||||
desc: "clear-selection",
|
||||
tokens: &[One(Literal("clear-selection"))],
|
||||
parser: parser::select
|
||||
},
|
||||
{ tags: ["select"],
|
||||
desc: "select <TERM>, selects envelopes matching with given term",
|
||||
tokens: &[One(Literal("select")), One(RestOfStringValue)],
|
||||
|
@ -482,6 +508,18 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
tokens: &[One(Literal("manage-mailboxes"))],
|
||||
parser: parser::manage_mailboxes
|
||||
},
|
||||
{ tags: ["man"],
|
||||
desc: "read documentation",
|
||||
tokens: {
|
||||
#[cfg(feature = "cli-docs")]
|
||||
{
|
||||
&[One(Literal("man")), One(AlternativeStrings(crate::manpages::POSSIBLE_VALUES))]
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
{ &[] }
|
||||
},
|
||||
parser: parser::view_manpage
|
||||
},
|
||||
{ tags: ["manage-jobs"],
|
||||
desc: "view and manage jobs",
|
||||
tokens: &[One(Literal("manage-jobs"))],
|
||||
|
@ -502,7 +540,7 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
/// Get command suggestions for input
|
||||
pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
||||
use crate::melib::ShellExpandTrait;
|
||||
let mut sugg = Default::default();
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
let _m = tokens.matches(&mut &(*input), &mut sugg);
|
||||
if _m.is_empty() {
|
||||
|
@ -527,17 +565,15 @@ mod tests {
|
|||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
// //println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let _ = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
// if !m.is_empty() {
|
||||
// //print!("{:?} ", desc);
|
||||
// //println!(" result = {:#?}\n\n", m);
|
||||
// }
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
|
@ -586,7 +622,7 @@ mod tests {
|
|||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg = Default::default();
|
||||
let mut sugg: HashSet<String> = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
|
||||
|
@ -652,9 +688,15 @@ mod tests {
|
|||
assert_eq!(
|
||||
parse_command(b"set foo").unwrap_err().to_string(),
|
||||
BadValue {
|
||||
inner: "Bad argument for `set`. Accepted arguments are [seen, unseen, plain, \
|
||||
threaded, compact, conversations]."
|
||||
.into(),
|
||||
inner: "foo".into(),
|
||||
suggestions: Some(&[
|
||||
"seen",
|
||||
"unseen",
|
||||
"plain",
|
||||
"threaded",
|
||||
"compact",
|
||||
"conversations"
|
||||
])
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
|
|
|
@ -23,10 +23,16 @@
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use melib::{email::mailto::Mailto, SortField, SortOrder};
|
||||
use melib::{email::mailto::Mailto, Flag, SortField, SortOrder};
|
||||
|
||||
use crate::components::{Component, ComponentId};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FlagAction {
|
||||
Set(Flag),
|
||||
Unset(Flag),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TagAction {
|
||||
Add(String),
|
||||
|
@ -52,6 +58,8 @@ pub enum ListingAction {
|
|||
Delete,
|
||||
OpenInNewTab,
|
||||
Tag(TagAction),
|
||||
Flag(FlagAction),
|
||||
ClearSelection,
|
||||
ToggleThreadSnooze,
|
||||
}
|
||||
|
||||
|
@ -62,6 +70,8 @@ pub enum TabAction {
|
|||
New(Option<Box<dyn Component>>),
|
||||
ManageMailboxes,
|
||||
ManageJobs,
|
||||
#[cfg(feature = "cli-docs")]
|
||||
Man(crate::manpages::ManPages),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -136,10 +146,10 @@ impl Action {
|
|||
pub fn needs_confirmation(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Action::Listing(ListingAction::Delete)
|
||||
| Action::MailingListAction(_)
|
||||
| Action::Mailbox(_, _)
|
||||
| Action::Quit
|
||||
Self::Listing(ListingAction::Delete)
|
||||
| Self::MailingListAction(_)
|
||||
| Self::Mailbox(_, _)
|
||||
| Self::Quit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ pub enum CommandError {
|
|||
},
|
||||
BadValue {
|
||||
inner: Cow<'static, str>,
|
||||
suggestions: Option<&'static [&'static str]>,
|
||||
},
|
||||
WrongNumberOfArguments {
|
||||
too_many: bool,
|
||||
|
@ -63,7 +64,25 @@ impl std::fmt::Display for CommandError {
|
|||
Self::Parsing { inner, kind: _ } => {
|
||||
write!(fmt, "Could not parse command: {}", inner)
|
||||
}
|
||||
Self::BadValue { inner } => {
|
||||
Self::BadValue {
|
||||
inner,
|
||||
suggestions: Some(suggs),
|
||||
} => {
|
||||
write!(fmt, "Bad value/argument: {}. Possible values are: ", inner)?;
|
||||
let len = suggs.len();
|
||||
for (i, val) in suggs.iter().enumerate() {
|
||||
if i == len.saturating_sub(1) {
|
||||
write!(fmt, "{}", val)?;
|
||||
} else {
|
||||
write!(fmt, "{}, ", val)?;
|
||||
}
|
||||
}
|
||||
write!(fmt, "")
|
||||
}
|
||||
Self::BadValue {
|
||||
inner,
|
||||
suggestions: None,
|
||||
} => {
|
||||
write!(fmt, "Bad value/argument: {}", inner)
|
||||
}
|
||||
Self::WrongNumberOfArguments {
|
||||
|
@ -121,3 +140,28 @@ impl std::fmt::Display for CommandError {
|
|||
}
|
||||
|
||||
impl std::error::Error for CommandError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CommandError;
|
||||
|
||||
#[test]
|
||||
fn test_command_error_display() {
|
||||
assert_eq!(
|
||||
&CommandError::BadValue {
|
||||
inner: "foo".into(),
|
||||
suggestions: Some(&[
|
||||
"seen",
|
||||
"unseen",
|
||||
"plain",
|
||||
"threaded",
|
||||
"compact",
|
||||
"conversations"
|
||||
])
|
||||
}
|
||||
.to_string(),
|
||||
"Bad value/argument: foo. Possible values are: seen, unseen, plain, threaded, \
|
||||
compact, conversations"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,22 +24,37 @@
|
|||
use super::*;
|
||||
use crate::command::{argcheck::*, error::*};
|
||||
|
||||
const FLAG_SUGGESTIONS: &[&str] = &[
|
||||
"passed",
|
||||
"replied",
|
||||
"seen or read",
|
||||
"junk or trash or trashed",
|
||||
"draft",
|
||||
"flagged",
|
||||
];
|
||||
|
||||
macro_rules! command_err {
|
||||
(nom $b:expr, $input: expr, $msg:literal) => {{
|
||||
(nom $b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
|
||||
let evaluated: IResult<&'_ [u8], _> = { $b };
|
||||
match evaluated {
|
||||
Err(_) => {
|
||||
let err = CommandError::BadValue { inner: $msg.into() };
|
||||
let err = CommandError::BadValue {
|
||||
inner: $msg.into(),
|
||||
suggestions: $suggs,
|
||||
};
|
||||
return Ok(($input, Err(err)));
|
||||
}
|
||||
Ok(v) => v,
|
||||
}
|
||||
}};
|
||||
($b:expr, $input: expr, $msg:literal) => {{
|
||||
($b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
|
||||
let evaluated = { $b };
|
||||
match evaluated {
|
||||
Err(_) => {
|
||||
let err = CommandError::BadValue { inner: $msg.into() };
|
||||
let err = CommandError::BadValue {
|
||||
inner: $msg.into(),
|
||||
suggestions: $suggs,
|
||||
};
|
||||
return Ok(($input, Err(err)));
|
||||
}
|
||||
Ok(v) => v,
|
||||
|
@ -101,6 +116,7 @@ pub fn listing_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErro
|
|||
open_in_new_tab,
|
||||
export_mbox,
|
||||
_tag,
|
||||
flag,
|
||||
))(input)
|
||||
}
|
||||
|
||||
|
@ -123,7 +139,7 @@ pub fn view(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
}
|
||||
|
||||
pub fn new_tab(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
alt((manage_mailboxes, manage_jobs, compose_action))(input)
|
||||
alt((manage_mailboxes, manage_jobs, compose_action, view_manpage))(input)
|
||||
}
|
||||
|
||||
pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
|
||||
|
@ -154,6 +170,125 @@ pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
|
|||
.and_then(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// Set/unset a flag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use meli::{melib::Flag, command::{Action,ListingAction, FlagAction, parser}};
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag set junk").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(
|
||||
/// matches!(
|
||||
/// parsed,
|
||||
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
|
||||
/// Flag::TRASHED
|
||||
/// ))))
|
||||
/// ),
|
||||
/// "{:?}",
|
||||
/// parsed
|
||||
/// );
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag unset junk").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(
|
||||
/// matches!(
|
||||
/// parsed,
|
||||
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Unset(
|
||||
/// Flag::TRASHED
|
||||
/// ))))
|
||||
/// ),
|
||||
/// "{:?}",
|
||||
/// parsed
|
||||
/// );
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag set draft").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(
|
||||
/// matches!(
|
||||
/// parsed,
|
||||
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
|
||||
/// Flag::DRAFT
|
||||
/// ))))
|
||||
/// ),
|
||||
/// "{:?}",
|
||||
/// parsed
|
||||
/// );
|
||||
///
|
||||
/// let (rest, parsed) = parser::flag(b"flag set xunk").unwrap();
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert_eq!(
|
||||
/// &parsed.unwrap_err().to_string(),
|
||||
/// "Bad value/argument: xunk is not a valid flag name. Possible values are: passed, replied, \
|
||||
/// seen or read, junk or trash or trashed, draft, flagged"
|
||||
/// );
|
||||
/// ```
|
||||
pub fn flag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
use melib::Flag;
|
||||
|
||||
fn parse_flag(s: &str) -> Option<Flag> {
|
||||
match s {
|
||||
o if o.eq_ignore_ascii_case("passed") => Some(Flag::PASSED),
|
||||
o if o.eq_ignore_ascii_case("replied") => Some(Flag::REPLIED),
|
||||
o if o.eq_ignore_ascii_case("seen") => Some(Flag::SEEN),
|
||||
o if o.eq_ignore_ascii_case("read") => Some(Flag::SEEN),
|
||||
o if o.eq_ignore_ascii_case("junk") => Some(Flag::TRASHED),
|
||||
o if o.eq_ignore_ascii_case("trash") => Some(Flag::TRASHED),
|
||||
o if o.eq_ignore_ascii_case("trashed") => Some(Flag::TRASHED),
|
||||
o if o.eq_ignore_ascii_case("draft") => Some(Flag::DRAFT),
|
||||
o if o.eq_ignore_ascii_case("flagged") => Some(Flag::FLAGGED),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
preceded(
|
||||
tag("flag"),
|
||||
alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
|
||||
let (input, _) = tag("set")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, flag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
let Some(flag) = parse_flag(flag) else {
|
||||
return Ok((
|
||||
b"",
|
||||
Err(CommandError::BadValue {
|
||||
inner: format!("{flag} is not a valid flag name").into(),
|
||||
suggestions: Some(FLAG_SUGGESTIONS),
|
||||
}),
|
||||
));
|
||||
};
|
||||
Ok((input, Ok(Listing(Flag(FlagAction::Set(flag))))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
|
||||
let (input, _) = tag("unset")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, flag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
let Some(flag) = parse_flag(flag) else {
|
||||
return Ok((
|
||||
b"",
|
||||
Err(CommandError::BadValue {
|
||||
inner: format!("{flag} is not a valid flag name").into(),
|
||||
suggestions: Some(FLAG_SUGGESTIONS),
|
||||
}),
|
||||
));
|
||||
};
|
||||
Ok((input, Ok(Listing(Flag(FlagAction::Unset(flag))))))
|
||||
},
|
||||
)),
|
||||
)(input.trim())
|
||||
}
|
||||
|
||||
pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, set};
|
||||
|
@ -173,9 +308,13 @@ pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, ret) = command_err!(nom
|
||||
alt((map(tag("seen"), |_| Listing(SetSeen)), map(tag("unseen"), |_| Listing(SetUnseen))))(input),
|
||||
alt((
|
||||
map(tag("seen"), |_| Listing(SetSeen)),
|
||||
map(tag("unseen"), |_| Listing(SetUnseen)
|
||||
)))(input),
|
||||
input,
|
||||
"Bad argument for `set`. Accepted arguments are [seen, unseen, plain, threaded, compact, conversations].");
|
||||
String::from_utf8_lossy(input.trim()).to_string(),
|
||||
Some(&["seen", "unseen", "plain", "threaded", "compact", "conversations"]));
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(ret)))
|
||||
|
@ -280,7 +419,8 @@ pub fn goto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
let (input, nth) = command_err!(nom
|
||||
usize_c(input),
|
||||
input,
|
||||
"Argument must be an integer.");
|
||||
"Argument must be an integer.",
|
||||
None);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Action::ViewMailbox(nth))))
|
||||
|
@ -336,6 +476,26 @@ pub fn search(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
Ok((input, Ok(Listing(Search(String::from(string))))))
|
||||
}
|
||||
pub fn select(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
#[inline]
|
||||
fn clear_selection(input: &[u8]) -> Option<IResult<&[u8], Result<Action, CommandError>>> {
|
||||
if !input.trim().starts_with(b"clear-selection") {
|
||||
return None;
|
||||
}
|
||||
#[inline]
|
||||
fn inner(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, clear_selection};
|
||||
let (input, _) = tag("clear-selection")(input.ltrim())?;
|
||||
arg_chk!(start check, input);
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Listing(ListingAction::ClearSelection))))
|
||||
}
|
||||
Some(inner(input))
|
||||
}
|
||||
if let Some(retval) = clear_selection(input) {
|
||||
return retval;
|
||||
}
|
||||
|
||||
let mut check = arg_init! { min_arg:1, max_arg: {u8::MAX}, select};
|
||||
let (input, _) = tag("select")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
|
@ -433,7 +593,8 @@ pub fn mailto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
let (input, val) = command_err!(
|
||||
parser(val.as_bytes()),
|
||||
val.as_bytes(),
|
||||
"Could not parse mailto value. If the value is valid, please report this bug."
|
||||
"Could not parse mailto value. If the value is valid, please report this bug.",
|
||||
None
|
||||
);
|
||||
Ok((input, Ok(Compose(Mailto(val)))))
|
||||
}
|
||||
|
@ -708,6 +869,19 @@ pub fn add_addresses_to_contacts(input: &[u8]) -> IResult<&[u8], Result<Action,
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(View(AddAddressesToContacts))))
|
||||
}
|
||||
|
||||
/// Set/unset a tag.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use meli::command::{Action,ListingAction, TagAction, parser::_tag};
|
||||
///
|
||||
/// let (rest, parsed) = _tag(b"tag add newsletters").unwrap();
|
||||
/// println!("parsed is {:?}", parsed);
|
||||
/// assert_eq!(rest, b"");
|
||||
/// assert!(matches!(parsed, Ok(Action::Listing(ListingAction::Tag(TagAction::Add(ref tagname)))) if tagname == "newsletters"), "{:?}", parsed);
|
||||
/// ```
|
||||
pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
preceded(
|
||||
tag("tag"),
|
||||
|
@ -721,7 +895,7 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
|
|||
let (input, tag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Listing(Tag(Add(tag.to_string()))))))
|
||||
Ok((input, Ok(Listing(Tag(TagAction::Add(tag.to_string()))))))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, tag};
|
||||
|
@ -732,11 +906,12 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
|
|||
let (input, tag) = quoted_argument(input.trim())?;
|
||||
arg_chk!(finish check, input);
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Listing(Tag(Remove(tag.to_string()))))))
|
||||
Ok((input, Ok(Listing(Tag(TagAction::Remove(tag.to_string()))))))
|
||||
},
|
||||
)),
|
||||
)(input.trim())
|
||||
}
|
||||
|
||||
pub fn print_account_setting(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:2, max_arg: 2, print};
|
||||
let (input, _) = tag("print")(input.trim())?;
|
||||
|
@ -796,7 +971,8 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
|||
return Ok((
|
||||
input,
|
||||
Err(CommandError::BadValue {
|
||||
inner: "Valid toggle values are thread_snooze, mouse, sign, encrypt.".into(),
|
||||
inner: String::from_utf8_lossy(input).to_string().into(),
|
||||
suggestions: Some(&["thread_snooze", "mouse", "sign", "encrypt"]),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
@ -823,6 +999,39 @@ pub fn manage_jobs(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, Ok(Tab(ManageJobs))))
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub fn view_manpage(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:1, max_arg: 1, view_manpage };
|
||||
let (input, _) = tag("man")(input.trim())?;
|
||||
arg_chk!(start check, input);
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
arg_chk!(inc check, input);
|
||||
let (input, manpage) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
arg_chk!(finish check, input);
|
||||
match crate::manpages::parse_manpage(manpage) {
|
||||
Ok(m) => Ok((input, Ok(Tab(Man(m))))),
|
||||
Err(err) => Ok((
|
||||
input,
|
||||
Err(CommandError::BadValue {
|
||||
inner: err.to_string().into(),
|
||||
suggestions: Some(crate::manpages::POSSIBLE_VALUES),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
pub fn view_manpage(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
Ok((
|
||||
input,
|
||||
Err(CommandError::Other {
|
||||
inner: "this meli binary has not been compiled with the cli-docs feature".into(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn quit(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
|
||||
let mut check = arg_init! { min_arg:0, max_arg: 0, quit};
|
||||
let (input, _) = tag("quit")(input.trim())?;
|
||||
|
|
239
meli/src/conf.rs
239
meli/src/conf.rs
|
@ -29,9 +29,14 @@ use std::{
|
|||
collections::HashSet,
|
||||
io::Read,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use melib::{backends::TagHash, search::Query, SortField, SortOrder, StderrLogger};
|
||||
use melib::{
|
||||
backends::{MailboxHash, TagHash},
|
||||
search::Query,
|
||||
ShellExpandTrait, SortField, SortOrder, StderrLogger,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
conf::deserializers::non_empty_opt_string,
|
||||
|
@ -62,7 +67,7 @@ use std::{
|
|||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
conf::{AccountSettings, MailboxConf, ToggleFlag},
|
||||
conf::{AccountSettings, ActionFlag, MailboxConf, ToggleFlag},
|
||||
error::*,
|
||||
};
|
||||
use pager::PagerSettings;
|
||||
|
@ -161,11 +166,17 @@ use crate::conf::deserializers::extra_settings;
|
|||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct FileAccount {
|
||||
pub root_mailbox: String,
|
||||
/// The mailbox that is the default to open / view for this account. Must be
|
||||
/// a valid mailbox path.
|
||||
///
|
||||
/// If not specified, the default is [`Self::root_mailbox`].
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub default_mailbox: Option<String>,
|
||||
pub format: String,
|
||||
pub identity: String,
|
||||
#[serde(default)]
|
||||
pub extra_identities: Vec<String>,
|
||||
#[serde(default = "none")]
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
#[serde(default = "false_val")]
|
||||
|
@ -180,14 +191,18 @@ pub struct FileAccount {
|
|||
pub order: (SortField, SortOrder),
|
||||
#[serde(default = "false_val")]
|
||||
pub manual_refresh: bool,
|
||||
#[serde(default = "none")]
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub refresh_command: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub conf_override: MailUIConf,
|
||||
#[serde(flatten)]
|
||||
#[serde(deserialize_with = "extra_settings")]
|
||||
pub extra: IndexMap<String, String>, /* use custom deserializer to convert any given value
|
||||
* (eg bool, number, etc) to string */
|
||||
#[serde(
|
||||
deserialize_with = "extra_settings",
|
||||
skip_serializing_if = "IndexMap::is_empty"
|
||||
)]
|
||||
/// Use custom deserializer to convert any given value (eg `bool`, number,
|
||||
/// etc) to `String`.
|
||||
pub extra: IndexMap<String, String>,
|
||||
}
|
||||
|
||||
impl FileAccount {
|
||||
|
@ -195,10 +210,6 @@ impl FileAccount {
|
|||
&self.mailboxes
|
||||
}
|
||||
|
||||
pub fn mailbox(&self) -> &str {
|
||||
&self.root_mailbox
|
||||
}
|
||||
|
||||
pub fn search_backend(&self) -> &SearchBackend {
|
||||
&self.search_backend
|
||||
}
|
||||
|
@ -230,6 +241,8 @@ pub struct FileSettings {
|
|||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct AccountConf {
|
||||
pub account: AccountSettings,
|
||||
pub default_mailbox: Option<MailboxHash>,
|
||||
pub sent_mailbox: Option<MailboxHash>,
|
||||
pub conf: FileAccount,
|
||||
pub conf_override: MailUIConf,
|
||||
pub mailbox_confs: IndexMap<String, FileMailboxConf>,
|
||||
|
@ -279,8 +292,10 @@ impl From<FileAccount> for AccountConf {
|
|||
};
|
||||
|
||||
let mailbox_confs = x.mailboxes.clone();
|
||||
AccountConf {
|
||||
Self {
|
||||
account: acc,
|
||||
default_mailbox: None,
|
||||
sent_mailbox: None,
|
||||
conf_override: x.conf_override.clone(),
|
||||
conf: x,
|
||||
mailbox_confs,
|
||||
|
@ -289,24 +304,23 @@ impl From<FileAccount> for AccountConf {
|
|||
}
|
||||
|
||||
pub fn get_config_file() -> Result<PathBuf> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").map_err(|err| {
|
||||
Error::new(format!(
|
||||
"Could not detect XDG directories for user: {}",
|
||||
err
|
||||
))
|
||||
.set_source(Some(std::sync::Arc::new(Box::new(err))))
|
||||
})?;
|
||||
match env::var("MELI_CONFIG") {
|
||||
Ok(path) => Ok(PathBuf::from(path)),
|
||||
Err(_) => Ok(xdg_dirs
|
||||
.place_config_file("config.toml")
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Cannot create configuration directory in {}",
|
||||
xdg_dirs.get_config_home().display()
|
||||
)
|
||||
})?),
|
||||
if let Ok(path) = env::var("MELI_CONFIG") {
|
||||
return Ok(PathBuf::from(path).expand());
|
||||
}
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").map_err(|err| {
|
||||
Error::new("Could not detect XDG directories for user")
|
||||
.set_source(Some(std::sync::Arc::new(Box::new(err))))
|
||||
.set_kind(ErrorKind::NotSupported)
|
||||
})?;
|
||||
xdg_dirs
|
||||
.place_config_file("config.toml")
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Cannot create configuration directory in {}",
|
||||
xdg_dirs.get_config_home().display()
|
||||
)
|
||||
})
|
||||
.chain_err_kind(ErrorKind::OSError)
|
||||
}
|
||||
|
||||
pub fn get_included_configs(conf_path: PathBuf) -> Result<Vec<PathBuf>> {
|
||||
|
@ -322,7 +336,7 @@ changequote(`"', `"')dnl
|
|||
let mut contents = String::new();
|
||||
while let Some((parent, p)) = stack.pop() {
|
||||
if !p.exists() || p.is_dir() {
|
||||
return Err(format!(
|
||||
return Err(Error::new(format!(
|
||||
"Path {}{included}{in_parent} {msg}.",
|
||||
p.display(),
|
||||
included = if parent.is_some() {
|
||||
|
@ -340,8 +354,8 @@ changequote(`"', `"')dnl
|
|||
} else {
|
||||
"is a directory, not a text file"
|
||||
}
|
||||
)
|
||||
.into());
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
contents.clear();
|
||||
let mut file = std::fs::File::open(&p)?;
|
||||
|
@ -354,11 +368,18 @@ changequote(`"', `"')dnl
|
|||
.spawn()
|
||||
{
|
||||
Ok(handle) => handle,
|
||||
Err(error) => match error.kind() {
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => {
|
||||
return Err("`m4` executable not found. Please install.".into())
|
||||
return Err(Error::new(
|
||||
"`m4` executable not found in PATH. Please provide an m4 binary.",
|
||||
)
|
||||
.set_kind(ErrorKind::Platform))
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new("Could not process configuration with `m4`")
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::OSError))
|
||||
}
|
||||
_ => return Err(error.into()),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -393,7 +414,7 @@ define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
|
|||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let mut handle = Command::new("m4")
|
||||
.current_dir(conf_path.parent().unwrap_or(Path::new("/")))
|
||||
.current_dir(conf_path.parent().unwrap_or_else(|| Path::new("/")))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
@ -407,12 +428,13 @@ define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
|
|||
}
|
||||
|
||||
impl FileSettings {
|
||||
pub fn new() -> Result<FileSettings> {
|
||||
pub fn new() -> Result<Self> {
|
||||
let config_path = get_config_file()?;
|
||||
if !config_path.exists() {
|
||||
let path_string = config_path.display().to_string();
|
||||
if path_string.is_empty() {
|
||||
return Err(Error::new("No configuration found."));
|
||||
return Err(Error::new("Given configuration path is empty.")
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
let ask = Ask {
|
||||
|
@ -424,33 +446,34 @@ impl FileSettings {
|
|||
#[cfg(not(test))]
|
||||
if ask.run() {
|
||||
create_config_file(&config_path)?;
|
||||
return Err(Error::new(
|
||||
"Edit the sample configuration and relaunch meli.",
|
||||
));
|
||||
return Err(
|
||||
Error::new("Edit the sample configuration and relaunch meli.")
|
||||
.set_kind(ErrorKind::Configuration),
|
||||
);
|
||||
}
|
||||
#[cfg(test)]
|
||||
return Ok(FileSettings::default());
|
||||
return Ok(Self::default());
|
||||
#[cfg(not(test))]
|
||||
return Err(Error::new("No configuration file found."));
|
||||
return Err(
|
||||
Error::new("No configuration file found.").set_kind(ErrorKind::Configuration)
|
||||
);
|
||||
}
|
||||
|
||||
FileSettings::validate(config_path, true, false)
|
||||
Self::validate(config_path, true, false)
|
||||
}
|
||||
|
||||
pub fn validate(path: PathBuf, interactive: bool, clear_extras: bool) -> Result<Self> {
|
||||
let s = pp::pp(&path)?;
|
||||
let map: toml::map::Map<String, toml::value::Value> =
|
||||
toml::from_str(&s).map_err(|err| {
|
||||
Error::new(format!(
|
||||
"{}:\nConfig file is invalid TOML: {}",
|
||||
path.display(),
|
||||
err
|
||||
))
|
||||
})?;
|
||||
/*
|
||||
* Check that a global composing option is set and return a user-friendly
|
||||
* error message because the default serde one is confusing.
|
||||
*/
|
||||
let map: toml::value::Table = toml::from_str(&s).map_err(|err| {
|
||||
Error::new(format!(
|
||||
"{}: Config file is invalid TOML; {}",
|
||||
path.display(),
|
||||
err
|
||||
))
|
||||
})?;
|
||||
|
||||
// Check that a global composing option is set and return a user-friendly
|
||||
// error message because the default serde one is confusing.
|
||||
if !map.contains_key("composing") {
|
||||
let err_msg = r#"You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows:
|
||||
|
||||
|
@ -469,25 +492,24 @@ This is required so that you don't accidentally start meli and find out later th
|
|||
};
|
||||
if ask.run() {
|
||||
let mut file = OpenOptions::new().append(true).open(&path)?;
|
||||
file.write_all("[composing]\nsend_mail = 'false'\n".as_bytes())
|
||||
file.write_all(b"[composing]\nsend_mail = 'false'\n")
|
||||
.map_err(|err| {
|
||||
Error::new(format!("Could not append to {}: {}", path.display(), err))
|
||||
})?;
|
||||
return FileSettings::validate(path, interactive, clear_extras);
|
||||
return Self::validate(path, interactive, clear_extras);
|
||||
}
|
||||
}
|
||||
return Err(Error::new(format!(
|
||||
"{}\n\nEdit the {} and relaunch meli.",
|
||||
if interactive { "" } else { err_msg },
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut s: FileSettings = toml::from_str(&s).map_err(|err| {
|
||||
Error::new(format!(
|
||||
"{}:\nConfig file contains errors: {}",
|
||||
path.display(),
|
||||
err
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
let mut s: Self = toml::from_str(&s).map_err(|err| {
|
||||
Error::new(format!("{}: Config file contains errors", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Configuration)
|
||||
})?;
|
||||
let backends = melib::backends::Backends::new();
|
||||
let Themes {
|
||||
|
@ -513,10 +535,11 @@ This is required so that you don't accidentally start meli and find out later th
|
|||
}
|
||||
}
|
||||
match s.terminal.theme.as_str() {
|
||||
"dark" | "light" => {}
|
||||
themes::DARK | themes::LIGHT => {}
|
||||
t if s.terminal.themes.other_themes.contains_key(t) => {}
|
||||
t => {
|
||||
return Err(Error::new(format!("Theme `{}` was not found.", t)));
|
||||
return Err(Error::new(format!("Theme `{}` was not found.", t))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,6 +557,7 @@ This is required so that you don't accidentally start meli and find out later th
|
|||
mailboxes,
|
||||
extra,
|
||||
manual_refresh,
|
||||
default_mailbox: _,
|
||||
refresh_command: _,
|
||||
search_backend: _,
|
||||
conf_override: _,
|
||||
|
@ -557,12 +581,14 @@ This is required so that you don't accidentally start meli and find out later th
|
|||
.collect(),
|
||||
extra: extra.into_iter().collect(),
|
||||
};
|
||||
s.validate_config()?;
|
||||
backends.validate_config(&lowercase_format, &mut s)?;
|
||||
if !s.extra.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Unrecognised configuration values: {:?}",
|
||||
s.extra
|
||||
)));
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
if clear_extras {
|
||||
acc.extra.clear();
|
||||
|
@ -586,17 +612,17 @@ pub struct Settings {
|
|||
pub terminal: TerminalSettings,
|
||||
pub log: LogSettings,
|
||||
#[serde(skip)]
|
||||
_logger: StderrLogger,
|
||||
pub _logger: StderrLogger,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new() -> Result<Settings> {
|
||||
pub fn new() -> Result<Self> {
|
||||
let fs = FileSettings::new()?;
|
||||
let mut s: IndexMap<String, AccountConf> = IndexMap::new();
|
||||
|
||||
for (id, x) in fs.accounts {
|
||||
let mut ac = AccountConf::from(x);
|
||||
ac.account.name = id.clone();
|
||||
ac.account.name.clone_from(&id);
|
||||
|
||||
s.insert(id, ac);
|
||||
}
|
||||
|
@ -607,7 +633,7 @@ impl Settings {
|
|||
_logger.change_log_dest(log_path.into());
|
||||
}
|
||||
|
||||
Ok(Settings {
|
||||
Ok(Self {
|
||||
accounts: s,
|
||||
pager: fs.pager,
|
||||
listing: fs.listing,
|
||||
|
@ -622,7 +648,7 @@ impl Settings {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn without_accounts() -> Result<Settings> {
|
||||
pub fn without_accounts() -> Result<Self> {
|
||||
let fs = FileSettings::new()?;
|
||||
let mut _logger = StderrLogger::new(fs.log.maximum_level);
|
||||
|
||||
|
@ -630,7 +656,7 @@ impl Settings {
|
|||
_logger.change_log_dest(log_path.into());
|
||||
}
|
||||
|
||||
Ok(Settings {
|
||||
Ok(Self {
|
||||
accounts: IndexMap::new(),
|
||||
pager: fs.pager,
|
||||
listing: fs.listing,
|
||||
|
@ -680,16 +706,29 @@ mod default_vals {
|
|||
None
|
||||
}
|
||||
|
||||
pub(in crate::conf) fn internal_value_false<T: std::convert::From<super::ToggleFlag>>() -> T {
|
||||
super::ToggleFlag::InternalVal(false).into()
|
||||
pub(in crate::conf) fn internal_value_false<T: std::convert::From<melib::conf::ToggleFlag>>(
|
||||
) -> T {
|
||||
melib::conf::ToggleFlag::InternalVal(false).into()
|
||||
}
|
||||
|
||||
pub(in crate::conf) fn internal_value_true<T: std::convert::From<super::ToggleFlag>>() -> T {
|
||||
super::ToggleFlag::InternalVal(true).into()
|
||||
pub(in crate::conf) fn internal_value_true<T: std::convert::From<melib::conf::ToggleFlag>>() -> T
|
||||
{
|
||||
melib::conf::ToggleFlag::InternalVal(true).into()
|
||||
}
|
||||
|
||||
pub(in crate::conf) fn ask<T: std::convert::From<super::ToggleFlag>>() -> T {
|
||||
super::ToggleFlag::Ask.into()
|
||||
pub(in crate::conf) fn action_internal_value_false<T: std::convert::From<melib::ActionFlag>>(
|
||||
) -> T {
|
||||
melib::conf::ActionFlag::InternalVal(false).into()
|
||||
}
|
||||
|
||||
//pub(in crate::conf) fn action_internal_value_true<
|
||||
// T: std::convert::From<melib::conf::ActionFlag>,
|
||||
//>() -> T {
|
||||
// melib::conf::ActionFlag::InternalVal(true).into()
|
||||
//}
|
||||
|
||||
pub(in crate::conf) fn ask<T: std::convert::From<melib::conf::ActionFlag>>() -> T {
|
||||
melib::conf::ActionFlag::Ask.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -763,10 +802,10 @@ impl<'de> Deserialize<'de> for IndexStyle {
|
|||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"Plain" | "plain" => Ok(IndexStyle::Plain),
|
||||
"Threaded" | "threaded" => Ok(IndexStyle::Threaded),
|
||||
"Compact" | "compact" => Ok(IndexStyle::Compact),
|
||||
"Conversations" | "conversations" => Ok(IndexStyle::Conversations),
|
||||
"Plain" | "plain" => Ok(Self::Plain),
|
||||
"Threaded" | "threaded" => Ok(Self::Threaded),
|
||||
"Compact" | "compact" => Ok(Self::Compact),
|
||||
"Conversations" | "conversations" => Ok(Self::Conversations),
|
||||
_ => Err(de::Error::custom("invalid `index_style` value")),
|
||||
}
|
||||
}
|
||||
|
@ -778,10 +817,10 @@ impl Serialize for IndexStyle {
|
|||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
IndexStyle::Plain => serializer.serialize_str("plain"),
|
||||
IndexStyle::Threaded => serializer.serialize_str("threaded"),
|
||||
IndexStyle::Compact => serializer.serialize_str("compact"),
|
||||
IndexStyle::Conversations => serializer.serialize_str("conversations"),
|
||||
Self::Plain => serializer.serialize_str("plain"),
|
||||
Self::Threaded => serializer.serialize_str("threaded"),
|
||||
Self::Compact => serializer.serialize_str("compact"),
|
||||
Self::Conversations => serializer.serialize_str("conversations"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -807,15 +846,15 @@ impl<'de> Deserialize<'de> for SearchBackend {
|
|||
if sqlite3.eq_ignore_ascii_case("sqlite3")
|
||||
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
|
||||
{
|
||||
Ok(SearchBackend::Sqlite3)
|
||||
Ok(Self::Sqlite3)
|
||||
}
|
||||
none if none.eq_ignore_ascii_case("none")
|
||||
|| none.eq_ignore_ascii_case("nothing")
|
||||
|| none.is_empty() =>
|
||||
{
|
||||
Ok(SearchBackend::None)
|
||||
Ok(Self::None)
|
||||
}
|
||||
auto if auto.eq_ignore_ascii_case("auto") => Ok(SearchBackend::Auto),
|
||||
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
|
||||
_ => Err(de::Error::custom("invalid `search_backend` value")),
|
||||
}
|
||||
}
|
||||
|
@ -828,9 +867,9 @@ impl Serialize for SearchBackend {
|
|||
{
|
||||
match self {
|
||||
#[cfg(feature = "sqlite3")]
|
||||
SearchBackend::Sqlite3 => serializer.serialize_str("sqlite3"),
|
||||
SearchBackend::None => serializer.serialize_str("none"),
|
||||
SearchBackend::Auto => serializer.serialize_str("auto"),
|
||||
Self::Sqlite3 => serializer.serialize_str("sqlite3"),
|
||||
Self::None => serializer.serialize_str("none"),
|
||||
Self::Auto => serializer.serialize_str("auto"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1042,6 +1081,7 @@ mod dotaddressable {
|
|||
impl DotAddressable for melib::LogLevel {}
|
||||
impl DotAddressable for PathBuf {}
|
||||
impl DotAddressable for ToggleFlag {}
|
||||
impl DotAddressable for ActionFlag {}
|
||||
impl DotAddressable for SearchBackend {}
|
||||
impl DotAddressable for melib::SpecialUsageMailbox {}
|
||||
impl<T: DotAddressable> DotAddressable for Option<T> {}
|
||||
|
@ -1263,7 +1303,7 @@ mod dotaddressable {
|
|||
|
||||
#[test]
|
||||
fn test_config_parse() {
|
||||
use std::{fmt::Write, fs, io::prelude::*, path::PathBuf};
|
||||
use std::{fmt::Write, fs, path::PathBuf};
|
||||
|
||||
struct ConfigFile {
|
||||
path: PathBuf,
|
||||
|
@ -1320,11 +1360,8 @@ send_mail = 'false'
|
|||
|
||||
impl ConfigFile {
|
||||
fn new(content: &str) -> std::result::Result<Self, std::io::Error> {
|
||||
let mut f = fs::File::open("/dev/urandom")?;
|
||||
let mut buf = [0u8; 16];
|
||||
f.read_exact(&mut buf)?;
|
||||
let mut filename = String::with_capacity(2 * 16);
|
||||
for byte in &buf {
|
||||
for byte in melib::utils::random::random_u64().to_be_bytes() {
|
||||
write!(&mut filename, "{:02X}", byte).unwrap();
|
||||
}
|
||||
let mut path = std::env::temp_dir();
|
||||
|
@ -1334,7 +1371,7 @@ send_mail = 'false'
|
|||
.append(true)
|
||||
.open(&path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
Ok(ConfigFile { path, file })
|
||||
Ok(Self { path, file })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1352,7 +1389,7 @@ send_mail = 'false'
|
|||
));
|
||||
new_file
|
||||
.file
|
||||
.write_all("[composing]\nsend_mail = 'false'\n".as_bytes())
|
||||
.write_all(b"[composing]\nsend_mail = 'false'\n")
|
||||
.unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
|
||||
assert_eq!(
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
//! Configuration for composing email.
|
||||
use std::collections::HashMap;
|
||||
|
||||
use melib::{email::HeaderName, ToggleFlag};
|
||||
use melib::{conf::ActionFlag, email::HeaderName};
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
|
||||
use super::{
|
||||
default_vals::{ask, false_val, none, true_val},
|
||||
|
@ -62,12 +63,12 @@ pub struct ComposingSettings {
|
|||
/// Default: empty
|
||||
#[serde(default, alias = "default-header-values")]
|
||||
pub default_header_values: HashMap<HeaderName, String>,
|
||||
/// Wrap header preample when editing a draft in an editor. This allows you
|
||||
/// Wrap header preamble when editing a draft in an editor. This allows you
|
||||
/// to write non-plain text email without the preamble creating syntax
|
||||
/// errors. They are stripped when you return from the editor. The
|
||||
/// values should be a two element array of strings, a prefix and suffix.
|
||||
/// Default: None
|
||||
#[serde(default, alias = "wrap-header-preample")]
|
||||
#[serde(default, alias = "wrap-header-preamble")]
|
||||
pub wrap_header_preamble: Option<(String, String)>,
|
||||
/// Store sent mail after successful submission. This setting is meant to be
|
||||
/// disabled for non-standard behaviour in gmail, which auto-saves sent
|
||||
|
@ -91,7 +92,7 @@ pub struct ComposingSettings {
|
|||
/// Forward emails as attachment? (Alternative is inline)
|
||||
/// Default: ask
|
||||
#[serde(default = "ask", alias = "forward-as-attachment")]
|
||||
pub forward_as_attachment: ToggleFlag,
|
||||
pub forward_as_attachment: ActionFlag,
|
||||
/// Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip
|
||||
/// Default: `["Re:", "RE:", "Fwd:", "Fw:", "回复:", "回覆:", "SV:", "Sv:",
|
||||
/// "VS:", "Antw:", "Doorst:", "VS:", "VL:", "REF:", "TR:", "TR:", "AW:",
|
||||
|
@ -114,7 +115,7 @@ pub struct ComposingSettings {
|
|||
|
||||
impl Default for ComposingSettings {
|
||||
fn default() -> Self {
|
||||
ComposingSettings {
|
||||
Self {
|
||||
send_mail: SendMail::ShellCommand("false".into()),
|
||||
editor_command: None,
|
||||
embedded_pty: false,
|
||||
|
@ -125,7 +126,7 @@ impl Default for ComposingSettings {
|
|||
wrap_header_preamble: None,
|
||||
attribution_format_string: None,
|
||||
attribution_use_posix_locale: true,
|
||||
forward_as_attachment: ToggleFlag::Ask,
|
||||
forward_as_attachment: ActionFlag::Ask,
|
||||
reply_prefix_list_to_strip: None,
|
||||
reply_prefix: res(),
|
||||
custom_compose_hooks: vec![],
|
||||
|
@ -176,7 +177,7 @@ pub mod strings {
|
|||
named_unit_variant!(server_submission);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SendMail {
|
||||
#[cfg(feature = "smtp")]
|
||||
|
@ -202,3 +203,103 @@ impl From<ComposeHook> for crate::mail::hooks::Hook {
|
|||
Self::new_shell_command(c.name.into(), c.command)
|
||||
}
|
||||
}
|
||||
const SENDMAIL_ERR_HELP: &str = r#"Invalid `send_mail` value.
|
||||
|
||||
Here are some valid examples:
|
||||
|
||||
Use server submission in protocols that support it (JMAP, NNTP)
|
||||
===============================================================
|
||||
|
||||
send_mail = "server_submission"
|
||||
|
||||
Using a shell script
|
||||
====================
|
||||
|
||||
send_mail = "msmtp --read-recipients --read-envelope-from"
|
||||
|
||||
Direct SMTP connection
|
||||
======================
|
||||
|
||||
send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } }
|
||||
|
||||
[composing.send_mail]
|
||||
hostname = "mail.example.com"
|
||||
port = 587
|
||||
auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } }
|
||||
security = { type = "TLS", danger_accept_invalid_certs = true } }
|
||||
|
||||
|
||||
`send_mail` direct SMTP connection fields:
|
||||
- hostname: text
|
||||
- port: valid port number
|
||||
- envelope_from: text (optional, default is empty),
|
||||
- auth: ...
|
||||
- security: ... (optional, default is "auto")
|
||||
- extensions: ... (optional, default is PIPELINING, CHUNKING, PRDR, 8BITMIME, BINARYMIME, SMTPUTF8, AUTH and DSN_NOTIFY)
|
||||
|
||||
Possible values for `send_mail.auth`:
|
||||
|
||||
No authentication:
|
||||
|
||||
auth = { type = "none" }
|
||||
|
||||
Regular authentication:
|
||||
Note: `require_auth` and `auth_type` are optional and can be skipped.
|
||||
|
||||
auth = { type = "auto", username = "...", password = "...", require_auth = true, auth_type = ... }
|
||||
|
||||
password can be:
|
||||
password = { type = "raw", value = "..." }
|
||||
password = { type = "command_eval", value = "/path/to/password_script.sh" }
|
||||
|
||||
XOAuth2 authentication:
|
||||
Note: `require_auth` is optional and can be skipped.
|
||||
auth = { type = "xoauth2", token_command = "...", require_auth = true }
|
||||
|
||||
Possible values for `send_mail.auth.auth_type` when `auth.type` is "auto":
|
||||
|
||||
auth_type = { plain = false, login = true }
|
||||
|
||||
Possible values for `send_mail.security`:
|
||||
Note that in all cases field `danger_accept_invalid_certs` is optional and its default value is false.
|
||||
|
||||
security = "none"
|
||||
security = { type = "auto", danger_accept_invalid_certs = false }
|
||||
security = { type = "STARTTLS", danger_accept_invalid_certs = false }
|
||||
security = { type = "TLS", danger_accept_invalid_certs = false }
|
||||
|
||||
Possible values for `send_mail.extensions` (All optional and have default values `true`:
|
||||
pipelining
|
||||
chunking
|
||||
8bitmime
|
||||
prdr
|
||||
binarymime
|
||||
smtputf8
|
||||
auth
|
||||
dsn_notify: Array of options e.g. ["FAILURE"]
|
||||
"#;
|
||||
|
||||
impl<'de> Deserialize<'de> for SendMail {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SendMailInner {
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp(melib::smtp::SmtpServerConf),
|
||||
#[serde(with = "strings::server_submission")]
|
||||
ServerSubmission,
|
||||
ShellCommand(String),
|
||||
}
|
||||
|
||||
match <SendMailInner>::deserialize(deserializer) {
|
||||
#[cfg(feature = "smtp")]
|
||||
Ok(SendMailInner::Smtp(v)) => Ok(Self::Smtp(v)),
|
||||
Ok(SendMailInner::ServerSubmission) => Ok(Self::ServerSubmission),
|
||||
Ok(SendMailInner::ShellCommand(v)) => Ok(Self::ShellCommand(v)),
|
||||
Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::{search::Query, Error, Result};
|
||||
use melib::{search::Query, Error, Result, ToggleFlag};
|
||||
|
||||
use super::{default_vals::*, DotAddressable, IndexStyle};
|
||||
|
||||
|
@ -130,6 +130,17 @@ pub struct ListingSettings {
|
|||
#[serde(default)]
|
||||
pub attachment_flag: Option<String>,
|
||||
|
||||
/// Flag to show if any thread entry contains your address as a receiver.
|
||||
/// Useful to make mailing list threads that CC you stand out.
|
||||
/// Default: "✸"
|
||||
#[serde(default)]
|
||||
pub highlight_self_flag: Option<String>,
|
||||
|
||||
/// Show `highlight_self_flag` or not.
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub highlight_self: ToggleFlag,
|
||||
|
||||
/// Should threads with different Subjects show a list of those
|
||||
/// subjects on the entry title?
|
||||
/// Default: "true"
|
||||
|
@ -185,6 +196,8 @@ impl Default for ListingSettings {
|
|||
thread_snoozed_flag: None,
|
||||
selected_flag: None,
|
||||
attachment_flag: None,
|
||||
highlight_self_flag: None,
|
||||
highlight_self: ToggleFlag::Unset,
|
||||
thread_subject_pack: true,
|
||||
threaded_repeat_identical_from_values: false,
|
||||
relative_menu_indices: true,
|
||||
|
@ -224,6 +237,8 @@ impl DotAddressable for ListingSettings {
|
|||
"thread_snoozed_flag" => self.thread_snoozed_flag.lookup(field, tail),
|
||||
"selected_flag" => self.selected_flag.lookup(field, tail),
|
||||
"attachment_flag" => self.attachment_flag.lookup(field, tail),
|
||||
"highlight_self_flag" => self.highlight_self_flag.lookup(field, tail),
|
||||
"highlight_self" => self.highlight_self.lookup(field, tail),
|
||||
"thread_subject_pack" => self.thread_subject_pack.lookup(field, tail),
|
||||
"threaded_repeat_identical_from_values" => self
|
||||
.threaded_repeat_identical_from_values
|
||||
|
|
|
@ -22,22 +22,22 @@
|
|||
|
||||
#![allow(clippy::derivable_impls)]
|
||||
|
||||
//! This module is automatically generated by config_macros.rs.
|
||||
//! This module is automatically generated by `config_macros.rs`.
|
||||
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { Self { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = " Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = " Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = " Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = " Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"●\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"☑\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with different Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > , # [doc = " In threaded listing style, repeat identical From column values within a"] # [doc = " thread. Not repeating adds empty space in the From column which"] # [doc = " might result in less visual clutter."] # [doc = " Default: \"false\""] # [serde (default)] pub threaded_repeat_identical_from_values : Option < bool > , # [doc = " Show relative indices in menu mailboxes to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-menu-indices")] # [serde (default)] pub relative_menu_indices : Option < bool > , # [doc = " Show relative indices in listings to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-list-indices")] # [serde (default)] pub relative_list_indices : Option < bool > , # [doc = " Hide sidebar on launch. Default: \"false\""] # [serde (alias = "hide-sidebar-on-launch")] # [serde (default)] pub hide_sidebar_on_launch : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None , threaded_repeat_identical_from_values : None , relative_menu_indices : None , relative_list_indices : None , hide_sidebar_on_launch : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = " Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = " Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = " Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = " Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"●\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"☑\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Flag to show if any thread entry contains your address as a receiver."] # [doc = " Useful to make mailing list threads that CC you stand out."] # [doc = " Default: \"✸\""] # [serde (default)] pub highlight_self_flag : Option < Option < String > > , # [doc = " Show `highlight_self_flag` or not."] # [doc = " Default: false"] # [serde (default)] pub highlight_self : Option < ToggleFlag > , # [doc = " Should threads with different Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > , # [doc = " In threaded listing style, repeat identical From column values within a"] # [doc = " thread. Not repeating adds empty space in the From column which"] # [doc = " might result in less visual clutter."] # [doc = " Default: \"false\""] # [serde (default)] pub threaded_repeat_identical_from_values : Option < bool > , # [doc = " Show relative indices in menu mailboxes to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-menu-indices")] # [serde (default)] pub relative_menu_indices : Option < bool > , # [doc = " Show relative indices in listings to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-list-indices")] # [serde (default)] pub relative_list_indices : Option < bool > , # [doc = " Hide sidebar on launch. Default: \"false\""] # [serde (alias = "hide-sidebar-on-launch")] # [serde (default)] pub hide_sidebar_on_launch : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { Self { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , highlight_self_flag : None , highlight_self : None , thread_subject_pack : None , threaded_repeat_identical_from_values : None , relative_menu_indices : None , relative_list_indices : None , hide_sidebar_on_launch : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { NotificationsSettingsOverride { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { Self { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { Self { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embedded_pty : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preamble when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preamble")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ActionFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { Self { send_mail : None , editor_command : None , embedded_pty : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { Self { colors : None , ignore_tags : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < bool > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < bool > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < bool > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < bool > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: None"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ToggleFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [cfg_attr (feature = "gpgme" , serde (alias = "remote-lookup-mechanisms"))] # [cfg (feature = "gpgme")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > , # [cfg (not (feature = "gpgme"))] # [cfg_attr (not (feature = "gpgme") , serde (alias = "remote-lookup-mechanisms"))] # [serde (default)] pub remote_lookup_mechanisms : Option < String > } impl Default for PGPSettingsOverride { fn default () -> Self { PGPSettingsOverride { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < ActionFlag > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < ActionFlag > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < ActionFlag > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < ActionFlag > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: False"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ActionFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [cfg_attr (feature = "gpgme" , serde (alias = "remote-lookup-mechanisms"))] # [cfg (feature = "gpgme")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > , # [cfg (not (feature = "gpgme"))] # [cfg_attr (not (feature = "gpgme") , serde (alias = "remote-lookup-mechanisms"))] # [serde (default)] pub remote_lookup_mechanisms : Option < String > } impl Default for PGPSettingsOverride { fn default () -> Self { Self { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } }
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::conf::ToggleFlag;
|
||||
use melib::conf::ActionFlag;
|
||||
|
||||
use super::default_vals::*;
|
||||
|
||||
|
@ -30,22 +30,22 @@ pub struct PGPSettings {
|
|||
/// auto verify signed e-mail according to RFC3156
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "auto-verify-signatures")]
|
||||
pub auto_verify_signatures: bool,
|
||||
pub auto_verify_signatures: ActionFlag,
|
||||
|
||||
/// auto decrypt encrypted e-mail
|
||||
/// Default: true
|
||||
#[serde(default = "true_val", alias = "auto-decrypt")]
|
||||
pub auto_decrypt: bool,
|
||||
pub auto_decrypt: ActionFlag,
|
||||
|
||||
/// always sign sent e-mail
|
||||
/// Default: false
|
||||
#[serde(default = "false_val", alias = "auto-sign")]
|
||||
pub auto_sign: bool,
|
||||
pub auto_sign: ActionFlag,
|
||||
|
||||
/// Auto encrypt sent e-mail
|
||||
/// Default: false
|
||||
#[serde(default = "false_val", alias = "auto-encrypt")]
|
||||
pub auto_encrypt: bool,
|
||||
pub auto_encrypt: ActionFlag,
|
||||
|
||||
// https://tools.ietf.org/html/rfc4880#section-12.2
|
||||
/// Default: None
|
||||
|
@ -61,9 +61,12 @@ pub struct PGPSettings {
|
|||
pub encrypt_key: Option<String>,
|
||||
|
||||
/// Allow remote lookups
|
||||
/// Default: None
|
||||
#[serde(default = "internal_value_false", alias = "allow-remote-lookups")]
|
||||
pub allow_remote_lookup: ToggleFlag,
|
||||
/// Default: False
|
||||
#[serde(
|
||||
default = "action_internal_value_false",
|
||||
alias = "allow-remote-lookups"
|
||||
)]
|
||||
pub allow_remote_lookup: ActionFlag,
|
||||
|
||||
/// Remote lookup mechanisms.
|
||||
/// Default: "local,wkd"
|
||||
|
@ -91,15 +94,15 @@ fn default_lookup_mechanism() -> melib::gpgme::LocateKey {
|
|||
|
||||
impl Default for PGPSettings {
|
||||
fn default() -> Self {
|
||||
PGPSettings {
|
||||
auto_verify_signatures: true,
|
||||
auto_decrypt: true,
|
||||
auto_sign: false,
|
||||
auto_encrypt: false,
|
||||
Self {
|
||||
auto_verify_signatures: true.into(),
|
||||
auto_decrypt: true.into(),
|
||||
auto_sign: false.into(),
|
||||
auto_encrypt: false.into(),
|
||||
sign_key: None,
|
||||
decrypt_key: None,
|
||||
encrypt_key: None,
|
||||
allow_remote_lookup: internal_value_false::<ToggleFlag>(),
|
||||
allow_remote_lookup: action_internal_value_false::<ActionFlag>(),
|
||||
#[cfg(feature = "gpgme")]
|
||||
remote_lookup_mechanisms: default_lookup_mechanism(),
|
||||
#[cfg(not(feature = "gpgme"))]
|
||||
|
|
|
@ -88,6 +88,12 @@ impl DotAddressable for Shortcuts {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandShortcut {
|
||||
pub shortcut: Key,
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
/// Create a struct holding all of a Component's shortcuts.
|
||||
#[macro_export]
|
||||
macro_rules! shortcut_key_values {
|
||||
|
@ -100,6 +106,7 @@ macro_rules! shortcut_key_values {
|
|||
#[serde(default)]
|
||||
#[serde(rename = $cname)]
|
||||
pub struct $name {
|
||||
pub commands: Vec<CommandShortcut>,
|
||||
$(pub $fname : Key),*
|
||||
}
|
||||
|
||||
|
@ -122,6 +129,7 @@ macro_rules! shortcut_key_values {
|
|||
impl Default for $name {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commands : vec![],
|
||||
$($fname: $default),*
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ pub struct TerminalSettings {
|
|||
|
||||
impl Default for TerminalSettings {
|
||||
fn default() -> Self {
|
||||
TerminalSettings {
|
||||
Self {
|
||||
theme: "dark".to_string(),
|
||||
themes: Themes::default(),
|
||||
ascii_drawing: false,
|
||||
|
|
|
@ -47,11 +47,14 @@ use crate::{
|
|||
Context,
|
||||
};
|
||||
|
||||
pub const LIGHT: &str = "light";
|
||||
pub const DARK: &str = "dark";
|
||||
|
||||
#[inline(always)]
|
||||
pub fn value(context: &Context, key: &'static str) -> ThemeAttribute {
|
||||
let theme = match context.settings.terminal.theme.as_str() {
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -66,8 +69,8 @@ pub fn value(context: &Context, key: &'static str) -> ThemeAttribute {
|
|||
#[inline(always)]
|
||||
pub fn fg_color(context: &Context, key: &'static str) -> Color {
|
||||
let theme = match context.settings.terminal.theme.as_str() {
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -82,8 +85,8 @@ pub fn fg_color(context: &Context, key: &'static str) -> Color {
|
|||
#[inline(always)]
|
||||
pub fn bg_color(context: &Context, key: &'static str) -> Color {
|
||||
let theme = match context.settings.terminal.theme.as_str() {
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -98,8 +101,8 @@ pub fn bg_color(context: &Context, key: &'static str) -> Color {
|
|||
#[inline(always)]
|
||||
pub fn attrs(context: &Context, key: &'static str) -> Attr {
|
||||
let theme = match context.settings.terminal.theme.as_str() {
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -318,6 +321,7 @@ const DEFAULT_KEYS: &[&str] = &[
|
|||
"mail.listing.attachment_flag",
|
||||
"mail.listing.thread_snooze_flag",
|
||||
"mail.listing.tag_default",
|
||||
"mail.listing.highlight_self",
|
||||
"pager.highlight_search",
|
||||
"pager.highlight_search_current",
|
||||
];
|
||||
|
@ -361,17 +365,17 @@ pub enum ColorField {
|
|||
Bg,
|
||||
}
|
||||
|
||||
/// The field a ThemeValue::Link refers to.
|
||||
/// The field a `ThemeValue::Link` refers to.
|
||||
trait ThemeLink {
|
||||
type LinkType;
|
||||
}
|
||||
|
||||
/// A color value that's a link can either refer to .fg or .bg field
|
||||
/// A color value that's a link can either refer to `.fg` or `.bg` field
|
||||
impl ThemeLink for Color {
|
||||
type LinkType = ColorField;
|
||||
}
|
||||
|
||||
/// An attr value that's a link can only refer to an .attr field
|
||||
/// An `attr` value that's a link can only refer to an `.attr` field
|
||||
impl ThemeLink for Attr {
|
||||
type LinkType = ();
|
||||
}
|
||||
|
@ -387,37 +391,37 @@ enum ThemeValue<T: ThemeLink> {
|
|||
|
||||
impl From<&'static str> for ThemeValue<Color> {
|
||||
fn from(from: &'static str) -> Self {
|
||||
ThemeValue::Link(from.into(), ColorField::LikeSelf)
|
||||
Self::Link(from.into(), ColorField::LikeSelf)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for ThemeValue<Attr> {
|
||||
fn from(from: &'static str) -> Self {
|
||||
ThemeValue::Link(from.into(), ())
|
||||
Self::Link(from.into(), ())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ThemeValue<Color> {
|
||||
fn from(from: Color) -> Self {
|
||||
ThemeValue::Value(from)
|
||||
Self::Value(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Attr> for ThemeValue<Attr> {
|
||||
fn from(from: Attr) -> Self {
|
||||
ThemeValue::Value(from)
|
||||
Self::Value(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThemeValue<Color> {
|
||||
fn default() -> Self {
|
||||
ThemeValue::Value(Color::Default)
|
||||
Self::Value(Color::Default)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThemeValue<Attr> {
|
||||
fn default() -> Self {
|
||||
ThemeValue::Value(Attr::DEFAULT)
|
||||
Self::Value(Attr::DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,11 +432,11 @@ impl<'de> Deserialize<'de> for ThemeValue<Attr> {
|
|||
{
|
||||
if let Ok(s) = <String>::deserialize(deserializer) {
|
||||
if let Some(stripped) = s.strip_prefix('$') {
|
||||
Ok(ThemeValue::Alias(stripped.to_string().into()))
|
||||
Ok(Self::Alias(stripped.to_string().into()))
|
||||
} else if let Ok(c) = Attr::from_string_de::<'de, D, String>(s.clone()) {
|
||||
Ok(ThemeValue::Value(c))
|
||||
Ok(Self::Value(c))
|
||||
} else {
|
||||
Ok(ThemeValue::Link(s.into(), ()))
|
||||
Ok(Self::Link(s.into(), ()))
|
||||
}
|
||||
} else {
|
||||
Err(de::Error::custom("invalid theme attribute value"))
|
||||
|
@ -446,9 +450,9 @@ impl Serialize for ThemeValue<Attr> {
|
|||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
ThemeValue::Value(s) => s.serialize(serializer),
|
||||
ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
|
||||
ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()),
|
||||
Self::Value(s) => s.serialize(serializer),
|
||||
Self::Alias(s) => format!("${}", s).serialize(serializer),
|
||||
Self::Link(s, ()) => serializer.serialize_str(s.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -459,15 +463,11 @@ impl Serialize for ThemeValue<Color> {
|
|||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
ThemeValue::Value(s) => s.serialize(serializer),
|
||||
ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
|
||||
ThemeValue::Link(s, ColorField::LikeSelf) => serializer.serialize_str(s.as_ref()),
|
||||
ThemeValue::Link(s, ColorField::Fg) => {
|
||||
serializer.serialize_str(format!("{}.fg", s).as_ref())
|
||||
}
|
||||
ThemeValue::Link(s, ColorField::Bg) => {
|
||||
serializer.serialize_str(format!("{}.bg", s).as_ref())
|
||||
}
|
||||
Self::Value(s) => s.serialize(serializer),
|
||||
Self::Alias(s) => format!("${}", s).serialize(serializer),
|
||||
Self::Link(s, ColorField::LikeSelf) => serializer.serialize_str(s.as_ref()),
|
||||
Self::Link(s, ColorField::Fg) => serializer.serialize_str(format!("{}.fg", s).as_ref()),
|
||||
Self::Link(s, ColorField::Bg) => serializer.serialize_str(format!("{}.bg", s).as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -479,21 +479,21 @@ impl<'de> Deserialize<'de> for ThemeValue<Color> {
|
|||
{
|
||||
if let Ok(s) = <String>::deserialize(deserializer) {
|
||||
if let Some(stripped) = s.strip_prefix('$') {
|
||||
Ok(ThemeValue::Alias(stripped.to_string().into()))
|
||||
Ok(Self::Alias(stripped.to_string().into()))
|
||||
} else if let Ok(c) = Color::from_string_de::<'de, D>(s.clone()) {
|
||||
Ok(ThemeValue::Value(c))
|
||||
Ok(Self::Value(c))
|
||||
} else if s.ends_with(".fg") {
|
||||
Ok(ThemeValue::Link(
|
||||
Ok(Self::Link(
|
||||
s[..s.len() - 3].to_string().into(),
|
||||
ColorField::Fg,
|
||||
))
|
||||
} else if s.ends_with(".bg") {
|
||||
Ok(ThemeValue::Link(
|
||||
Ok(Self::Link(
|
||||
s[..s.len() - 3].to_string().into(),
|
||||
ColorField::Bg,
|
||||
))
|
||||
} else {
|
||||
Ok(ThemeValue::Link(s.into(), ColorField::LikeSelf))
|
||||
Ok(Self::Link(s.into(), ColorField::LikeSelf))
|
||||
}
|
||||
} else {
|
||||
Err(de::Error::custom("invalid theme color value"))
|
||||
|
@ -569,7 +569,7 @@ mod regexp {
|
|||
impl Eq for RegexpWrapper {}
|
||||
|
||||
impl PartialEq for RegexpWrapper {
|
||||
fn eq(&self, other: &RegexpWrapper) -> bool {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.as_str().eq(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
@ -652,8 +652,8 @@ mod regexp {
|
|||
key: &'static str,
|
||||
) -> SmallVec<[TextFormatter<'ctx>; 64]> {
|
||||
let theme = match context.settings.terminal.theme.as_str() {
|
||||
"light" => &context.settings.terminal.themes.light,
|
||||
"dark" => &context.settings.terminal.themes.dark,
|
||||
self::LIGHT => &context.settings.terminal.themes.light,
|
||||
self::DARK => &context.settings.terminal.themes.dark,
|
||||
t => context
|
||||
.settings
|
||||
.terminal
|
||||
|
@ -827,7 +827,7 @@ impl<'de> Deserialize<'de> for Themes {
|
|||
attrs: Option<ThemeValue<Attr>>,
|
||||
}
|
||||
|
||||
let mut ret = Themes::default();
|
||||
let mut ret = Self::default();
|
||||
let mut s = <ThemesOptions>::deserialize(deserializer)?;
|
||||
for tk in s.other_themes.keys() {
|
||||
ret.other_themes.insert(tk.clone(), ret.dark.clone());
|
||||
|
@ -1037,7 +1037,7 @@ impl Themes {
|
|||
}
|
||||
ThemeValue::Alias(ref ident) => {
|
||||
if !theme.color_aliases.contains_key(ident.as_ref()) {
|
||||
Some((Some(key), "alias", "nonexistant color alias", ident))
|
||||
Some((Some(key), "alias", "nonexistent color alias", ident))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -1054,7 +1054,7 @@ impl Themes {
|
|||
}
|
||||
ThemeValue::Alias(ref ident) => {
|
||||
if !theme.attr_aliases.contains_key(ident.as_ref()) {
|
||||
Some((Some(key), "alias", "nonexistant color alias", ident))
|
||||
Some((Some(key), "alias", "nonexistent color alias", ident))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -1070,7 +1070,7 @@ impl Themes {
|
|||
}
|
||||
} else if let ThemeValue::Alias(ref ident) = a.fg {
|
||||
if !theme.color_aliases.contains_key(ident.as_ref()) {
|
||||
Some((Some(key), "fg alias", "nonexistant color alias", ident))
|
||||
Some((Some(key), "fg alias", "nonexistent color alias", ident))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -1087,7 +1087,7 @@ impl Themes {
|
|||
}
|
||||
} else if let ThemeValue::Alias(ref ident) = a.bg {
|
||||
if !theme.color_aliases.contains_key(ident.as_ref()) {
|
||||
Some((Some(key), "bg alias", "nonexistant color alias", ident))
|
||||
Some((Some(key), "bg alias", "nonexistent color alias", ident))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -1107,7 +1107,7 @@ impl Themes {
|
|||
Some((
|
||||
Some(key),
|
||||
"attrs alias",
|
||||
"nonexistant text attribute alias",
|
||||
"nonexistent text attribute alias",
|
||||
ident,
|
||||
))
|
||||
} else {
|
||||
|
@ -1145,7 +1145,7 @@ impl Themes {
|
|||
keys.push((
|
||||
Some(key),
|
||||
"fg alias",
|
||||
"nonexistant color alias in `text_format_regexps`",
|
||||
"nonexistent color alias in `text_format_regexps`",
|
||||
ident,
|
||||
));
|
||||
}
|
||||
|
@ -1166,7 +1166,7 @@ impl Themes {
|
|||
keys.push((
|
||||
Some(key),
|
||||
"bg alias",
|
||||
"nonexistant color alias in `text_format_regexps`",
|
||||
"nonexistent color alias in `text_format_regexps`",
|
||||
ident,
|
||||
));
|
||||
}
|
||||
|
@ -1187,7 +1187,7 @@ impl Themes {
|
|||
keys.push((
|
||||
Some(key),
|
||||
"attrs alias",
|
||||
"nonexistant text attribute alias in `text_format_regexps`",
|
||||
"nonexistent text attribute alias in `text_format_regexps`",
|
||||
ident,
|
||||
));
|
||||
}
|
||||
|
@ -1217,10 +1217,10 @@ impl Themes {
|
|||
}
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
let hash_set: HashSet<&'static str> = DEFAULT_KEYS.iter().copied().collect();
|
||||
Themes::validate_keys("light", &self.light, &hash_set)?;
|
||||
Themes::validate_keys("dark", &self.dark, &hash_set)?;
|
||||
Self::validate_keys(self::LIGHT, &self.light, &hash_set)?;
|
||||
Self::validate_keys(self::DARK, &self.dark, &hash_set)?;
|
||||
for (name, t) in self.other_themes.iter() {
|
||||
Themes::validate_keys(name, t, &hash_set)?;
|
||||
Self::validate_keys(name, t, &hash_set)?;
|
||||
}
|
||||
if let Err(err) = is_cyclic(&self.light) {
|
||||
return Err(Error::new(format!("light theme contains a cycle: {}", err)));
|
||||
|
@ -1238,8 +1238,8 @@ impl Themes {
|
|||
|
||||
pub fn key_to_string(&self, key: &str, unlink: bool) -> String {
|
||||
let theme = match key {
|
||||
"light" => &self.light,
|
||||
"dark" => &self.dark,
|
||||
self::LIGHT => &self.light,
|
||||
self::DARK => &self.dark,
|
||||
t => self.other_themes.get(t).unwrap_or(&self.dark),
|
||||
};
|
||||
let mut ret = String::new();
|
||||
|
@ -1274,10 +1274,10 @@ impl Themes {
|
|||
impl std::fmt::Display for Themes {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let mut ret = String::new();
|
||||
ret.push_str(&self.key_to_string("dark", true));
|
||||
ret.push_str(&self.key_to_string(self::DARK, true));
|
||||
|
||||
ret.push_str("\n\n");
|
||||
ret.push_str(&self.key_to_string("light", true));
|
||||
ret.push_str(&self.key_to_string(self::LIGHT, true));
|
||||
for name in self.other_themes.keys() {
|
||||
ret.push_str("\n\n");
|
||||
ret.push_str(&self.key_to_string(name, true));
|
||||
|
@ -1288,7 +1288,7 @@ impl std::fmt::Display for Themes {
|
|||
|
||||
impl Default for Themes {
|
||||
#[allow(clippy::needless_update)]
|
||||
fn default() -> Themes {
|
||||
fn default() -> Self {
|
||||
let mut light = IndexMap::default();
|
||||
let mut dark = IndexMap::default();
|
||||
let other_themes = IndexMap::default();
|
||||
|
@ -1731,10 +1731,19 @@ impl Default for Themes {
|
|||
attrs: Attr::BOLD
|
||||
}
|
||||
);
|
||||
add!(
|
||||
"mail.listing.highlight_self",
|
||||
light = {
|
||||
fg: Color::BLUE,
|
||||
},
|
||||
dark = {
|
||||
fg: Color::BLUE,
|
||||
}
|
||||
);
|
||||
|
||||
add!("pager.highlight_search", light = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD });
|
||||
add!("pager.highlight_search_current", light = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD });
|
||||
Themes {
|
||||
Self {
|
||||
light: Theme {
|
||||
keys: light,
|
||||
attr_aliases: Default::default(),
|
||||
|
@ -1807,8 +1816,8 @@ impl Serialize for Themes {
|
|||
other_themes.insert(name.to_string(), new_map);
|
||||
}
|
||||
|
||||
other_themes.insert("light".to_string(), light);
|
||||
other_themes.insert("dark".to_string(), dark);
|
||||
other_themes.insert(self::LIGHT.to_string(), light);
|
||||
other_themes.insert(self::DARK.to_string(), dark);
|
||||
other_themes.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
@ -2085,7 +2094,7 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
|||
fn test_theme_parsing() {
|
||||
/* MUST SUCCEED: default themes should be valid */
|
||||
let def = Themes::default();
|
||||
assert!(def.validate().is_ok());
|
||||
def.validate().unwrap();
|
||||
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user
|
||||
* redefinitions */
|
||||
const TEST_STR: &str = r#"[dark]
|
||||
|
@ -2121,27 +2130,27 @@ fn test_theme_parsing() {
|
|||
),
|
||||
Color::Byte(15), // White
|
||||
);
|
||||
assert!(parsed.validate().is_ok());
|
||||
parsed.validate().unwrap();
|
||||
/* MUST FAIL: theme `dark` contains a cycle */
|
||||
const HAS_CYCLE: &str = r#"[dark]
|
||||
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
|
||||
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
|
||||
assert!(parsed.validate().is_err());
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: theme `dark` contains an invalid key */
|
||||
const HAS_INVALID_KEYS: &str = r#"[dark]
|
||||
"asdfsafsa" = { fg = "Black" }
|
||||
"#;
|
||||
let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
|
||||
assert!(parsed.is_err());
|
||||
parsed.unwrap_err();
|
||||
/* MUST SUCCEED: alias $Jebediah resolves to a valid color */
|
||||
const TEST_ALIAS_STR: &str = r##"[dark]
|
||||
color_aliases= { "Jebediah" = "#b4da55" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah" }
|
||||
"##;
|
||||
let parsed: Themes = toml::from_str(TEST_ALIAS_STR).unwrap();
|
||||
assert!(parsed.validate().is_ok());
|
||||
parsed.validate().unwrap();
|
||||
assert_eq!(
|
||||
unlink_fg(
|
||||
&parsed.dark,
|
||||
|
@ -2150,48 +2159,48 @@ color_aliases= { "Jebediah" = "#b4da55" }
|
|||
),
|
||||
Color::Rgb(180, 218, 85)
|
||||
);
|
||||
/* MUST FAIL: Mispell color alias $Jebediah as $Jebedia */
|
||||
/* MUST FAIL: Misspell color alias $Jebediah as $Jebedia */
|
||||
const TEST_INVALID_ALIAS_STR: &str = r##"[dark]
|
||||
color_aliases= { "Jebediah" = "#b4da55" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebedia" }
|
||||
"##;
|
||||
let parsed: Themes = toml::from_str(TEST_INVALID_ALIAS_STR).unwrap();
|
||||
assert!(parsed.validate().is_err());
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: Color alias $Jebediah is defined as itself */
|
||||
const TEST_CYCLIC_ALIAS_STR: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$Jebediah" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR).unwrap();
|
||||
assert!(parsed.validate().is_err());
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: Attr alias $Jebediah is defined as itself */
|
||||
const TEST_CYCLIC_ALIAS_ATTR_STR: &str = r#"[dark]
|
||||
attr_aliases= { "Jebediah" = "$Jebediah" }
|
||||
"mail.listing.tag_default" = { attrs = "$Jebediah" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_ATTR_STR).unwrap();
|
||||
assert!(parsed.validate().is_err());
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST FAIL: alias $Jebediah resolves to a cycle */
|
||||
const TEST_CYCLIC_ALIAS_STR_2: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_2).unwrap();
|
||||
assert!(parsed.validate().is_err());
|
||||
parsed.validate().unwrap_err();
|
||||
/* MUST SUCCEED: alias $Jebediah resolves to a key's field */
|
||||
const TEST_CYCLIC_ALIAS_STR_3: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.bg" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_3).unwrap();
|
||||
assert!(parsed.validate().is_ok());
|
||||
parsed.validate().unwrap();
|
||||
/* MUST FAIL: alias $Jebediah resolves to an invalid key */
|
||||
const TEST_INVALID_LINK_KEY_FIELD_STR: &str = r#"[dark]
|
||||
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.attrs" }
|
||||
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
|
||||
"#;
|
||||
let parsed: Themes = toml::from_str(TEST_INVALID_LINK_KEY_FIELD_STR).unwrap();
|
||||
assert!(parsed.validate().is_err());
|
||||
parsed.validate().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::Card;
|
||||
|
||||
use crate::{
|
||||
|
@ -59,7 +58,7 @@ impl std::fmt::Display for ContactManager {
|
|||
impl ContactManager {
|
||||
pub fn new(context: &Context) -> Self {
|
||||
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
|
||||
ContactManager {
|
||||
Self {
|
||||
id: ComponentId::default(),
|
||||
parent_id: None,
|
||||
card: Card::new(),
|
||||
|
@ -200,7 +199,7 @@ impl Component for ContactManager {
|
|||
None => {}
|
||||
Some(true) => {
|
||||
let fields = std::mem::take(&mut self.form).collect().unwrap();
|
||||
let fields: HashMap<String, String> = fields
|
||||
let fields: IndexMap<String, String> = fields
|
||||
.into_iter()
|
||||
.map(|(s, v)| {
|
||||
(
|
||||
|
@ -227,14 +226,14 @@ impl Component for ContactManager {
|
|||
}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
if let UIEvent::InsertInput(_) = event {
|
||||
if matches!(event, UIEvent::InsertInput(_)) {
|
||||
self.has_changes = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ViewMode::ReadOnly => {
|
||||
if let &mut UIEvent::Input(Key::Esc) = event {
|
||||
if matches!(event, UIEvent::Input(Key::Esc)) {
|
||||
if self.can_quit_cleanly(context) {
|
||||
self.unrealize(context);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
use std::cmp;
|
||||
|
||||
use melib::{backends::AccountHash, text_processing::TextProcessing, Card, CardId, Draft};
|
||||
use melib::{backends::AccountHash, text::TextProcessing, Card, CardId, Draft};
|
||||
|
||||
use crate::{
|
||||
conf, contacts::editor::ContactManager, shortcut, terminal::*, Action::Tab, Component,
|
||||
|
@ -88,7 +88,7 @@ impl ContactList {
|
|||
index: i,
|
||||
})
|
||||
.collect();
|
||||
ContactList {
|
||||
Self {
|
||||
accounts,
|
||||
cursor_pos: 0,
|
||||
new_cursor_pos: 0,
|
||||
|
@ -112,13 +112,13 @@ impl ContactList {
|
|||
}
|
||||
|
||||
pub fn for_account(pos: usize, context: &Context) -> Self {
|
||||
ContactList {
|
||||
Self {
|
||||
account_pos: pos,
|
||||
..Self::new(context)
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &mut Context) {
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
self.data_columns.clear();
|
||||
let account = &context.accounts[self.account_pos];
|
||||
let book = &account.address_book;
|
||||
|
@ -274,7 +274,7 @@ impl ContactList {
|
|||
grid: &mut CellBuffer,
|
||||
area: Area,
|
||||
a: &AccountMenuEntry,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
) {
|
||||
let width = area.width();
|
||||
let must_highlight_account: bool = self.account_pos == a.index;
|
||||
|
@ -838,6 +838,25 @@ impl Component for ContactList {
|
|||
self.movement = Some(PageMovement::End);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.contact_list
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ impl JobExecutor {
|
|||
/// A queue that holds scheduled tasks.
|
||||
pub fn new(sender: Sender<ThreadEvent>) -> Self {
|
||||
// Create a queue.
|
||||
let mut ret = JobExecutor {
|
||||
let mut ret = Self {
|
||||
global_queue: Arc::new(Injector::new()),
|
||||
workers: vec![],
|
||||
parkers: vec![],
|
||||
|
@ -185,7 +185,10 @@ impl JobExecutor {
|
|||
jobs: Arc::new(Mutex::new(IndexMap::default())),
|
||||
};
|
||||
let mut workers = vec![];
|
||||
for _ in 0..num_cpus::get().max(1) {
|
||||
for _ in 0..std::thread::available_parallelism()
|
||||
.map(Into::into)
|
||||
.unwrap_or(1)
|
||||
{
|
||||
let new_worker = Worker::new_fifo();
|
||||
ret.workers.push(new_worker.stealer());
|
||||
let p = Parker::new();
|
||||
|
@ -234,6 +237,7 @@ impl JobExecutor {
|
|||
}
|
||||
|
||||
/// Spawns a future with a generic return value `R`
|
||||
#[inline(always)]
|
||||
pub fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
|
@ -295,6 +299,7 @@ impl JobExecutor {
|
|||
|
||||
/// Spawns a future with a generic return value `R` that might block on a
|
||||
/// new thread
|
||||
#[inline(always)]
|
||||
pub fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
|
||||
where
|
||||
F: Future<Output = R> + Send + 'static,
|
||||
|
@ -306,7 +311,7 @@ impl JobExecutor {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn create_timer(self: Arc<JobExecutor>, interval: Duration, value: Duration) -> Timer {
|
||||
pub fn create_timer(self: Arc<Self>, interval: Duration, value: Duration) -> Timer {
|
||||
let timer = TimerPrivate {
|
||||
interval,
|
||||
cancel: Arc::new(Mutex::new(false)),
|
||||
|
@ -415,7 +420,7 @@ impl JobExecutor {
|
|||
|
||||
pub type JobChannel<T> = oneshot::Receiver<T>;
|
||||
|
||||
/// JoinHandle for the future that allows us to cancel the task.
|
||||
/// `JoinHandle` for the future that allows us to cancel the task.
|
||||
#[derive(Debug)]
|
||||
pub struct JoinHandle<T> {
|
||||
pub task: Arc<Mutex<Option<async_task::Task<()>>>>,
|
||||
|
|
|
@ -110,7 +110,7 @@ impl JobManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &mut Context) {
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
self.set_dirty(true);
|
||||
|
||||
let mut entries = (*context.main_loop_handler.job_executor.jobs.lock().unwrap()).clone();
|
||||
|
|
|
@ -19,6 +19,58 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![deny(
|
||||
rustdoc::redundant_explicit_links,
|
||||
/* groups */
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::complexity,
|
||||
clippy::perf,
|
||||
clippy::cargo,
|
||||
clippy::nursery,
|
||||
clippy::style,
|
||||
/* restriction */
|
||||
clippy::dbg_macro,
|
||||
clippy::rc_buffer,
|
||||
clippy::as_underscore,
|
||||
clippy::assertions_on_result_states,
|
||||
/* rustdoc */
|
||||
rustdoc::broken_intra_doc_links,
|
||||
/* pedantic */
|
||||
//clippy::cast_lossless,
|
||||
//clippy::cast_possible_wrap,
|
||||
//clippy::ptr_as_ptr,
|
||||
clippy::doc_markdown,
|
||||
clippy::expect_fun_call,
|
||||
clippy::bool_to_int_with_if,
|
||||
clippy::borrow_as_ptr,
|
||||
clippy::cast_ptr_alignment,
|
||||
clippy::large_futures,
|
||||
clippy::waker_clone_wake,
|
||||
clippy::unused_enumerate_index,
|
||||
clippy::unnecessary_fallible_conversions,
|
||||
clippy::struct_field_names,
|
||||
clippy::manual_hash_one,
|
||||
clippy::into_iter_without_iter,
|
||||
)]
|
||||
#![allow(
|
||||
clippy::option_if_let_else,
|
||||
clippy::missing_const_for_fn,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::multiple_crate_versions,
|
||||
clippy::significant_drop_in_scrutinee,
|
||||
clippy::cognitive_complexity,
|
||||
clippy::manual_clamp
|
||||
)]
|
||||
/* Source Code Annotation Tags:
|
||||
*
|
||||
* Global tags (in tagref format <https://github.com/stepchowfun/tagref>) for source code
|
||||
* annotation:
|
||||
*
|
||||
* - tags from melib/src/lib.rs.
|
||||
* - [tag:hardcoded_color_value] Replace hardcoded color values with user configurable ones.
|
||||
*/
|
||||
|
||||
//!
|
||||
//! This crate contains the frontend stuff of the application. The application
|
||||
//! entry way on `src/bin.rs` creates an event loop and passes input to a
|
||||
|
@ -38,7 +90,6 @@ pub use melib::uuid;
|
|||
|
||||
pub extern crate bitflags;
|
||||
pub extern crate serde_json;
|
||||
#[macro_use]
|
||||
pub extern crate smallvec;
|
||||
pub extern crate termion;
|
||||
|
||||
|
@ -49,11 +100,13 @@ static GLOBAL: System = System;
|
|||
|
||||
pub extern crate melib;
|
||||
pub use melib::{
|
||||
error::*, log, AccountHash, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel, Mail, Mailbox,
|
||||
MailboxHash, ThreadHash, ToggleFlag,
|
||||
error::*, log, AccountHash, ActionFlag, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel,
|
||||
Mail, Mailbox, MailboxHash, ThreadHash, ToggleFlag,
|
||||
};
|
||||
|
||||
pub mod args;
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub mod manpages;
|
||||
pub mod subcommands;
|
||||
|
||||
#[macro_use]
|
||||
|
|
|
@ -30,7 +30,7 @@ use melib::{
|
|||
|
||||
use super::*;
|
||||
use crate::{
|
||||
melib::text_processing::{TextProcessing, Truncate},
|
||||
melib::text::{TextProcessing, Truncate},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
|
|
|
@ -134,12 +134,12 @@ enum ViewMode {
|
|||
impl ViewMode {
|
||||
#[inline]
|
||||
fn is_edit(&self) -> bool {
|
||||
matches!(self, ViewMode::Edit)
|
||||
matches!(self, Self::Edit)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_edit_attachments(&self) -> bool {
|
||||
matches!(self, ViewMode::EditAttachments { .. })
|
||||
matches!(self, Self::EditAttachments { .. })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ impl Composer {
|
|||
pager.set_show_scrollbar(true);
|
||||
let mut form = FormWidget::default();
|
||||
form.set_cursor(2);
|
||||
Composer {
|
||||
Self {
|
||||
reply_context: None,
|
||||
account_hash: AccountHash::default(),
|
||||
cursor: Cursor::Headers,
|
||||
|
@ -195,9 +195,9 @@ impl Composer {
|
|||
}
|
||||
|
||||
pub fn with_account(account_hash: AccountHash, context: &Context) -> Self {
|
||||
let mut ret = Composer {
|
||||
let mut ret = Self {
|
||||
account_hash,
|
||||
..Composer::new(context)
|
||||
..Self::new(context)
|
||||
};
|
||||
|
||||
// Add user's custom hooks.
|
||||
|
@ -237,8 +237,7 @@ impl Composer {
|
|||
);
|
||||
}
|
||||
if *account_settings!(context[account_hash].composing.format_flowed) {
|
||||
ret.pager
|
||||
.set_reflow(melib::text_processing::Reflow::FormatFlowed);
|
||||
ret.pager.set_reflow(melib::text::Reflow::FormatFlowed);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
@ -249,7 +248,7 @@ impl Composer {
|
|||
bytes: &[u8],
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
let mut ret = Composer::with_account(account_hash, context);
|
||||
let mut ret = Self::with_account(account_hash, context);
|
||||
// Add user's custom hooks.
|
||||
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
|
||||
.iter()
|
||||
|
@ -274,10 +273,10 @@ impl Composer {
|
|||
pub fn reply_to(
|
||||
coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
|
||||
reply_body: String,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
reply_to_all: bool,
|
||||
) -> Self {
|
||||
let mut ret = Composer::with_account(account_hash, context);
|
||||
let mut ret = Self::with_account(account_hash, context);
|
||||
// Add user's custom hooks.
|
||||
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
|
||||
.iter()
|
||||
|
@ -388,15 +387,11 @@ impl Composer {
|
|||
to.extend(envelope.from().iter().cloned());
|
||||
}
|
||||
to.extend(envelope.to().iter().cloned());
|
||||
if let Ok(ours) = TryInto::<Address>::try_into(
|
||||
context.accounts[&coordinates.0]
|
||||
.settings
|
||||
.account()
|
||||
.make_display_name()
|
||||
.as_str(),
|
||||
) {
|
||||
to.remove(&ours);
|
||||
}
|
||||
let ours = context.accounts[&coordinates.0]
|
||||
.settings
|
||||
.account()
|
||||
.make_display_name();
|
||||
to.remove(&ours);
|
||||
ret.draft.set_header(HeaderName::TO, {
|
||||
let mut ret: String =
|
||||
to.into_iter()
|
||||
|
@ -428,7 +423,7 @@ impl Composer {
|
|||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str()),
|
||||
envelope.from().get(0),
|
||||
envelope.from().first(),
|
||||
envelope.date(),
|
||||
*account_settings!(
|
||||
context[ret.account_hash]
|
||||
|
@ -452,9 +447,9 @@ impl Composer {
|
|||
pub fn reply_to_select(
|
||||
coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
|
||||
reply_body: String,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
) -> Self {
|
||||
let mut ret = Composer::reply_to(coordinates, reply_body, context, false);
|
||||
let mut ret = Self::reply_to(coordinates, reply_body, context, false);
|
||||
let account = &context.accounts[&account_hash];
|
||||
let parent_message = account.collection.get_env(coordinates.2);
|
||||
/* If message is from a mailing list and we detect a List-Post header, ask
|
||||
|
@ -500,17 +495,17 @@ impl Composer {
|
|||
pub fn reply_to_author(
|
||||
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
|
||||
reply_body: String,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
) -> Self {
|
||||
Composer::reply_to(coordinates, reply_body, context, false)
|
||||
Self::reply_to(coordinates, reply_body, context, false)
|
||||
}
|
||||
|
||||
pub fn reply_to_all(
|
||||
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
|
||||
reply_body: String,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
) -> Self {
|
||||
Composer::reply_to(coordinates, reply_body, context, true)
|
||||
Self::reply_to(coordinates, reply_body, context, true)
|
||||
}
|
||||
|
||||
pub fn forward(
|
||||
|
@ -518,9 +513,9 @@ impl Composer {
|
|||
bytes: &[u8],
|
||||
env: &Envelope,
|
||||
as_attachment: bool,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
) -> Self {
|
||||
let mut composer = Composer::with_account(coordinates.0, context);
|
||||
let mut composer = Self::with_account(coordinates.0, context);
|
||||
let mut draft: Draft = Draft::default();
|
||||
draft.set_header(HeaderName::SUBJECT, format!("Fwd: {}", env.subject()));
|
||||
let preamble = format!(
|
||||
|
@ -672,7 +667,7 @@ To: {}
|
|||
}
|
||||
};
|
||||
|
||||
(addr, desc)
|
||||
(addr.to_string(), desc)
|
||||
})
|
||||
.map(AutoCompleteEntry::from)
|
||||
.collect::<Vec<AutoCompleteEntry>>()
|
||||
|
@ -689,7 +684,12 @@ To: {}
|
|||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
grid.clear_area(area, theme_default);
|
||||
#[cfg(feature = "gpgme")]
|
||||
if self.gpg_state.sign_mail.is_true() {
|
||||
if self
|
||||
.gpg_state
|
||||
.sign_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true()
|
||||
{
|
||||
let key_list = self
|
||||
.gpg_state
|
||||
.sign_keys
|
||||
|
@ -732,7 +732,12 @@ To: {}
|
|||
);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
if self.gpg_state.encrypt_mail.is_true() {
|
||||
if self
|
||||
.gpg_state
|
||||
.encrypt_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true()
|
||||
{
|
||||
let key_list = self
|
||||
.gpg_state
|
||||
.encrypt_keys
|
||||
|
@ -868,10 +873,9 @@ impl Component for Composer {
|
|||
|
||||
if !self.initialized {
|
||||
#[cfg(feature = "gpgme")]
|
||||
if self.gpg_state.sign_mail.is_unset() {
|
||||
self.gpg_state.sign_mail = ToggleFlag::InternalVal(*account_settings!(
|
||||
context[self.account_hash].pgp.auto_sign
|
||||
));
|
||||
if self.gpg_state.sign_mail.is_none() {
|
||||
self.gpg_state.sign_mail =
|
||||
Some(*account_settings!(context[self.account_hash].pgp.auto_sign));
|
||||
}
|
||||
if !self.draft.headers().contains_key(HeaderName::FROM)
|
||||
|| self.draft.headers()[HeaderName::FROM].is_empty()
|
||||
|
@ -881,7 +885,8 @@ impl Component for Composer {
|
|||
context.accounts[&self.account_hash]
|
||||
.settings
|
||||
.account()
|
||||
.make_display_name(),
|
||||
.make_display_name()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
self.pager.update_from_str(self.draft.body(), Some(77));
|
||||
|
@ -1021,8 +1026,8 @@ impl Component for Composer {
|
|||
guard.grid.set_dirty(false);
|
||||
self.dirty = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
self.embedded_dimensions = (area.width(), area.height());
|
||||
}
|
||||
|
@ -1172,7 +1177,7 @@ impl Component for Composer {
|
|||
(ViewMode::Send(ref selector), UIEvent::FinishedUIDialog(id, result))
|
||||
if selector.id() == *id =>
|
||||
{
|
||||
if let Some(true) = result.downcast_ref::<bool>() {
|
||||
if matches!(result.downcast_ref::<bool>(), Some(true)) {
|
||||
self.update_draft();
|
||||
match send_draft_async(
|
||||
#[cfg(feature = "gpgme")]
|
||||
|
@ -1458,12 +1463,20 @@ impl Component for Composer {
|
|||
#[cfg(feature = "gpgme")]
|
||||
match self.cursor {
|
||||
Cursor::Sign => {
|
||||
let is_true = self.gpg_state.sign_mail.is_true();
|
||||
self.gpg_state.sign_mail = ToggleFlag::from(!is_true);
|
||||
let is_true = self
|
||||
.gpg_state
|
||||
.sign_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true();
|
||||
self.gpg_state.sign_mail = Some(ActionFlag::from(!is_true));
|
||||
}
|
||||
Cursor::Encrypt => {
|
||||
let is_true = self.gpg_state.encrypt_mail.is_true();
|
||||
self.gpg_state.encrypt_mail = ToggleFlag::from(!is_true);
|
||||
let is_true = self
|
||||
.gpg_state
|
||||
.encrypt_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true();
|
||||
self.gpg_state.encrypt_mail = Some(ActionFlag::from(!is_true));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
@ -1482,7 +1495,8 @@ impl Component for Composer {
|
|||
..
|
||||
} = self;
|
||||
|
||||
for err in hooks
|
||||
// Collect errors in a vector because filter_map borrows context
|
||||
let errors = hooks
|
||||
.iter_mut()
|
||||
.filter_map(|h| {
|
||||
if let Err(err) = h(context, draft) {
|
||||
|
@ -1491,8 +1505,8 @@ impl Component for Composer {
|
|||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
.collect::<Vec<_>>();
|
||||
for err in errors {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: None,
|
||||
source: None,
|
||||
|
@ -1671,7 +1685,7 @@ impl Component for Composer {
|
|||
)
|
||||
.map_err(|_err| -> Error { "No valid sender address in `From:`".into() })
|
||||
.and_then(|(_, list)| {
|
||||
list.get(0)
|
||||
list.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| "No valid sender address in `From:`".into())
|
||||
})
|
||||
|
@ -1686,7 +1700,7 @@ impl Component for Composer {
|
|||
)
|
||||
}) {
|
||||
Ok(widget) => {
|
||||
self.gpg_state.sign_mail = ToggleFlag::from(true);
|
||||
self.gpg_state.sign_mail = Some(ActionFlag::from(true));
|
||||
self.mode = ViewMode::SelectEncryptKey(false, widget);
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -1712,7 +1726,7 @@ impl Component for Composer {
|
|||
)
|
||||
.map_err(|_err| -> Error { "No valid recipient addresses in `To:`".into() })
|
||||
.and_then(|(_, list)| {
|
||||
list.get(0)
|
||||
list.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| "No valid recipient addresses in `To:`".into())
|
||||
})
|
||||
|
@ -1727,7 +1741,7 @@ impl Component for Composer {
|
|||
)
|
||||
}) {
|
||||
Ok(widget) => {
|
||||
self.gpg_state.encrypt_mail = ToggleFlag::from(true);
|
||||
self.gpg_state.encrypt_mail = Some(ActionFlag::from(true));
|
||||
self.mode = ViewMode::SelectEncryptKey(true, widget);
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -1862,10 +1876,11 @@ impl Component for Composer {
|
|||
};
|
||||
|
||||
if *account_settings!(context[self.account_hash].composing.embedded_pty) {
|
||||
let command = [editor, f.path().display().to_string()].join(" ");
|
||||
match crate::terminal::embedded::create_pty(
|
||||
self.embedded_dimensions.0,
|
||||
self.embedded_dimensions.1,
|
||||
[editor, f.path().display().to_string()].join(" "),
|
||||
&command,
|
||||
) {
|
||||
Ok(terminal) => {
|
||||
self.embedded_pty = Some(EmbeddedPty {
|
||||
|
@ -1877,14 +1892,17 @@ impl Component for Composer {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::ChangeMode(UIMode::Embedded));
|
||||
context.replies.push_back(UIEvent::Fork(ForkType::Embedded(
|
||||
self.embedded_pty
|
||||
context.replies.push_back(UIEvent::Fork(ForkType::Embedded {
|
||||
id: "editor".into(),
|
||||
command: Some(command.into()),
|
||||
pid: self
|
||||
.embedded_pty
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.child_pid,
|
||||
)));
|
||||
}));
|
||||
self.mode = ViewMode::EmbeddedPty;
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -1973,7 +1991,7 @@ impl Component for Composer {
|
|||
});
|
||||
return false;
|
||||
}
|
||||
match File::create_temp_file(&[], None, None, None, true)
|
||||
let res = File::create_temp_file(&[], None, None, None, true)
|
||||
.and_then(|f| {
|
||||
let std_file = f.as_std_file()?;
|
||||
Ok((
|
||||
|
@ -1985,8 +2003,8 @@ impl Component for Composer {
|
|||
.spawn()?,
|
||||
))
|
||||
})
|
||||
.and_then(|(f, child)| Ok((f, child.wait_with_output()?.stderr)))
|
||||
{
|
||||
.and_then(|(f, child)| Ok((f, child.wait_with_output()?.stderr)));
|
||||
match res {
|
||||
Ok((f, stderr)) => {
|
||||
if !stderr.is_empty() {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
|
@ -2166,20 +2184,47 @@ impl Component for Composer {
|
|||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
Action::Compose(ComposeAction::ToggleSign) => {
|
||||
let is_true = self.gpg_state.sign_mail.is_true();
|
||||
self.gpg_state.sign_mail = ToggleFlag::from(!is_true);
|
||||
let is_true = self
|
||||
.gpg_state
|
||||
.sign_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true();
|
||||
self.gpg_state.sign_mail = Some(ActionFlag::from(!is_true));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
Action::Compose(ComposeAction::ToggleEncrypt) => {
|
||||
let is_true = self.gpg_state.encrypt_mail.is_true();
|
||||
self.gpg_state.encrypt_mail = ToggleFlag::from(!is_true);
|
||||
let is_true = self
|
||||
.gpg_state
|
||||
.encrypt_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true();
|
||||
self.gpg_state.encrypt_mail = Some(ActionFlag::from(!is_true));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.composing
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
@ -2480,7 +2525,7 @@ pub fn save_draft(
|
|||
|
||||
pub fn send_draft_async(
|
||||
#[cfg(feature = "gpgme")] gpg_state: gpg::GpgComposeState,
|
||||
context: &mut Context,
|
||||
context: &Context,
|
||||
account_hash: AccountHash,
|
||||
mut draft: Draft,
|
||||
mailbox_type: SpecialUsageMailbox,
|
||||
|
@ -2501,13 +2546,22 @@ pub fn send_draft_async(
|
|||
>,
|
||||
> = vec![];
|
||||
#[cfg(feature = "gpgme")]
|
||||
if gpg_state.sign_mail.is_true() && !gpg_state.encrypt_mail.is_true() {
|
||||
if gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true()
|
||||
&& !gpg_state
|
||||
.encrypt_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true()
|
||||
{
|
||||
filters_stack.push(Box::new(crate::mail::pgp::sign_filter(
|
||||
gpg_state.sign_keys,
|
||||
)?));
|
||||
} else if gpg_state.encrypt_mail.is_true() {
|
||||
} else if gpg_state
|
||||
.encrypt_mail
|
||||
.unwrap_or(ActionFlag::False)
|
||||
.is_true()
|
||||
{
|
||||
filters_stack.push(Box::new(crate::mail::pgp::encrypt_filter(
|
||||
if gpg_state.sign_mail.is_true() {
|
||||
if gpg_state.sign_mail.unwrap_or(ActionFlag::False).is_true() {
|
||||
Some(gpg_state.sign_keys.clone())
|
||||
} else {
|
||||
None
|
||||
|
@ -2643,7 +2697,7 @@ hello world.
|
|||
let envelope =
|
||||
Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let mut context = Context::new_mock(&tempdir);
|
||||
let context = Context::new_mock(&tempdir);
|
||||
let account_hash = context.accounts[0].hash();
|
||||
let mailbox_hash = MailboxHash::default();
|
||||
let envelope_hash = envelope.hash();
|
||||
|
@ -2653,7 +2707,7 @@ hello world.
|
|||
let composer = Composer::reply_to(
|
||||
(account_hash, mailbox_hash, envelope_hash),
|
||||
String::new(),
|
||||
&mut context,
|
||||
&context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -2682,7 +2736,7 @@ hello world.
|
|||
let composer = Composer::reply_to(
|
||||
(account_hash, mailbox_hash, envelope_hash),
|
||||
String::new(),
|
||||
&mut context,
|
||||
&context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
|
@ -54,7 +54,7 @@ impl EditAttachments {
|
|||
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel));
|
||||
buttons.set_focus(true);
|
||||
buttons.set_cursor(1);
|
||||
EditAttachments {
|
||||
Self {
|
||||
account_hash,
|
||||
mode: EditAttachmentMode::Overview,
|
||||
buttons,
|
||||
|
|
|
@ -29,7 +29,7 @@ pub enum KeySelection {
|
|||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ToggleFlag,
|
||||
allow_remote_lookup: ActionFlag,
|
||||
},
|
||||
Error {
|
||||
id: ComponentId,
|
||||
|
@ -52,8 +52,8 @@ impl KeySelection {
|
|||
secret: bool,
|
||||
local: bool,
|
||||
pattern: String,
|
||||
allow_remote_lookup: ToggleFlag,
|
||||
context: &mut Context,
|
||||
allow_remote_lookup: ActionFlag,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
use melib::gpgme::*;
|
||||
let mut ctx = Context::new()?;
|
||||
|
@ -69,7 +69,7 @@ impl KeySelection {
|
|||
.spawn_specialized("gpg::keylist".into(), job);
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(KeySelection::LoadingKeys {
|
||||
Ok(Self::LoadingKeys {
|
||||
handle,
|
||||
secret,
|
||||
local,
|
||||
|
@ -83,11 +83,11 @@ impl KeySelection {
|
|||
impl Component for KeySelection {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
Self::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
} => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
|
||||
KeySelection::Error { ref err, .. } => {
|
||||
Self::Error { ref err, .. } => {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
grid.write_string(
|
||||
&err.to_string(),
|
||||
|
@ -98,13 +98,13 @@ impl Component for KeySelection {
|
|||
Some(0),
|
||||
);
|
||||
}
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
|
||||
Self::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
Self::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
ref mut handle,
|
||||
secret,
|
||||
|
@ -131,10 +131,10 @@ impl Component for KeySelection {
|
|||
Ok(w) => {
|
||||
*self = w;
|
||||
}
|
||||
Err(err) => *self = KeySelection::Error { err, id },
|
||||
Err(err) => *self = Self::Error { err, id },
|
||||
}
|
||||
} else if !*local && allow_remote_lookup.is_ask() {
|
||||
*self = KeySelection::Error {
|
||||
*self = Self::Error {
|
||||
err: Error::new(format!(
|
||||
"No keys found for {}, perform remote lookup?",
|
||||
pattern
|
||||
|
@ -142,12 +142,12 @@ impl Component for KeySelection {
|
|||
id,
|
||||
}
|
||||
} else {
|
||||
*self = KeySelection::Error {
|
||||
*self = Self::Error {
|
||||
err: Error::new(format!("No keys found for {}.", pattern)),
|
||||
id,
|
||||
}
|
||||
}
|
||||
if let KeySelection::Error { ref err, .. } = self {
|
||||
if let Self::Error { ref err, .. } = self {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
|
@ -177,17 +177,17 @@ impl Component for KeySelection {
|
|||
move |id: ComponentId, results: &[melib::gpgme::Key]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(results.get(0).cloned()),
|
||||
Box::new(results.first().cloned()),
|
||||
))
|
||||
},
|
||||
)),
|
||||
context,
|
||||
));
|
||||
widget.set_dirty(true);
|
||||
*self = KeySelection::Loaded { widget, keys };
|
||||
*self = Self::Loaded { widget, keys };
|
||||
}
|
||||
Ok(Some(Err(err))) => {
|
||||
*self = KeySelection::Error {
|
||||
*self = Self::Error {
|
||||
err,
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
|
@ -197,30 +197,30 @@ impl Component for KeySelection {
|
|||
}
|
||||
_ => progress_spinner.process_event(event, context),
|
||||
},
|
||||
KeySelection::Error { .. } => false,
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.process_event(event, context),
|
||||
Self::Error { .. } => false,
|
||||
Self::Loaded { ref mut widget, .. } => widget.process_event(event, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
Self::LoadingKeys {
|
||||
ref progress_spinner,
|
||||
..
|
||||
} => progress_spinner.is_dirty(),
|
||||
KeySelection::Error { .. } => true,
|
||||
KeySelection::Loaded { ref widget, .. } => widget.is_dirty(),
|
||||
Self::Error { .. } => true,
|
||||
Self::Loaded { ref widget, .. } => widget.is_dirty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
Self::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
} => progress_spinner.set_dirty(value),
|
||||
KeySelection::Error { .. } => {}
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.set_dirty(value),
|
||||
Self::Error { .. } => {}
|
||||
Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,29 +228,27 @@ impl Component for KeySelection {
|
|||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
match self {
|
||||
KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => {
|
||||
ShortcutMaps::default()
|
||||
}
|
||||
KeySelection::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
Self::LoadingKeys { .. } | Self::Error { .. } => ShortcutMaps::default(),
|
||||
Self::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
Self::LoadingKeys {
|
||||
ref progress_spinner,
|
||||
..
|
||||
} => progress_spinner.id(),
|
||||
KeySelection::Error { ref id, .. } => *id,
|
||||
KeySelection::Loaded { ref widget, .. } => widget.id(),
|
||||
Self::Error { ref id, .. } => *id,
|
||||
Self::Loaded { ref widget, .. } => widget.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GpgComposeState {
|
||||
pub sign_mail: ToggleFlag,
|
||||
pub encrypt_mail: ToggleFlag,
|
||||
pub sign_mail: Option<ActionFlag>,
|
||||
pub encrypt_mail: Option<ActionFlag>,
|
||||
pub encrypt_keys: Vec<melib::gpgme::Key>,
|
||||
pub encrypt_for_self: bool,
|
||||
pub sign_keys: Vec<melib::gpgme::Key>,
|
||||
|
@ -258,9 +256,9 @@ pub struct GpgComposeState {
|
|||
|
||||
impl Default for GpgComposeState {
|
||||
fn default() -> Self {
|
||||
GpgComposeState {
|
||||
sign_mail: ToggleFlag::Unset,
|
||||
encrypt_mail: ToggleFlag::Unset,
|
||||
Self {
|
||||
sign_mail: None,
|
||||
encrypt_mail: None,
|
||||
encrypt_keys: vec![],
|
||||
encrypt_for_self: true,
|
||||
sign_keys: vec![],
|
||||
|
|
|
@ -31,7 +31,8 @@ use std::{
|
|||
|
||||
use futures::future::try_join_all;
|
||||
use melib::{
|
||||
backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, FlagOp, UnixTimestamp,
|
||||
backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, Flag, FlagOp,
|
||||
ShellExpandTrait, UnixTimestamp,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
|
@ -41,25 +42,11 @@ use crate::{
|
|||
components::ExtendShortcutsMaps,
|
||||
};
|
||||
|
||||
// [ref:TODO]: emoji_text_presentation_selector should be printed along with the chars
|
||||
// before it but not as a separate Cell
|
||||
//macro_rules! emoji_text_presentation_selector {
|
||||
// () => {
|
||||
// "\u{FE0E}"
|
||||
// };
|
||||
//}
|
||||
//
|
||||
//pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎",
|
||||
// emoji_text_presentation_selector!()); pub const DEFAULT_SELECTED_FLAG: &str =
|
||||
// concat!("☑️", emoji_text_presentation_selector!());
|
||||
// pub const DEFAULT_UNSEEN_FLAG: &str = concat!("●",
|
||||
// emoji_text_presentation_selector!()); pub const DEFAULT_SNOOZED_FLAG: &str =
|
||||
// concat!("💤", emoji_text_presentation_selector!());
|
||||
|
||||
pub const DEFAULT_ATTACHMENT_FLAG: &str = "📎";
|
||||
pub const DEFAULT_SELECTED_FLAG: &str = "☑️";
|
||||
pub const DEFAULT_UNSEEN_FLAG: &str = "●";
|
||||
pub const DEFAULT_SNOOZED_FLAG: &str = "💤";
|
||||
pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎", emoji_text_presentation_selector!());
|
||||
pub const DEFAULT_SELECTED_FLAG: &str = concat!("☑️", emoji_text_presentation_selector!());
|
||||
pub const DEFAULT_UNSEEN_FLAG: &str = concat!("●", emoji_text_presentation_selector!());
|
||||
pub const DEFAULT_SNOOZED_FLAG: &str = concat!("💤", emoji_text_presentation_selector!());
|
||||
pub const DEFAULT_HIGHLIGHT_SELF_FLAG: &str = concat!("✸", emoji_text_presentation_selector!());
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RowsState<T> {
|
||||
|
@ -281,6 +268,7 @@ pub struct ColorCache {
|
|||
pub even_highlighted_selected: ThemeAttribute,
|
||||
pub odd_highlighted_selected: ThemeAttribute,
|
||||
pub tag_default: ThemeAttribute,
|
||||
pub highlight_self: ThemeAttribute,
|
||||
|
||||
// Conversations
|
||||
pub subject: ThemeAttribute,
|
||||
|
@ -290,6 +278,12 @@ pub struct ColorCache {
|
|||
|
||||
impl ColorCache {
|
||||
pub fn new(context: &Context, style: IndexStyle) -> Self {
|
||||
let default = Self {
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
highlight_self: crate::conf::value(context, "mail.listing.highlight_self"),
|
||||
..Self::default()
|
||||
};
|
||||
let mut ret = match style {
|
||||
IndexStyle::Plain => Self {
|
||||
even: crate::conf::value(context, "mail.listing.plain.even"),
|
||||
|
@ -311,9 +305,7 @@ impl ColorCache {
|
|||
"mail.listing.plain.even_highlighted_selected",
|
||||
),
|
||||
odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"),
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
..Self::default()
|
||||
..default
|
||||
},
|
||||
IndexStyle::Threaded => Self {
|
||||
even_unseen: crate::conf::value(context, "mail.listing.plain.even_unseen"),
|
||||
|
@ -335,9 +327,7 @@ impl ColorCache {
|
|||
),
|
||||
even: crate::conf::value(context, "mail.listing.plain.even"),
|
||||
odd: crate::conf::value(context, "mail.listing.plain.odd"),
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
..Self::default()
|
||||
..default
|
||||
},
|
||||
IndexStyle::Compact => Self {
|
||||
even_unseen: crate::conf::value(context, "mail.listing.compact.even_unseen"),
|
||||
|
@ -362,12 +352,9 @@ impl ColorCache {
|
|||
),
|
||||
even: crate::conf::value(context, "mail.listing.compact.even"),
|
||||
odd: crate::conf::value(context, "mail.listing.compact.odd"),
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
theme_default: crate::conf::value(context, "theme_default"),
|
||||
..Self::default()
|
||||
..default
|
||||
},
|
||||
IndexStyle::Conversations => Self {
|
||||
theme_default: crate::conf::value(context, "mail.listing.conversations"),
|
||||
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
|
||||
from: crate::conf::value(context, "mail.listing.conversations.from"),
|
||||
date: crate::conf::value(context, "mail.listing.conversations.date"),
|
||||
|
@ -378,13 +365,13 @@ impl ColorCache {
|
|||
context,
|
||||
"mail.listing.conversations.highlighted_selected",
|
||||
),
|
||||
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
|
||||
..Self::default()
|
||||
..default
|
||||
},
|
||||
};
|
||||
if !context.settings.terminal.use_color() {
|
||||
ret.highlighted.attrs |= Attr::REVERSE;
|
||||
ret.tag_default.attrs |= Attr::REVERSE;
|
||||
ret.highlight_self.attrs |= Attr::REVERSE;
|
||||
ret.even_highlighted.attrs |= Attr::REVERSE;
|
||||
ret.odd_highlighted.attrs |= Attr::REVERSE;
|
||||
ret.even_highlighted_selected.attrs |= Attr::REVERSE | Attr::DIM;
|
||||
|
@ -401,6 +388,8 @@ pub struct EntryStrings {
|
|||
pub flag: FlagString,
|
||||
pub from: FromString,
|
||||
pub tags: TagString,
|
||||
pub unseen: bool,
|
||||
pub highlight_self: bool,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
@ -469,6 +458,86 @@ column_str!(struct SubjectString(String));
|
|||
column_str!(struct FlagString(String));
|
||||
column_str!(struct TagString(String, SmallVec<[Option<Color>; 8]>));
|
||||
|
||||
impl FlagString {
|
||||
pub(self) fn new(
|
||||
flags: Flag,
|
||||
is_selected: bool,
|
||||
is_snoozed: bool,
|
||||
is_unseen: bool,
|
||||
has_attachments: bool,
|
||||
context: &Context,
|
||||
coordinates: (AccountHash, MailboxHash),
|
||||
) -> Self {
|
||||
Self(format!(
|
||||
"{flag_passed}{flag_replied}{flag_seen}{flag_trashed}{flag_draft}{flag_flagged} \
|
||||
{selected}{snoozed}{unseen}{attachments}{whitespace}",
|
||||
flag_passed = Some("P")
|
||||
.filter(|_| flags.contains(Flag::PASSED))
|
||||
.unwrap_or_default(),
|
||||
flag_replied = Some("R")
|
||||
.filter(|_| flags.contains(Flag::REPLIED))
|
||||
.unwrap_or_default(),
|
||||
flag_seen = Some("S")
|
||||
.filter(|_| flags.contains(Flag::SEEN))
|
||||
.unwrap_or_default(),
|
||||
flag_trashed = Some("T")
|
||||
.filter(|_| flags.contains(Flag::TRASHED))
|
||||
.unwrap_or_default(),
|
||||
flag_draft = Some("D")
|
||||
.filter(|_| flags.contains(Flag::DRAFT))
|
||||
.unwrap_or_default(),
|
||||
flag_flagged = Some("F")
|
||||
.filter(|_| flags.contains(Flag::FLAGGED))
|
||||
.unwrap_or_default(),
|
||||
selected = if is_selected {
|
||||
mailbox_settings!(context[coordinates.0][&coordinates.1].listing.selected_flag)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SELECTED_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
snoozed = if is_snoozed {
|
||||
mailbox_settings!(
|
||||
context[coordinates.0][&coordinates.1]
|
||||
.listing
|
||||
.thread_snoozed_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SNOOZED_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
unseen = if is_unseen {
|
||||
mailbox_settings!(context[coordinates.0][&coordinates.1].listing.unseen_flag)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_UNSEEN_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
attachments = if has_attachments {
|
||||
mailbox_settings!(
|
||||
context[coordinates.0][&coordinates.1]
|
||||
.listing
|
||||
.attachment_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_ATTACHMENT_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
whitespace = if is_selected || is_unseen || is_snoozed || has_attachments {
|
||||
" "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct MailboxMenuEntry {
|
||||
depth: usize,
|
||||
|
@ -488,6 +557,18 @@ struct AccountMenuEntry {
|
|||
entries: SmallVec<[MailboxMenuEntry; 16]>,
|
||||
}
|
||||
|
||||
impl AccountMenuEntry {
|
||||
fn entry_by_hash(&self, needle: MailboxHash) -> Option<usize> {
|
||||
self.entries.iter().enumerate().find_map(|(i, e)| {
|
||||
if e.mailbox_hash == needle {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MailListingTrait: ListingTrait {
|
||||
fn as_component(&self) -> &dyn Component
|
||||
where
|
||||
|
@ -524,9 +605,9 @@ pub trait MailListingTrait: ListingTrait {
|
|||
};
|
||||
let account = &mut context.accounts[&account_hash];
|
||||
match a {
|
||||
ListingAction::SetSeen => {
|
||||
ListingAction::Flag(FlagAction::Set(Flag::SEEN)) | ListingAction::SetSeen => {
|
||||
if let Err(err) = account.set_flags(
|
||||
env_hashes.clone(),
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::Set(Flag::SEEN)],
|
||||
) {
|
||||
|
@ -535,9 +616,9 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
}
|
||||
ListingAction::SetUnseen => {
|
||||
ListingAction::Flag(FlagAction::Unset(Flag::SEEN)) | ListingAction::SetUnseen => {
|
||||
if let Err(err) = account.set_flags(
|
||||
env_hashes.clone(),
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::UnSet(Flag::SEEN)],
|
||||
) {
|
||||
|
@ -546,9 +627,31 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
}
|
||||
ListingAction::Tag(Add(ref tag_str)) => {
|
||||
ListingAction::Flag(FlagAction::Set(flag)) => {
|
||||
if let Err(err) = account.set_flags(
|
||||
env_hashes.clone(),
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::Set(*flag)],
|
||||
) {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
}
|
||||
}
|
||||
ListingAction::Flag(FlagAction::Unset(flag)) => {
|
||||
if let Err(err) = account.set_flags(
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::UnSet(*flag)],
|
||||
) {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
}
|
||||
}
|
||||
ListingAction::Tag(TagAction::Add(ref tag_str)) => {
|
||||
if let Err(err) = account.set_flags(
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::SetTag(tag_str.into())],
|
||||
) {
|
||||
|
@ -557,9 +660,9 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
}
|
||||
ListingAction::Tag(Remove(ref tag_str)) => {
|
||||
ListingAction::Tag(TagAction::Remove(ref tag_str)) => {
|
||||
if let Err(err) = account.set_flags(
|
||||
env_hashes.clone(),
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::UnSetTag(tag_str.into())],
|
||||
) {
|
||||
|
@ -676,6 +779,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
if path.is_relative() {
|
||||
path = context.current_dir().join(&path);
|
||||
}
|
||||
path = path.expand();
|
||||
let account = &mut context.accounts[&account_hash];
|
||||
let format = (*format).unwrap_or_default();
|
||||
let collection = account.collection.clone();
|
||||
|
@ -719,7 +823,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
format.append(
|
||||
&mut file,
|
||||
bytes.as_slice(),
|
||||
env.from().get(0),
|
||||
env.from().first(),
|
||||
Some(env.date()),
|
||||
(env.flags(), tags),
|
||||
MboxMetadata::CClient,
|
||||
|
@ -736,7 +840,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
format.append(
|
||||
&mut file,
|
||||
bytes.as_slice(),
|
||||
env.from().get(0),
|
||||
env.from().first(),
|
||||
Some(env.date()),
|
||||
(env.flags(), tags),
|
||||
MboxMetadata::CClient,
|
||||
|
@ -775,7 +879,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
kind: Some(NotificationType::Error(err.kind)),
|
||||
},
|
||||
Ok(Some(Ok(path))) => UIEvent::Notification {
|
||||
title: Some("Succesfully exported mbox".into()),
|
||||
title: Some("Successfully exported mbox".into()),
|
||||
source: None,
|
||||
body: format!("Wrote to file {}", path.display()).into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
|
@ -972,11 +1076,11 @@ enum MenuEntryCursor {
|
|||
}
|
||||
|
||||
impl std::ops::Sub<MenuEntryCursor> for isize {
|
||||
type Output = isize;
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: MenuEntryCursor) -> isize {
|
||||
fn sub(self, other: MenuEntryCursor) -> Self {
|
||||
if let MenuEntryCursor::Mailbox(v) = other {
|
||||
v as isize - self
|
||||
v as Self - self
|
||||
} else {
|
||||
self - 1
|
||||
}
|
||||
|
@ -1015,7 +1119,7 @@ pub struct Listing {
|
|||
prev_ratio: usize,
|
||||
menu_width: WidgetWidth,
|
||||
focus: ListingFocus,
|
||||
view: Box<ThreadView>,
|
||||
view: Option<Box<ThreadView>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Listing {
|
||||
|
@ -1093,8 +1197,9 @@ impl Component for Listing {
|
|||
} else {
|
||||
self.component.draw(grid, area, context);
|
||||
if self.component.unfocused() {
|
||||
self.view
|
||||
.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if right_component_width == 0 {
|
||||
|
@ -1117,8 +1222,9 @@ impl Component for Listing {
|
|||
let area = area.skip_cols(mid + 1);
|
||||
self.component.draw(grid, area, context);
|
||||
if self.component.unfocused() {
|
||||
self.view
|
||||
.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1320,17 +1426,21 @@ impl Component for Listing {
|
|||
match content.downcast_ref::<ListingMessage>().copied() {
|
||||
None => {}
|
||||
Some(ListingMessage::FocusUpdate { new_value }) => {
|
||||
self.view.process_event(
|
||||
&mut UIEvent::VisibilityChange(!matches!(new_value, Focus::None)),
|
||||
context,
|
||||
);
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.process_event(
|
||||
&mut UIEvent::VisibilityChange(!matches!(new_value, Focus::None)),
|
||||
context,
|
||||
);
|
||||
}
|
||||
if matches!(new_value, Focus::Entry) {
|
||||
// Need to clear gap between sidebar and listing component, if any.
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
Some(ListingMessage::UpdateView) => {
|
||||
self.view.set_dirty(true);
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.set_dirty(true);
|
||||
}
|
||||
}
|
||||
Some(ListingMessage::OpenEntryUnderCursor {
|
||||
env_hash,
|
||||
|
@ -1339,8 +1449,10 @@ impl Component for Listing {
|
|||
go_to_first_unread,
|
||||
}) => {
|
||||
let (a, m) = self.component.coordinates();
|
||||
self.view.unrealize(context);
|
||||
self.view = Box::new(ThreadView::new(
|
||||
if let Some(view) = self.view.take() {
|
||||
view.unrealize(context);
|
||||
}
|
||||
self.view = Some(Box::new(ThreadView::new(
|
||||
(a, m, env_hash),
|
||||
thread_hash,
|
||||
Some(env_hash),
|
||||
|
@ -1351,7 +1463,7 @@ impl Component for Listing {
|
|||
Some(ThreadViewFocus::MailView)
|
||||
},
|
||||
context,
|
||||
));
|
||||
)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -1378,7 +1490,13 @@ impl Component for Listing {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
if self.component.unfocused() && self.view.process_event(event, context) {
|
||||
if self.component.unfocused()
|
||||
&& self
|
||||
.view
|
||||
.as_mut()
|
||||
.map(|v| v.process_event(event, context))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1390,7 +1508,12 @@ impl Component for Listing {
|
|||
}
|
||||
}
|
||||
if (self.focus == ListingFocus::Mailbox && self.status.is_none())
|
||||
&& ((self.component.unfocused() && self.view.process_event(event, context))
|
||||
&& ((self.component.unfocused()
|
||||
&& self
|
||||
.view
|
||||
.as_mut()
|
||||
.map(|v| v.process_event(event, context))
|
||||
.unwrap_or(false))
|
||||
|| self.component.process_event(event, context))
|
||||
{
|
||||
return true;
|
||||
|
@ -1540,7 +1663,15 @@ impl Component for Listing {
|
|||
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"]) => {
|
||||
if self.cursor_pos.account + amount < self.accounts.len() {
|
||||
self.cursor_pos.account += amount;
|
||||
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.cursor_pos.account;
|
||||
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -1548,7 +1679,15 @@ impl Component for Listing {
|
|||
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"]) => {
|
||||
if self.cursor_pos.account >= amount {
|
||||
self.cursor_pos.account -= amount;
|
||||
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.cursor_pos.account;
|
||||
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -1606,12 +1745,13 @@ impl Component for Listing {
|
|||
return true;
|
||||
}
|
||||
Action::Listing(ListingAction::Import(file_path, mailbox_path)) => {
|
||||
let file_path = file_path.expand();
|
||||
let account = &mut context.accounts[self.cursor_pos.account];
|
||||
if let Err(err) = account
|
||||
.mailbox_by_path(mailbox_path)
|
||||
.and_then(|mailbox_hash| {
|
||||
Ok((
|
||||
std::fs::read(file_path).chain_err_summary(|| {
|
||||
std::fs::read(&file_path).chain_err_summary(|| {
|
||||
format!("Could not read {}", file_path.display())
|
||||
})?,
|
||||
mailbox_hash,
|
||||
|
@ -1635,17 +1775,38 @@ impl Component for Listing {
|
|||
| Action::Listing(a @ ListingAction::CopyToOtherAccount(_, _))
|
||||
| Action::Listing(a @ ListingAction::MoveToOtherAccount(_, _))
|
||||
| Action::Listing(a @ ListingAction::ExportMbox(_, _))
|
||||
| Action::Listing(a @ ListingAction::Flag(_))
|
||||
| Action::Listing(a @ ListingAction::Tag(_)) => {
|
||||
let focused = self.component.get_focused_items(context);
|
||||
self.component.perform_action(context, focused, a);
|
||||
let should_be_unselected: bool = matches!(
|
||||
a,
|
||||
ListingAction::Delete
|
||||
| ListingAction::MoveTo(_)
|
||||
| ListingAction::MoveToOtherAccount(_, _)
|
||||
);
|
||||
let mut row_updates: SmallVec<[EnvelopeHash; 8]> = SmallVec::new();
|
||||
for (k, v) in self.component.selection().iter_mut() {
|
||||
if *v {
|
||||
*v = false;
|
||||
*v = !should_be_unselected;
|
||||
row_updates.push(*k);
|
||||
}
|
||||
}
|
||||
self.component.row_updates().extend(row_updates);
|
||||
return true;
|
||||
}
|
||||
Action::Listing(ListingAction::ClearSelection) => {
|
||||
// Clear selection.
|
||||
let row_updates: SmallVec<[EnvelopeHash; 8]> =
|
||||
self.component.get_focused_items(context);
|
||||
for h in &row_updates {
|
||||
if let Some(val) = self.component.selection().get_mut(h) {
|
||||
*val = false;
|
||||
}
|
||||
}
|
||||
self.component.row_updates().extend(row_updates);
|
||||
self.component.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
@ -1844,6 +2005,7 @@ impl Component for Listing {
|
|||
&& self.component.modifier_command().is_some() =>
|
||||
{
|
||||
self.component.set_modifier_command(Some(Modifier::Union));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.component.unfocused()
|
||||
|
@ -1852,6 +2014,7 @@ impl Component for Listing {
|
|||
{
|
||||
self.component
|
||||
.set_modifier_command(Some(Modifier::Difference));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.component.unfocused()
|
||||
|
@ -1862,12 +2025,14 @@ impl Component for Listing {
|
|||
{
|
||||
self.component
|
||||
.set_modifier_command(Some(Modifier::Intersection));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.component.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["next_entry"]) =>
|
||||
{
|
||||
self.component.next_entry(context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.component.unfocused()
|
||||
|
@ -1876,6 +2041,42 @@ impl Component for Listing {
|
|||
) =>
|
||||
{
|
||||
self.component.prev_entry(context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
if !self.component.unfocused() =>
|
||||
{
|
||||
// Clear selection.
|
||||
let row_updates: SmallVec<[EnvelopeHash; 8]> =
|
||||
self.component.get_focused_items(context);
|
||||
for h in &row_updates {
|
||||
if let Some(val) = self.component.selection().get_mut(h) {
|
||||
*val = false;
|
||||
}
|
||||
}
|
||||
self.component.row_updates().extend(row_updates);
|
||||
self.component.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
if !self.cmd_buf.is_empty() =>
|
||||
{
|
||||
self.cmd_buf.clear();
|
||||
self.component.set_modifier_active(false);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => {
|
||||
self.cmd_buf.push(*c);
|
||||
self.component.set_modifier_active(true);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
|
||||
self.cmd_buf.clone(),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -1900,7 +2101,7 @@ impl Component for Listing {
|
|||
&& self.menu_cursor_pos.menu == MenuEntryCursor::Status =>
|
||||
{
|
||||
self.cursor_pos = self.menu_cursor_pos;
|
||||
self.open_status(self.menu_cursor_pos.account, context);
|
||||
self.change_account(context);
|
||||
self.set_dirty(true);
|
||||
self.focus = ListingFocus::Mailbox;
|
||||
self.ratio = self.prev_ratio;
|
||||
|
@ -1987,9 +2188,17 @@ impl Component for Listing {
|
|||
} => {
|
||||
if *account > 0 {
|
||||
*account -= 1;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(
|
||||
self.accounts[*account].entries.len().saturating_sub(1),
|
||||
);
|
||||
self.menu_cursor_pos.menu =
|
||||
if self.accounts[*account].entries.is_empty() {
|
||||
MenuEntryCursor::Status
|
||||
} else {
|
||||
MenuEntryCursor::Mailbox(
|
||||
self.accounts[*account]
|
||||
.entries
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
)
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -2022,7 +2231,12 @@ impl Component for Listing {
|
|||
} if !self.accounts[*account].entries.is_empty()
|
||||
&& *menu == MenuEntryCursor::Status =>
|
||||
{
|
||||
*menu = MenuEntryCursor::Mailbox(0);
|
||||
if let Some(idx) = context.accounts[*account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[*account].entry_by_hash(h))
|
||||
{
|
||||
*menu = MenuEntryCursor::Mailbox(idx);
|
||||
}
|
||||
}
|
||||
// If current account has no mailboxes, go to next account
|
||||
CursorPos {
|
||||
|
@ -2161,15 +2375,32 @@ impl Component for Listing {
|
|||
{
|
||||
if self.menu_cursor_pos.account + amount >= self.accounts.len() {
|
||||
// Go to last mailbox.
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(
|
||||
self.accounts[self.menu_cursor_pos.account]
|
||||
.entries
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
);
|
||||
self.menu_cursor_pos.menu = if self.accounts
|
||||
[self.menu_cursor_pos.account]
|
||||
.entries
|
||||
.is_empty()
|
||||
{
|
||||
MenuEntryCursor::Status
|
||||
} else {
|
||||
MenuEntryCursor::Mailbox(
|
||||
self.accounts[self.menu_cursor_pos.account]
|
||||
.entries
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
)
|
||||
};
|
||||
} else if self.menu_cursor_pos.account + amount < self.accounts.len() {
|
||||
self.menu_cursor_pos.account += amount;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.menu_cursor_pos.account;
|
||||
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
|
||||
[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -2179,7 +2410,16 @@ impl Component for Listing {
|
|||
{
|
||||
if self.menu_cursor_pos.account >= amount {
|
||||
self.menu_cursor_pos.account -= amount;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.menu_cursor_pos.account;
|
||||
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
|
||||
[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -2205,12 +2445,24 @@ impl Component for Listing {
|
|||
menu: MenuEntryCursor::Mailbox(0)
|
||||
}
|
||||
) {
|
||||
// Can't go anywhere upwards, we're on top already.
|
||||
return true;
|
||||
}
|
||||
if self.menu_cursor_pos.menu == MenuEntryCursor::Mailbox(0) {
|
||||
self.menu_cursor_pos.account = 0;
|
||||
} else {
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
match (
|
||||
self.menu_cursor_pos.menu,
|
||||
context.accounts[self.menu_cursor_pos.account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| {
|
||||
self.accounts[self.menu_cursor_pos.account].entry_by_hash(h)
|
||||
}),
|
||||
) {
|
||||
(MenuEntryCursor::Mailbox(0), _) => {
|
||||
self.menu_cursor_pos.account = 0;
|
||||
}
|
||||
(MenuEntryCursor::Mailbox(_), Some(v)) => {
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(v);
|
||||
}
|
||||
_ => return true,
|
||||
}
|
||||
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
|
||||
self.menu_scrollbar_show_timer.rearm();
|
||||
|
@ -2248,11 +2500,20 @@ impl Component for Listing {
|
|||
self.accounts[*account].entries.len().saturating_sub(1)
|
||||
) {
|
||||
*account = self.accounts.len().saturating_sub(1);
|
||||
*menu = MenuEntryCursor::Mailbox(0);
|
||||
} else {
|
||||
*menu = if let Some(idx) = context.accounts[*account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[*account].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else if !self.accounts[*account].entries.is_empty() {
|
||||
*menu = MenuEntryCursor::Mailbox(
|
||||
self.accounts[*account].entries.len().saturating_sub(1),
|
||||
);
|
||||
} else {
|
||||
*menu = MenuEntryCursor::Status;
|
||||
}
|
||||
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
|
||||
self.menu_scrollbar_show_timer.rearm();
|
||||
|
@ -2339,6 +2600,25 @@ impl Component for Listing {
|
|||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.listing
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
@ -2352,7 +2632,7 @@ impl Component for Listing {
|
|||
.map(Component::is_dirty)
|
||||
.unwrap_or_else(|| self.component.is_dirty())
|
||||
|| if self.component.unfocused() {
|
||||
self.view.is_dirty()
|
||||
self.view.as_ref().map(|v| v.is_dirty()).unwrap_or(false)
|
||||
} else {
|
||||
self.component.is_dirty()
|
||||
}
|
||||
|
@ -2365,7 +2645,9 @@ impl Component for Listing {
|
|||
} else {
|
||||
self.component.set_dirty(value);
|
||||
if self.component.unfocused() {
|
||||
self.view.set_dirty(value);
|
||||
if let Some(ref mut view) = self.view {
|
||||
view.set_dirty(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2373,7 +2655,9 @@ impl Component for Listing {
|
|||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = ShortcutMaps::default();
|
||||
if self.focus != ListingFocus::Menu && self.component.unfocused() {
|
||||
map.extend_shortcuts(self.view.shortcuts(context));
|
||||
if let Some(ref view) = self.view {
|
||||
map.extend_shortcuts(view.shortcuts(context));
|
||||
}
|
||||
}
|
||||
map.extend_shortcuts(if let Some(s) = self.status.as_ref() {
|
||||
s.shortcuts(context)
|
||||
|
@ -2497,12 +2781,12 @@ impl Listing {
|
|||
})
|
||||
.collect();
|
||||
let first_account_hash = account_entries[0].hash;
|
||||
let mut ret = Listing {
|
||||
let mut ret = Self {
|
||||
component: Offline(OfflineListing::new((
|
||||
first_account_hash,
|
||||
MailboxHash::default(),
|
||||
))),
|
||||
view: Box::<ThreadView>::default(),
|
||||
view: None,
|
||||
accounts: account_entries,
|
||||
status: None,
|
||||
dirty: true,
|
||||
|
@ -2553,14 +2837,14 @@ impl Listing {
|
|||
.iter()
|
||||
.map(|entry| entry.entries.len() + 1)
|
||||
.sum::<usize>();
|
||||
let min_width: usize = 2 * area.width();
|
||||
let min_width: usize = area.width();
|
||||
let (width, height) = self.menu.grid().size();
|
||||
let cursor = match self.focus {
|
||||
ListingFocus::Mailbox => self.cursor_pos,
|
||||
ListingFocus::Menu => self.menu_cursor_pos,
|
||||
};
|
||||
if min_width > width || height < total_height || self.dirty {
|
||||
let _ = self.menu.resize(min_width * 2, total_height);
|
||||
let _ = self.menu.resize(min_width, total_height);
|
||||
let mut y = 0;
|
||||
for a in 0..self.accounts.len() {
|
||||
let menu_area = self.menu.area().skip_rows(y);
|
||||
|
@ -2635,7 +2919,7 @@ impl Listing {
|
|||
}
|
||||
|
||||
/// Print a single account in the menu area.
|
||||
fn print_account(&mut self, mut area: Area, aidx: usize, context: &mut Context) -> usize {
|
||||
fn print_account(&mut self, mut area: Area, aidx: usize, context: &Context) -> usize {
|
||||
let account_y = self.menu.area().height() - area.height();
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Line {
|
||||
|
@ -3072,9 +3356,24 @@ impl Listing {
|
|||
self.status(context),
|
||||
)));
|
||||
}
|
||||
MenuEntryCursor::Status => {
|
||||
MenuEntryCursor::Status if context.is_online(account_hash).is_ok() => {
|
||||
self.open_status(self.cursor_pos.account, context);
|
||||
}
|
||||
MenuEntryCursor::Status => {
|
||||
self.component.unrealize(context);
|
||||
self.component =
|
||||
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
|
||||
self.component.realize(self.id().into(), context);
|
||||
self.component
|
||||
.process_event(&mut UIEvent::VisibilityChange(true), context);
|
||||
self.status = None;
|
||||
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.sidebar_divider = *account_settings!(context[account_hash].listing.sidebar_divider);
|
||||
self.set_dirty(true);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -28,7 +28,7 @@ use super::*;
|
|||
use crate::{components::PageMovement, jobs::JoinHandle};
|
||||
|
||||
macro_rules! row_attr {
|
||||
($field:ident, $color_cache:expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{
|
||||
($field:ident, $color_cache:expr, unseen: $unseen:expr, highlighted: $highlighted:expr, selected: $selected:expr $(,)*) => {{
|
||||
let color_cache = &$color_cache;
|
||||
let unseen = $unseen;
|
||||
let highlighted = $highlighted;
|
||||
|
@ -65,7 +65,7 @@ macro_rules! row_attr {
|
|||
},
|
||||
}
|
||||
}};
|
||||
($color_cache:expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{
|
||||
($color_cache:expr, unseen: $unseen:expr, highlighted: $highlighted:expr, selected: $selected:expr $(,)*) => {{
|
||||
let color_cache = &$color_cache;
|
||||
let unseen = $unseen;
|
||||
let highlighted = $highlighted;
|
||||
|
@ -227,23 +227,20 @@ impl MailListingTrait for ConversationsListing {
|
|||
if !force && old_cursor_pos == self.new_cursor_pos && old_mailbox_hash == self.cursor_pos.1
|
||||
{
|
||||
self.kick_parent(self.parent, ListingMessage::UpdateView, context);
|
||||
} else if self.unfocused() {
|
||||
if let Some((thread_hash, env_hash)) = self
|
||||
} else if self.unfocused()
|
||||
&& self
|
||||
.get_thread_under_cursor(self.cursor_pos.2)
|
||||
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])))
|
||||
{
|
||||
self.kick_parent(
|
||||
self.parent,
|
||||
ListingMessage::OpenEntryUnderCursor {
|
||||
thread_hash,
|
||||
env_hash,
|
||||
show_thread: true,
|
||||
go_to_first_unread: true,
|
||||
},
|
||||
context,
|
||||
);
|
||||
self.set_focus(Focus::Entry, context);
|
||||
}
|
||||
.and_then(|thread| {
|
||||
self.rows
|
||||
.thread_to_env
|
||||
.get(&thread)
|
||||
.and_then(|e| Some((thread, e.first()?)))
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.set_focus(Focus::Entry, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,38 +409,32 @@ impl ListingTrait for ConversationsListing {
|
|||
|
||||
fn next_entry(&mut self, context: &mut Context) {
|
||||
if self
|
||||
.get_thread_under_cursor(self.cursor_pos.2 + 1)
|
||||
.get_thread_under_cursor(self.new_cursor_pos.2 + 1)
|
||||
.is_some()
|
||||
{
|
||||
// [ref:TODO]: makes this less ugly.
|
||||
self.movement = Some(PageMovement::Down(1));
|
||||
self.perform_movement(None);
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.cursor_pos.2 += 1;
|
||||
self.new_cursor_pos.2 += 1;
|
||||
self.set_focus(Focus::Entry, context);
|
||||
self.cursor_pos.2 -= 1;
|
||||
self.new_cursor_pos.2 -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_entry(&mut self, context: &mut Context) {
|
||||
if self.cursor_pos.2 == 0 {
|
||||
if self.new_cursor_pos.2 == 0 {
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.get_thread_under_cursor(self.cursor_pos.2 - 1)
|
||||
.get_thread_under_cursor(self.new_cursor_pos.2 - 1)
|
||||
.is_some()
|
||||
{
|
||||
// [ref:TODO]: makes this less ugly.
|
||||
self.movement = Some(PageMovement::Up(1));
|
||||
self.perform_movement(None);
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.cursor_pos.2 -= 1;
|
||||
self.new_cursor_pos.2 -= 1;
|
||||
self.set_focus(Focus::Entry, context);
|
||||
self.cursor_pos.2 += 1;
|
||||
self.new_cursor_pos.2 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,42 +465,12 @@ impl ListingTrait for ConversationsListing {
|
|||
return;
|
||||
}
|
||||
let rows = area.height() / 3;
|
||||
|
||||
if rows == 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
match mvm {
|
||||
PageMovement::Up(amount) => {
|
||||
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(amount);
|
||||
}
|
||||
PageMovement::PageUp(multiplier) => {
|
||||
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(rows * multiplier);
|
||||
}
|
||||
PageMovement::Down(amount) => {
|
||||
if self.new_cursor_pos.2 + amount + 1 < self.length {
|
||||
self.new_cursor_pos.2 += amount;
|
||||
} else {
|
||||
self.new_cursor_pos.2 = self.length.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
PageMovement::PageDown(multiplier) => {
|
||||
if self.new_cursor_pos.2 + rows * multiplier + 1 < self.length {
|
||||
self.new_cursor_pos.2 += rows * multiplier;
|
||||
} else if self.new_cursor_pos.2 + rows * multiplier > self.length {
|
||||
self.new_cursor_pos.2 = self.length.saturating_sub(1);
|
||||
} else {
|
||||
self.new_cursor_pos.2 = (self.length.saturating_sub(1) / rows) * rows;
|
||||
}
|
||||
}
|
||||
PageMovement::Right(_) | PageMovement::Left(_) => {}
|
||||
PageMovement::Home => {
|
||||
self.new_cursor_pos.2 = 0;
|
||||
}
|
||||
PageMovement::End => {
|
||||
self.new_cursor_pos.2 = self.length.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.perform_movement(Some(rows));
|
||||
|
||||
let prev_page_no = (self.cursor_pos.2).wrapping_div(rows);
|
||||
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
|
||||
|
@ -551,6 +512,7 @@ impl ListingTrait for ConversationsListing {
|
|||
context,
|
||||
);
|
||||
|
||||
self.force_draw = false;
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
|
@ -643,8 +605,13 @@ impl ListingTrait for ConversationsListing {
|
|||
}
|
||||
Focus::Entry => {
|
||||
if let Some((thread_hash, env_hash)) = self
|
||||
.get_thread_under_cursor(self.cursor_pos.2)
|
||||
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])))
|
||||
.get_thread_under_cursor(self.new_cursor_pos.2)
|
||||
.and_then(|thread| {
|
||||
self.rows
|
||||
.thread_to_env
|
||||
.get(&thread)
|
||||
.and_then(|e| Some((thread, *e.first()?)))
|
||||
})
|
||||
{
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
|
@ -658,6 +625,7 @@ impl ListingTrait for ConversationsListing {
|
|||
},
|
||||
context,
|
||||
);
|
||||
self.cursor_pos.2 = self.new_cursor_pos.2;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
@ -713,7 +681,7 @@ impl ConversationsListing {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn make_entry_string(
|
||||
fn make_entry_string(
|
||||
&self,
|
||||
root_envelope: &Envelope,
|
||||
context: &Context,
|
||||
|
@ -788,13 +756,23 @@ impl ConversationsListing {
|
|||
} else {
|
||||
subject
|
||||
}),
|
||||
flag: FlagString(format!(
|
||||
"{}{}",
|
||||
if thread.has_attachments() { "📎" } else { "" },
|
||||
if thread.snoozed() { "💤" } else { "" }
|
||||
)),
|
||||
from: FromString(Address::display_name_slice(from).to_string()),
|
||||
flag: FlagString::new(
|
||||
root_envelope.flags(),
|
||||
self.rows
|
||||
.selection
|
||||
.get(&root_envelope.hash())
|
||||
.cloned()
|
||||
.unwrap_or(false),
|
||||
thread.snoozed(),
|
||||
thread.unseen() > 0,
|
||||
thread.has_attachments(),
|
||||
context,
|
||||
(self.cursor_pos.0, self.cursor_pos.1),
|
||||
),
|
||||
from: FromString(Address::display_name_slice(from)),
|
||||
tags: TagString(tags_string, colors),
|
||||
unseen: thread.unseen() > 0,
|
||||
highlight_self: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -886,9 +864,9 @@ impl ConversationsListing {
|
|||
|
||||
let row_attr = row_attr!(
|
||||
self.color_cache,
|
||||
thread.unseen() > 0,
|
||||
self.cursor_pos.2 == idx,
|
||||
self.rows.is_thread_selected(*thread_hash)
|
||||
unseen: thread.unseen() > 0,
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.rows.is_thread_selected(*thread_hash)
|
||||
);
|
||||
// draw flags
|
||||
let (mut x, _) = grid.write_string(
|
||||
|
@ -900,17 +878,17 @@ impl ConversationsListing {
|
|||
None,
|
||||
);
|
||||
if !strings.flag.is_empty() {
|
||||
for c in grid.row_iter(area, x..(x + 3), 0) {
|
||||
for c in grid.row_iter(area, x..(x + 1), 0) {
|
||||
grid[c].set_bg(row_attr.bg);
|
||||
}
|
||||
x += 3;
|
||||
x += 1;
|
||||
}
|
||||
let subject_attr = row_attr!(
|
||||
subject,
|
||||
self.color_cache,
|
||||
thread.unseen() > 0,
|
||||
self.cursor_pos.2 == idx,
|
||||
self.rows.is_thread_selected(*thread_hash)
|
||||
unseen: thread.unseen() > 0,
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.rows.is_thread_selected(*thread_hash)
|
||||
);
|
||||
// draw subject
|
||||
let (x_, subject_overflowed) = grid.write_string(
|
||||
|
@ -959,9 +937,9 @@ impl ConversationsListing {
|
|||
let date_attr = row_attr!(
|
||||
date,
|
||||
self.color_cache,
|
||||
thread.unseen() > 0,
|
||||
self.cursor_pos.2 == idx,
|
||||
self.rows.is_thread_selected(*thread_hash)
|
||||
unseen: thread.unseen() > 0,
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.rows.is_thread_selected(*thread_hash)
|
||||
);
|
||||
x = 0;
|
||||
x += grid
|
||||
|
@ -981,9 +959,9 @@ impl ConversationsListing {
|
|||
let from_attr = row_attr!(
|
||||
from,
|
||||
self.color_cache,
|
||||
thread.unseen() > 0,
|
||||
self.cursor_pos.2 == idx,
|
||||
self.rows.is_thread_selected(*thread_hash)
|
||||
unseen: thread.unseen() > 0,
|
||||
highlighted: self.cursor_pos.2 == idx,
|
||||
selected: self.rows.is_thread_selected(*thread_hash)
|
||||
);
|
||||
// draw from
|
||||
x += grid
|
||||
|
@ -1002,6 +980,43 @@ impl ConversationsListing {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_movement(&mut self, height: Option<usize>) {
|
||||
let rows = height.unwrap_or(1);
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
match mvm {
|
||||
PageMovement::Up(amount) => {
|
||||
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(amount);
|
||||
}
|
||||
PageMovement::PageUp(multiplier) => {
|
||||
self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(rows * multiplier);
|
||||
}
|
||||
PageMovement::Down(amount) => {
|
||||
if self.new_cursor_pos.2 + amount + 1 < self.length {
|
||||
self.new_cursor_pos.2 += amount;
|
||||
} else {
|
||||
self.new_cursor_pos.2 = self.length.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
PageMovement::PageDown(multiplier) => {
|
||||
if self.new_cursor_pos.2 + rows * multiplier + 1 < self.length {
|
||||
self.new_cursor_pos.2 += rows * multiplier;
|
||||
} else if self.new_cursor_pos.2 + rows * multiplier > self.length {
|
||||
self.new_cursor_pos.2 = self.length.saturating_sub(1);
|
||||
} else {
|
||||
self.new_cursor_pos.2 = (self.length.saturating_sub(1) / rows) * rows;
|
||||
}
|
||||
}
|
||||
PageMovement::Right(_) | PageMovement::Left(_) => {}
|
||||
PageMovement::Home => {
|
||||
self.new_cursor_pos.2 = 0;
|
||||
}
|
||||
PageMovement::End => {
|
||||
self.new_cursor_pos.2 = self.length.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ConversationsListing {
|
||||
|
@ -1045,7 +1060,9 @@ impl Component for ConversationsListing {
|
|||
if let Some(mvm) = self.movement.as_ref() {
|
||||
match mvm {
|
||||
PageMovement::Up(amount) => {
|
||||
for c in self.cursor_pos.2.saturating_sub(*amount)..=self.cursor_pos.2 {
|
||||
for c in self.new_cursor_pos.2.saturating_sub(*amount)
|
||||
..=self.new_cursor_pos.2
|
||||
{
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows.update_selection_with_thread(
|
||||
thread,
|
||||
|
@ -1061,8 +1078,8 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
if modifier == Modifier::Intersection {
|
||||
for c in (0..self.cursor_pos.2.saturating_sub(*amount))
|
||||
.chain((self.cursor_pos.2 + 2)..self.length)
|
||||
for c in (0..self.new_cursor_pos.2.saturating_sub(*amount))
|
||||
.chain((self.new_cursor_pos.2 + 2)..self.length)
|
||||
{
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows
|
||||
|
@ -1072,8 +1089,8 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
PageMovement::PageUp(multiplier) => {
|
||||
for c in self.cursor_pos.2.saturating_sub(rows * multiplier)
|
||||
..=self.cursor_pos.2
|
||||
for c in self.new_cursor_pos.2.saturating_sub(rows * multiplier)
|
||||
..=self.new_cursor_pos.2
|
||||
{
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows.update_selection_with_thread(
|
||||
|
@ -1091,8 +1108,8 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
PageMovement::Down(amount) => {
|
||||
for c in self.cursor_pos.2
|
||||
..std::cmp::min(self.length, self.cursor_pos.2 + amount + 1)
|
||||
for c in self.new_cursor_pos.2
|
||||
..std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
|
||||
{
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows.update_selection_with_thread(
|
||||
|
@ -1109,9 +1126,9 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
if modifier == Modifier::Intersection {
|
||||
for c in (0..self.cursor_pos.2).chain(
|
||||
(std::cmp::min(self.length, self.cursor_pos.2 + amount + 1) + 1)
|
||||
..self.length,
|
||||
for c in (0..self.new_cursor_pos.2).chain(
|
||||
(std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
|
||||
+ 1)..self.length,
|
||||
) {
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows
|
||||
|
@ -1121,9 +1138,9 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
PageMovement::PageDown(multiplier) => {
|
||||
for c in self.cursor_pos.2
|
||||
for c in self.new_cursor_pos.2
|
||||
..std::cmp::min(
|
||||
self.cursor_pos.2 + rows * multiplier + 1,
|
||||
self.new_cursor_pos.2 + rows * multiplier + 1,
|
||||
self.length,
|
||||
)
|
||||
{
|
||||
|
@ -1142,9 +1159,9 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
if modifier == Modifier::Intersection {
|
||||
for c in (0..self.cursor_pos.2).chain(
|
||||
for c in (0..self.new_cursor_pos.2).chain(
|
||||
(std::cmp::min(
|
||||
self.cursor_pos.2 + rows * multiplier + 1,
|
||||
self.new_cursor_pos.2 + rows * multiplier + 1,
|
||||
self.length,
|
||||
) + 1)..self.length,
|
||||
) {
|
||||
|
@ -1157,7 +1174,7 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
PageMovement::Right(_) | PageMovement::Left(_) => {}
|
||||
PageMovement::Home => {
|
||||
for c in 0..=self.cursor_pos.2 {
|
||||
for c in 0..=self.new_cursor_pos.2 {
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows.update_selection_with_thread(
|
||||
thread,
|
||||
|
@ -1173,7 +1190,7 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
if modifier == Modifier::Intersection {
|
||||
for c in (self.cursor_pos.2 + 1)..self.length {
|
||||
for c in (self.new_cursor_pos.2 + 1)..self.length {
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows
|
||||
.update_selection_with_thread(thread, |e| *e = false);
|
||||
|
@ -1182,7 +1199,7 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
PageMovement::End => {
|
||||
for c in self.cursor_pos.2..self.length {
|
||||
for c in self.new_cursor_pos.2..self.length {
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows.update_selection_with_thread(
|
||||
thread,
|
||||
|
@ -1198,7 +1215,7 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
}
|
||||
if modifier == Modifier::Intersection {
|
||||
for c in 0..self.cursor_pos.2 {
|
||||
for c in 0..self.new_cursor_pos.2 {
|
||||
if let Some(thread) = self.get_thread_under_cursor(c) {
|
||||
self.rows
|
||||
.update_selection_with_thread(thread, |e| *e = false);
|
||||
|
@ -1217,7 +1234,7 @@ impl Component for ConversationsListing {
|
|||
self.update_line(context, row);
|
||||
let row: usize = self.rows.env_order[&row];
|
||||
|
||||
let page_no = (self.cursor_pos.2).wrapping_div(rows);
|
||||
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
|
||||
|
||||
let top_idx = page_no * rows;
|
||||
// Update row only if it's currently visible
|
||||
|
@ -1229,11 +1246,21 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
if self.force_draw {
|
||||
// Draw the entire list
|
||||
let area = if matches!(self.focus, Focus::Entry) {
|
||||
area.take_cols(area.width() / 3)
|
||||
} else {
|
||||
area
|
||||
};
|
||||
self.draw_list(grid, area, context);
|
||||
self.force_draw = false;
|
||||
}
|
||||
} else {
|
||||
// Draw the entire list
|
||||
let area = if matches!(self.focus, Focus::Entry) {
|
||||
area.take_cols(area.width() / 3)
|
||||
} else {
|
||||
area
|
||||
};
|
||||
self.draw_list(grid, area, context);
|
||||
}
|
||||
}
|
||||
|
@ -1257,6 +1284,11 @@ impl Component for ConversationsListing {
|
|||
let shortcuts = self.shortcuts(context);
|
||||
|
||||
match (&event, self.focus) {
|
||||
(UIEvent::VisibilityChange(true), _) => {
|
||||
self.force_draw = true;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref k), Focus::Entry)
|
||||
if shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"]) =>
|
||||
{
|
||||
|
@ -1458,19 +1490,6 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
_ => {}
|
||||
},
|
||||
UIEvent::Input(Key::Esc)
|
||||
if !self.unfocused()
|
||||
&& self
|
||||
.rows
|
||||
.selection
|
||||
.values()
|
||||
.cloned()
|
||||
.any(std::convert::identity) =>
|
||||
{
|
||||
self.rows.clear_selection();
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char(''))
|
||||
if !self.unfocused() && !&self.filter_term.is_empty() =>
|
||||
{
|
||||
|
@ -1502,6 +1521,25 @@ impl Component for ConversationsListing {
|
|||
}
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.listing
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -1510,8 +1548,9 @@ impl Component for ConversationsListing {
|
|||
|
||||
fn is_dirty(&self) -> bool {
|
||||
match self.focus {
|
||||
Focus::None => self.dirty,
|
||||
Focus::Entry => self.dirty,
|
||||
Focus::None | Focus::Entry => {
|
||||
self.dirty || self.force_draw || !self.rows.row_updates.is_empty()
|
||||
}
|
||||
Focus::EntryFullscreen => false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ impl std::fmt::Display for OfflineListing {
|
|||
|
||||
impl OfflineListing {
|
||||
pub fn new(cursor_pos: (AccountHash, MailboxHash)) -> Box<Self> {
|
||||
Box::new(OfflineListing {
|
||||
Box::new(Self {
|
||||
cursor_pos,
|
||||
_row_updates: SmallVec::new(),
|
||||
_selection: HashMap::default(),
|
||||
|
@ -220,7 +220,13 @@ impl Component for OfflineListing {
|
|||
if let Some(msg) = msg.clone() {
|
||||
self.messages.push(msg);
|
||||
}
|
||||
self.dirty = true
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::ChangeMode(UIMode::Normal)
|
||||
| UIEvent::Resize
|
||||
| UIEvent::ConfigReload { old_settings: _ }
|
||||
| UIEvent::VisibilityChange(_) => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -141,13 +141,13 @@ pub fn encrypt_filter(
|
|||
Default::default(),
|
||||
ctx.encrypt(sign_keys, encrypt_keys, data)?.await?,
|
||||
);
|
||||
a.content_disposition = ContentDisposition::from(r#"attachment; filename="msg.asc""#.as_bytes());
|
||||
a.content_disposition = ContentDisposition::from(br#"attachment; filename="msg.asc""#);
|
||||
a
|
||||
};
|
||||
let mut a: AttachmentBuilder = AttachmentBuilder::new("Version: 1\n".as_bytes());
|
||||
let mut a: AttachmentBuilder = AttachmentBuilder::new(b"Version: 1\n");
|
||||
|
||||
a.set_content_type_from_bytes("application/pgp-encrypted".as_bytes());
|
||||
a.set_content_disposition(ContentDisposition::from("attachment".as_bytes()));
|
||||
a.set_content_type_from_bytes(b"application/pgp-encrypted");
|
||||
a.set_content_disposition(ContentDisposition::from(b"attachment"));
|
||||
let parts = vec![a, sig_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Ok(Attachment::new(
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use melib::{MailBackendExtensionStatus, SpecialUsageMailbox};
|
||||
|
||||
use super::*;
|
||||
|
@ -41,7 +43,7 @@ impl std::fmt::Display for AccountStatus {
|
|||
}
|
||||
|
||||
impl AccountStatus {
|
||||
pub fn new(account_pos: usize, theme_default: ThemeAttribute) -> AccountStatus {
|
||||
pub fn new(account_pos: usize, theme_default: ThemeAttribute) -> Self {
|
||||
let default_cell = {
|
||||
let mut ret = Cell::with_char(' ');
|
||||
ret.set_fg(theme_default.fg)
|
||||
|
@ -54,7 +56,7 @@ impl AccountStatus {
|
|||
content.grid_mut().set_growable(true);
|
||||
_ = content.resize(80, 20);
|
||||
|
||||
AccountStatus {
|
||||
Self {
|
||||
cursor: (0, 0),
|
||||
account_pos,
|
||||
content,
|
||||
|
@ -171,17 +173,19 @@ impl AccountStatus {
|
|||
a.backend_capabilities.supports_search,
|
||||
) {
|
||||
(SearchBackend::Auto, true) | (SearchBackend::None, true) => {
|
||||
"backend-side search".to_string()
|
||||
Cow::Borrowed("backend-side search")
|
||||
}
|
||||
(SearchBackend::Auto, false) | (SearchBackend::None, false) => {
|
||||
"none (search will be slow)".to_string()
|
||||
Cow::Borrowed("none (search will be slow)")
|
||||
}
|
||||
#[cfg(feature = "sqlite3")]
|
||||
(SearchBackend::Sqlite3, _) => {
|
||||
if let Ok(path) = crate::sqlite3::db_path() {
|
||||
format!("sqlite3 database {}", path.display())
|
||||
} else {
|
||||
"sqlite3 database".to_string()
|
||||
match crate::sqlite3::AccountCache::db_path(&a.name) {
|
||||
Ok(Some(path)) => {
|
||||
Cow::Owned(format!("sqlite3 database: {}", path.display()))
|
||||
}
|
||||
Ok(None) => Cow::Borrowed("sqlite3 database: uninitialized"),
|
||||
Err(err) => Cow::Owned(format!("sqlite3 error: {err}")),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -94,7 +94,7 @@ impl MailView {
|
|||
initialize_now: bool,
|
||||
context: &mut Context,
|
||||
) -> Self {
|
||||
let mut ret = MailView {
|
||||
let mut ret = Self {
|
||||
coordinates,
|
||||
dirty: true,
|
||||
contact_selector: None,
|
||||
|
@ -260,7 +260,7 @@ impl MailView {
|
|||
}
|
||||
let envelope: EnvelopeRef = account.collection.get_env(coordinates.2);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut entries: IndexMap<Card, (Card, String)> = IndexMap::default();
|
||||
for addr in envelope.from().iter().chain(envelope.to().iter()) {
|
||||
let mut new_card: Card = Card::new();
|
||||
new_card
|
||||
|
@ -269,12 +269,12 @@ impl MailView {
|
|||
if let Some(display_name) = addr.get_display_name() {
|
||||
new_card.set_name(display_name);
|
||||
}
|
||||
entries.push((new_card, format!("{}", addr)));
|
||||
entries.insert(new_card.clone(), (new_card, format!("{}", addr)));
|
||||
}
|
||||
drop(envelope);
|
||||
self.contact_selector = Some(Box::new(Selector::new(
|
||||
"select contacts to add",
|
||||
entries,
|
||||
entries.into_iter().map(|(_, v)| v).collect(),
|
||||
false,
|
||||
Some(Box::new(move |id: ComponentId, results: &[Card]| {
|
||||
Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
|
||||
|
@ -356,6 +356,7 @@ impl Component for MailView {
|
|||
|
||||
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let Some(ref mut s) = self.contact_selector {
|
||||
// [ref:FIXME]: contact_selector should not forward navigation events and return true
|
||||
if s.process_event(event, context) {
|
||||
return true;
|
||||
}
|
||||
|
@ -499,7 +500,7 @@ impl Component for MailView {
|
|||
move |id: ComponentId, result: &[Option<PendingReplyAction>]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(result.get(0).cloned().flatten()),
|
||||
Box::new(result.first().cloned().flatten()),
|
||||
))
|
||||
},
|
||||
)),
|
||||
|
@ -665,7 +666,7 @@ impl Component for MailView {
|
|||
/* autosend or open unsubscribe option */
|
||||
let unsubscribe = actions.unsubscribe.as_ref().unwrap();
|
||||
for option in unsubscribe.iter() {
|
||||
/* [ref:TODO]: Ask for confirmation before proceding with an action */
|
||||
/* [ref:TODO]: Ask for confirmation before proceeding with an action */
|
||||
match option {
|
||||
list_management::ListAction::Email(email) => {
|
||||
if let Ok(mailto) = Mailto::try_from(*email) {
|
||||
|
@ -675,7 +676,8 @@ impl Component for MailView {
|
|||
context.accounts[&coordinates.0]
|
||||
.settings
|
||||
.account()
|
||||
.make_display_name(),
|
||||
.make_display_name()
|
||||
.to_string(),
|
||||
);
|
||||
/* Manually drop stuff because borrowck doesn't do it
|
||||
* on its own */
|
||||
|
@ -725,12 +727,16 @@ impl Component for MailView {
|
|||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
context.children.push(child);
|
||||
context
|
||||
.children
|
||||
.entry(url_launcher.to_string().into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Couldn't launch {:?}: {}",
|
||||
"Couldn't launch {}: {}",
|
||||
url_launcher, err
|
||||
)),
|
||||
));
|
||||
|
@ -765,11 +771,15 @@ impl Component for MailView {
|
|||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => context.children.push(child),
|
||||
Ok(child) => context
|
||||
.children
|
||||
.entry(url_launcher.to_string().into())
|
||||
.or_default()
|
||||
.push(child),
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Couldn't launch {:?}: {}",
|
||||
"Couldn't launch {}: {}",
|
||||
url_launcher, err
|
||||
)),
|
||||
));
|
||||
|
@ -789,6 +799,25 @@ impl Component for MailView {
|
|||
.push_back(UIEvent::Action(Tab(New(Some(Box::new(new_tab))))));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.envelope_view
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
|
|
@ -87,7 +87,7 @@ impl EnvelopeView {
|
|||
) -> Self {
|
||||
let view_settings = view_settings.unwrap_or_default();
|
||||
let body = Box::new(AttachmentBuilder::new(&mail.bytes).build());
|
||||
let mut ret = EnvelopeView {
|
||||
let mut ret = Self {
|
||||
pager: pager.unwrap_or_default(),
|
||||
subview,
|
||||
dirty: true,
|
||||
|
@ -152,8 +152,8 @@ impl EnvelopeView {
|
|||
format!("Failed to start html filter process: {}", filter_invocation,)
|
||||
.into(),
|
||||
),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
source: Some(err.into()),
|
||||
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
}));
|
||||
// [ref:FIXME]: add `v` configurable shortcut
|
||||
|
@ -221,7 +221,7 @@ impl EnvelopeView {
|
|||
}
|
||||
}
|
||||
for a in parts {
|
||||
EnvelopeView::attachment_to_display_helper(
|
||||
Self::attachment_to_display_helper(
|
||||
a,
|
||||
main_loop_handler,
|
||||
active_jobs,
|
||||
|
@ -257,7 +257,7 @@ impl EnvelopeView {
|
|||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
if view_settings.auto_verify_signatures {
|
||||
if view_settings.auto_verify_signatures.is_true() {
|
||||
let verify_fut = crate::mail::pgp::verify(a.clone());
|
||||
let handle = main_loop_handler
|
||||
.job_executor
|
||||
|
@ -271,7 +271,7 @@ impl EnvelopeView {
|
|||
job_id: handle.job_id,
|
||||
display: {
|
||||
let mut v = vec![];
|
||||
EnvelopeView::attachment_to_display_helper(
|
||||
Self::attachment_to_display_helper(
|
||||
&parts[0],
|
||||
main_loop_handler,
|
||||
active_jobs,
|
||||
|
@ -288,7 +288,7 @@ impl EnvelopeView {
|
|||
inner: Box::new(a.clone()),
|
||||
display: {
|
||||
let mut v = vec![];
|
||||
EnvelopeView::attachment_to_display_helper(
|
||||
Self::attachment_to_display_helper(
|
||||
&parts[0],
|
||||
main_loop_handler,
|
||||
active_jobs,
|
||||
|
@ -317,7 +317,7 @@ impl EnvelopeView {
|
|||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
if view_settings.auto_decrypt {
|
||||
if view_settings.auto_decrypt.is_true() {
|
||||
let decrypt_fut = crate::mail::pgp::decrypt(a.raw().to_vec());
|
||||
let handle = main_loop_handler
|
||||
.job_executor
|
||||
|
@ -342,7 +342,7 @@ impl EnvelopeView {
|
|||
}
|
||||
_ => {
|
||||
for a in parts {
|
||||
EnvelopeView::attachment_to_display_helper(
|
||||
Self::attachment_to_display_helper(
|
||||
a,
|
||||
main_loop_handler,
|
||||
active_jobs,
|
||||
|
@ -475,7 +475,7 @@ impl EnvelopeView {
|
|||
} => {
|
||||
if show_comments {
|
||||
if description.is_empty() {
|
||||
acc.push_str("Succesfully decrypted.\n\n");
|
||||
acc.push_str("Successfully decrypted.\n\n");
|
||||
} else {
|
||||
acc.push_str(description);
|
||||
acc.push_str("\n\n");
|
||||
|
@ -1081,13 +1081,13 @@ impl Component for EnvelopeView {
|
|||
self.view_settings.body_theme = crate::conf::value(context, "mail.view.body");
|
||||
self.pager = Pager::from_string(
|
||||
text,
|
||||
Some(context),
|
||||
context,
|
||||
Some(cursor_pos),
|
||||
None,
|
||||
self.view_settings.body_theme,
|
||||
);
|
||||
if let Some(ref filter) = self.view_settings.pager_filter {
|
||||
self.pager.filter(filter);
|
||||
self.pager.filter(filter, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1269,7 +1269,7 @@ impl Component for EnvelopeView {
|
|||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
|
||||
if let Some(attachment) = self.open_attachment(lidx, context) {
|
||||
if let Ok(()) = crate::mailcap::MailcapEntry::execute(attachment, context) {
|
||||
if crate::mailcap::MailcapEntry::execute(attachment, context).is_ok() {
|
||||
self.set_dirty(true);
|
||||
} else {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
|
@ -1294,15 +1294,15 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
match save_attachment(&path, &self.mail.bytes) {
|
||||
Err(err) => {
|
||||
log::error!("Failed to create file at {}: {err}", path.display());
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("Failed to create file at {}", path.display()).into(),
|
||||
),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
source: Some(err),
|
||||
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
});
|
||||
log::error!("Failed to create file at {}: {err}", path.display());
|
||||
return true;
|
||||
}
|
||||
Ok(()) => {
|
||||
|
@ -1336,15 +1336,15 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
match save_attachment(&path, &u.decode(Default::default())) {
|
||||
Err(err) => {
|
||||
log::error!("Failed to create file at {}: {err}", path.display());
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("Failed to create file at {}", path.display()).into(),
|
||||
),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
source: Some(err),
|
||||
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
});
|
||||
log::error!("Failed to create file at {}: {err}", path.display());
|
||||
}
|
||||
Ok(()) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
|
@ -1365,15 +1365,15 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
match save_attachment(&path, &self.mail.bytes) {
|
||||
Err(err) => {
|
||||
log::error!("Failed to create file at {}: {err}", path.display());
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("Failed to create file at {}", path.display()).into(),
|
||||
),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
source: Some(err),
|
||||
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
});
|
||||
log::error!("Failed to create file at {}: {err}", path.display());
|
||||
return true;
|
||||
}
|
||||
Ok(()) => {
|
||||
|
@ -1411,7 +1411,7 @@ impl Component for EnvelopeView {
|
|||
ContentType::MessageRfc822 => {
|
||||
match Mail::new(attachment.body().to_vec(), Some(Flag::SEEN)) {
|
||||
Ok(wrapper) => {
|
||||
self.subview = Some(Box::new(EnvelopeView::new(
|
||||
self.subview = Some(Box::new(Self::new(
|
||||
wrapper,
|
||||
None,
|
||||
None,
|
||||
|
@ -1441,7 +1441,7 @@ impl Component for EnvelopeView {
|
|||
let attachment_type = attachment.mime_type();
|
||||
let filename = attachment.filename();
|
||||
if let Ok(command) = query_default_app(&attachment_type) {
|
||||
match File::create_temp_file(
|
||||
let res = File::create_temp_file(
|
||||
&attachment.decode(Default::default()),
|
||||
filename.as_deref(),
|
||||
None,
|
||||
|
@ -1462,10 +1462,15 @@ impl Component for EnvelopeView {
|
|||
.stdout(Stdio::piped())
|
||||
.spawn()?,
|
||||
))
|
||||
}) {
|
||||
});
|
||||
match res {
|
||||
Ok((p, child)) => {
|
||||
context.temp_files.push(p);
|
||||
context.children.push(child);
|
||||
context
|
||||
.children
|
||||
.entry(command.into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
|
@ -1559,13 +1564,17 @@ impl Component for EnvelopeView {
|
|||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
context.children.push(child);
|
||||
context
|
||||
.children
|
||||
.entry(url_launcher.to_string().into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification {
|
||||
title: Some(format!("Failed to launch {:?}", url_launcher).into()),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
source: Some(err.into()),
|
||||
kind: Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ use melib::{
|
|||
attachment_types::{ContentType, MultipartType, Text},
|
||||
error::*,
|
||||
parser::BytesExt,
|
||||
text_processing::Truncate,
|
||||
text::Truncate,
|
||||
utils::xdg::query_default_app,
|
||||
Attachment, Result,
|
||||
};
|
||||
|
@ -76,7 +76,7 @@ impl std::fmt::Display for ViewFilter {
|
|||
}
|
||||
|
||||
impl ViewFilter {
|
||||
pub fn new_html(body: &Attachment, context: &mut Context) -> Result<Self> {
|
||||
pub fn new_html(body: &Attachment, context: &Context) -> Result<Self> {
|
||||
fn run(cmd: &str, args: &[&str], bytes: &[u8]) -> Result<String> {
|
||||
let mut html_filter = Command::new(cmd)
|
||||
.args(args)
|
||||
|
@ -306,11 +306,7 @@ impl ViewFilter {
|
|||
})
|
||||
}
|
||||
|
||||
fn html_process_event(
|
||||
_self: &mut ViewFilter,
|
||||
event: &mut UIEvent,
|
||||
context: &mut Context,
|
||||
) -> bool {
|
||||
fn html_process_event(_self: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if matches!(event, UIEvent::Input(key) if *key == context.settings.shortcuts.envelope_view.open_html)
|
||||
{
|
||||
let command = context
|
||||
|
@ -328,7 +324,7 @@ impl ViewFilter {
|
|||
command
|
||||
};
|
||||
if let Some(command) = command {
|
||||
match File::create_temp_file(&_self.unfiltered, None, None, Some("html"), true)
|
||||
let res = File::create_temp_file(&_self.unfiltered, None, None, Some("html"), true)
|
||||
.and_then(|p| {
|
||||
let exec_cmd = desktop_exec_to_command(
|
||||
&command,
|
||||
|
@ -344,13 +340,18 @@ impl ViewFilter {
|
|||
.stdout(Stdio::piped())
|
||||
.spawn()?,
|
||||
))
|
||||
}) {
|
||||
});
|
||||
match res {
|
||||
Ok((p, child)) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::UpdateSubStatus(command.to_string()),
|
||||
StatusEvent::UpdateSubStatus(command.clone()),
|
||||
));
|
||||
context.temp_files.push(p);
|
||||
context.children.push(child);
|
||||
context
|
||||
.children
|
||||
.entry(command.into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::{text_processing::Truncate, Envelope, Error, Mail, Result};
|
||||
use melib::{text::Truncate, Envelope, Error, Mail, Result};
|
||||
|
||||
use super::{EnvelopeView, MailView, ViewSettings};
|
||||
use crate::{jobs::JoinHandle, mailbox_settings, Component, Context, ShortcutMaps, UIEvent};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PendingReplyAction {
|
||||
Reply,
|
||||
ReplyToAuthor,
|
||||
|
@ -149,7 +149,7 @@ impl MailViewState {
|
|||
}),
|
||||
context.main_loop_handler.clone(),
|
||||
));
|
||||
self_.state = MailViewState::Loaded {
|
||||
self_.state = Self::Loaded {
|
||||
env,
|
||||
bytes,
|
||||
env_view,
|
||||
|
@ -192,7 +192,7 @@ impl MailViewState {
|
|||
|
||||
impl Default for MailViewState {
|
||||
fn default() -> Self {
|
||||
MailViewState::Init {
|
||||
Self::Init {
|
||||
pending_action: None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ use crate::components::PageMovement;
|
|||
#[derive(Debug)]
|
||||
struct ThreadEntry {
|
||||
index: (usize, ThreadNodeHash, usize),
|
||||
/// (indentation, thread_node index, line number in listing)
|
||||
/// (indentation, `thread_node` index, line number in listing)
|
||||
indentation: usize,
|
||||
msg_hash: EnvelopeHash,
|
||||
seen: bool,
|
||||
|
@ -51,7 +51,7 @@ pub enum ThreadViewFocus {
|
|||
MailView,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct ThreadView {
|
||||
new_cursor_pos: usize,
|
||||
cursor_pos: usize,
|
||||
|
@ -73,11 +73,11 @@ pub struct ThreadView {
|
|||
}
|
||||
|
||||
impl ThreadView {
|
||||
/// @coordinates: (account index, mailbox_hash, root set thread_node index)
|
||||
/// @expanded_hash: optional position of expanded entry when we render the
|
||||
/// ThreadView.
|
||||
/// default: expanded message is the last one.
|
||||
/// @context: current context
|
||||
/// @`coordinates`: (account index, `mailbox_hash`, root set `thread_node`
|
||||
/// index)
|
||||
/// @`expanded_hash`: optional position of expanded entry when we
|
||||
/// render the `ThreadView`.
|
||||
/// default: expanded message is the last one.
|
||||
pub fn new(
|
||||
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
|
||||
thread_group: ThreadHash,
|
||||
|
@ -86,7 +86,7 @@ impl ThreadView {
|
|||
focus: Option<ThreadViewFocus>,
|
||||
context: &mut Context,
|
||||
) -> Self {
|
||||
let mut view = ThreadView {
|
||||
let mut view = Self {
|
||||
reversed: false,
|
||||
coordinates,
|
||||
thread_group,
|
||||
|
@ -106,7 +106,11 @@ impl ThreadView {
|
|||
//],
|
||||
use_color: context.settings.terminal.use_color(),
|
||||
horizontal: None,
|
||||
..Default::default()
|
||||
expanded_pos: 0,
|
||||
new_expanded_pos: 0,
|
||||
visible_entries: vec![],
|
||||
movement: None,
|
||||
content: Screen::<Virtual>::default(),
|
||||
};
|
||||
view.initiate(expanded_hash, go_to_first_unread, context);
|
||||
view.new_cursor_pos = view.new_expanded_pos;
|
||||
|
@ -147,10 +151,8 @@ impl ThreadView {
|
|||
{
|
||||
self.entries[new_cursor].hidden = old_entries[old_cursor].hidden;
|
||||
old_cursor += 1;
|
||||
new_cursor += 1;
|
||||
} else {
|
||||
new_cursor += 1;
|
||||
}
|
||||
new_cursor += 1;
|
||||
self.recalc_visible_entries();
|
||||
}
|
||||
|
||||
|
@ -636,162 +638,68 @@ impl ThreadView {
|
|||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
//fn draw_horz(&mut self, grid: &mut CellBuffer, area: Area, context: &mut
|
||||
// Context) { if self.entries.is_empty() {
|
||||
// return;
|
||||
// }
|
||||
// let upper_left = area.upper_left();
|
||||
// let bottom_right = area.bottom_right();
|
||||
// let total_rows = area.height();
|
||||
fn draw_horz(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if self.entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// let pager_ratio = *mailbox_settings!(
|
||||
// context[self.coordinates.0][&self.coordinates.1]
|
||||
// .pager
|
||||
// .pager_ratio
|
||||
// );
|
||||
// let mut bottom_entity_rows = (pager_ratio * total_rows) / 100;
|
||||
let mid = self.content.area().width().min(area.height() / 2);
|
||||
|
||||
// if bottom_entity_rows > total_rows {
|
||||
// bottom_entity_rows = total_rows.saturating_sub(1);
|
||||
// }
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
// First draw the thread subject on the first row
|
||||
if self.dirty {
|
||||
grid.clear_area(area, theme_default);
|
||||
let account = &context.accounts[&self.coordinates.0];
|
||||
let threads = account.collection.get_threads(self.coordinates.1);
|
||||
let thread_root = threads.thread_iter(self.thread_group).next().unwrap().1;
|
||||
let thread_node = &threads.thread_nodes()[&thread_root];
|
||||
let i = thread_node.message().unwrap_or_else(|| {
|
||||
let mut iter_ptr = thread_node.children()[0];
|
||||
while threads.thread_nodes()[&iter_ptr].message().is_none() {
|
||||
iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
|
||||
}
|
||||
threads.thread_nodes()[&iter_ptr].message().unwrap()
|
||||
});
|
||||
let envelope: EnvelopeRef = account.collection.get_env(i);
|
||||
|
||||
// let mut mid = get_y(upper_left) + total_rows - bottom_entity_rows;
|
||||
// if mid >= get_y(bottom_right) {
|
||||
// mid = get_y(bottom_right) / 2;
|
||||
// }
|
||||
// let mid = mid;
|
||||
grid.write_string(
|
||||
&envelope.subject(),
|
||||
theme_default.fg,
|
||||
theme_default.bg,
|
||||
theme_default.attrs,
|
||||
area,
|
||||
Some(0),
|
||||
);
|
||||
context.dirty_areas.push_back(area);
|
||||
};
|
||||
|
||||
// let theme_default = crate::conf::value(context, "theme_default");
|
||||
// // First draw the thread subject on the first row
|
||||
// let y = {
|
||||
// grid.clear_area(area, theme_default);
|
||||
// let account = &context.accounts[&self.coordinates.0];
|
||||
// let threads = account.collection.get_threads(self.coordinates.1);
|
||||
// let thread_root =
|
||||
// threads.thread_iter(self.thread_group).next().unwrap().1; let
|
||||
// thread_node = &threads.thread_nodes()[&thread_root]; let i =
|
||||
// thread_node.message().unwrap_or_else(|| { let mut iter_ptr =
|
||||
// thread_node.children()[0]; while
|
||||
// threads.thread_nodes()[&iter_ptr].message().is_none() {
|
||||
// iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; }
|
||||
// threads.thread_nodes()[&iter_ptr].message().unwrap()
|
||||
// });
|
||||
// let envelope: EnvelopeRef = account.collection.get_env(i);
|
||||
let area = area.skip_rows(2);
|
||||
let (width, height) = self.content.area().size();
|
||||
if height == 0 || height == self.cursor_pos || width == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// let (x, y) = grid.write_string(
|
||||
// &envelope.subject(),
|
||||
// theme_default.fg,
|
||||
// theme_default.bg,
|
||||
// theme_default.attrs,
|
||||
// area,
|
||||
// Some(get_x(upper_left)),
|
||||
// );
|
||||
// for x in x..=get_x(bottom_right) {
|
||||
// grid[(x, y)]
|
||||
// .set_ch(' ')
|
||||
// .set_fg(theme_default.fg)
|
||||
// .set_bg(theme_default.bg);
|
||||
// }
|
||||
// context.dirty_areas.push_back(area);
|
||||
// y + 2
|
||||
// };
|
||||
|
||||
// for x in get_x(upper_left)..=get_x(bottom_right) {
|
||||
// set_and_join_box(grid, (x, y - 1), BoxBoundary::Horizontal);
|
||||
// grid[(x, y - 1)]
|
||||
// .set_fg(theme_default.fg)
|
||||
// .set_bg(theme_default.bg);
|
||||
// }
|
||||
|
||||
// let (width, height) = self.content.area().size();
|
||||
// if height == 0 || height == self.cursor_pos || width == 0 {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// grid.clear_area(area.skip_rows(y).take_rows(mid + 1), theme_default);
|
||||
|
||||
// match self.focus {
|
||||
// ThreadViewFocus::None => {
|
||||
// let area = area.skip_rows(y).take_rows(mid);
|
||||
// let rows = area.height() / 2;
|
||||
// if rows == 0 {
|
||||
// return;
|
||||
// }
|
||||
// let page_no = (self.new_cursor_pos).wrapping_div(rows);
|
||||
// let top_idx = page_no * rows;
|
||||
|
||||
// grid.copy_area(
|
||||
// self.content.grid(),
|
||||
// area,
|
||||
// self.content.area().skip_rows(top_idx),
|
||||
// );
|
||||
// context.dirty_areas.push_back(area);
|
||||
// }
|
||||
// ThreadViewFocus::Thread => {
|
||||
// let area = {
|
||||
// let val = area.skip_rows(y);
|
||||
// if val.height() < 20 {
|
||||
// area
|
||||
// } else {
|
||||
// val
|
||||
// }
|
||||
// };
|
||||
// let rows = area.height() / 2;
|
||||
// if rows == 0 {
|
||||
// return;
|
||||
// }
|
||||
// let page_no = (self.new_cursor_pos).wrapping_div(rows);
|
||||
// let top_idx = page_no * rows;
|
||||
|
||||
// grid.copy_area(
|
||||
// self.content.grid(),
|
||||
// area,
|
||||
// self.content.area().skip_rows(top_idx),
|
||||
// );
|
||||
// context.dirty_areas.push_back(area);
|
||||
// }
|
||||
// ThreadViewFocus::MailView => { /* show only envelope */ }
|
||||
// }
|
||||
|
||||
// match self.focus {
|
||||
// ThreadViewFocus::None => {
|
||||
// {
|
||||
// let area = {
|
||||
// let val = area.skip_rows(mid);
|
||||
// if val.height() < 20 {
|
||||
// area
|
||||
// } else {
|
||||
// val
|
||||
// }
|
||||
// };
|
||||
// context.dirty_areas.push_back(area);
|
||||
// for x in get_x(area.upper_left())..=get_x(area.bottom_right())
|
||||
// { set_and_join_box(grid, (x, mid),
|
||||
// BoxBoundary::Horizontal); grid[(x, mid)]
|
||||
// .set_fg(theme_default.fg)
|
||||
// .set_bg(theme_default.bg);
|
||||
// }
|
||||
// }
|
||||
// {
|
||||
// let area = area.skip_rows(y).take_rows(mid - 1);
|
||||
// self.draw_list(grid, area, context);
|
||||
// }
|
||||
// let area = area.take_rows(mid);
|
||||
// self.entries[self.new_expanded_pos]
|
||||
// .mailview
|
||||
// .draw(grid, area, context);
|
||||
// }
|
||||
// ThreadViewFocus::Thread => {
|
||||
// self.dirty = true;
|
||||
// self.draw_list(grid, area.skip_rows(y), context);
|
||||
// }
|
||||
// ThreadViewFocus::MailView => {
|
||||
// self.entries[self.new_expanded_pos]
|
||||
// .mailview
|
||||
// .draw(grid, area, context);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
match self.focus {
|
||||
ThreadViewFocus::None => {
|
||||
self.draw_list(grid, area.take_rows(mid), context);
|
||||
self.entries[self.new_expanded_pos].mailview.draw(
|
||||
grid,
|
||||
area.skip_rows(mid + 1),
|
||||
context,
|
||||
);
|
||||
}
|
||||
ThreadViewFocus::Thread => {
|
||||
self.dirty = true;
|
||||
self.draw_list(grid, area.skip_rows(0), context);
|
||||
}
|
||||
ThreadViewFocus::MailView => {
|
||||
self.entries[self.new_expanded_pos]
|
||||
.mailview
|
||||
.draw(grid, area, context);
|
||||
}
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
fn recalc_visible_entries(&mut self) {
|
||||
if self.entries.is_empty() {
|
||||
|
@ -865,7 +773,7 @@ impl ThreadView {
|
|||
|
||||
impl std::fmt::Display for ThreadView {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if let Some(e) = self.entries.get(0) {
|
||||
if let Some(e) = self.entries.first() {
|
||||
e.mailview.fmt(fmt)
|
||||
} else {
|
||||
write!(fmt, "view thread")
|
||||
|
@ -891,6 +799,8 @@ impl Component for ThreadView {
|
|||
self.entries[self.new_expanded_pos]
|
||||
.mailview
|
||||
.draw(grid, area, context);
|
||||
} else if Some(true) == self.horizontal {
|
||||
self.draw_horz(grid, area, context);
|
||||
} else {
|
||||
self.draw_vert(grid, area, context);
|
||||
}
|
||||
|
@ -898,7 +808,10 @@ impl Component for ThreadView {
|
|||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let (UIEvent::Action(Listing(OpenInNewTab)), false) = (&event, self.entries.is_empty()) {
|
||||
if matches!(
|
||||
(&event, self.entries.is_empty()),
|
||||
(UIEvent::Action(Listing(OpenInNewTab)), false)
|
||||
) {
|
||||
// Handle this before self.mailview does
|
||||
let mut new_tab = Self::new(
|
||||
self.coordinates,
|
||||
|
@ -934,7 +847,7 @@ impl Component for ThreadView {
|
|||
if let Some(ref mut v) = self.horizontal {
|
||||
*v = !*v;
|
||||
} else {
|
||||
self.horizontal = Some(false);
|
||||
self.horizontal = Some(true);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
true
|
||||
|
@ -1102,7 +1015,28 @@ impl Component for ThreadView {
|
|||
}
|
||||
false
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context
|
||||
.settings
|
||||
.shortcuts
|
||||
.thread_view
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
// [ref:VERIFY]: In what case do we need to forward a handled UIEvent to all
|
||||
// entries?
|
||||
if self
|
||||
.entries
|
||||
.iter_mut()
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
|
||||
use std::fmt::Write as IoWrite;
|
||||
|
||||
use melib::{attachment_types::Charset, error::*, pgp::DecryptionMetadata, Attachment, Result};
|
||||
use melib::{
|
||||
attachment_types::Charset, conf::ActionFlag, error::*, pgp::DecryptionMetadata, Attachment,
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
conf::shortcuts::EnvelopeViewShortcuts,
|
||||
|
@ -43,8 +46,8 @@ pub struct ViewSettings {
|
|||
pub sticky_headers: bool,
|
||||
pub show_date_in_my_timezone: bool,
|
||||
pub show_extra_headers: Vec<String>,
|
||||
pub auto_verify_signatures: bool,
|
||||
pub auto_decrypt: bool,
|
||||
pub auto_verify_signatures: ActionFlag,
|
||||
pub auto_decrypt: ActionFlag,
|
||||
}
|
||||
|
||||
impl Default for ViewSettings {
|
||||
|
@ -61,8 +64,8 @@ impl Default for ViewSettings {
|
|||
sticky_headers: false,
|
||||
show_date_in_my_timezone: false,
|
||||
show_extra_headers: vec![],
|
||||
auto_verify_signatures: true,
|
||||
auto_decrypt: true,
|
||||
auto_verify_signatures: ActionFlag::InternalVal(true),
|
||||
auto_decrypt: ActionFlag::InternalVal(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,7 +187,13 @@ impl ViewOptions {
|
|||
.collect::<Vec<Link>>();
|
||||
}
|
||||
for (lidx, l) in links.iter().enumerate().rev() {
|
||||
text.insert_str(l.start, &format!("[{}]", lidx));
|
||||
let mut start = l.start;
|
||||
while start < text.len() && !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
if start < text.len() {
|
||||
text.insert_str(start, &format!("[{}]", lidx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
|
||||
use std::{fs::File, io::Write, os::unix::fs::PermissionsExt, path::Path};
|
||||
|
||||
use melib::Result;
|
||||
use melib::{Result, ShellExpandTrait};
|
||||
|
||||
pub fn save_attachment(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
let mut f = File::create(path)?;
|
||||
let mut f = File::create(path.expand())?;
|
||||
let mut permissions = f.metadata()?.permissions();
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
|
|
|
@ -24,7 +24,7 @@ use indexmap::IndexMap;
|
|||
use melib::{backends::AccountHash, SortOrder};
|
||||
|
||||
use super::*;
|
||||
use crate::{accounts::MailboxEntry, melib::text_processing::TextProcessing};
|
||||
use crate::{accounts::MailboxEntry, melib::text::TextProcessing};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum MailboxAction {
|
||||
|
@ -96,7 +96,7 @@ impl MailboxManager {
|
|||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
let mut data_columns = DataColumns::default();
|
||||
data_columns.theme_config.set_single_theme(theme_default);
|
||||
MailboxManager {
|
||||
Self {
|
||||
cursor_pos: 0,
|
||||
new_cursor_pos: 0,
|
||||
account_hash,
|
||||
|
@ -117,7 +117,7 @@ impl MailboxManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &mut Context) {
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
let account = &context.accounts[self.account_pos];
|
||||
self.length = account.mailbox_entries.len();
|
||||
let mut entries = account.mailbox_entries.clone();
|
||||
|
|
|
@ -24,13 +24,12 @@
|
|||
//! Implements [RFC 1524 A User Agent Configuration Mechanism For Multimedia
|
||||
//! Mail Format Information](https://www.rfc-editor.org/rfc/inline-errata/rfc1524.html)
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use melib::{email::Attachment, log, text_processing::GlobMatch, Error, Result};
|
||||
use melib::{email::Attachment, log, text::GlobMatch, Error, Result};
|
||||
|
||||
use crate::{
|
||||
state::Context,
|
||||
|
@ -77,7 +76,6 @@ impl MailcapEntry {
|
|||
}
|
||||
}
|
||||
|
||||
let mut hash_map = HashMap::new();
|
||||
let mut content = String::new();
|
||||
|
||||
std::fs::File::open(mailcap_path.as_path())?.read_to_string(&mut content)?;
|
||||
|
@ -111,13 +109,12 @@ impl MailcapEntry {
|
|||
}
|
||||
}
|
||||
|
||||
result = Some(MailcapEntry {
|
||||
result = Some(Self {
|
||||
command: cmd.to_string(),
|
||||
copiousoutput,
|
||||
});
|
||||
break;
|
||||
}
|
||||
hash_map.insert(key.to_string(), cmd.to_string());
|
||||
} else {
|
||||
let mut parts_iter = l.split(';');
|
||||
let key = parts_iter.next().unwrap();
|
||||
|
@ -134,25 +131,23 @@ impl MailcapEntry {
|
|||
}
|
||||
}
|
||||
|
||||
result = Some(MailcapEntry {
|
||||
result = Some(Self {
|
||||
command: cmd.to_string(),
|
||||
copiousoutput,
|
||||
});
|
||||
break;
|
||||
}
|
||||
hash_map.insert(key.to_string(), cmd.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
None => Err(Error::new("Not found")),
|
||||
Some(MailcapEntry {
|
||||
Some(Self {
|
||||
command,
|
||||
copiousoutput,
|
||||
}) => {
|
||||
let parts = split_command!(command);
|
||||
let (cmd, args) = (parts[0], &parts[1..]);
|
||||
let mut f = None;
|
||||
let mut needs_stdin = true;
|
||||
let params = a.parameters();
|
||||
/* [ref:TODO]: See mailcap(5)
|
||||
|
@ -173,7 +168,6 @@ impl MailcapEntry {
|
|||
true,
|
||||
)?;
|
||||
let p = _f.path().display().to_string();
|
||||
f = Some(_f);
|
||||
Ok(p)
|
||||
}
|
||||
"%t" => Ok(a.content_type().to_string()),
|
||||
|
|
|
@ -101,6 +101,16 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
Some(SubCommand::EditConfig) => {
|
||||
return subcommands::edit_config();
|
||||
}
|
||||
Some(SubCommand::PrintConfigPath) => {
|
||||
let config_path = crate::conf::get_config_file()?;
|
||||
println!("{}", config_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
Some(SubCommand::Man(ManOpt {})) => {
|
||||
return Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"));
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
Some(SubCommand::Man(ManOpt { page, no_raw })) => {
|
||||
return subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw));
|
||||
}
|
||||
|
@ -117,7 +127,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
Some(SubCommand::View { .. }) => {}
|
||||
Some(SubCommand::PrintUsedPaths) => {
|
||||
Some(SubCommand::PrintAppDirectories) => {
|
||||
println!(
|
||||
"{}",
|
||||
xdg::BaseDirectories::with_prefix("meli")
|
||||
|
@ -133,13 +143,25 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
println!("{}", temp_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
Some(SubCommand::InstallMan {
|
||||
destination_path: _,
|
||||
}) => {
|
||||
return Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"));
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
Some(SubCommand::InstallMan { destination_path }) => {
|
||||
match args::manpages::ManPages::install(destination_path) {
|
||||
match crate::manpages::ManPages::install(destination_path) {
|
||||
Ok(p) => println!("Installed at {}.", p.display()),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Some(SubCommand::PrintLogPath) => {
|
||||
let settings = crate::conf::Settings::new()?;
|
||||
println!("{}", settings._logger.log_dest().display());
|
||||
return Ok(());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use std::{
|
||||
env, fs,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use flate2::bufread::GzDecoder;
|
||||
use melib::{log, ShellExpandTrait};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub const POSSIBLE_VALUES: &[&str] = &[
|
||||
"meli",
|
||||
"meli.1",
|
||||
"conf",
|
||||
"meli.conf",
|
||||
"meli.conf.5",
|
||||
"themes",
|
||||
"meli-themes",
|
||||
"meli-themes.5",
|
||||
"guide",
|
||||
"meli.7",
|
||||
];
|
||||
|
||||
pub fn parse_manpage(src: &str) -> Result<ManPages> {
|
||||
match src {
|
||||
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
|
||||
"meli.7" | "guide" => Ok(ManPages::Guide),
|
||||
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
|
||||
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
|
||||
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Choose manpage
|
||||
pub enum ManPages {
|
||||
/// meli(1)
|
||||
Main = 0,
|
||||
/// meli.conf(5)
|
||||
Conf = 1,
|
||||
/// meli-themes(5)
|
||||
Themes = 2,
|
||||
/// meli(7)
|
||||
Guide = 3,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ManPages {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Main => "meli.1",
|
||||
Self::Conf => "meli.conf.5",
|
||||
Self::Themes => "meli-themes.5",
|
||||
Self::Guide => "meli.7",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManPages {
|
||||
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
|
||||
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
|
||||
tries.push(p.into());
|
||||
let p = p.expand();
|
||||
p.exists()
|
||||
&& p.is_dir()
|
||||
&& fs::metadata(p)
|
||||
.ok()
|
||||
.map(|m| !m.permissions().readonly())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
let mut tries = vec![];
|
||||
let Some(mut path) = destination
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
.or_else(|| {
|
||||
if let Some(paths) = env::var_os("MANPATH") {
|
||||
if let Some(path) = env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
env::home_dir()
|
||||
.map(|p| p.join(".local").join("share").join("man"))
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
})
|
||||
else {
|
||||
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
|
||||
};
|
||||
path = path.expand();
|
||||
|
||||
for (p, dir) in [
|
||||
(Self::Main, "man1"),
|
||||
(Self::Conf, "man5"),
|
||||
(Self::Themes, "man5"),
|
||||
(Self::Guide, "man7"),
|
||||
] {
|
||||
let text = crate::subcommands::man(p, true)?;
|
||||
path.push(dir);
|
||||
std::fs::create_dir_all(&path).map_err(|err| {
|
||||
Error::new(format!("Could not create {} directory.", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
path.push(&p.to_string());
|
||||
|
||||
fs::write(&path, text.as_bytes()).map_err(|err| {
|
||||
Error::new(format!("Could not write to {}", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
log::trace!("Installed {} to {}", p, path.display());
|
||||
path.pop();
|
||||
path.pop();
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn read(self, source: bool) -> Result<String> {
|
||||
const MANPAGES: [&[u8]; 4] = [
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")),
|
||||
];
|
||||
const MANPAGES_MDOC: [&[u8]; 4] = [
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")),
|
||||
];
|
||||
|
||||
let mut gz = GzDecoder::new(if source {
|
||||
MANPAGES_MDOC[self as usize]
|
||||
} else {
|
||||
MANPAGES[self as usize]
|
||||
});
|
||||
let mut v = String::with_capacity(
|
||||
str::parse::<usize>(unsafe {
|
||||
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("{:?} was not compressed with size comment header", self)),
|
||||
);
|
||||
gz.read_to_string(&mut v)?;
|
||||
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Helper function to remove backspace markup from mandoc output.
|
||||
pub fn remove_markup(input: &str) -> Result<String> {
|
||||
use std::{
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
let mut child = Command::new("col")
|
||||
.arg("-b")
|
||||
.arg("-x")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
child.stdin.as_mut().unwrap().write_all(input.as_bytes())?;
|
||||
Ok(String::from_utf8_lossy(&child.wait_with_output()?.stdout).to_string())
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ mod dbus {
|
|||
|
||||
impl DbusNotifications {
|
||||
pub fn new(context: &Context) -> Self {
|
||||
DbusNotifications {
|
||||
Self {
|
||||
rate_limit: RateLimit::new(
|
||||
1000,
|
||||
1000,
|
||||
|
@ -183,7 +183,7 @@ pub struct NotificationCommand {
|
|||
|
||||
impl NotificationCommand {
|
||||
pub fn new() -> Self {
|
||||
NotificationCommand::default()
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,12 +213,13 @@ impl Component for NotificationCommand {
|
|||
}
|
||||
}
|
||||
|
||||
let mut script = context.settings.notifications.script.as_ref();
|
||||
if *kind == Some(NotificationType::NewMail)
|
||||
let script = if *kind == Some(NotificationType::NewMail)
|
||||
&& context.settings.notifications.new_mail_script.is_some()
|
||||
{
|
||||
script = context.settings.notifications.new_mail_script.as_ref();
|
||||
}
|
||||
context.settings.notifications.new_mail_script.as_ref()
|
||||
} else {
|
||||
context.settings.notifications.script.as_ref()
|
||||
};
|
||||
|
||||
if let Some(ref bin) = script {
|
||||
match Command::new(bin)
|
||||
|
@ -230,7 +231,11 @@ impl Component for NotificationCommand {
|
|||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
context.children.push(child);
|
||||
context
|
||||
.children
|
||||
.entry(stringify!(NotificationCommand).into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Could not run notification script: {err}.");
|
||||
|
@ -261,7 +266,11 @@ impl Component for NotificationCommand {
|
|||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
context.children.push(child);
|
||||
context
|
||||
.children
|
||||
.entry(stringify!(NotificationCommand).into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
return false;
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -378,7 +387,7 @@ impl Component for DisplayMessageBox {
|
|||
}) = self.messages.get(self.pos)
|
||||
{
|
||||
let noto_colors = crate::conf::value(context, "status.notification");
|
||||
use crate::melib::text_processing::{Reflow, TextProcessing};
|
||||
use crate::melib::text::{Reflow, TextProcessing};
|
||||
|
||||
let box_width = area.width() / 3;
|
||||
if box_width < 10 {
|
||||
|
|
|
@ -250,7 +250,7 @@ impl PluginBackend {
|
|||
plugin,
|
||||
channel: Arc::new(Mutex::new(channel)),
|
||||
collection: Default::default(),
|
||||
is_online: Arc::new(Mutex::new((now, Err(Error::new("Unitialized"))))),
|
||||
is_online: Arc::new(Mutex::new((now, Err(Error::new("Uninitialized"))))),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -26,22 +26,23 @@ use std::{
|
|||
};
|
||||
|
||||
use melib::{
|
||||
backends::{MailBackend, ResultFuture},
|
||||
backends::MailBackend,
|
||||
email::{Envelope, EnvelopeHash},
|
||||
log,
|
||||
search::{
|
||||
escape_double_quote,
|
||||
Query::{self, *},
|
||||
},
|
||||
utils::sqlite3::{self as melib_sqlite3, rusqlite::params, DatabaseDescription},
|
||||
Error, Result, SortField, SortOrder,
|
||||
smol,
|
||||
utils::sqlite3::{rusqlite::params, DatabaseDescription},
|
||||
Error, Result, ResultIntoError, SortField, SortOrder,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::melib::ResultIntoError;
|
||||
|
||||
const DB: DatabaseDescription = DatabaseDescription {
|
||||
name: "index.db",
|
||||
identifier: None,
|
||||
application_prefix: "meli",
|
||||
init_script: Some(
|
||||
"CREATE TABLE IF NOT EXISTS envelopes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
@ -113,10 +114,6 @@ END; ",
|
|||
version: 1,
|
||||
};
|
||||
|
||||
pub fn db_path() -> Result<PathBuf> {
|
||||
melib_sqlite3::db_path(DB.name)
|
||||
}
|
||||
|
||||
//#[inline(always)]
|
||||
//fn fts5_bareword(w: &str) -> Cow<str> {
|
||||
// if w == "AND" || w == "OR" || w == "NOT" {
|
||||
|
@ -140,152 +137,194 @@ pub fn db_path() -> Result<PathBuf> {
|
|||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
pub async fn insert(
|
||||
envelope: Envelope,
|
||||
backend: Arc<RwLock<Box<dyn MailBackend>>>,
|
||||
acc_name: String,
|
||||
) -> Result<()> {
|
||||
let db_path = db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Err(Error::new(
|
||||
"Database hasn't been initialised. Run `reindex` command",
|
||||
));
|
||||
}
|
||||
|
||||
let conn = melib_sqlite3::open_db(db_path)?;
|
||||
pub struct AccountCache;
|
||||
|
||||
let op = backend
|
||||
.read()
|
||||
.unwrap()
|
||||
.operation(envelope.hash())?
|
||||
.as_bytes()?;
|
||||
impl AccountCache {
|
||||
pub async fn insert(
|
||||
envelope: Envelope,
|
||||
backend: Arc<RwLock<Box<dyn MailBackend>>>,
|
||||
acc_name: String,
|
||||
) -> Result<()> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.clone().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
|
||||
let body = match op.await.map(|bytes| envelope.body_bytes(&bytes)) {
|
||||
Ok(body) => body.text(),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to open envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
return Err(err);
|
||||
if !db_desc.exists().unwrap_or(false) {
|
||||
return Err(Error::new(format!(
|
||||
"Database hasn't been initialised. Run `reindex {acc_name}` command"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = conn.execute(
|
||||
"INSERT OR IGNORE INTO accounts (name) VALUES (?1)",
|
||||
params![acc_name,],
|
||||
) {
|
||||
log::error!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
return Err(Error::new(err.to_string()));
|
||||
}
|
||||
let account_id: i32 = {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id FROM accounts WHERE name = ?")
|
||||
.unwrap();
|
||||
let x = stmt
|
||||
.query_map(params![acc_name], |row| row.get(0))
|
||||
let op = backend
|
||||
.read()
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
x
|
||||
};
|
||||
if let Err(err) = conn
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, bcc, \
|
||||
subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, \
|
||||
timestamp)
|
||||
.operation(envelope.hash())?
|
||||
.as_bytes()?;
|
||||
|
||||
let body = match op.await.map(|bytes| envelope.body_bytes(&bytes)) {
|
||||
Ok(body) => body.text(),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to open envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
|
||||
let tx =
|
||||
conn.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
|
||||
if let Err(err) = tx.execute(
|
||||
"INSERT OR IGNORE INTO accounts (name) VALUES (?1)",
|
||||
params![acc_name,],
|
||||
) {
|
||||
log::error!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
)));
|
||||
}
|
||||
let account_id: i32 = {
|
||||
let mut stmt = tx
|
||||
.prepare("SELECT id FROM accounts WHERE name = ?")
|
||||
.unwrap();
|
||||
let x = stmt
|
||||
.query_map(params![acc_name], |row| row.get(0))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
x
|
||||
};
|
||||
if let Err(err) = tx
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, \
|
||||
bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, \
|
||||
body_text, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
params![
|
||||
account_id,
|
||||
envelope.hash().to_be_bytes().to_vec(),
|
||||
envelope.date_as_str(),
|
||||
envelope.field_from_to_string(),
|
||||
envelope.field_to_to_string(),
|
||||
envelope.field_cc_to_string(),
|
||||
envelope.field_bcc_to_string(),
|
||||
envelope.subject().into_owned().trim_end_matches('\u{0}'),
|
||||
envelope.message_id_display().to_string(),
|
||||
envelope
|
||||
.in_reply_to_display()
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_default(),
|
||||
envelope.field_references_to_string(),
|
||||
i64::from(envelope.flags().bits()),
|
||||
if envelope.has_attachments() { 1 } else { 0 },
|
||||
body,
|
||||
envelope.date().to_be_bytes().to_vec()
|
||||
],
|
||||
)
|
||||
.map_err(|e| Error::new(e.to_string()))
|
||||
{
|
||||
log::error!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove(env_hash: EnvelopeHash) -> Result<()> {
|
||||
let db_path = db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Err(Error::new(
|
||||
"Database hasn't been initialised. Run `reindex` command",
|
||||
));
|
||||
params![
|
||||
account_id,
|
||||
envelope.hash().to_be_bytes().to_vec(),
|
||||
envelope.date_as_str(),
|
||||
envelope.field_from_to_string(),
|
||||
envelope.field_to_to_string(),
|
||||
envelope.field_cc_to_string(),
|
||||
envelope.field_bcc_to_string(),
|
||||
envelope.subject().into_owned().trim_end_matches('\u{0}'),
|
||||
envelope.message_id_display().to_string(),
|
||||
envelope
|
||||
.in_reply_to_display()
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_default(),
|
||||
envelope.field_references_to_string(),
|
||||
i64::from(envelope.flags().bits()),
|
||||
i32::from(envelope.has_attachments()),
|
||||
body,
|
||||
envelope.date().to_be_bytes().to_vec()
|
||||
],
|
||||
)
|
||||
.map_err(|e| Error::new(e.to_string()))
|
||||
{
|
||||
drop(tx);
|
||||
log::error!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
} else {
|
||||
tx.commit()?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let conn = melib_sqlite3::open_db(db_path)?;
|
||||
if let Err(err) = conn
|
||||
.execute(
|
||||
"DELETE FROM envelopes WHERE hash = ?",
|
||||
params![env_hash.to_be_bytes().to_vec(),],
|
||||
)
|
||||
.map_err(|e| Error::new(e.to_string()))
|
||||
{
|
||||
log::error!("Failed to remove envelope {env_hash}: {err}");
|
||||
return Err(err);
|
||||
pub async fn remove(acc_name: String, env_hash: EnvelopeHash) -> Result<()> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.clone().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
let db_path = db_desc.db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"Database hasn't been initialised. Run `reindex {acc_name}` command"
|
||||
)));
|
||||
}
|
||||
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
let tx =
|
||||
conn.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
|
||||
if let Err(err) = tx.execute(
|
||||
"DELETE FROM envelopes WHERE hash = ?",
|
||||
params![env_hash.to_be_bytes().to_vec(),],
|
||||
) {
|
||||
drop(tx);
|
||||
log::error!("Failed to remove envelope {env_hash}: {err}");
|
||||
return Err(Error::new(format!(
|
||||
"Failed to remove envelope {env_hash}: {err}"
|
||||
)));
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn index(context: &mut crate::state::Context, account_index: usize) -> ResultFuture<()> {
|
||||
let account = &context.accounts[account_index];
|
||||
let (acc_name, acc_mutex, backend_mutex): (String, Arc<RwLock<_>>, Arc<_>) = (
|
||||
account.name().to_string(),
|
||||
account.collection.envelopes.clone(),
|
||||
account.backend.clone(),
|
||||
);
|
||||
let conn = melib_sqlite3::open_or_create_db(&DB, Some(acc_name.as_str()))?;
|
||||
let env_hashes = acc_mutex
|
||||
.read()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
pub async fn index(
|
||||
acc_name: Arc<String>,
|
||||
collection: melib::Collection,
|
||||
backend_mutex: Arc<RwLock<Box<dyn MailBackend>>>,
|
||||
) -> Result<()> {
|
||||
let acc_mutex = collection.envelopes.clone();
|
||||
let db_desc = Arc::new(DatabaseDescription {
|
||||
identifier: Some(acc_name.to_string().into()),
|
||||
..DB.clone()
|
||||
});
|
||||
let env_hashes = acc_mutex
|
||||
.read()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
/* Sleep, index and repeat in order not to block the main process */
|
||||
Ok(Box::pin(async move {
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO accounts (name) VALUES (?1)",
|
||||
params![acc_name.as_str(),],
|
||||
)
|
||||
.chain_err_summary(|| "Failed to update index:")?;
|
||||
/* Sleep, index and repeat in order not to block the main process */
|
||||
let account_id: i32 = {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id FROM accounts WHERE name = ?")
|
||||
.unwrap();
|
||||
let x = stmt
|
||||
.query_map(params![acc_name.as_str()], |row| row.get(0))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
x
|
||||
let acc_name = Arc::clone(&acc_name);
|
||||
let db_desc = Arc::clone(&db_desc);
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
let tx = conn
|
||||
.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO accounts (name) VALUES (?1)",
|
||||
params![acc_name.as_str(),],
|
||||
)
|
||||
.chain_err_summary(|| "Failed to update index:")?;
|
||||
let account_id = {
|
||||
let mut stmt = tx
|
||||
.prepare("SELECT id FROM accounts WHERE name = ?")
|
||||
.unwrap();
|
||||
let x = stmt
|
||||
.query_map(params![acc_name.as_str()], |row| row.get(0))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
x
|
||||
};
|
||||
tx.commit()?;
|
||||
Ok::<i32, Error>(account_id)
|
||||
})
|
||||
.await?
|
||||
};
|
||||
let mut ctr = 0;
|
||||
log::trace!(
|
||||
|
@ -296,90 +335,131 @@ pub fn index(context: &mut crate::state::Context, account_index: usize) -> Resul
|
|||
);
|
||||
for chunk in env_hashes.chunks(200) {
|
||||
ctr += chunk.len();
|
||||
for env_hash in chunk {
|
||||
let mut op = backend_mutex.read().unwrap().operation(*env_hash)?;
|
||||
let mut chunk_bytes = Vec::with_capacity(chunk.len());
|
||||
for &env_hash in chunk {
|
||||
let mut op = backend_mutex.read().unwrap().operation(env_hash)?;
|
||||
let bytes = op
|
||||
.as_bytes()?
|
||||
.await
|
||||
.chain_err_summary(|| format!("Failed to open envelope {}", env_hash))?;
|
||||
let envelopes_lck = acc_mutex.read().unwrap();
|
||||
if let Some(e) = envelopes_lck.get(env_hash) {
|
||||
let body = e.body_bytes(&bytes).text().replace('\0', "");
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, \
|
||||
cc, bcc, subject, message_id, in_reply_to, _references, flags, \
|
||||
has_attachments, body_text, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
params![
|
||||
account_id,
|
||||
e.hash().to_be_bytes().to_vec(),
|
||||
e.date_as_str(),
|
||||
e.field_from_to_string(),
|
||||
e.field_to_to_string(),
|
||||
e.field_cc_to_string(),
|
||||
e.field_bcc_to_string(),
|
||||
e.subject().into_owned().trim_end_matches('\u{0}'),
|
||||
e.message_id_display().to_string(),
|
||||
e.in_reply_to_display()
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_default(),
|
||||
e.field_references_to_string(),
|
||||
i64::from(e.flags().bits()),
|
||||
if e.has_attachments() { 1 } else { 0 },
|
||||
body,
|
||||
e.date().to_be_bytes().to_vec()
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!("Failed to insert envelope {}", e.message_id_display())
|
||||
})?;
|
||||
}
|
||||
chunk_bytes.push((env_hash, bytes));
|
||||
}
|
||||
let sleep_dur = std::time::Duration::from_millis(20);
|
||||
std::thread::sleep(sleep_dur);
|
||||
{
|
||||
let acc_mutex = acc_mutex.clone();
|
||||
let db_desc = Arc::clone(&db_desc);
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
let tx = conn.transaction_with_behavior(
|
||||
melib::rusqlite::TransactionBehavior::Immediate,
|
||||
)?;
|
||||
let envelopes_lck = acc_mutex.read().unwrap();
|
||||
for (env_hash, bytes) in chunk_bytes {
|
||||
if let Some(e) = envelopes_lck.get(&env_hash) {
|
||||
let body = e.body_bytes(&bytes).text().replace('\0', "");
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, \
|
||||
_to, cc, bcc, subject, message_id, in_reply_to, _references, \
|
||||
flags, has_attachments, body_text, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
params![
|
||||
account_id,
|
||||
e.hash().to_be_bytes().to_vec(),
|
||||
e.date_as_str(),
|
||||
e.field_from_to_string(),
|
||||
e.field_to_to_string(),
|
||||
e.field_cc_to_string(),
|
||||
e.field_bcc_to_string(),
|
||||
e.subject().into_owned().trim_end_matches('\u{0}'),
|
||||
e.message_id_display().to_string(),
|
||||
e.in_reply_to_display()
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_default(),
|
||||
e.field_references_to_string(),
|
||||
i64::from(e.flags().bits()),
|
||||
i32::from(e.has_attachments()),
|
||||
body,
|
||||
e.date().to_be_bytes().to_vec()
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!("Failed to insert envelope {}", e.message_id_display())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok::<(), Error>(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
let sleep_dur = std::time::Duration::from_millis(50);
|
||||
smol::Timer::after(sleep_dur).await;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
query: &Query,
|
||||
(sort_field, sort_order): (SortField, SortOrder),
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
let db_path = db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Err(Error::new(
|
||||
"Database hasn't been initialised. Run `reindex` command",
|
||||
));
|
||||
}
|
||||
|
||||
let conn = melib_sqlite3::open_db(db_path)?;
|
||||
pub async fn search(
|
||||
acc_name: String,
|
||||
query: Query,
|
||||
(sort_field, sort_order): (SortField, SortOrder),
|
||||
) -> Result<SmallVec<[EnvelopeHash; 512]>> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.clone().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
|
||||
let sort_field = match sort_field {
|
||||
SortField::Subject => "subject",
|
||||
SortField::Date => "timestamp",
|
||||
};
|
||||
if !db_desc.exists().unwrap_or(false) {
|
||||
return Err(Error::new(format!(
|
||||
"Database hasn't been initialised for account `{}`. Run `reindex` command to \
|
||||
build an index.",
|
||||
acc_name
|
||||
)));
|
||||
}
|
||||
let query = query_to_sql(&query);
|
||||
|
||||
let sort_order = match sort_order {
|
||||
SortOrder::Asc => "ASC",
|
||||
SortOrder::Desc => "DESC",
|
||||
};
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&format!(
|
||||
"SELECT hash FROM envelopes WHERE {} ORDER BY {} {};",
|
||||
query_to_sql(query),
|
||||
sort_field,
|
||||
sort_order
|
||||
))
|
||||
.map_err(|e| Error::new(e.to_string()))?;
|
||||
let sort_field = match sort_field {
|
||||
SortField::Subject => "subject",
|
||||
SortField::Date => "timestamp",
|
||||
};
|
||||
|
||||
let results = stmt
|
||||
.query_map([], |row| row.get::<_, EnvelopeHash>(0))
|
||||
.map_err(Error::from)?
|
||||
.map(|item| item.map_err(Error::from))
|
||||
.collect::<Result<SmallVec<[EnvelopeHash; 512]>>>();
|
||||
Ok(Box::pin(async { results }))
|
||||
let sort_order = match sort_order {
|
||||
SortOrder::Asc => "ASC",
|
||||
SortOrder::Desc => "DESC",
|
||||
};
|
||||
|
||||
let tx = conn.transaction()?;
|
||||
let mut stmt = tx
|
||||
.prepare(&format!(
|
||||
"SELECT hash FROM envelopes WHERE {} ORDER BY {} {};",
|
||||
query, sort_field, sort_order
|
||||
))
|
||||
.map_err(|e| Error::new(e.to_string()))?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map([], |row| row.get::<_, EnvelopeHash>(0))
|
||||
.map_err(Error::from)?
|
||||
.map(|item| item.map_err(Error::from))
|
||||
.collect::<Result<SmallVec<[EnvelopeHash; 512]>>>();
|
||||
x
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn db_path(acc_name: &str) -> Result<Option<PathBuf>> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.to_string().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
let db_path = db_desc.db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(db_path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Translates a `Query` to an Sqlite3 expression in a `String`.
|
||||
|
|
|
@ -35,9 +35,10 @@
|
|||
//! [`ThreadEvent`].
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeSet,
|
||||
env,
|
||||
os::unix::io::RawFd,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
thread,
|
||||
|
@ -59,7 +60,7 @@ use crate::{
|
|||
};
|
||||
|
||||
struct InputHandler {
|
||||
pipe: (RawFd, RawFd),
|
||||
pipe: (OwnedFd, OwnedFd),
|
||||
rx: Receiver<InputCommand>,
|
||||
tx: Sender<InputCommand>,
|
||||
state_tx: Sender<ThreadEvent>,
|
||||
|
@ -76,17 +77,19 @@ impl InputHandler {
|
|||
* thread will receive it and die. */
|
||||
//let _ = self.rx.try_iter().count();
|
||||
let rx = self.rx.clone();
|
||||
let pipe = self.pipe.0;
|
||||
let pipe = nix::unistd::dup(self.pipe.0.as_raw_fd())
|
||||
.expect("Fatal: Could not dup() input pipe file descriptor");
|
||||
let tx = self.state_tx.clone();
|
||||
thread::Builder::new()
|
||||
.name("input-thread".to_string())
|
||||
.spawn(move || {
|
||||
let pipe = unsafe { OwnedFd::from_raw_fd(pipe) };
|
||||
get_events(
|
||||
|i| {
|
||||
tx.send(ThreadEvent::Input(i)).unwrap();
|
||||
},
|
||||
&rx,
|
||||
pipe,
|
||||
&pipe,
|
||||
working,
|
||||
)
|
||||
})
|
||||
|
@ -95,7 +98,7 @@ impl InputHandler {
|
|||
}
|
||||
|
||||
fn kill(&self) {
|
||||
let _ = nix::unistd::write(self.pipe.1, &[1]);
|
||||
let _ = nix::unistd::write(self.pipe.1.as_raw_fd(), &[1]);
|
||||
self.tx.send(InputCommand::Kill).unwrap();
|
||||
}
|
||||
|
||||
|
@ -141,8 +144,8 @@ pub struct Context {
|
|||
receiver: Receiver<ThreadEvent>,
|
||||
input_thread: InputHandler,
|
||||
current_dir: PathBuf,
|
||||
pub children: Vec<std::process::Child>,
|
||||
|
||||
/// Children processes
|
||||
pub children: IndexMap<Cow<'static, str>, Vec<std::process::Child>>,
|
||||
pub temp_files: Vec<File>,
|
||||
}
|
||||
|
||||
|
@ -160,7 +163,7 @@ impl Context {
|
|||
}
|
||||
|
||||
pub fn is_online_idx(&mut self, account_pos: usize) -> Result<()> {
|
||||
let Context {
|
||||
let Self {
|
||||
ref mut accounts,
|
||||
ref mut replies,
|
||||
..
|
||||
|
@ -203,9 +206,7 @@ impl Context {
|
|||
crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>());
|
||||
let job_executor = Arc::new(JobExecutor::new(sender.clone()));
|
||||
let input_thread = unbounded();
|
||||
let input_thread_pipe = nix::unistd::pipe()
|
||||
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
|
||||
.unwrap();
|
||||
let input_thread_pipe = crate::types::pipe().unwrap();
|
||||
let backends = Backends::new();
|
||||
let settings = Box::new(Settings::new().unwrap());
|
||||
let accounts = vec![{
|
||||
|
@ -241,7 +242,7 @@ impl Context {
|
|||
let accounts = accounts.into_iter().map(|acc| (acc.hash(), acc)).collect();
|
||||
let working = Arc::new(());
|
||||
let control = Arc::downgrade(&working);
|
||||
Context {
|
||||
Self {
|
||||
accounts,
|
||||
settings,
|
||||
dirty_areas: VecDeque::with_capacity(0),
|
||||
|
@ -250,7 +251,7 @@ impl Context {
|
|||
unrealized: IndexSet::default(),
|
||||
temp_files: Vec::new(),
|
||||
current_dir: std::env::current_dir().unwrap(),
|
||||
children: vec![],
|
||||
children: IndexMap::default(),
|
||||
|
||||
input_thread: InputHandler {
|
||||
pipe: input_thread_pipe,
|
||||
|
@ -293,18 +294,38 @@ impl Drop for State {
|
|||
// When done, restore the defaults to avoid messing with the terminal.
|
||||
self.screen.switch_to_main_screen();
|
||||
use nix::sys::wait::{waitpid, WaitPidFlag};
|
||||
for child in self.context.children.iter_mut() {
|
||||
if let Err(err) = waitpid(
|
||||
nix::unistd::Pid::from_raw(child.id() as i32),
|
||||
Some(WaitPidFlag::WNOHANG),
|
||||
) {
|
||||
log::warn!("Failed to wait on subprocess {}: {}", child.id(), err);
|
||||
}
|
||||
for (id, pid, err) in self
|
||||
.context
|
||||
.children
|
||||
.iter_mut()
|
||||
.flat_map(|(i, v)| v.drain(..).map(move |v| (i, v)))
|
||||
.filter_map(|(id, child)| {
|
||||
if let Err(err) = waitpid(
|
||||
nix::unistd::Pid::from_raw(child.id() as i32),
|
||||
Some(WaitPidFlag::WNOHANG),
|
||||
) {
|
||||
Some((id, child.id(), err))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
log::trace!("Failed to wait on subprocess {} ({}): {}", id, pid, err);
|
||||
}
|
||||
if let Some(ForkType::Embedded(child_pid)) = self.child.take() {
|
||||
if let Some(ForkType::Embedded { id, command, pid }) = self.child.take() {
|
||||
/* Try wait, we don't want to block */
|
||||
if let Err(e) = waitpid(child_pid, Some(WaitPidFlag::WNOHANG)) {
|
||||
log::warn!("Failed to wait on subprocess {}: {}", child_pid, e);
|
||||
if let Err(err) = waitpid(pid, Some(WaitPidFlag::WNOHANG)) {
|
||||
log::trace!(
|
||||
"Failed to wait on embedded process {} {} ({}): {}",
|
||||
id,
|
||||
if let Some(v) = command.as_ref() {
|
||||
v.as_ref()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
pid,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,8 +342,7 @@ impl State {
|
|||
* it from reading stdin, see get_events() for details
|
||||
*/
|
||||
let input_thread = unbounded();
|
||||
let input_thread_pipe = nix::unistd::pipe()
|
||||
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)?;
|
||||
let input_thread_pipe = crate::types::pipe()?;
|
||||
let backends = Backends::new();
|
||||
let settings = Box::new(if let Some(settings) = settings {
|
||||
settings
|
||||
|
@ -410,7 +430,7 @@ impl State {
|
|||
Screen::draw_horizontal_segment_no_color
|
||||
});
|
||||
let message_box = DisplayMessageBox::new(&screen);
|
||||
let mut s = State {
|
||||
let mut s = Self {
|
||||
screen,
|
||||
child: None,
|
||||
mode: UIMode::Normal,
|
||||
|
@ -429,7 +449,7 @@ impl State {
|
|||
unrealized: IndexSet::default(),
|
||||
temp_files: Vec::new(),
|
||||
current_dir: std::env::current_dir()?,
|
||||
children: vec![],
|
||||
children: IndexMap::default(),
|
||||
|
||||
input_thread: InputHandler {
|
||||
pipe: input_thread_pipe,
|
||||
|
@ -536,6 +556,8 @@ impl State {
|
|||
}
|
||||
let mut areas: smallvec::SmallVec<[Area; 8]> =
|
||||
self.context.dirty_areas.drain(0..).collect();
|
||||
|
||||
let can_draw_above_screen: bool = !matches!(self.mode, UIMode::Embedded | UIMode::Fork);
|
||||
if self.message_box.active {
|
||||
let now = datetime::now();
|
||||
if self
|
||||
|
@ -555,7 +577,7 @@ impl State {
|
|||
/* Sort by x_start, ie upper_left corner's x coordinate */
|
||||
areas.sort_by(|a, b| a.upper_left().0.partial_cmp(&b.upper_left().0).unwrap());
|
||||
|
||||
if self.message_box.active {
|
||||
if self.message_box.active && can_draw_above_screen {
|
||||
/* Check if any dirty area intersects with the area occupied by
|
||||
* floating notification box */
|
||||
let displ = self.message_box.cached_area();
|
||||
|
@ -606,7 +628,7 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
if self.message_box.is_dirty() && self.message_box.active {
|
||||
if self.message_box.is_dirty() && self.message_box.active && can_draw_above_screen {
|
||||
if !self.message_box.is_empty() {
|
||||
if !self.message_box.initialised {
|
||||
{
|
||||
|
@ -639,7 +661,7 @@ impl State {
|
|||
}
|
||||
}
|
||||
self.message_box.set_dirty(false);
|
||||
} else if self.message_box.is_dirty() {
|
||||
} else if self.message_box.is_dirty() && can_draw_above_screen {
|
||||
/* Clear area previously occupied by floating notification box */
|
||||
if self.message_box.cached_area().generation() == self.screen.area().generation() {
|
||||
for row in self
|
||||
|
@ -654,7 +676,7 @@ impl State {
|
|||
self.message_box.set_dirty(false);
|
||||
}
|
||||
|
||||
if !self.overlay.is_empty() {
|
||||
if !self.overlay.is_empty() && can_draw_above_screen {
|
||||
let area: Area = self.screen.area();
|
||||
let overlay_area = area.center_inside((
|
||||
if self.screen.cols() / 3 > 30 {
|
||||
|
@ -703,7 +725,7 @@ impl State {
|
|||
}
|
||||
|
||||
pub fn can_quit_cleanly(&mut self) -> bool {
|
||||
let State {
|
||||
let Self {
|
||||
ref mut components,
|
||||
ref context,
|
||||
..
|
||||
|
@ -800,38 +822,36 @@ impl State {
|
|||
});
|
||||
return;
|
||||
}
|
||||
match crate::sqlite3::index(&mut self.context, account_index) {
|
||||
Ok(job) => {
|
||||
let handle = self
|
||||
.context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_blocking("sqlite3::index".into(), job);
|
||||
self.context.accounts[account_index].active_jobs.insert(
|
||||
handle.job_id,
|
||||
crate::accounts::JobRequest::Generic {
|
||||
name: "Message index rebuild".into(),
|
||||
handle,
|
||||
on_finish: None,
|
||||
log_level: LogLevel::INFO,
|
||||
},
|
||||
);
|
||||
self.context.replies.push_back(UIEvent::Notification {
|
||||
title: None,
|
||||
source: None,
|
||||
body: "Message index rebuild started.".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
self.context.replies.push_back(UIEvent::Notification {
|
||||
title: Some("Message index rebuild failed".into()),
|
||||
source: None,
|
||||
body: err.to_string().into(),
|
||||
kind: Some(NotificationType::Error(err.kind)),
|
||||
});
|
||||
}
|
||||
}
|
||||
let account = &self.context.accounts[account_index];
|
||||
let (acc_name, backend_mutex): (Arc<String>, Arc<_>) = (
|
||||
Arc::new(account.name().to_string()),
|
||||
account.backend.clone(),
|
||||
);
|
||||
let job = crate::sqlite3::AccountCache::index(
|
||||
acc_name,
|
||||
account.collection.clone(),
|
||||
backend_mutex,
|
||||
);
|
||||
let handle = self
|
||||
.context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized("sqlite3::index".into(), job);
|
||||
self.context.accounts[account_index].active_jobs.insert(
|
||||
handle.job_id,
|
||||
crate::accounts::JobRequest::Generic {
|
||||
name: "Message index rebuild".into(),
|
||||
handle,
|
||||
on_finish: None,
|
||||
log_level: LogLevel::INFO,
|
||||
},
|
||||
);
|
||||
self.context.replies.push_back(UIEvent::Notification {
|
||||
title: None,
|
||||
source: None,
|
||||
body: "Message index rebuild started.".into(),
|
||||
kind: Some(NotificationType::Info),
|
||||
});
|
||||
}
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
AccountAction(_, ReIndex) => {
|
||||
|
@ -839,7 +859,7 @@ impl State {
|
|||
title: None,
|
||||
source: None,
|
||||
body: "Message index rebuild failed: meli is not built with sqlite3 support."
|
||||
.to_string(),
|
||||
.into(),
|
||||
kind: Some(NotificationType::Error(ErrorKind::None)),
|
||||
});
|
||||
}
|
||||
|
@ -894,6 +914,27 @@ impl State {
|
|||
)))
|
||||
.unwrap();
|
||||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
Tab(Man(manpage)) => match manpage
|
||||
.read(false)
|
||||
.map(|text| crate::manpages::ManPages::remove_markup(&text).unwrap_or(text))
|
||||
{
|
||||
Ok(m) => self.rcv_event(UIEvent::Action(Tab(New(Some(Box::new(
|
||||
Pager::from_string(
|
||||
m,
|
||||
&self.context,
|
||||
None,
|
||||
None,
|
||||
crate::conf::value(&self.context, "theme_default"),
|
||||
),
|
||||
)))))),
|
||||
Err(err) => self.context.replies.push_back(UIEvent::Notification {
|
||||
title: None,
|
||||
body: "Encountered an error while retrieving manual page.".into(),
|
||||
source: Some(err),
|
||||
kind: Some(NotificationType::Error(ErrorKind::Bug)),
|
||||
}),
|
||||
},
|
||||
v => {
|
||||
self.rcv_event(UIEvent::Action(v));
|
||||
}
|
||||
|
@ -928,8 +969,8 @@ impl State {
|
|||
));
|
||||
|
||||
self.overlay.insert(new.id(), new);
|
||||
} else if let Action::ReloadConfiguration = action {
|
||||
match Settings::new().and_then(|new_settings| {
|
||||
} else if matches!(action, Action::ReloadConfiguration) {
|
||||
let res = Settings::new().and_then(|new_settings| {
|
||||
let old_accounts = self
|
||||
.context
|
||||
.settings
|
||||
|
@ -962,7 +1003,8 @@ impl State {
|
|||
return Err("No changes detected.".into());
|
||||
}
|
||||
Ok(Box::new(new_settings))
|
||||
}) {
|
||||
});
|
||||
match res {
|
||||
Ok(new_settings) => {
|
||||
let old_settings =
|
||||
std::mem::replace(&mut self.context.settings, new_settings);
|
||||
|
@ -986,7 +1028,7 @@ impl State {
|
|||
}
|
||||
Err(err) => {
|
||||
self.context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!("invalid command: {err}")),
|
||||
StatusEvent::DisplayMessage(format!("Invalid command `{cmd}`: {err}")),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -1005,8 +1047,12 @@ impl State {
|
|||
self.context.restore_input();
|
||||
return;
|
||||
}
|
||||
UIEvent::Fork(ForkType::Generic(child)) => {
|
||||
self.context.children.push(child);
|
||||
UIEvent::Fork(ForkType::Generic {
|
||||
id,
|
||||
command: _,
|
||||
child,
|
||||
}) => {
|
||||
self.context.children.entry(id).or_default().push(child);
|
||||
return;
|
||||
}
|
||||
UIEvent::Fork(child) => {
|
||||
|
@ -1194,24 +1240,27 @@ impl State {
|
|||
|
||||
pub fn try_wait_on_child(&mut self) -> Option<bool> {
|
||||
let should_return_flag = match self.child {
|
||||
Some(ForkType::NewDraft(_, ref mut c)) => {
|
||||
let w = c.try_wait();
|
||||
Some(ForkType::Generic {
|
||||
ref id,
|
||||
ref command,
|
||||
ref mut child,
|
||||
}) => {
|
||||
let w = child.try_wait();
|
||||
match w {
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => false,
|
||||
Err(err) => {
|
||||
log::error!("Failed to wait on editor process: {err}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ForkType::Generic(ref mut c)) => {
|
||||
let w = c.try_wait();
|
||||
match w {
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => false,
|
||||
Err(err) => {
|
||||
log::error!("Failed to wait on child process: {err}");
|
||||
log::error!(
|
||||
"Failed to wait on child process {} {} ({}): {}",
|
||||
id,
|
||||
if let Some(v) = command.as_ref() {
|
||||
v.as_ref()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
child.id(),
|
||||
err
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,15 +27,13 @@ use std::{
|
|||
};
|
||||
|
||||
use crossbeam::channel::{Receiver, Sender};
|
||||
#[cfg(feature = "cli-docs")]
|
||||
use flate2::bufread::GzDecoder;
|
||||
use melib::Result;
|
||||
use melib::{Result, ShellExpandTrait};
|
||||
|
||||
use crate::{args::*, *};
|
||||
use crate::*;
|
||||
|
||||
pub fn create_config(path: Option<PathBuf>) -> Result<()> {
|
||||
let config_path = if let Some(path) = path {
|
||||
path
|
||||
path.expand()
|
||||
} else {
|
||||
conf::get_config_file()?
|
||||
};
|
||||
|
@ -74,33 +72,7 @@ pub fn edit_config() -> Result<()> {
|
|||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub fn man(page: manpages::ManPages, source: bool) -> Result<String> {
|
||||
const MANPAGES: [&[u8]; 4] = [
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")),
|
||||
];
|
||||
const MANPAGES_MDOC: [&[u8]; 4] = [
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.mdoc.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.mdoc.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.mdoc.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.mdoc.gz")),
|
||||
];
|
||||
|
||||
let mut gz = GzDecoder::new(if source {
|
||||
MANPAGES_MDOC[page as usize]
|
||||
} else {
|
||||
MANPAGES[page as usize]
|
||||
});
|
||||
let mut v = String::with_capacity(
|
||||
str::parse::<usize>(unsafe {
|
||||
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("{:?} was not compressed with size comment header", page)),
|
||||
);
|
||||
gz.read_to_string(&mut v)?;
|
||||
|
||||
Ok(v)
|
||||
page.read(source)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
|
@ -133,8 +105,8 @@ pub fn pager(v: String, no_raw: Option<Option<bool>>) -> Result<()> {
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
pub fn man(_: ManOpt) -> Result<()> {
|
||||
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"))
|
||||
pub fn man(_: crate::args::ManOpt) -> Result<()> {
|
||||
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
|
||||
}
|
||||
|
||||
pub fn compiled_with() -> Result<()> {
|
||||
|
@ -159,7 +131,7 @@ pub fn compiled_with() -> Result<()> {
|
|||
|
||||
pub fn test_config(path: Option<PathBuf>) -> Result<()> {
|
||||
let config_path = if let Some(path) = path {
|
||||
path
|
||||
path.expand()
|
||||
} else {
|
||||
crate::conf::get_config_file()?
|
||||
};
|
||||
|
@ -172,6 +144,7 @@ pub fn view(
|
|||
sender: Sender<ThreadEvent>,
|
||||
receiver: Receiver<ThreadEvent>,
|
||||
) -> Result<State> {
|
||||
let path = path.expand();
|
||||
if !path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"`{}` is not a valid path",
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
use std::{collections::BTreeMap, io::Write};
|
||||
|
||||
use super::*;
|
||||
use crate::melib::text_processing::TextProcessing;
|
||||
use crate::melib::text::TextProcessing;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SVGScreenshotFilter {
|
||||
|
@ -37,7 +37,7 @@ impl std::fmt::Display for SVGScreenshotFilter {
|
|||
|
||||
impl SVGScreenshotFilter {
|
||||
pub fn new() -> Self {
|
||||
SVGScreenshotFilter {
|
||||
Self {
|
||||
save_screenshot: false,
|
||||
id: ComponentId::default(),
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ impl Component for SVGScreenshotFilter {
|
|||
* rectangle element
|
||||
* inserted along with the `use` elements
|
||||
*
|
||||
* Each row is arbritarily set at 17px high, and each character cell is 8
|
||||
* Each row is arbitrarily set at 17px high, and each character cell is 8
|
||||
* pixels wide. Rectangle cells each have one extra pixel (so 18px *
|
||||
* 9px) in their dimensions in order to cover the spacing between
|
||||
* cells.
|
||||
|
@ -98,7 +98,7 @@ impl Component for SVGScreenshotFilter {
|
|||
* - Whenever the foreground color changes, emit a text element with the
|
||||
* accumulated
|
||||
* text in the specific foreground color.
|
||||
* - Whenever the backgrund color changes, emit a rectangle element filled
|
||||
* - Whenever the background color changes, emit a rectangle element filled
|
||||
* with the
|
||||
* specific background color.
|
||||
*/
|
||||
|
@ -431,20 +431,22 @@ impl Component for SVGScreenshotFilter {
|
|||
kind: None,
|
||||
});
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
||||
if let UIEvent::Input(Key::F(6)) = event {
|
||||
if matches!(
|
||||
event,
|
||||
UIEvent::Input(Key::F(6)) | UIEvent::CmdInput(Key::F(6))
|
||||
) {
|
||||
self.save_screenshot = true;
|
||||
true
|
||||
} else if let UIEvent::CmdInput(Key::F(6)) = event {
|
||||
self.save_screenshot = true;
|
||||
true
|
||||
} else if let UIEvent::EmbeddedInput((Key::F(6), _)) = event {
|
||||
} else if matches!(event, UIEvent::EmbeddedInput((Key::F(6), _))) {
|
||||
self.save_screenshot = true;
|
||||
false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, _value: bool) {}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
|
|
|
@ -28,8 +28,6 @@ mod color;
|
|||
mod screen;
|
||||
pub use color::*;
|
||||
#[macro_use]
|
||||
pub mod position;
|
||||
#[macro_use]
|
||||
pub mod cells;
|
||||
#[macro_use]
|
||||
pub mod keys;
|
||||
|
@ -41,7 +39,31 @@ use std::io::{BufRead, Write};
|
|||
pub use braille::BraillePixelIter;
|
||||
pub use screen::{Area, Screen, ScreenGeneration, StateStdout, Tty, Virtual};
|
||||
|
||||
pub use self::{cells::*, keys::*, position::*, text_editing::*};
|
||||
pub use self::{cells::*, keys::*, text_editing::*};
|
||||
|
||||
/// A type alias for a `(x, y)` position on screen.
|
||||
pub type Pos = (usize, usize);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Alignment {
|
||||
/// Stretch to fill all space if possible, center if no meaningful way to
|
||||
/// stretch.
|
||||
Fill,
|
||||
/// Snap to left or top side, leaving space on right or bottom.
|
||||
Start,
|
||||
/// Snap to right or bottom side, leaving space on left or top.
|
||||
End,
|
||||
/// Center natural width of widget inside the allocation.
|
||||
#[default]
|
||||
Center,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! emoji_text_presentation_selector {
|
||||
() => {
|
||||
'\u{FE0E}'
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* CSI events we use
|
||||
|
|
|
@ -145,7 +145,7 @@ pub struct BraillePixelIter {
|
|||
|
||||
impl From<&[u16]> for BraillePixelIter {
|
||||
fn from(from: &[u16]) -> Self {
|
||||
BraillePixelIter {
|
||||
Self {
|
||||
columns: [
|
||||
Braille16bitColumn {
|
||||
bitmaps: (
|
||||
|
|
|
@ -30,12 +30,12 @@ use std::{
|
|||
|
||||
use melib::{
|
||||
log,
|
||||
text_processing::{search::KMP, wcwidth},
|
||||
text::{search::KMP, wcwidth},
|
||||
};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::{position::*, Area, Color, ScreenGeneration};
|
||||
use super::{Area, Color, Pos, ScreenGeneration};
|
||||
use crate::{state::Context, ThemeAttribute};
|
||||
|
||||
/// In a scroll region up and down cursor movements shift the region vertically.
|
||||
|
@ -204,7 +204,13 @@ impl CellBuffer {
|
|||
}
|
||||
|
||||
let blank = blank.unwrap_or(self.default_cell);
|
||||
self.buf = vec![blank; newlen];
|
||||
let oldbuf = std::mem::replace(&mut self.buf, vec![blank; newlen]);
|
||||
let (oldcols, oldrows) = (self.cols, self.rows);
|
||||
for y in 0..oldrows.min(newrows) {
|
||||
let row_length = oldcols.min(newcols);
|
||||
self.buf[y * newcols..(y * newcols + row_length)]
|
||||
.copy_from_slice(&oldbuf[y * oldcols..(y * oldcols + row_length)]);
|
||||
}
|
||||
self.cols = newcols;
|
||||
self.rows = newrows;
|
||||
true
|
||||
|
@ -393,6 +399,16 @@ impl CellBuffer {
|
|||
return BoundsIterator::empty(self.generation());
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_x(p: Pos) -> usize {
|
||||
p.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_y(p: Pos) -> usize {
|
||||
p.1
|
||||
}
|
||||
|
||||
BoundsIterator {
|
||||
width: area.width(),
|
||||
height: area.height(),
|
||||
|
@ -499,20 +515,6 @@ impl CellBuffer {
|
|||
|
||||
/// Change foreground and background colors in an `Area`
|
||||
pub fn change_colors(&mut self, area: Area, fg_color: Color, bg_color: Color) {
|
||||
if cfg!(feature = "debug-tracing") {
|
||||
let bounds = self.size();
|
||||
let upper_left = area.upper_left();
|
||||
let bottom_right = area.bottom_right();
|
||||
let (x, y) = upper_left;
|
||||
if y > (get_y(bottom_right))
|
||||
|| x > get_x(bottom_right)
|
||||
|| y >= get_y(bounds)
|
||||
|| x >= get_x(bounds)
|
||||
{
|
||||
log::debug!("BUG: Invalid area in change_colors:\n area: {:?}", area);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for row in self.bounds_iter(area) {
|
||||
for c in row {
|
||||
self[c].set_fg(fg_color).set_bg(bg_color);
|
||||
|
@ -522,20 +524,6 @@ impl CellBuffer {
|
|||
|
||||
/// Change [`ThemeAttribute`] in an `Area`
|
||||
pub fn change_theme(&mut self, area: Area, theme: ThemeAttribute) {
|
||||
if cfg!(feature = "debug-tracing") {
|
||||
let bounds = self.size();
|
||||
let upper_left = area.upper_left();
|
||||
let bottom_right = area.bottom_right();
|
||||
let (x, y) = upper_left;
|
||||
if y > (get_y(bottom_right))
|
||||
|| x > get_x(bottom_right)
|
||||
|| y >= get_y(bounds)
|
||||
|| x >= get_x(bounds)
|
||||
{
|
||||
log::debug!("BUG: Invalid area in change_theme:\n area: {:?}", area);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for row in self.bounds_iter(area) {
|
||||
for c in row {
|
||||
self[c]
|
||||
|
@ -563,6 +551,16 @@ impl CellBuffer {
|
|||
return dest.upper_left();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_x(p: Pos) -> usize {
|
||||
p.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_y(p: Pos) -> usize {
|
||||
p.1
|
||||
}
|
||||
|
||||
let mut ret = dest.bottom_right();
|
||||
let mut src_x = get_x(src.upper_left());
|
||||
let mut src_y = get_y(src.upper_left());
|
||||
|
@ -643,10 +641,22 @@ impl CellBuffer {
|
|||
if area.is_empty() {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_x(p: Pos) -> usize {
|
||||
p.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_y(p: Pos) -> usize {
|
||||
p.1
|
||||
}
|
||||
|
||||
let mut bounds = self.size();
|
||||
let upper_left = area.upper_left();
|
||||
let bottom_right = area.bottom_right();
|
||||
let (mut x, mut y) = upper_left;
|
||||
let mut prev_coords = upper_left;
|
||||
if y == get_y(bounds) || x == get_x(bounds) {
|
||||
if self.growable {
|
||||
if !self.resize(
|
||||
|
@ -681,10 +691,17 @@ impl CellBuffer {
|
|||
}
|
||||
}
|
||||
for c in s.chars() {
|
||||
if c == crate::emoji_text_presentation_selector!() {
|
||||
let prev_attrs = self[prev_coords].attrs();
|
||||
self[prev_coords].set_attrs(prev_attrs | Attr::FORCE_TEXT);
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '\r' {
|
||||
continue;
|
||||
}
|
||||
if c == '\n' {
|
||||
prev_coords = (x, y);
|
||||
y += 1;
|
||||
if let Some(_x) = line_break {
|
||||
x = _x + get_x(upper_left);
|
||||
|
@ -707,6 +724,7 @@ impl CellBuffer {
|
|||
}
|
||||
break;
|
||||
}
|
||||
prev_coords = (x, y);
|
||||
if c == '\t' {
|
||||
self[(x, y)].set_ch(' ');
|
||||
x += 1;
|
||||
|
@ -993,7 +1011,10 @@ impl Cell {
|
|||
self.attrs
|
||||
}
|
||||
|
||||
pub fn set_attrs(&mut self, newattrs: Attr) -> &mut Self {
|
||||
pub fn set_attrs(&mut self, mut newattrs: Attr) -> &mut Self {
|
||||
if self.attrs.intersects(Attr::FORCE_TEXT) {
|
||||
newattrs |= Attr::FORCE_TEXT;
|
||||
}
|
||||
if !self.keep_attrs {
|
||||
self.attrs = newattrs;
|
||||
}
|
||||
|
@ -1076,14 +1097,15 @@ bitflags::bitflags! {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Attr: u8 {
|
||||
/// Terminal default.
|
||||
const DEFAULT = 0b000_0000;
|
||||
const BOLD = 0b000_0001;
|
||||
const DIM = 0b000_0010;
|
||||
const ITALICS = 0b000_0100;
|
||||
const UNDERLINE = 0b000_1000;
|
||||
const BLINK = 0b001_0000;
|
||||
const REVERSE = 0b010_0000;
|
||||
const HIDDEN = 0b100_0000;
|
||||
const DEFAULT = 0;
|
||||
const BOLD = 1;
|
||||
const DIM = Self::BOLD.bits() << 1;
|
||||
const ITALICS = Self::DIM.bits() << 1;
|
||||
const UNDERLINE = Self::ITALICS.bits() << 1;
|
||||
const BLINK = Self::UNDERLINE.bits() << 1;
|
||||
const REVERSE = Self::BLINK.bits() << 1;
|
||||
const HIDDEN = Self::REVERSE.bits() << 1;
|
||||
const FORCE_TEXT = Self::HIDDEN.bits() << 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1104,6 +1126,7 @@ impl std::fmt::Display for Attr {
|
|||
Self::BLINK => write!(f, "Blink"),
|
||||
Self::REVERSE => write!(f, "Reverse"),
|
||||
Self::HIDDEN => write!(f, "Hidden"),
|
||||
Self::FORCE_TEXT => write!(f, "ForceTextRepresentation"),
|
||||
combination => {
|
||||
let mut ctr = 0;
|
||||
if combination.intersects(Self::BOLD) {
|
||||
|
@ -1151,6 +1174,12 @@ impl std::fmt::Display for Attr {
|
|||
}
|
||||
Self::HIDDEN.fmt(f)?;
|
||||
}
|
||||
if combination.intersects(Self::FORCE_TEXT) {
|
||||
if ctr > 0 {
|
||||
write!(f, "|")?;
|
||||
}
|
||||
Self::FORCE_TEXT.fmt(f)?;
|
||||
}
|
||||
write!(f, "")
|
||||
}
|
||||
}
|
||||
|
@ -1193,6 +1222,7 @@ impl Attr {
|
|||
"Blink" => Ok(Self::BLINK),
|
||||
"Reverse" => Ok(Self::REVERSE),
|
||||
"Hidden" => Ok(Self::HIDDEN),
|
||||
"ForceTextRepresentation" => Ok(Self::FORCE_TEXT),
|
||||
combination if combination.contains('|') => {
|
||||
let mut ret = Self::DEFAULT;
|
||||
for c in combination.trim().split('|') {
|
||||
|
@ -1706,31 +1736,30 @@ pub mod boundaries {
|
|||
/// Returns the inner area of the created box.
|
||||
pub fn create_box(grid: &mut CellBuffer, area: Area) -> Area {
|
||||
debug_assert_eq!(grid.generation(), area.generation());
|
||||
let upper_left = area.upper_left();
|
||||
let bottom_right = area.bottom_right();
|
||||
|
||||
if !grid.ascii_drawing {
|
||||
for x in get_x(upper_left)..get_x(bottom_right) {
|
||||
grid[(x, get_y(upper_left))].set_ch(HORZ_BOUNDARY);
|
||||
grid[(x, get_y(bottom_right))].set_ch(HORZ_BOUNDARY);
|
||||
for (top, bottom) in grid
|
||||
.bounds_iter(area.nth_row(0))
|
||||
.zip(grid.bounds_iter(area.nth_row(area.height().saturating_sub(1))))
|
||||
{
|
||||
for c in top.chain(bottom) {
|
||||
grid[c].set_ch(HORZ_BOUNDARY);
|
||||
}
|
||||
}
|
||||
|
||||
for y in get_y(upper_left)..get_y(bottom_right) {
|
||||
grid[(get_x(upper_left), y)].set_ch(VERT_BOUNDARY);
|
||||
grid[(get_x(bottom_right), y)].set_ch(VERT_BOUNDARY);
|
||||
for (left, right) in grid
|
||||
.bounds_iter(area.nth_col(0))
|
||||
.zip(grid.bounds_iter(area.nth_col(area.width().saturating_sub(1))))
|
||||
{
|
||||
for c in left.chain(right) {
|
||||
grid[c].set_ch(VERT_BOUNDARY);
|
||||
}
|
||||
}
|
||||
set_and_join_box(grid, upper_left, BoxBoundary::Horizontal);
|
||||
set_and_join_box(
|
||||
grid,
|
||||
set_x(upper_left, get_x(bottom_right)),
|
||||
BoxBoundary::Horizontal,
|
||||
);
|
||||
set_and_join_box(
|
||||
grid,
|
||||
set_y(upper_left, get_y(bottom_right)),
|
||||
BoxBoundary::Vertical,
|
||||
);
|
||||
set_and_join_box(grid, bottom_right, BoxBoundary::Vertical);
|
||||
|
||||
set_and_join_box(grid, area.upper_left(), BoxBoundary::Horizontal);
|
||||
set_and_join_box(grid, area.upper_right(), BoxBoundary::Horizontal);
|
||||
set_and_join_box(grid, area.bottom_left(), BoxBoundary::Vertical);
|
||||
set_and_join_box(grid, area.bottom_right(), BoxBoundary::Vertical);
|
||||
}
|
||||
|
||||
area.skip(1, 1).skip_rows_from_end(1).skip_cols_from_end(1)
|
||||
|
@ -1828,7 +1857,7 @@ pub enum WidgetWidth {
|
|||
mod tests {
|
||||
use crate::terminal::{Screen, Virtual};
|
||||
|
||||
//use melib::text_processing::{Reflow, TextProcessing, _ALICE_CHAPTER_1};
|
||||
//use melib::text::{Reflow, TextProcessing, _ALICE_CHAPTER_1};
|
||||
|
||||
#[test]
|
||||
fn test_cellbuffer_search() {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,6 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::{
|
||||
ffi::{CString, OsStr},
|
||||
os::unix::{
|
||||
|
@ -69,16 +71,16 @@ ioctl_write_ptr_bad!(
|
|||
);
|
||||
|
||||
ioctl_none_bad!(
|
||||
/// Set controling terminal fd for current session.
|
||||
/// Set controlling terminal fd for current session.
|
||||
set_controlling_terminal,
|
||||
TIOCSCTTY
|
||||
);
|
||||
|
||||
/// Create a new pseudoterminal (PTY) with given width, size and execute
|
||||
/// `command` in it.
|
||||
pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mutex<Terminal>>> {
|
||||
pub fn create_pty(width: usize, height: usize, command: &str) -> Result<Arc<Mutex<Terminal>>> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let (frontend_fd, backend_name) = {
|
||||
let (frontend_fd, backend_name): (nix::pty::PtyMaster, String) = {
|
||||
// Open a new PTY frontend
|
||||
let frontend_fd = posix_openpt(OFlag::O_RDWR)?;
|
||||
|
||||
|
@ -103,7 +105,7 @@ pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mu
|
|||
(frontend_fd, backend_name)
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let (frontend_fd, backend_fd) = {
|
||||
let (frontend_fd, backend_fd): (OwnedFd, OwnedFd) = {
|
||||
let winsize = Winsize {
|
||||
ws_row: <u16>::try_from(height).unwrap(),
|
||||
ws_col: <u16>::try_from(width).unwrap(),
|
||||
|
@ -122,12 +124,12 @@ pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mu
|
|||
let backend_fd = open(Path::new(&backend_name), OFlag::O_RDWR, stat::Mode::empty())?;
|
||||
|
||||
// assign stdin, stdout, stderr to the pty
|
||||
dup2(backend_fd, STDIN_FILENO).unwrap();
|
||||
dup2(backend_fd, STDOUT_FILENO).unwrap();
|
||||
dup2(backend_fd, STDERR_FILENO).unwrap();
|
||||
dup2(backend_fd.as_raw_fd(), STDIN_FILENO).unwrap();
|
||||
dup2(backend_fd.as_raw_fd(), STDOUT_FILENO).unwrap();
|
||||
dup2(backend_fd.as_raw_fd(), STDERR_FILENO).unwrap();
|
||||
/* Become session leader */
|
||||
nix::unistd::setsid().unwrap();
|
||||
match unsafe { set_controlling_terminal(backend_fd) } {
|
||||
match unsafe { set_controlling_terminal(backend_fd.as_raw_fd()) } {
|
||||
Ok(c) if c < 0 => {
|
||||
log::error!(
|
||||
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {c}",
|
||||
|
|
|
@ -30,18 +30,17 @@
|
|||
//! * a struct containing the PID of the child process that talks to this
|
||||
//! pseudoterminal.
|
||||
//! * a [`std::fs::File`] handle to the child process's standard input stream.
|
||||
//! * an [`EmbeddedGrid`] which is a wrapper over
|
||||
//! [`CellBuffer`](crate::CellBuffer) along with the properties needed to
|
||||
//! maintain a proper state machine that keeps track of ongoing escape code
|
||||
//! operations.
|
||||
//! * an [`EmbeddedGrid`] which is a wrapper over [`CellBuffer`] along with
|
||||
//! the properties needed to maintain a proper state machine that keeps
|
||||
//! track of ongoing escape code operations.
|
||||
//!
|
||||
//! ## Creation
|
||||
//!
|
||||
//! To create a [`Terminal`], see [`create_pty`](super::create_pty).
|
||||
//! To create a [`Terminal`], see [`create_pty`].
|
||||
|
||||
use melib::{
|
||||
error::{Error, Result},
|
||||
text_processing::wcwidth,
|
||||
text::wcwidth,
|
||||
};
|
||||
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
|
||||
|
||||
|
@ -257,6 +256,21 @@ impl EmbeddedGrid {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn to_string_debug(&self) -> String {
|
||||
let mut out = String::with_capacity(4096);
|
||||
out.push_str(&format!("screen_buffer: {:?}\n", self.screen_buffer));
|
||||
let grid = self.buffer();
|
||||
for y in 0..self.terminal_size().1 {
|
||||
for x in 0..self.terminal_size().0 {
|
||||
out.push(grid[(x, y)].ch());
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
*/
|
||||
|
||||
#[inline]
|
||||
pub fn set_dirty(&mut self, value: bool) {
|
||||
self.dirty = value;
|
||||
|
@ -314,8 +328,7 @@ impl EmbeddedGrid {
|
|||
}
|
||||
|
||||
pub fn process_byte(&mut self, stdin: &mut std::fs::File, byte: u8) {
|
||||
let area = self.area();
|
||||
let EmbeddedGrid {
|
||||
let Self {
|
||||
ref mut cursor,
|
||||
ref mut scroll_region,
|
||||
ref mut terminal_size,
|
||||
|
@ -338,12 +351,12 @@ impl EmbeddedGrid {
|
|||
initialized: _,
|
||||
ref mut dirty,
|
||||
} = self;
|
||||
let mut grid = normal_screen.grid_mut();
|
||||
let mut screen = normal_screen;
|
||||
|
||||
let is_alternate = match *screen_buffer {
|
||||
ScreenBuffer::Normal => false,
|
||||
_ => {
|
||||
grid = alternate_screen.grid_mut();
|
||||
screen = alternate_screen;
|
||||
true
|
||||
}
|
||||
};
|
||||
|
@ -354,7 +367,10 @@ impl EmbeddedGrid {
|
|||
if !is_alternate {
|
||||
cursor.0 = 0;
|
||||
if cursor.1 >= terminal_size.1 {
|
||||
if !grid.resize(std::cmp::max(1, grid.cols()), grid.rows() + 2, None) {
|
||||
if !screen.resize(
|
||||
std::cmp::max(1, screen.grid().cols()),
|
||||
screen.grid().rows() + 2,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
scroll_region.bottom += 1;
|
||||
|
@ -402,6 +418,11 @@ impl EmbeddedGrid {
|
|||
(cursor_x!(), cursor_y!())
|
||||
};
|
||||
}
|
||||
macro_rules! area {
|
||||
() => {{
|
||||
screen.area()
|
||||
}};
|
||||
}
|
||||
|
||||
let mut state = &mut self.state;
|
||||
match (byte, &mut state) {
|
||||
|
@ -422,7 +443,9 @@ impl EmbeddedGrid {
|
|||
// ESCD Linefeed
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
if cursor.1 == scroll_region.bottom {
|
||||
grid.scroll_up(scroll_region, scroll_region.top, 1);
|
||||
screen
|
||||
.grid_mut()
|
||||
.scroll_up(scroll_region, scroll_region.top, 1);
|
||||
*dirty = true;
|
||||
} else {
|
||||
cursor.1 += 1;
|
||||
|
@ -436,7 +459,7 @@ impl EmbeddedGrid {
|
|||
//log::trace!("erasing from {:?} to {:?}", cursor, terminal_size);
|
||||
for y in cursor.1..terminal_size.1 {
|
||||
for x in cursor.0..terminal_size.0 {
|
||||
grid[(x, y)] = Cell::default();
|
||||
screen.grid_mut()[(x, y)] = Cell::default();
|
||||
}
|
||||
}
|
||||
*dirty = true;
|
||||
|
@ -446,7 +469,7 @@ impl EmbeddedGrid {
|
|||
// ESCK Erase from the cursor to the end of the line
|
||||
//log::trace!("sending {}", EscCode::from((&(*state), byte)));
|
||||
for x in cursor.0..terminal_size.0 {
|
||||
grid[(x, cursor.1)] = Cell::default();
|
||||
screen.grid_mut()[(x, cursor.1)] = Cell::default();
|
||||
}
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
|
@ -487,7 +510,7 @@ impl EmbeddedGrid {
|
|||
|
||||
if cursor.1 + 1 < terminal_size.1 || !is_alternate {
|
||||
if cursor.1 == scroll_region.bottom && is_alternate {
|
||||
grid.scroll_up(scroll_region, cursor.1, 1);
|
||||
screen.grid_mut().scroll_up(scroll_region, cursor.1, 1);
|
||||
*dirty = true;
|
||||
} else {
|
||||
increase_cursor_y!();
|
||||
|
@ -569,12 +592,14 @@ impl EmbeddedGrid {
|
|||
}
|
||||
}
|
||||
};
|
||||
//log::trace!("c = {:?}\tcursor={:?}", c, cursor);
|
||||
|
||||
*codepoints = CodepointBuf::None;
|
||||
if *auto_wrap_mode && *wrap_next {
|
||||
*wrap_next = false;
|
||||
if cursor.1 == scroll_region.bottom {
|
||||
grid.scroll_up(scroll_region, scroll_region.top, 1);
|
||||
screen
|
||||
.grid_mut()
|
||||
.scroll_up(scroll_region, scroll_region.top, 1);
|
||||
} else {
|
||||
cursor.1 += 1;
|
||||
}
|
||||
|
@ -584,14 +609,15 @@ impl EmbeddedGrid {
|
|||
//if c == '↪' {
|
||||
//log::trace!("↪ cursor is {:?}", cursor_val!());
|
||||
//}
|
||||
grid[cursor_val!()].set_ch(c);
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
grid[cursor_val!()].set_attrs(*attrs);
|
||||
screen.grid_mut()[cursor_val!()]
|
||||
.set_ch(c)
|
||||
.set_fg(*fg_color)
|
||||
.set_bg(*bg_color)
|
||||
.set_attrs(*attrs);
|
||||
match wcwidth(u32::from(c)) {
|
||||
Some(0) | None => {
|
||||
/* Skip drawing zero width characters */
|
||||
grid[cursor_val!()].set_empty(true);
|
||||
screen.grid_mut()[cursor_val!()].set_empty(true);
|
||||
}
|
||||
Some(1) => {}
|
||||
Some(n) => {
|
||||
|
@ -599,10 +625,11 @@ impl EmbeddedGrid {
|
|||
* drawn over. Set it as empty to skip drawing it. */
|
||||
for _ in 1..n {
|
||||
increase_cursor_x!();
|
||||
grid[cursor_val!()].set_empty(true);
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
grid[cursor_val!()].set_attrs(*attrs);
|
||||
screen.grid_mut()[cursor_val!()]
|
||||
.set_empty(true)
|
||||
.set_fg(*fg_color)
|
||||
.set_bg(*bg_color)
|
||||
.set_attrs(*attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -621,9 +648,10 @@ impl EmbeddedGrid {
|
|||
*fg_color = Color::Default;
|
||||
*bg_color = Color::Default;
|
||||
*attrs = Attr::DEFAULT;
|
||||
grid[cursor_val!()].set_fg(Color::Default);
|
||||
grid[cursor_val!()].set_bg(Color::Default);
|
||||
grid[cursor_val!()].set_attrs(Attr::DEFAULT);
|
||||
screen.grid_mut()[cursor_val!()]
|
||||
.set_fg(Color::Default)
|
||||
.set_bg(Color::Default)
|
||||
.set_attrs(Attr::DEFAULT);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -652,12 +680,13 @@ impl EmbeddedGrid {
|
|||
}
|
||||
b"25" => {
|
||||
*show_cursor = true;
|
||||
*prev_fg_color = Some(grid[cursor_val!()].fg());
|
||||
*prev_bg_color = Some(grid[cursor_val!()].bg());
|
||||
*prev_attrs = Some(grid[cursor_val!()].attrs());
|
||||
grid[cursor_val!()].set_fg(Color::Black);
|
||||
grid[cursor_val!()].set_bg(Color::White);
|
||||
grid[cursor_val!()].set_attrs(Attr::DEFAULT);
|
||||
*prev_fg_color = Some(screen.grid_mut()[cursor_val!()].fg());
|
||||
*prev_bg_color = Some(screen.grid_mut()[cursor_val!()].bg());
|
||||
*prev_attrs = Some(screen.grid_mut()[cursor_val!()].attrs());
|
||||
screen.grid_mut()[cursor_val!()]
|
||||
.set_fg(Color::Black)
|
||||
.set_bg(Color::White)
|
||||
.set_attrs(Attr::DEFAULT);
|
||||
*dirty = true;
|
||||
}
|
||||
b"1047" | b"1049" => {
|
||||
|
@ -685,19 +714,19 @@ impl EmbeddedGrid {
|
|||
b"25" => {
|
||||
*show_cursor = false;
|
||||
if let Some(fg_color) = prev_fg_color.take() {
|
||||
grid[cursor_val!()].set_fg(fg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_fg(fg_color);
|
||||
} else {
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
|
||||
}
|
||||
if let Some(bg_color) = prev_bg_color.take() {
|
||||
grid[cursor_val!()].set_bg(bg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_bg(bg_color);
|
||||
} else {
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
|
||||
}
|
||||
if let Some(attrs) = prev_attrs.take() {
|
||||
grid[cursor_val!()].set_attrs(attrs);
|
||||
screen.grid_mut()[cursor_val!()].set_attrs(attrs);
|
||||
} else {
|
||||
grid[cursor_val!()].set_attrs(*attrs);
|
||||
screen.grid_mut()[cursor_val!()].set_attrs(*attrs);
|
||||
}
|
||||
*dirty = true;
|
||||
}
|
||||
|
@ -724,7 +753,8 @@ impl EmbeddedGrid {
|
|||
/* Erase in Display (ED), VT100. */
|
||||
/* Erase Below (default). */
|
||||
|
||||
grid.clear_area(
|
||||
let area = area!();
|
||||
screen.grid_mut().clear_area(
|
||||
area.skip_rows(std::cmp::min(
|
||||
cursor.1 + 1 + scroll_region.top,
|
||||
terminal_size.1.saturating_sub(1),
|
||||
|
@ -740,7 +770,7 @@ impl EmbeddedGrid {
|
|||
/* Erase to right (Default) */
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
for x in cursor.0..terminal_size.0 {
|
||||
grid[(x, cursor.1)] = Cell::default();
|
||||
screen.grid_mut()[(x, cursor.1)] = Cell::default();
|
||||
}
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
|
@ -755,7 +785,7 @@ impl EmbeddedGrid {
|
|||
1
|
||||
};
|
||||
|
||||
grid.scroll_down(scroll_region, cursor.1, n);
|
||||
screen.grid_mut().scroll_down(scroll_region, cursor.1, n);
|
||||
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
*dirty = true;
|
||||
|
@ -771,7 +801,7 @@ impl EmbeddedGrid {
|
|||
1
|
||||
};
|
||||
|
||||
grid.scroll_up(scroll_region, cursor.1, n);
|
||||
screen.grid_mut().scroll_up(scroll_region, cursor.1, n);
|
||||
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
*dirty = true;
|
||||
|
@ -794,7 +824,7 @@ impl EmbeddedGrid {
|
|||
/* Erase to right (Default) */
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
for x in cursor.0..terminal_size.0 {
|
||||
grid[(x, cursor.1)] = Cell::default();
|
||||
screen.grid_mut()[(x, cursor.1)] = Cell::default();
|
||||
}
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
|
@ -803,7 +833,7 @@ impl EmbeddedGrid {
|
|||
/* Erase in Line (ED), VT100. */
|
||||
/* Erase to left (Default) */
|
||||
for x in 0..=cursor.0 {
|
||||
grid[(x, cursor.1)] = Cell::default();
|
||||
screen.grid_mut()[(x, cursor.1)] = Cell::default();
|
||||
}
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
*dirty = true;
|
||||
|
@ -814,12 +844,13 @@ impl EmbeddedGrid {
|
|||
/* Erase all */
|
||||
for y in 0..terminal_size.1 {
|
||||
for x in 0..terminal_size.0 {
|
||||
grid[(x, y)] = Cell::default();
|
||||
screen.grid_mut()[(x, y)] = Cell::default();
|
||||
}
|
||||
}
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
|
||||
grid.clear_area(
|
||||
let area = area!();
|
||||
screen.grid_mut().clear_area(
|
||||
area.take_cols(terminal_size.0).take_rows(terminal_size.1),
|
||||
Default::default(),
|
||||
);
|
||||
|
@ -830,7 +861,8 @@ impl EmbeddedGrid {
|
|||
/* Erase in Display (ED), VT100. */
|
||||
/* Erase Below (default). */
|
||||
|
||||
grid.clear_area(
|
||||
let area = area!();
|
||||
screen.grid_mut().clear_area(
|
||||
area.skip_rows(std::cmp::min(
|
||||
cursor.1 + 1 + scroll_region.top,
|
||||
terminal_size.1.saturating_sub(1),
|
||||
|
@ -845,7 +877,8 @@ impl EmbeddedGrid {
|
|||
/* Erase in Display (ED), VT100. */
|
||||
/* Erase Above */
|
||||
|
||||
grid.clear_area(
|
||||
let area = area!();
|
||||
screen.grid_mut().clear_area(
|
||||
area.take_rows(cursor.1.saturating_sub(1) + scroll_region.top),
|
||||
Default::default(),
|
||||
);
|
||||
|
@ -857,7 +890,8 @@ impl EmbeddedGrid {
|
|||
/* Erase in Display (ED), VT100. */
|
||||
/* Erase All */
|
||||
|
||||
grid.clear_area(area, Default::default());
|
||||
let area = area!();
|
||||
screen.grid_mut().clear_area(area, Default::default());
|
||||
//log::trace!("{}", EscCode::from((&(*state), byte)));
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
|
@ -878,7 +912,7 @@ impl EmbeddedGrid {
|
|||
break;
|
||||
}
|
||||
}
|
||||
grid[(cur_x, cur_y)] = Cell::default();
|
||||
screen.grid_mut()[(cur_x, cur_y)] = Cell::default();
|
||||
cur_x += 1;
|
||||
ctr += 1;
|
||||
}
|
||||
|
@ -949,11 +983,11 @@ impl EmbeddedGrid {
|
|||
/* scroll down */
|
||||
for y in scroll_region.top..scroll_region.bottom {
|
||||
for x in 0..terminal_size.1 {
|
||||
grid[(x, y)] = grid[(x, y + 1)];
|
||||
screen.grid_mut()[(x, y)] = screen.grid()[(x, y + 1)];
|
||||
}
|
||||
}
|
||||
for x in 0..terminal_size.1 {
|
||||
grid[(x, scroll_region.bottom)] = Cell::default();
|
||||
screen.grid_mut()[(x, scroll_region.bottom)] = Cell::default();
|
||||
}
|
||||
} else if offset + cursor.1 < terminal_size.1 {
|
||||
cursor.1 += offset;
|
||||
|
@ -1060,10 +1094,11 @@ impl EmbeddedGrid {
|
|||
};
|
||||
|
||||
for i in 0..(terminal_size.0 - cursor.0 - offset) {
|
||||
grid[(cursor.0 + i, cursor.1)] = grid[(cursor.0 + i + offset, cursor.1)];
|
||||
screen.grid_mut()[(cursor.0 + i, cursor.1)] =
|
||||
screen.grid()[(cursor.0 + i + offset, cursor.1)];
|
||||
}
|
||||
for x in (terminal_size.0 - offset)..terminal_size.0 {
|
||||
grid[(x, cursor.1)].set_ch(' ');
|
||||
screen.grid_mut()[(x, cursor.1)].set_ch(' ');
|
||||
}
|
||||
//log::trace!(
|
||||
// "Delete {} Character(s) with cursor at {:?} ",
|
||||
|
@ -1203,9 +1238,10 @@ impl EmbeddedGrid {
|
|||
);
|
||||
}
|
||||
}
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
grid[cursor_val!()].set_attrs(*attrs);
|
||||
screen.grid_mut()[cursor_val!()]
|
||||
.set_fg(*fg_color)
|
||||
.set_bg(*bg_color)
|
||||
.set_attrs(*attrs);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1315,9 +1351,10 @@ impl EmbeddedGrid {
|
|||
}
|
||||
}
|
||||
}
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
grid[cursor_val!()].set_attrs(*attrs);
|
||||
screen.grid_mut()[cursor_val!()]
|
||||
.set_fg(*fg_color)
|
||||
.set_bg(*bg_color)
|
||||
.set_attrs(*attrs);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1420,7 +1457,7 @@ impl EmbeddedGrid {
|
|||
} else {
|
||||
Color::Default
|
||||
};
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1436,7 +1473,7 @@ impl EmbeddedGrid {
|
|||
} else {
|
||||
Color::Default
|
||||
};
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1505,7 +1542,7 @@ impl EmbeddedGrid {
|
|||
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
|
||||
_ => Color::Default,
|
||||
};
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1529,7 +1566,7 @@ impl EmbeddedGrid {
|
|||
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
|
||||
_ => Color::Default,
|
||||
};
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1552,7 +1589,7 @@ impl EmbeddedGrid {
|
|||
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
|
||||
_ => Color::Default,
|
||||
};
|
||||
grid[cursor_val!()].set_fg(*fg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_fg(*fg_color);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
@ -1575,7 +1612,7 @@ impl EmbeddedGrid {
|
|||
(Ok(r), Ok(g), Ok(b)) => Color::Rgb(r, g, b),
|
||||
_ => Color::Default,
|
||||
};
|
||||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
screen.grid_mut()[cursor_val!()].set_bg(*bg_color);
|
||||
*dirty = true;
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
|
||||
|
||||
use crossbeam::{channel::Receiver, select};
|
||||
use nix::poll::{poll, PollFd, PollFlags};
|
||||
|
@ -120,11 +120,11 @@ pub enum MouseButton {
|
|||
Middle,
|
||||
/// Mouse wheel is going up.
|
||||
///
|
||||
/// This event is typically only used with Mouse::Press.
|
||||
/// This event is typically only used with [`MouseEvent::Press`].
|
||||
WheelUp,
|
||||
/// Mouse wheel is going down.
|
||||
///
|
||||
/// This event is typically only used with Mouse::Press.
|
||||
/// This event is typically only used with [`MouseEvent::Press`].
|
||||
WheelDown,
|
||||
}
|
||||
|
||||
|
@ -173,31 +173,31 @@ impl std::fmt::Display for Key {
|
|||
|
||||
impl<'a> From<&'a String> for Key {
|
||||
fn from(v: &'a String) -> Self {
|
||||
Key::Paste(v.to_string())
|
||||
Self::Paste(v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TermionKey> for Key {
|
||||
fn from(k: TermionKey) -> Self {
|
||||
match k {
|
||||
TermionKey::Backspace => Key::Backspace,
|
||||
TermionKey::Left => Key::Left,
|
||||
TermionKey::Right => Key::Right,
|
||||
TermionKey::Up => Key::Up,
|
||||
TermionKey::Down => Key::Down,
|
||||
TermionKey::Home => Key::Home,
|
||||
TermionKey::End => Key::End,
|
||||
TermionKey::PageUp => Key::PageUp,
|
||||
TermionKey::PageDown => Key::PageDown,
|
||||
TermionKey::Delete => Key::Delete,
|
||||
TermionKey::Insert => Key::Insert,
|
||||
TermionKey::F(u) => Key::F(u),
|
||||
TermionKey::Char(c) => Key::Char(c),
|
||||
TermionKey::Alt(c) => Key::Alt(c),
|
||||
TermionKey::Ctrl(c) => Key::Ctrl(c),
|
||||
TermionKey::Null => Key::Null,
|
||||
TermionKey::Esc => Key::Esc,
|
||||
_ => Key::Char(' '),
|
||||
TermionKey::Backspace => Self::Backspace,
|
||||
TermionKey::Left => Self::Left,
|
||||
TermionKey::Right => Self::Right,
|
||||
TermionKey::Up => Self::Up,
|
||||
TermionKey::Down => Self::Down,
|
||||
TermionKey::Home => Self::Home,
|
||||
TermionKey::End => Self::End,
|
||||
TermionKey::PageUp => Self::PageUp,
|
||||
TermionKey::PageDown => Self::PageDown,
|
||||
TermionKey::Delete => Self::Delete,
|
||||
TermionKey::Insert => Self::Insert,
|
||||
TermionKey::F(u) => Self::F(u),
|
||||
TermionKey::Char(c) => Self::Char(c),
|
||||
TermionKey::Alt(c) => Self::Alt(c),
|
||||
TermionKey::Ctrl(c) => Self::Ctrl(c),
|
||||
TermionKey::Null => Self::Null,
|
||||
TermionKey::Esc => Self::Esc,
|
||||
_ => Self::Char(' '),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,9 +215,10 @@ enum InputMode {
|
|||
Paste(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
/// Main process sends commands to the input thread.
|
||||
pub enum InputCommand {
|
||||
#[default]
|
||||
/// Exit thread
|
||||
Kill,
|
||||
}
|
||||
|
@ -236,11 +237,13 @@ pub enum InputCommand {
|
|||
pub fn get_events(
|
||||
mut closure: impl FnMut((Key, Vec<u8>)),
|
||||
rx: &Receiver<InputCommand>,
|
||||
new_command_fd: RawFd,
|
||||
new_command_fd: &OwnedFd,
|
||||
working: std::sync::Arc<()>,
|
||||
) {
|
||||
let stdin = std::io::stdin();
|
||||
let stdin_fd = PollFd::new(std::io::stdin().as_raw_fd(), PollFlags::POLLIN);
|
||||
let stdin2 = std::io::stdin();
|
||||
let stdin2_fd = stdin2.as_fd();
|
||||
let stdin_fd = PollFd::new(&stdin2_fd, PollFlags::POLLIN);
|
||||
let new_command_pollfd = nix::poll::PollFd::new(new_command_fd, nix::poll::PollFlags::POLLIN);
|
||||
let mut input_mode = InputMode::Normal;
|
||||
let mut paste_buf = String::with_capacity(256);
|
||||
|
@ -296,11 +299,12 @@ pub fn get_events(
|
|||
let mut error_fd_set = nix::sys::select::FdSet::new();
|
||||
error_fd_set.insert(new_command_fd);
|
||||
let timeval: nix::sys::time::TimeSpec = nix::sys::time::TimeSpec::seconds(2);
|
||||
if nix::sys::select::pselect(None, Some(&mut read_fd_set), None, Some(&mut error_fd_set), Some(&timeval), None).is_err() || error_fd_set.highest() == Some(new_command_fd) || read_fd_set.highest() != Some(new_command_fd) {
|
||||
let pselect_result = nix::sys::select::pselect(None, Some(&mut read_fd_set), None, Some(&mut error_fd_set), Some(&timeval), None);
|
||||
if pselect_result.is_err() || error_fd_set.highest().map(|bfd| bfd.as_raw_fd()) == Some(new_command_fd.as_raw_fd()) || read_fd_set.highest().map(|bfd| bfd.as_raw_fd()) != Some(new_command_fd.as_raw_fd()) {
|
||||
continue 'poll_while;
|
||||
};
|
||||
let _ = nix::unistd::read(new_command_fd, buf.as_mut());
|
||||
match cmd.unwrap() {
|
||||
let _ = nix::unistd::read(new_command_fd.as_raw_fd(), buf.as_mut());
|
||||
match cmd.unwrap_or_default() {
|
||||
InputCommand::Kill => return,
|
||||
}
|
||||
}
|
||||
|
@ -399,27 +403,27 @@ impl Serialize for Key {
|
|||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Key::Backspace => serializer.serialize_str("Backspace"),
|
||||
Key::Left => serializer.serialize_str("Left"),
|
||||
Key::Right => serializer.serialize_str("Right"),
|
||||
Key::Up => serializer.serialize_str("Up"),
|
||||
Key::Down => serializer.serialize_str("Down"),
|
||||
Key::Home => serializer.serialize_str("Home"),
|
||||
Key::End => serializer.serialize_str("End"),
|
||||
Key::PageUp => serializer.serialize_str("PageUp"),
|
||||
Key::PageDown => serializer.serialize_str("PageDown"),
|
||||
Key::Delete => serializer.serialize_str("Delete"),
|
||||
Key::Insert => serializer.serialize_str("Insert"),
|
||||
Key::Esc => serializer.serialize_str("Esc"),
|
||||
Key::Char('\n') => serializer.serialize_str("Enter"),
|
||||
Key::Char('\t') => serializer.serialize_str("Tab"),
|
||||
Key::Char(c) => serializer.serialize_char(*c),
|
||||
Key::F(n) => serializer.serialize_str(&format!("F{}", n)),
|
||||
Key::Alt(c) => serializer.serialize_str(&format!("M-{}", c)),
|
||||
Key::Ctrl(c) => serializer.serialize_str(&format!("C-{}", c)),
|
||||
Key::Null => serializer.serialize_str("Null"),
|
||||
Key::Mouse(mev) => mev.serialize(serializer),
|
||||
Key::Paste(s) => serializer.serialize_str(s),
|
||||
Self::Backspace => serializer.serialize_str("Backspace"),
|
||||
Self::Left => serializer.serialize_str("Left"),
|
||||
Self::Right => serializer.serialize_str("Right"),
|
||||
Self::Up => serializer.serialize_str("Up"),
|
||||
Self::Down => serializer.serialize_str("Down"),
|
||||
Self::Home => serializer.serialize_str("Home"),
|
||||
Self::End => serializer.serialize_str("End"),
|
||||
Self::PageUp => serializer.serialize_str("PageUp"),
|
||||
Self::PageDown => serializer.serialize_str("PageDown"),
|
||||
Self::Delete => serializer.serialize_str("Delete"),
|
||||
Self::Insert => serializer.serialize_str("Insert"),
|
||||
Self::Esc => serializer.serialize_str("Esc"),
|
||||
Self::Char('\n') => serializer.serialize_str("Enter"),
|
||||
Self::Char('\t') => serializer.serialize_str("Tab"),
|
||||
Self::Char(c) => serializer.serialize_char(*c),
|
||||
Self::F(n) => serializer.serialize_str(&format!("F{}", n)),
|
||||
Self::Alt(c) => serializer.serialize_str(&format!("M-{}", c)),
|
||||
Self::Ctrl(c) => serializer.serialize_str(&format!("C-{}", c)),
|
||||
Self::Null => serializer.serialize_str("Null"),
|
||||
Self::Mouse(mev) => mev.serialize(serializer),
|
||||
Self::Paste(s) => serializer.serialize_str(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -439,11 +443,23 @@ fn test_key_serde() {
|
|||
);
|
||||
};
|
||||
($s:literal, err $v:literal) => {
|
||||
test_key!($s, err $v, "^")
|
||||
};
|
||||
($s:literal, err $v:literal, $extra:literal) => {
|
||||
assert_eq!(
|
||||
toml::from_str::<V>(std::concat!("k = \"", $s, "\""))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
$v.to_string()
|
||||
std::concat!(
|
||||
"TOML parse error at line 1, column 5\n |\n1 | k = \"",
|
||||
$s,
|
||||
"\"\n | ",
|
||||
$extra,
|
||||
"^^^^\n",
|
||||
$v,
|
||||
'\n',
|
||||
)
|
||||
.to_string()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -468,9 +484,9 @@ fn test_key_serde() {
|
|||
test_key!("M-a", ok Key::Alt('a') );
|
||||
test_key!("F1", ok Key::F(1) );
|
||||
test_key!("F12", ok Key::F(12) );
|
||||
test_key!("C-V", err "`V` should be a lowercase and alphanumeric character instead. for key `k` at line 1 column 5");
|
||||
test_key!("M-V", err "`V` should be a lowercase and alphanumeric character instead. for key `k` at line 1 column 5");
|
||||
test_key!("F13", err "`13` should be a number 1 <= n <= 12 instead. for key `k` at line 1 column 5");
|
||||
test_key!("Fc", err "`c` should be a number 1 <= n <= 12 instead. for key `k` at line 1 column 5");
|
||||
test_key!("adsfsf", err "Cannot derive shortcut from `adsfsf`. Please consult the manual for valid key inputs. for key `k` at line 1 column 5");
|
||||
test_key!("C-V", err "`V` should be a lowercase and alphanumeric character instead.");
|
||||
test_key!("M-V", err "`V` should be a lowercase and alphanumeric character instead.");
|
||||
test_key!("F13", err "`13` should be a number 1 <= n <= 12 instead.");
|
||||
test_key!("Fc", err "`c` should be a number 1 <= n <= 12 instead.", "");
|
||||
test_key!("adsfsf", err "Cannot derive shortcut from `adsfsf`. Please consult the manual for valid key inputs.", "^^^^");
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2017-2018 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Simple type definitions and macro helper for a `(x, y)` position on the
|
||||
//! terminal and the areas they define. An [`Area`] consists of two points: the
|
||||
//! upper left and bottom right corners.
|
||||
|
||||
/// A `(x, y)` position on screen.
|
||||
pub type Pos = (usize, usize);
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_x(p: Pos) -> usize {
|
||||
p.0
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn get_y(p: Pos) -> usize {
|
||||
p.1
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn set_x(p: Pos, new_x: usize) -> Pos {
|
||||
(new_x, p.1)
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn set_y(p: Pos, new_y: usize) -> Pos {
|
||||
(p.0, new_y)
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn pos_inc(p: Pos, inc: (usize, usize)) -> Pos {
|
||||
(p.0 + inc.0, p.1 + inc.1)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn pos_dec(p: Pos, dec: (usize, usize)) -> Pos {
|
||||
(p.0.saturating_sub(dec.0), p.1.saturating_sub(dec.1))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Alignment {
|
||||
/// Stretch to fill all space if possible, center if no meaningful way to
|
||||
/// stretch.
|
||||
Fill,
|
||||
/// Snap to left or top side, leaving space on right or bottom.
|
||||
Start,
|
||||
/// Snap to right or bottom side, leaving space on left or top.
|
||||
End,
|
||||
/// Center natural width of widget inside the allocation.
|
||||
#[default]
|
||||
Center,
|
||||
}
|
|
@ -27,9 +27,9 @@ use termion::{clear, cursor, raw::IntoRawMode, screen::AlternateScreen};
|
|||
|
||||
use crate::{
|
||||
terminal::{
|
||||
cells::CellBuffer, position::*, BracketModeEnd, BracketModeStart, Cell, Color,
|
||||
DisableMouse, DisableSGRMouse, EnableMouse, EnableSGRMouse,
|
||||
RestoreWindowTitleIconFromStack, SaveWindowTitleIconToStack,
|
||||
cells::CellBuffer, Alignment, BracketModeEnd, BracketModeStart, Cell, Color, DisableMouse,
|
||||
DisableSGRMouse, EnableMouse, EnableSGRMouse, Pos, RestoreWindowTitleIconFromStack,
|
||||
SaveWindowTitleIconToStack,
|
||||
},
|
||||
Attr, Context,
|
||||
};
|
||||
|
@ -45,9 +45,10 @@ type DrawHorizontalSegmentFn = fn(&mut CellBuffer, &mut StateStdout, usize, usiz
|
|||
pub struct ScreenGeneration((u64, u64));
|
||||
|
||||
impl ScreenGeneration {
|
||||
pub const NIL: ScreenGeneration = Self((0, 0));
|
||||
pub const NIL: Self = Self((0, 0));
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn next(self) -> Self {
|
||||
Self(uuid::Uuid::new_v4().as_u64_pair())
|
||||
}
|
||||
|
@ -234,7 +235,7 @@ impl Screen<Tty> {
|
|||
Self::init(Tty {
|
||||
stdout: None,
|
||||
mouse: false,
|
||||
draw_horizontal_segment_fn: Screen::draw_horizontal_segment,
|
||||
draw_horizontal_segment_fn: Self::draw_horizontal_segment,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -328,7 +329,10 @@ impl Screen<Tty> {
|
|||
}
|
||||
|
||||
pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) {
|
||||
let mut stdout = BufWriter::with_capacity(240 * 80, Box::new(std::io::stdout()) as _);
|
||||
let mut stdout = BufWriter::with_capacity(
|
||||
240 * 80,
|
||||
Box::new(std::io::stdout()) as Box<dyn std::io::Write>,
|
||||
);
|
||||
|
||||
write!(
|
||||
&mut stdout,
|
||||
|
@ -402,6 +406,9 @@ impl Screen<Tty> {
|
|||
}
|
||||
if !c.empty() {
|
||||
write!(stdout, "{}", c.ch()).unwrap();
|
||||
if c.attrs().intersects(Attr::FORCE_TEXT) {
|
||||
_ = write!(stdout, "\u{FE0E}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -429,6 +436,9 @@ impl Screen<Tty> {
|
|||
}
|
||||
if !c.empty() {
|
||||
write!(stdout, "{}", c.ch()).unwrap();
|
||||
if c.attrs().intersects(Attr::FORCE_TEXT) {
|
||||
_ = write!(stdout, "\u{FE0E}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -512,6 +522,30 @@ impl<D: private::Sealed> From<&Screen<D>> for Area {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convenience trait to turn both single `usize` values and `(usize, _)`
|
||||
/// positions to `x` coordinate.
|
||||
pub trait IntoColumns: private::Sealed {
|
||||
#[must_use]
|
||||
fn into(self) -> usize;
|
||||
}
|
||||
|
||||
impl private::Sealed for usize {}
|
||||
impl private::Sealed for Pos {}
|
||||
|
||||
impl IntoColumns for usize {
|
||||
#[must_use]
|
||||
fn into(self) -> usize {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoColumns for Pos {
|
||||
#[must_use]
|
||||
fn into(self) -> usize {
|
||||
get_x(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Area {
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
|
@ -536,6 +570,7 @@ impl Area {
|
|||
|
||||
/// Get `n`th row of `area` or its last one.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn nth_row(&self, n: usize) -> Self {
|
||||
let Self {
|
||||
offset,
|
||||
|
@ -565,6 +600,7 @@ impl Area {
|
|||
|
||||
/// Get `n`th col of `area` or its last one.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn nth_col(&self, n: usize) -> Self {
|
||||
let Self {
|
||||
offset,
|
||||
|
@ -593,6 +629,7 @@ impl Area {
|
|||
}
|
||||
|
||||
/// Place box given by `(width, height)` in corner of `area`
|
||||
#[must_use]
|
||||
pub fn place_inside(&self, (width, height): (usize, usize), upper: bool, left: bool) -> Self {
|
||||
if self.is_empty() || width < 3 || height < 3 {
|
||||
return *self;
|
||||
|
@ -635,6 +672,7 @@ impl Area {
|
|||
|
||||
/// Place given area of dimensions `(width, height)` inside `area` according
|
||||
/// to given alignment
|
||||
#[must_use]
|
||||
pub fn align_inside(
|
||||
&self,
|
||||
(width, height): (usize, usize),
|
||||
|
@ -668,6 +706,7 @@ impl Area {
|
|||
|
||||
/// Place box given by `dimensions` in center of `area`
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn center_inside(&self, dimensions: (usize, usize)) -> Self {
|
||||
self.align_inside(dimensions, Alignment::Center, Alignment::Center)
|
||||
}
|
||||
|
@ -703,6 +742,7 @@ impl Area {
|
|||
/// assert_eq!(body.height(), 18);
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn skip_rows(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.height());
|
||||
if self.is_empty() || self.upper_left.1 + n > self.bottom_right.1 {
|
||||
|
@ -734,6 +774,7 @@ impl Area {
|
|||
/// assert_eq!(header, area.take_rows(2));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn skip_rows_from_end(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.height());
|
||||
if self.is_empty() || self.bottom_right.1 < n {
|
||||
|
@ -746,6 +787,21 @@ impl Area {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
fn _skip_cols_inner(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.width());
|
||||
if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n {
|
||||
return self.into_empty();
|
||||
}
|
||||
|
||||
Self {
|
||||
offset: pos_inc(self.offset, (n, 0)),
|
||||
upper_left: pos_inc(self.upper_left, (n, 0)),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip the first `n` rows and return the remaining area.
|
||||
/// Return value will be an empty area if `n` is more than the width.
|
||||
///
|
||||
|
@ -763,17 +819,10 @@ impl Area {
|
|||
/// assert_eq!(indent.width(), 118);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn skip_cols(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.width());
|
||||
if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n {
|
||||
return self.into_empty();
|
||||
}
|
||||
|
||||
Self {
|
||||
offset: pos_inc(self.offset, (n, 0)),
|
||||
upper_left: pos_inc(self.upper_left, (n, 0)),
|
||||
..*self
|
||||
}
|
||||
#[must_use]
|
||||
pub fn skip_cols(&self, n: impl IntoColumns) -> Self {
|
||||
let n: usize = n.into();
|
||||
self._skip_cols_inner(n)
|
||||
}
|
||||
|
||||
/// Skip the last `n` rows and return the remaining area.
|
||||
|
@ -794,6 +843,7 @@ impl Area {
|
|||
/// assert_eq!(indent, area.take_cols(118));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn skip_cols_from_end(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.width());
|
||||
if self.is_empty() || self.bottom_right.0 < n {
|
||||
|
@ -807,6 +857,7 @@ impl Area {
|
|||
|
||||
/// Shortcut for using `Area::skip_cols` and `Area::skip_rows` together.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn skip(&self, n_cols: usize, n_rows: usize) -> Self {
|
||||
self.skip_cols(n_cols).skip_rows(n_rows)
|
||||
}
|
||||
|
@ -828,6 +879,7 @@ impl Area {
|
|||
/// assert_eq!(header.height(), 2);
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn take_rows(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.height());
|
||||
if self.is_empty() || self.bottom_right.1 < (self.height() - n) {
|
||||
|
@ -860,6 +912,7 @@ impl Area {
|
|||
/// assert_eq!(header.width(), 2);
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn take_cols(&self, n: usize) -> Self {
|
||||
let n = std::cmp::min(n, self.width());
|
||||
if self.is_empty() || self.bottom_right.0 < (self.width() - n) {
|
||||
|
@ -877,31 +930,49 @@ impl Area {
|
|||
|
||||
/// Shortcut for using `Area::take_cols` and `Area::take_rows` together.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn take(&self, n_cols: usize, n_rows: usize) -> Self {
|
||||
self.take_cols(n_cols).take_rows(n_rows)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn upper_left(&self) -> Pos {
|
||||
self.upper_left
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn bottom_right(&self) -> Pos {
|
||||
self.bottom_right
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn upper_right(&self) -> Pos {
|
||||
set_x(self.upper_left, get_x(self.bottom_right))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn bottom_left(&self) -> Pos {
|
||||
set_y(self.upper_left, get_y(self.bottom_right))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn offset(&self) -> Pos {
|
||||
self.offset
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn generation(&self) -> ScreenGeneration {
|
||||
self.generation
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn new_empty(generation: ScreenGeneration) -> Self {
|
||||
Self {
|
||||
offset: (0, 0),
|
||||
|
@ -915,6 +986,7 @@ impl Area {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn into_empty(self) -> Self {
|
||||
Self {
|
||||
offset: (0, 0),
|
||||
|
@ -926,12 +998,43 @@ impl Area {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.empty
|
||||
|| (self.upper_left.0 > self.bottom_right.0 || self.upper_left.1 > self.bottom_right.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[must_use]
|
||||
const fn pos_inc(p: Pos, inc: (usize, usize)) -> Pos {
|
||||
(p.0 + inc.0, p.1 + inc.1)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[must_use]
|
||||
const fn get_x(p: Pos) -> usize {
|
||||
p.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[must_use]
|
||||
const fn get_y(p: Pos) -> usize {
|
||||
p.1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[must_use]
|
||||
const fn set_x(p: Pos, new_x: usize) -> Pos {
|
||||
(new_x, p.1)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[must_use]
|
||||
const fn set_y(p: Pos, new_y: usize) -> Pos {
|
||||
(p.0, new_y)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::text_processing::TextProcessing;
|
||||
use melib::text::TextProcessing;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct UText {
|
||||
|
@ -30,7 +30,7 @@ pub struct UText {
|
|||
|
||||
impl UText {
|
||||
pub fn new(content: String) -> Self {
|
||||
UText {
|
||||
Self {
|
||||
cursor_pos: content.len(),
|
||||
grapheme_cursor_pos: content.split_graphemes().len(),
|
||||
content,
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
//! `vi`.
|
||||
//!
|
||||
//! [`UIEvent`] is the type passed around
|
||||
//! [`Component`](crate::components::Component)'s when something happens.
|
||||
//! [`Component`]'s when something happens.
|
||||
|
||||
#[macro_use]
|
||||
mod helpers;
|
||||
|
@ -100,9 +100,16 @@ pub enum ForkType {
|
|||
/// Already finished fork, we only want to restore input/output
|
||||
Finished,
|
||||
/// Embedded pty
|
||||
Embedded(Pid),
|
||||
Generic(std::process::Child),
|
||||
NewDraft(File, std::process::Child),
|
||||
Embedded {
|
||||
id: Cow<'static, str>,
|
||||
command: Option<Cow<'static, str>>,
|
||||
pid: Pid,
|
||||
},
|
||||
Generic {
|
||||
id: Cow<'static, str>,
|
||||
command: Option<Cow<'static, str>>,
|
||||
child: std::process::Child,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
@ -117,12 +124,12 @@ pub enum NotificationType {
|
|||
impl std::fmt::Display for NotificationType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
NotificationType::Info => write!(f, "info"),
|
||||
NotificationType::Error(melib::error::ErrorKind::None) => write!(f, "error"),
|
||||
NotificationType::Error(kind) => write!(f, "error: {}", kind),
|
||||
NotificationType::NewMail => write!(f, "new mail"),
|
||||
NotificationType::SentMail => write!(f, "sent mail"),
|
||||
NotificationType::Saved => write!(f, "saved"),
|
||||
Self::Info => write!(f, "info"),
|
||||
Self::Error(melib::error::ErrorKind::None) => write!(f, "error"),
|
||||
Self::Error(kind) => write!(f, "error: {}", kind),
|
||||
Self::NewMail => write!(f, "new mail"),
|
||||
Self::SentMail => write!(f, "sent mail"),
|
||||
Self::Saved => write!(f, "saved"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,8 +147,8 @@ pub enum UIEvent {
|
|||
Command(String),
|
||||
Notification {
|
||||
title: Option<Cow<'static, str>>,
|
||||
source: Option<Error>,
|
||||
body: Cow<'static, str>,
|
||||
source: Option<Error>,
|
||||
kind: Option<NotificationType>,
|
||||
},
|
||||
Action(Action),
|
||||
|
@ -187,7 +194,7 @@ impl std::fmt::Debug for CallbackFn {
|
|||
|
||||
impl From<RefreshEvent> for UIEvent {
|
||||
fn from(event: RefreshEvent) -> Self {
|
||||
UIEvent::RefreshEvent(Box::new(event))
|
||||
Self::RefreshEvent(Box::new(event))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,11 +214,11 @@ impl std::fmt::Display for UIMode {
|
|||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
UIMode::Normal => "NORMAL",
|
||||
UIMode::Insert => "INSERT",
|
||||
UIMode::Command => "COMMAND",
|
||||
UIMode::Fork => "FORK",
|
||||
UIMode::Embedded => "EMBEDDED",
|
||||
Self::Normal => "NORMAL",
|
||||
Self::Insert => "INSERT",
|
||||
Self::Command => "COMMAND",
|
||||
Self::Fork => "FORK",
|
||||
Self::Embedded => "EMBEDDED",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -233,15 +240,15 @@ pub mod segment_tree {
|
|||
}
|
||||
|
||||
impl From<SmallVec<[u8; 1024]>> for SegmentTree {
|
||||
fn from(val: SmallVec<[u8; 1024]>) -> SegmentTree {
|
||||
SegmentTree::new(val)
|
||||
fn from(val: SmallVec<[u8; 1024]>) -> Self {
|
||||
Self::new(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl SegmentTree {
|
||||
pub fn new(val: SmallVec<[u8; 1024]>) -> SegmentTree {
|
||||
pub fn new(val: SmallVec<[u8; 1024]>) -> Self {
|
||||
if val.is_empty() {
|
||||
return SegmentTree {
|
||||
return Self {
|
||||
array: val.clone(),
|
||||
tree: val,
|
||||
};
|
||||
|
@ -262,7 +269,7 @@ pub mod segment_tree {
|
|||
segment_tree[i] = std::cmp::max(segment_tree[2 * i], segment_tree[2 * i + 1]);
|
||||
}
|
||||
|
||||
SegmentTree {
|
||||
Self {
|
||||
array: val,
|
||||
tree: segment_tree,
|
||||
}
|
||||
|
@ -348,7 +355,7 @@ pub struct RateLimit {
|
|||
|
||||
impl RateLimit {
|
||||
pub fn new(reqs: u64, millis: u64, job_executor: Arc<JobExecutor>) -> Self {
|
||||
RateLimit {
|
||||
Self {
|
||||
last_tick: std::time::Instant::now(),
|
||||
timer: job_executor.create_timer(
|
||||
std::time::Duration::from_secs(0),
|
||||
|
@ -389,7 +396,7 @@ pub enum ContactEvent {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum ComposeEvent {
|
||||
SetReceipients(Vec<melib::Address>),
|
||||
SetRecipients(Vec<melib::Address>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -23,11 +23,14 @@ use std::{
|
|||
fs,
|
||||
fs::OpenOptions,
|
||||
io::{Read, Write},
|
||||
os::unix::fs::PermissionsExt,
|
||||
os::{
|
||||
fd::{FromRawFd, OwnedFd},
|
||||
unix::fs::PermissionsExt,
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use melib::{error::*, uuid::Uuid};
|
||||
use melib::{error::*, uuid::Uuid, ShellExpandTrait};
|
||||
|
||||
/// Temporary file that can optionally cleaned up when it is dropped.
|
||||
#[derive(Debug)]
|
||||
|
@ -53,6 +56,7 @@ impl File {
|
|||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&self.path)
|
||||
.chain_err_summary(|| format!("Could not create/open path {}", self.path.display()))
|
||||
}
|
||||
|
@ -73,7 +77,7 @@ impl File {
|
|||
inner(&self.path).chain_err_summary(|| format!("Can't read {}", self.path.display()))
|
||||
}
|
||||
|
||||
/// Returned `File` will be deleted when dropped if delete_on_drop is set,
|
||||
/// Returned `File` will be deleted when dropped if `delete_on_drop` is set,
|
||||
/// so make sure to add it on `context.temp_files` to reap it later.
|
||||
pub fn create_temp_file(
|
||||
bytes: &[u8],
|
||||
|
@ -101,7 +105,8 @@ impl File {
|
|||
path.set_extension(ext);
|
||||
}
|
||||
fn inner(path: &Path, bytes: &[u8], delete_on_drop: bool) -> Result<File> {
|
||||
let mut f = std::fs::File::create(path)?;
|
||||
let path = path.expand();
|
||||
let mut f = std::fs::File::create(&path)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
|
@ -111,7 +116,7 @@ impl File {
|
|||
f.write_all(bytes)?;
|
||||
f.flush()?;
|
||||
Ok(File {
|
||||
path: path.to_path_buf(),
|
||||
path,
|
||||
delete_on_drop,
|
||||
})
|
||||
}
|
||||
|
@ -120,6 +125,18 @@ impl File {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn pipe() -> Result<(OwnedFd, OwnedFd)> {
|
||||
nix::unistd::pipe()
|
||||
.map(|(fd1, fd2)| unsafe { (OwnedFd::from_raw_fd(fd1), OwnedFd::from_raw_fd(fd2)) })
|
||||
.map_err(|err| {
|
||||
Error::new("Could not create pipe")
|
||||
.set_source(Some(
|
||||
(Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>).into(),
|
||||
))
|
||||
.set_kind(ErrorKind::OSError)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
//! Various useful utilities.
|
||||
|
||||
use melib::{text_processing::Reflow, ShellExpandTrait};
|
||||
use melib::{text::Reflow, ShellExpandTrait};
|
||||
|
||||
pub type AutoCompleteFn = Box<dyn Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send + Sync>;
|
||||
|
||||
|
@ -49,7 +49,7 @@ use std::collections::HashSet;
|
|||
use indexmap::IndexMap;
|
||||
|
||||
pub use self::tables::*;
|
||||
use crate::{components::ExtendShortcutsMaps, jobs::JobId, melib::text_processing::TextProcessing};
|
||||
use crate::{components::ExtendShortcutsMaps, jobs::JobId, melib::text::TextProcessing};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum SearchMovement {
|
||||
|
@ -114,7 +114,7 @@ impl StatusBar {
|
|||
None => {}
|
||||
}
|
||||
|
||||
StatusBar {
|
||||
Self {
|
||||
container,
|
||||
status: String::with_capacity(256),
|
||||
status_message: String::with_capacity(256),
|
||||
|
@ -717,7 +717,7 @@ impl Component for StatusBar {
|
|||
self.dirty = true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::BufSet(s)) => {
|
||||
self.display_buffer = s.clone();
|
||||
self.display_buffer.clone_from(s);
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::UpdateStatus(ref mut s)) => {
|
||||
|
@ -812,9 +812,12 @@ impl Component for StatusBar {
|
|||
|
||||
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
|
||||
let mut ret = IndexMap::default();
|
||||
ret.insert(self.container.id(), &self.container as _);
|
||||
ret.insert(self.ex_buffer.id(), &self.ex_buffer as _);
|
||||
ret.insert(self.progress_spinner.id(), &self.progress_spinner as _);
|
||||
ret.insert(self.container.id(), &self.container as &dyn Component);
|
||||
ret.insert(self.ex_buffer.id(), &self.ex_buffer as &dyn Component);
|
||||
ret.insert(
|
||||
self.progress_spinner.id(),
|
||||
&self.progress_spinner as &dyn Component,
|
||||
);
|
||||
ret
|
||||
}
|
||||
|
||||
|
@ -862,11 +865,11 @@ pub struct Tabbed {
|
|||
impl Tabbed {
|
||||
pub fn new(children: Vec<Box<dyn Component>>, context: &Context) -> Self {
|
||||
let pinned = children.len();
|
||||
let mut ret = Tabbed {
|
||||
let mut ret = Self {
|
||||
help_view: HelpView {
|
||||
content: Screen::<Virtual>::new(),
|
||||
curr_views: children
|
||||
.get(0)
|
||||
.first()
|
||||
.map(|c| c.shortcuts(context))
|
||||
.unwrap_or_default(),
|
||||
cursor: (0, 0),
|
||||
|
@ -979,11 +982,14 @@ impl Component for Tabbed {
|
|||
}
|
||||
if (children_maps == self.help_view.curr_views) && must_redraw_shortcuts {
|
||||
let dialog_area = area.align_inside(
|
||||
/* add box perimeter padding */
|
||||
pos_inc(self.help_view.content.area().size(), (1, 1)),
|
||||
/* horizontal */
|
||||
// add box perimeter padding
|
||||
{
|
||||
let (w, h) = self.help_view.content.area().size();
|
||||
(w + 1, h + 1)
|
||||
},
|
||||
// horizontal
|
||||
Alignment::Center,
|
||||
/* vertical */
|
||||
// vertical
|
||||
Alignment::Center,
|
||||
);
|
||||
context.dirty_areas.push_back(dialog_area);
|
||||
|
@ -1154,11 +1160,14 @@ impl Component for Tabbed {
|
|||
}
|
||||
self.help_view.curr_views = children_maps;
|
||||
let dialog_area = area.align_inside(
|
||||
/* add box perimeter padding */
|
||||
pos_inc(self.help_view.content.area().size(), (1, 1)),
|
||||
/* horizontal */
|
||||
// add box perimeter padding
|
||||
{
|
||||
let (w, h) = self.help_view.content.area().size();
|
||||
(w + 1, h + 1)
|
||||
},
|
||||
// horizontal
|
||||
Alignment::Center,
|
||||
/* vertical */
|
||||
// vertical
|
||||
Alignment::Center,
|
||||
);
|
||||
context.dirty_areas.push_back(dialog_area);
|
||||
|
@ -1187,7 +1196,7 @@ impl Component for Tabbed {
|
|||
let (width, height) = self.help_view.content.area().size();
|
||||
let (cols, rows) = inner_area.size();
|
||||
if let Some(ref mut search) = self.help_view.search {
|
||||
use crate::melib::text_processing::search::KMP;
|
||||
use crate::melib::text::search::KMP;
|
||||
search.positions = self
|
||||
.help_view
|
||||
.content
|
||||
|
@ -1377,7 +1386,7 @@ impl Component for Tabbed {
|
|||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => {
|
||||
UIEvent::Action(Tab(New(ref mut e @ Some(_)))) => {
|
||||
self.add_component(e.take().unwrap(), context);
|
||||
self.children[self.cursor_pos]
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
|
@ -1559,7 +1568,7 @@ impl Component for Tabbed {
|
|||
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
|
||||
let mut ret = IndexMap::default();
|
||||
for c in &self.children {
|
||||
ret.insert(c.id(), c as _);
|
||||
ret.insert(c.id(), c as &dyn Component);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
|
|
@ -38,11 +38,11 @@ enum SelectorCursor {
|
|||
|
||||
/// Shows a little window with options for user to select.
|
||||
///
|
||||
/// Instantiate with Selector::new(). Set single_only to true if user should
|
||||
/// Instantiate with `Selector::new()`. Set `single_only` to true if user should
|
||||
/// only choose one of the options. After passing input events to this
|
||||
/// component, check Selector::is_done to see if the user has finalised their
|
||||
/// choices. Collect the choices by consuming the Selector with
|
||||
/// Selector::collect()
|
||||
/// component, check `Selector::is_done` to see if the user has finalised their
|
||||
/// choices. Collect the choices by consuming the `Selector` with
|
||||
/// `Selector::collect()`
|
||||
pub struct Selector<
|
||||
T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send,
|
||||
F: 'static + Sync + Send,
|
||||
|
@ -54,11 +54,14 @@ pub struct Selector<
|
|||
theme_default: ThemeAttribute,
|
||||
|
||||
cursor: SelectorCursor,
|
||||
scroll_x_cursor: usize,
|
||||
movement: Option<PageMovement>,
|
||||
vertical_alignment: Alignment,
|
||||
horizontal_alignment: Alignment,
|
||||
title: String,
|
||||
|
||||
/// If true, user has finished their selection
|
||||
content: Screen<Virtual>,
|
||||
initialized: bool,
|
||||
/// If `true`, user has finished their selection
|
||||
done: bool,
|
||||
done_fn: F,
|
||||
dirty: bool,
|
||||
|
@ -94,7 +97,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static
|
|||
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
|
||||
PartialEq for Selector<T, F>
|
||||
{
|
||||
fn eq(&self, other: &Selector<T, F>) -> bool {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.entries == other.entries
|
||||
}
|
||||
}
|
||||
|
@ -127,6 +130,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
* cursor */
|
||||
self.entries[c].1 = !self.entries[c].1;
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
|
||||
|
@ -169,6 +173,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
}
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
|
@ -181,6 +186,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
}
|
||||
self.cursor = SelectorCursor::Entry(c - 1);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
|
@ -190,6 +196,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
let c = self.entries.len().saturating_sub(1);
|
||||
self.cursor = SelectorCursor::Entry(c);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
|
@ -203,6 +210,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
}
|
||||
self.cursor = SelectorCursor::Entry(c + 1);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(_))
|
||||
|
@ -211,6 +219,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
|
@ -218,6 +227,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
{
|
||||
self.cursor = SelectorCursor::Cancel;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Cancel)
|
||||
|
@ -225,15 +235,62 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"])
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::Left(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::Right(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::PageUp(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::PageDown(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::Home);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::End);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -256,6 +313,7 @@ impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component f
|
|||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
self.dirty = value;
|
||||
self.initialized = false;
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
|
@ -285,6 +343,7 @@ impl Component for UIConfirmationDialog {
|
|||
self.unrealize(context);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||
|
@ -292,6 +351,7 @@ impl Component for UIConfirmationDialog {
|
|||
* cursor */
|
||||
self.entries[c].1 = !self.entries[c].1;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
|
||||
|
@ -301,6 +361,7 @@ impl Component for UIConfirmationDialog {
|
|||
self.unrealize(context);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Esc), _) => {
|
||||
|
@ -314,6 +375,7 @@ impl Component for UIConfirmationDialog {
|
|||
_ = self.done();
|
||||
self.cancel(context);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return false;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
|
||||
|
@ -326,6 +388,7 @@ impl Component for UIConfirmationDialog {
|
|||
self.unrealize(context);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
|
@ -338,6 +401,7 @@ impl Component for UIConfirmationDialog {
|
|||
}
|
||||
self.cursor = SelectorCursor::Entry(c - 1);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
|
@ -347,6 +411,7 @@ impl Component for UIConfirmationDialog {
|
|||
let c = self.entries.len().saturating_sub(1);
|
||||
self.cursor = SelectorCursor::Entry(c);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Unfocused)
|
||||
|
@ -357,6 +422,7 @@ impl Component for UIConfirmationDialog {
|
|||
}
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
|
@ -370,6 +436,7 @@ impl Component for UIConfirmationDialog {
|
|||
}
|
||||
self.cursor = SelectorCursor::Entry(c + 1);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(_))
|
||||
|
@ -378,6 +445,7 @@ impl Component for UIConfirmationDialog {
|
|||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
|
@ -385,6 +453,7 @@ impl Component for UIConfirmationDialog {
|
|||
{
|
||||
self.cursor = SelectorCursor::Cancel;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Cancel)
|
||||
|
@ -392,6 +461,7 @@ impl Component for UIConfirmationDialog {
|
|||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
|
@ -456,9 +526,13 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
entries: identifiers,
|
||||
entry_titles,
|
||||
cursor: SelectorCursor::Unfocused,
|
||||
scroll_x_cursor: 0,
|
||||
movement: None,
|
||||
vertical_alignment: Alignment::Center,
|
||||
horizontal_alignment: Alignment::Center,
|
||||
title: title.to_string(),
|
||||
content: Screen::<Virtual>::new(),
|
||||
initialized: false,
|
||||
done: false,
|
||||
done_fn,
|
||||
dirty: true,
|
||||
|
@ -485,18 +559,18 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted");
|
||||
if !context.settings.terminal.use_color() {
|
||||
highlighted_attrs.attrs |= Attr::REVERSE;
|
||||
}
|
||||
|
||||
let shortcuts = context.settings.shortcuts.general.key_values();
|
||||
let navigate_help_string = format!(
|
||||
"Navigate options with {} to go down, {} to go up, select with {}",
|
||||
"Navigate options with {} to go down, {} to go up, select with {}, cancel with {}",
|
||||
shortcuts["scroll_down"],
|
||||
shortcuts["scroll_up"],
|
||||
Key::Char('\n')
|
||||
Key::Char('\n'),
|
||||
Key::Esc
|
||||
);
|
||||
let width = std::cmp::max(
|
||||
self.entry_titles.iter().map(|e| e.len()).max().unwrap_or(0) + 3,
|
||||
|
@ -507,33 +581,38 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
+ 3
|
||||
// buttons row
|
||||
+ if self.single_only { 1 } else { 5 };
|
||||
let dialog_area = area.align_inside(
|
||||
(width, height),
|
||||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
);
|
||||
let inner_area = create_box(grid, dialog_area);
|
||||
grid.clear_area(inner_area, self.theme_default);
|
||||
if !self.content.resize_with_context(width, height, context) {
|
||||
self.dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.write_string(
|
||||
let inner_area = self.content.area();
|
||||
let (_, y) = self.content.grid_mut().write_string(
|
||||
&self.title,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::BOLD,
|
||||
dialog_area.skip_cols(2),
|
||||
inner_area.skip_cols(2),
|
||||
None,
|
||||
);
|
||||
|
||||
grid.write_string(
|
||||
&navigate_help_string,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::ITALICS,
|
||||
dialog_area.skip_cols(2).skip_rows(height),
|
||||
None,
|
||||
);
|
||||
let y = self
|
||||
.content
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
&navigate_help_string,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::ITALICS,
|
||||
inner_area.skip_cols(2).skip_rows(y + 2),
|
||||
None,
|
||||
)
|
||||
.1
|
||||
+ y
|
||||
+ 2;
|
||||
|
||||
let inner_area = inner_area.skip_cols(1).skip_rows(y + 2);
|
||||
|
||||
let inner_area = inner_area.skip_cols(1).skip_rows(1);
|
||||
/* Extra room for buttons Okay/Cancel */
|
||||
if self.single_only {
|
||||
for (i, e) in self.entry_titles.iter().enumerate() {
|
||||
|
@ -542,7 +621,14 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
grid.write_string(e, attr.fg, attr.bg, attr.attrs, inner_area.nth_row(i), None);
|
||||
self.content.grid_mut().write_string(
|
||||
e,
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
attr.attrs,
|
||||
inner_area.nth_row(i),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (i, e) in self.entry_titles.iter().enumerate() {
|
||||
|
@ -551,7 +637,7 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
grid.write_string(
|
||||
self.content.grid_mut().write_string(
|
||||
&format!("[{}] {}", if self.entries[i].1 { "x" } else { " " }, e),
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
|
@ -566,7 +652,7 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
let (x, y) = grid.write_string(
|
||||
let (x, y) = self.content.grid_mut().write_string(
|
||||
OK,
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
|
@ -579,7 +665,7 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
grid.write_string(
|
||||
self.content.grid_mut().write_string(
|
||||
CANCEL,
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
|
@ -588,6 +674,145 @@ impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + S
|
|||
None,
|
||||
);
|
||||
}
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted");
|
||||
if !context.settings.terminal.use_color() {
|
||||
highlighted_attrs.attrs |= Attr::REVERSE;
|
||||
}
|
||||
if !self.initialized {
|
||||
// [ref:FIXME]: don't re-initialize when the only change is highlight index.
|
||||
self.initialize(context);
|
||||
}
|
||||
let (width, height) = self.content.area().size();
|
||||
let dialog_area = area.align_inside(
|
||||
(width + 2, height + 2),
|
||||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
);
|
||||
let inner_area = create_box(grid, dialog_area);
|
||||
let rows = inner_area.height();
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
match mvm {
|
||||
PageMovement::Up(_) | PageMovement::Down(_) => {}
|
||||
PageMovement::Right(amount) => {
|
||||
self.scroll_x_cursor = self.scroll_x_cursor.saturating_add(amount);
|
||||
}
|
||||
PageMovement::Left(amount) => {
|
||||
self.scroll_x_cursor = self.scroll_x_cursor.saturating_sub(amount);
|
||||
}
|
||||
PageMovement::PageUp(multiplier) => match self.cursor {
|
||||
SelectorCursor::Unfocused => {
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Entry(c) => {
|
||||
self.cursor = SelectorCursor::Entry(c.saturating_sub(multiplier * rows));
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel
|
||||
if !self.entry_titles.is_empty() =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Entry(
|
||||
self.entry_titles.len().saturating_sub(multiplier * rows),
|
||||
);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel => {}
|
||||
},
|
||||
PageMovement::PageDown(multiplier) => match self.cursor {
|
||||
SelectorCursor::Unfocused => {
|
||||
self.cursor = SelectorCursor::Entry(
|
||||
self.entry_titles
|
||||
.len()
|
||||
.saturating_sub(1)
|
||||
.min(multiplier * rows),
|
||||
);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Entry(c)
|
||||
if c.saturating_add(multiplier * rows) < self.entry_titles.len()
|
||||
&& !self.entry_titles.is_empty() =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Entry(
|
||||
self.entry_titles
|
||||
.len()
|
||||
.saturating_sub(1)
|
||||
.min(c.saturating_add(multiplier * rows)),
|
||||
);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Entry(_) => {
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel => {}
|
||||
},
|
||||
PageMovement::Home if !self.entry_titles.is_empty() => {
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.initialize(context);
|
||||
}
|
||||
PageMovement::End
|
||||
if matches!(self.cursor, SelectorCursor::Ok | SelectorCursor::Cancel) => {}
|
||||
PageMovement::End
|
||||
if !matches!(self.cursor, SelectorCursor::Entry(c) if c +1 == self.entry_titles.len())
|
||||
&& !self.entry_titles.is_empty() =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Entry(self.entry_titles.len().saturating_sub(1));
|
||||
self.initialize(context);
|
||||
}
|
||||
PageMovement::Home | PageMovement::End => {}
|
||||
}
|
||||
}
|
||||
let skip_rows = match self.cursor {
|
||||
SelectorCursor::Unfocused => 0,
|
||||
SelectorCursor::Entry(e) if e >= rows => e.min(height.saturating_sub(rows)),
|
||||
SelectorCursor::Entry(_) => 0,
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel => height.saturating_sub(rows),
|
||||
};
|
||||
|
||||
self.scroll_x_cursor = self
|
||||
.scroll_x_cursor
|
||||
.min(width.saturating_sub(inner_area.width()));
|
||||
grid.copy_area(
|
||||
self.content.grid(),
|
||||
inner_area,
|
||||
self.content
|
||||
.area()
|
||||
.skip_cols(self.scroll_x_cursor)
|
||||
.skip_rows(skip_rows),
|
||||
);
|
||||
|
||||
if height > dialog_area.height() {
|
||||
let inner_area = inner_area.skip_rows(1);
|
||||
ScrollBar::default().set_show_arrows(true).draw(
|
||||
grid,
|
||||
inner_area.nth_col(inner_area.width().saturating_sub(1)),
|
||||
context,
|
||||
// position
|
||||
skip_rows,
|
||||
// visible_rows
|
||||
inner_area.height(),
|
||||
// length
|
||||
height,
|
||||
);
|
||||
}
|
||||
if width > dialog_area.width() {
|
||||
let inner_area = inner_area.skip_cols(1);
|
||||
ScrollBar::default().set_show_arrows(true).draw_horizontal(
|
||||
grid,
|
||||
inner_area.nth_row(inner_area.height().saturating_sub(1)),
|
||||
context,
|
||||
// position
|
||||
self.scroll_x_cursor,
|
||||
// visible_cols
|
||||
inner_area.width(),
|
||||
// length
|
||||
width,
|
||||
);
|
||||
}
|
||||
context.dirty_areas.push_back(dialog_area);
|
||||
self.dirty = false;
|
||||
}
|
||||
|
|
|
@ -19,16 +19,16 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::text_processing::{LineBreakText, Truncate};
|
||||
use melib::text::{LineBreakText, Truncate};
|
||||
|
||||
use super::*;
|
||||
use crate::terminal::embedded::EmbeddedGrid;
|
||||
use crate::{jobs::JoinHandle, terminal::embedded::EmbeddedGrid};
|
||||
|
||||
/// A pager for text.
|
||||
/// `Pager` holds its own content in its own `CellBuffer` and when `draw` is
|
||||
/// called, it draws the current view of the text. It is responsible for
|
||||
/// scrolling etc.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Pager {
|
||||
text: String,
|
||||
cursor: (usize, usize),
|
||||
|
@ -51,12 +51,39 @@ pub struct Pager {
|
|||
/// events.
|
||||
rows_lt_height: bool,
|
||||
filtered_content: Option<(String, Result<EmbeddedGrid>)>,
|
||||
filter_job: Option<(String, JoinHandle<Result<EmbeddedGrid>>)>,
|
||||
text_lines: Vec<String>,
|
||||
line_breaker: LineBreakText,
|
||||
movement: Option<PageMovement>,
|
||||
id: ComponentId,
|
||||
}
|
||||
|
||||
impl Clone for Pager {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
filter_job: None,
|
||||
text: self.text.clone(),
|
||||
cursor: self.cursor,
|
||||
reflow: self.reflow,
|
||||
height: self.height,
|
||||
width: self.width,
|
||||
minimum_width: self.minimum_width,
|
||||
search: self.search.clone(),
|
||||
dirty: true,
|
||||
colors: self.colors,
|
||||
initialised: false,
|
||||
show_scrollbar: self.show_scrollbar,
|
||||
cols_lt_width: self.cols_lt_width,
|
||||
rows_lt_height: self.rows_lt_height,
|
||||
filtered_content: self.filtered_content.clone(),
|
||||
text_lines: self.text_lines.clone(),
|
||||
line_breaker: self.line_breaker.clone(),
|
||||
movement: self.movement,
|
||||
id: ComponentId::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Pager {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "pager")
|
||||
|
@ -124,31 +151,19 @@ impl Pager {
|
|||
|
||||
pub fn from_string(
|
||||
text: String,
|
||||
context: Option<&Context>,
|
||||
context: &Context,
|
||||
cursor_pos: Option<usize>,
|
||||
mut width: Option<usize>,
|
||||
colors: ThemeAttribute,
|
||||
) -> Self {
|
||||
let pager_filter: Option<&String> = if let Some(context) = context {
|
||||
context.settings.pager.filter.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pager_filter: Option<&String> = context.settings.pager.filter.as_ref();
|
||||
|
||||
let pager_minimum_width: usize = if let Some(context) = context {
|
||||
context.settings.pager.minimum_width
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let pager_minimum_width: usize = context.settings.pager.minimum_width;
|
||||
|
||||
let reflow: Reflow = if let Some(context) = context {
|
||||
if context.settings.pager.split_long_lines {
|
||||
Reflow::All
|
||||
} else {
|
||||
Reflow::No
|
||||
}
|
||||
} else {
|
||||
let reflow: Reflow = if context.settings.pager.split_long_lines {
|
||||
Reflow::All
|
||||
} else {
|
||||
Reflow::No
|
||||
};
|
||||
|
||||
if let Some(ref mut width) = width.as_mut() {
|
||||
|
@ -174,20 +189,20 @@ impl Pager {
|
|||
};
|
||||
|
||||
if let Some(bin) = pager_filter {
|
||||
ret.filter(bin);
|
||||
ret.filter(bin, context);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn filter(&mut self, cmd: &str) {
|
||||
let _f = |bin: &str, text: &str| -> Result<EmbeddedGrid> {
|
||||
pub fn filter(&mut self, cmd: &str, context: &Context) {
|
||||
async fn filter_fut(bin: String, text: String) -> Result<EmbeddedGrid> {
|
||||
use std::{
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
let mut filter_child = Command::new("sh")
|
||||
.args(["-c", bin])
|
||||
.args(["-c", &bin])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
|
@ -208,13 +223,13 @@ impl Pager {
|
|||
embedded.process_byte(&mut dev_null, b);
|
||||
}
|
||||
Ok(embedded)
|
||||
};
|
||||
let buf = _f(cmd, &self.text);
|
||||
if let Some((width, height)) = buf.as_ref().ok().map(EmbeddedGrid::terminal_size) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
self.filtered_content = Some((cmd.to_string(), buf));
|
||||
let fut = Box::pin(filter_fut(cmd.to_string(), self.text.clone()));
|
||||
let handle = context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_blocking(format!("Running pager filter {cmd}").into(), fut);
|
||||
self.filter_job = Some((cmd.to_string(), handle));
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self) -> usize {
|
||||
|
@ -244,7 +259,7 @@ impl Pager {
|
|||
self.height = self.text_lines.len();
|
||||
self.width = width;
|
||||
if let Some(ref mut search) = self.search {
|
||||
use melib::text_processing::search::KMP;
|
||||
use melib::text::search::KMP;
|
||||
search.positions.clear();
|
||||
for (y, l) in self.text_lines.iter().enumerate() {
|
||||
search.positions.extend(
|
||||
|
@ -278,7 +293,7 @@ impl Pager {
|
|||
_context: &mut Context,
|
||||
up_to: usize,
|
||||
) {
|
||||
if self.line_breaker.is_finished() {
|
||||
if self.line_breaker.is_finished() || self.filtered_content.is_some() {
|
||||
return;
|
||||
}
|
||||
let old_lines_no = self.text_lines.len();
|
||||
|
@ -294,7 +309,7 @@ impl Pager {
|
|||
};
|
||||
let new_lines_no = self.text_lines.len() - old_lines_no;
|
||||
if let Some(ref mut search) = self.search {
|
||||
use melib::text_processing::search::KMP;
|
||||
use melib::text::search::KMP;
|
||||
for (y, l) in self.text_lines.iter().enumerate().skip(old_lines_no) {
|
||||
search.positions.extend(
|
||||
l.kmp_search(&search.pattern)
|
||||
|
@ -317,15 +332,14 @@ impl Pager {
|
|||
.area()
|
||||
.skip_cols(self.cursor.0)
|
||||
.skip_rows(self.cursor.1)
|
||||
.take_cols(content.terminal_size().0.saturating_sub(area.width()))
|
||||
.take_rows(content.terminal_size().1.saturating_sub(area.height())),
|
||||
.take_cols(content.terminal_size().0.min(area.width()))
|
||||
.take_rows(content.terminal_size().1.min(area.height())),
|
||||
);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
|
||||
cmd.to_string(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
Err(ref err) => {
|
||||
let mut cmd = cmd.as_str();
|
||||
|
@ -338,6 +352,7 @@ impl Pager {
|
|||
))));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -759,11 +774,35 @@ impl Component for Pager {
|
|||
return true;
|
||||
}
|
||||
UIEvent::Action(View(Filter(ref cmd))) => {
|
||||
self.filter(cmd);
|
||||
self.filter(cmd, context);
|
||||
self.initialised = false;
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
|
||||
if self
|
||||
.filter_job
|
||||
.as_ref()
|
||||
.map(|(_, h)| h == job_id)
|
||||
.unwrap_or(false) =>
|
||||
{
|
||||
let (cmd, mut handle) = self.filter_job.take().unwrap();
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* search was canceled */ }
|
||||
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
|
||||
Ok(Some(buf)) => {
|
||||
if let Some((width, height)) =
|
||||
buf.as_ref().ok().map(EmbeddedGrid::terminal_size)
|
||||
{
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
self.filtered_content = Some((cmd, buf));
|
||||
}
|
||||
}
|
||||
self.initialised = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::Action(Action::Listing(ListingAction::Search(pattern))) => {
|
||||
self.search = Some(SearchPattern {
|
||||
pattern: pattern.to_string(),
|
||||
|
@ -835,13 +874,26 @@ impl Component for Pager {
|
|||
String::new(),
|
||||
)));
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if context.settings.shortcuts.pager.commands.iter().any(|cmd| {
|
||||
if cmd.shortcut == *key {
|
||||
for cmd in &cmd.command {
|
||||
context.replies.push_back(UIEvent::Command(cmd.to_string()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
self.dirty || !self.initialised
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
|
@ -864,4 +916,12 @@ impl Component for Pager {
|
|||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
|
||||
if self.id != uuid {
|
||||
return;
|
||||
}
|
||||
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ impl<const N: usize> Default for DataColumns<N> {
|
|||
for elem in &mut data[..] {
|
||||
elem.write(cl());
|
||||
}
|
||||
let ptr = &data as *const [MaybeUninit<T>; N];
|
||||
let ptr = std::ptr::addr_of!(data);
|
||||
unsafe { (ptr as *const [T; N]).read() }
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ impl<const N: usize> DataColumns<N> {
|
|||
width_accum += self.widths[i];
|
||||
}
|
||||
// add column gaps
|
||||
width_accum += 2 * N.saturating_sub(1);
|
||||
width_accum += N.saturating_sub(1);
|
||||
debug_assert!(growees >= growees_max);
|
||||
if width_accum >= screen_width || screen_height == 0 || screen_width == 0 || growees == 0 {
|
||||
self.width_accum = width_accum;
|
||||
|
@ -260,7 +260,7 @@ impl<const N: usize> DataColumns<N> {
|
|||
break;
|
||||
}
|
||||
x_offset -= self.widths[col];
|
||||
x_offset = x_offset.saturating_sub(2);
|
||||
x_offset = x_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
for col in start_col..N {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::melib::text_processing::Truncate;
|
||||
use crate::melib::text::Truncate;
|
||||
|
||||
pub struct TextField {
|
||||
inner: UText,
|
||||
|
@ -39,7 +39,7 @@ impl std::fmt::Debug for TextField {
|
|||
}
|
||||
|
||||
impl Default for TextField {
|
||||
fn default() -> TextField {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: UText::new(String::with_capacity(256)),
|
||||
autocomplete: None,
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
use std::{borrow::Cow, collections::HashMap, time::Duration};
|
||||
|
||||
use super::*;
|
||||
use crate::melib::text_processing::TextProcessing;
|
||||
use crate::melib::text::TextProcessing;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
enum FormFocus {
|
||||
|
@ -49,8 +49,8 @@ impl std::fmt::Debug for Field {
|
|||
}
|
||||
|
||||
impl Default for Field {
|
||||
fn default() -> Field {
|
||||
Field::Text(TextField::default())
|
||||
fn default() -> Self {
|
||||
Self::Text(TextField::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,8 +253,8 @@ impl<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> FormWidget<T>
|
|||
action: (Cow<'static, str>, T),
|
||||
cursor_up_shortcut: Key,
|
||||
cursor_down_shortcut: Key,
|
||||
) -> FormWidget<T> {
|
||||
FormWidget {
|
||||
) -> Self {
|
||||
Self {
|
||||
buttons: ButtonWidget::new(action),
|
||||
focus: FormFocus::Fields,
|
||||
hide_buttons: false,
|
||||
|
@ -556,8 +556,8 @@ impl<T> ButtonWidget<T>
|
|||
where
|
||||
T: 'static + std::fmt::Debug + Copy + Default + Send + Sync,
|
||||
{
|
||||
pub fn new(init_val: (Cow<'static, str>, T)) -> ButtonWidget<T> {
|
||||
ButtonWidget {
|
||||
pub fn new(init_val: (Cow<'static, str>, T)) -> Self {
|
||||
Self {
|
||||
layout: vec![init_val.0.clone()],
|
||||
buttons: vec![init_val].into_iter().collect(),
|
||||
result: None,
|
||||
|
@ -676,7 +676,7 @@ impl AutoCompleteEntry {
|
|||
|
||||
impl From<String> for AutoCompleteEntry {
|
||||
fn from(val: String) -> Self {
|
||||
AutoCompleteEntry {
|
||||
Self {
|
||||
entry: val,
|
||||
description: String::new(),
|
||||
}
|
||||
|
@ -686,7 +686,7 @@ impl From<String> for AutoCompleteEntry {
|
|||
impl From<&(&str, &str, TokenStream)> for AutoCompleteEntry {
|
||||
fn from(val: &(&str, &str, TokenStream)) -> Self {
|
||||
let (a, b, _) = val;
|
||||
AutoCompleteEntry {
|
||||
Self {
|
||||
entry: a.to_string(),
|
||||
description: b.to_string(),
|
||||
}
|
||||
|
@ -696,7 +696,7 @@ impl From<&(&str, &str, TokenStream)> for AutoCompleteEntry {
|
|||
impl From<(String, String)> for AutoCompleteEntry {
|
||||
fn from(val: (String, String)) -> Self {
|
||||
let (a, b) = val;
|
||||
AutoCompleteEntry {
|
||||
Self {
|
||||
entry: a,
|
||||
description: b,
|
||||
}
|
||||
|
@ -731,7 +731,7 @@ impl Component for AutoComplete {
|
|||
}
|
||||
let page_no = (self.cursor.saturating_sub(1)).wrapping_div(rows);
|
||||
let top_idx = page_no * rows;
|
||||
let x_offset = if rows < self.entries.len() { 1 } else { 0 };
|
||||
let x_offset = usize::from(rows < self.entries.len());
|
||||
|
||||
grid.clear_area(area, crate::conf::value(context, "theme_default"));
|
||||
let width = self
|
||||
|
@ -817,7 +817,7 @@ impl Component for AutoComplete {
|
|||
|
||||
impl AutoComplete {
|
||||
pub fn new(entries: Vec<AutoCompleteEntry>) -> Box<Self> {
|
||||
let mut ret = AutoComplete {
|
||||
let mut ret = Self {
|
||||
entries: Vec::new(),
|
||||
cursor: 0,
|
||||
dirty: true,
|
||||
|
@ -1102,7 +1102,7 @@ impl ProgressSpinner {
|
|||
theme_attr.attrs |= Attr::REVERSE;
|
||||
}
|
||||
theme_attr.attrs |= Attr::BOLD;
|
||||
ProgressSpinner {
|
||||
Self {
|
||||
timer,
|
||||
stage: 0,
|
||||
kind: Ok(kind),
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "melib"
|
||||
version = "0.8.5-rc.3"
|
||||
version = "0.8.5"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
rust-version = "1.68.2"
|
||||
rust-version = "1.70.0"
|
||||
|
||||
homepage = "https://meli.delivery"
|
||||
repository = "https://git.meli.delivery/meli/meli.git"
|
||||
homepage = "https://meli-email.org"
|
||||
repository = "https://git.meli-email.org/meli/meli.git"
|
||||
description = "library for e-mail clients and other e-mail applications"
|
||||
keywords = ["mail", "mua", "maildir", "imap", "jmap"]
|
||||
categories = ["email", "parser-implementations"]
|
||||
|
@ -37,9 +37,9 @@ libc = { version = "0.2.125", features = ["extra_traits"] }
|
|||
libloading = "^0.7"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
native-tls = { version = "0.2.3", default-features = false, optional = true }
|
||||
nix = "^0.24"
|
||||
nix = { version = "0.27", default-features = false, features = ["fs", "socket", "dir", "hostname"] }
|
||||
nom = { version = "7" }
|
||||
notify = { version = "4.0.15", optional = true }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
polling = "2.8"
|
||||
regex = { version = "1" }
|
||||
rusqlite = { version = "^0.29", default-features = false, features = ["array"], optional = true }
|
||||
|
@ -49,9 +49,9 @@ serde_json = { version = "1.0", features = ["raw_value"] }
|
|||
serde_path_to_error = { version = "0.1" }
|
||||
smallvec = { version = "^1.5.0", features = ["serde"] }
|
||||
smol = "1.0.0"
|
||||
socket2 = { version = "0.4", features = [] }
|
||||
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
|
||||
socket2 = { version = "0.5", features = [] }
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false }
|
||||
url = { version = "2.4", optional = true }
|
||||
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
|
||||
xdg = "2.1.0"
|
||||
|
||||
|
@ -64,27 +64,24 @@ http = ["isahc"]
|
|||
http-static = ["isahc", "isahc/static-curl"]
|
||||
imap = ["imap-codec", "tls"]
|
||||
imap-trace = ["imap"]
|
||||
jmap = ["http"]
|
||||
jmap = ["http", "url/serde"]
|
||||
jmap-trace = ["jmap"]
|
||||
nntp = ["tls"]
|
||||
nntp-trace = ["nntp"]
|
||||
maildir = ["notify"]
|
||||
mbox = ["notify"]
|
||||
notmuch = []
|
||||
notmuch = ["notify"]
|
||||
smtp = ["tls", "base64"]
|
||||
smtp-trace = ["smtp"]
|
||||
sqlite3 = ["rusqlite"]
|
||||
sqlite3-static = ["sqlite3", "rusqlite/bundled-full"]
|
||||
tls = ["native-tls"]
|
||||
tls-static = ["tls", "native-tls/vendored"]
|
||||
text-processing = []
|
||||
unicode-algorithms = ["text-processing", "unicode-segmentation"]
|
||||
unicode-algorithms-cached = ["text-processing", "unicode-segmentation"]
|
||||
vcard = []
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1.0.16" }
|
||||
|
||||
[dev-dependencies]
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
mailin-embedded = { version = "0.8", features = ["rtls"] }
|
||||
stderrlog = "^0.5"
|
||||
|
|
|
@ -22,24 +22,6 @@ Library for handling mail.
|
|||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `sqlite` | `rusqlite` | Used in IMAP cache. |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `unicode-algorithms` | `unicode-segmentation` | Linebreaking algo etc |
|
||||
| | | For a fresh clean build, |
|
||||
| | | Network access is |
|
||||
| | | required to fetch data |
|
||||
| | | from Unicode's website. |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `unicode-algorithms-cached` | `unicode-segmentation` | Linebreaking algo etc |
|
||||
| | | but it uses a cached |
|
||||
| | | version of Unicode data |
|
||||
| | | which might be stale. |
|
||||
| | | |
|
||||
| | | Use this feature instead |
|
||||
| | | of the previous one for |
|
||||
| | | building without network |
|
||||
| | | access. |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `unicode-algorithms` | `unicode-segmentation` | |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `vcard` | | vcard parsing |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `gpgme` | | GPG use with libgpgme |
|
||||
|
|
|
@ -21,15 +21,14 @@
|
|||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
#[cfg(any(feature = "unicode-algorithms", feature = "unicode-algorithms-cached"))]
|
||||
include!("src/text_processing/types.rs");
|
||||
include!("src/text/types.rs");
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
#[cfg(any(feature = "unicode-algorithms", feature = "unicode-algorithms-cached"))]
|
||||
{
|
||||
const MOD_PATH: &str = "src/text_processing/tables.rs";
|
||||
const MOD_PATH: &str = "src/text/tables.rs";
|
||||
println!("cargo:rerun-if-env-changed=UNICODE_REGENERATE_TABLES");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed={}", MOD_PATH);
|
||||
println!("cargo:rerun-if-changed={MOD_PATH}");
|
||||
/* Line break tables */
|
||||
use std::{
|
||||
fs::File,
|
||||
|
@ -54,24 +53,6 @@ fn main() -> Result<(), std::io::Error> {
|
|||
);
|
||||
return Ok(());
|
||||
}
|
||||
if cfg!(feature = "unicode-algorithms-cached") {
|
||||
const CACHED_MODULE: &[u8] =
|
||||
include_bytes!(concat!("./src/text_processing/tables.rs.gz"));
|
||||
|
||||
let mut gz = GzDecoder::new(CACHED_MODULE);
|
||||
use flate2::bufread::GzDecoder;
|
||||
let mut v = String::with_capacity(
|
||||
8, /*
|
||||
str::parse::<usize>(unsafe {
|
||||
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("was not compressed with size comment header",)),*/
|
||||
);
|
||||
gz.read_to_string(&mut v)?;
|
||||
let mut file = File::create(mod_path)?;
|
||||
file.write_all(v.as_bytes())?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut child = Command::new("curl")
|
||||
.args(["-o", "-", LINE_BREAK_TABLE_URL])
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -350,7 +331,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let mut file = File::create(mod_path)?;
|
||||
file.write_all(
|
||||
br#"/*
|
||||
* meli - text_processing crate.
|
||||
* meli - text crate.
|
||||
*
|
||||
* Copyright 2017-2020 Manos Pitsidianakis
|
||||
*
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue